惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

有赞技术团队
有赞技术团队
G
Google Developers Blog
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
人人都是产品经理
人人都是产品经理
J
Java Code Geeks
P
Proofpoint News Feed
V
Visual Studio Blog
爱范儿
爱范儿
The Cloudflare Blog
博客园 - 叶小钗
V
V2EX
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
M
MIT News - Artificial intelligence
Microsoft Security Blog
Microsoft Security Blog
博客园 - 聂微东
H
Help Net Security
B
Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 【当耐特】
量子位
宝玉的分享
宝玉的分享
WordPress大学
WordPress大学
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix(imessage): wire reply attachments through send-rich -...
omarshahine · 2026-05-10 · via Recent Commits to openclaw:main

@@ -278,6 +278,144 @@ describe("imessage message actions", () => {

278278

);

279279

});

280280281+

describe("reply with attachment (openclaw/imsg#114 plumbing)", () => {

282+

// The core message-action runner hydrates path/media/filePath/etc.

283+

// through the outbound media resolver (mediaLocalRoots/sandbox/size)

284+

// before reaching this handler, writing the result into `buffer` +

285+

// `filename`. These tests cover the post-hydration contract: the

286+

// handler trusts only the buffer and refuses any unhydrated path

287+

// param so an agent cannot bypass the resolver.

288+

const stringPath = "/tmp/cute-lobster.png";

289+

const base64Png = Buffer.from("PNGDATA").toString("base64");

290+291+

function readLastAttachment():

292+

| {

293+

kind?: string;

294+

buffer?: Uint8Array;

295+

filename?: string;

296+

}

297+

| undefined {

298+

const call = runtimeMock.sendRichMessage.mock.calls.at(-1)?.[0] as

299+

| { attachment?: { kind: string; buffer?: Uint8Array; filename?: string } }

300+

| undefined;

301+

return call?.attachment;

302+

}

303+304+

it("threads a hydrated buffer attachment through to sendRichMessage when imsg supports send-rich --file", async () => {

305+

probeMock.getCachedIMessagePrivateApiStatus.mockReturnValue({

306+

available: true,

307+

v2Ready: true,

308+

selectors: {},

309+

cliCapabilities: { sendRichSupportsAttachment: true },

310+

});

311+

runtimeMock.resolveChatGuidForTarget.mockResolvedValue("iMessage;+;resolved-ident");

312+

runtimeMock.sendRichMessage.mockResolvedValue({ messageId: "reply-guid" });

313+314+

await imessageMessageActions.handleAction?.({

315+

action: "reply",

316+

cfg: cfg(),

317+

params: {

318+

chatIdentifier: "team-thread",

319+

messageId: "message-guid",

320+

text: "🦞 here it is",

321+

buffer: base64Png,

322+

filename: "card.png",

323+

},

324+

} as never);

325+

expect(runtimeMock.sendRichMessage).toHaveBeenCalledWith(

326+

expect.objectContaining({ replyToMessageId: "message-guid" }),

327+

);

328+

const attachment = readLastAttachment();

329+

expect(attachment?.kind).toBe("buffer");

330+

expect(attachment?.filename).toBe("card.png");

331+

expect(Buffer.from(attachment?.buffer ?? new Uint8Array()).toString()).toBe("PNGDATA");

332+

});

333+334+

it("falls back to attachment.bin when filename is missing (post-hydration)", async () => {

335+

probeMock.getCachedIMessagePrivateApiStatus.mockReturnValue({

336+

available: true,

337+

v2Ready: true,

338+

selectors: {},

339+

cliCapabilities: { sendRichSupportsAttachment: true },

340+

});

341+

runtimeMock.resolveChatGuidForTarget.mockResolvedValue("iMessage;+;resolved-ident");

342+

runtimeMock.sendRichMessage.mockResolvedValue({ messageId: "reply-guid" });

343+344+

await imessageMessageActions.handleAction?.({

345+

action: "reply",

346+

cfg: cfg(),

347+

params: {

348+

chatIdentifier: "team-thread",

349+

messageId: "message-guid",

350+

text: "🦞 here it is",

351+

buffer: base64Png,

352+

},

353+

} as never);

354+

expect(readLastAttachment()?.filename).toBe("attachment.bin");

355+

});

356+357+

it("rejects unhydrated path-shaped params so agents cannot bypass the media resolver", async () => {

358+

// The runner's hydrateAttachmentParamsForAction loads any

359+

// path/media/filePath/mediaUrl/fileUrl through the media resolver

360+

// and writes the result into `buffer`. If we ever see a path-shaped

361+

// param without a `buffer`, hydration was skipped — refuse instead

362+

// of forwarding a raw host path to imsg.

363+

probeMock.getCachedIMessagePrivateApiStatus.mockReturnValue({

364+

available: true,

365+

v2Ready: true,

366+

selectors: {},

367+

cliCapabilities: { sendRichSupportsAttachment: true },

368+

});

369+

runtimeMock.resolveChatGuidForTarget.mockResolvedValue("iMessage;+;resolved-ident");

370+371+

for (const field of ["filePath", "path", "media", "mediaUrl", "fileUrl"]) {

372+

runtimeMock.sendRichMessage.mockClear();

373+

await expect(

374+

imessageMessageActions.handleAction?.({

375+

action: "reply",

376+

cfg: cfg(),

377+

params: {

378+

chatIdentifier: "team-thread",

379+

messageId: "message-guid",

380+

text: "🦞 here it is",

381+

[field]: stringPath,

382+

},

383+

} as never),

384+

).rejects.toThrow(/did not pass through the outbound media resolver/);

385+

expect(runtimeMock.sendRichMessage).not.toHaveBeenCalled();

386+

}

387+

});

388+389+

it("rejects reply + attachment when imsg does not advertise send-rich --file", async () => {

390+

// Older imsg builds reject `--file` on send-rich, so refuse loudly

391+

// here rather than letting send-rich ship the text alone and silently

392+

// drop the attachment (the original openclaw/openclaw#79822 symptom).

393+

probeMock.getCachedIMessagePrivateApiStatus.mockReturnValue({

394+

available: true,

395+

v2Ready: true,

396+

selectors: {},

397+

cliCapabilities: { sendRichSupportsAttachment: false },

398+

});

399+

runtimeMock.resolveChatGuidForTarget.mockResolvedValue("iMessage;+;resolved-ident");

400+401+

runtimeMock.sendRichMessage.mockClear();

402+

await expect(

403+

imessageMessageActions.handleAction?.({

404+

action: "reply",

405+

cfg: cfg(),

406+

params: {

407+

chatIdentifier: "team-thread",

408+

messageId: "message-guid",

409+

text: "🦞 here it is",

410+

buffer: base64Png,

411+

filename: "card.png",

412+

},

413+

} as never),

414+

).rejects.toThrow(/needs an imsg build that exposes `send-rich --file`/);

415+

expect(runtimeMock.sendRichMessage).not.toHaveBeenCalled();

416+

});

417+

});

418+281419

describe("phone-number target end-to-end (regressions caught the hard way)", () => {

282420

it("synthesizes iMessage;-;<phone> chat_identifier from a handle target and sends through to sendReaction", async () => {

283421

// Scenario from prod: agent calls react with `target:"+12069106512"` and a