Skip to content

Commit a79ecb2

Browse files
wanglu241wangwllu
authored andcommitted
fix(outbound-attachment): propagate trusted-html marker write failure
Addresses ClawSweeper P1: swallowed marker write failure. On marker write failure, best-effort delete the staged file then rethrow so the caller's normal media-failure fallback handles it instead of a later silent reload drop. Adds a test.
1 parent 4eed006 commit a79ecb2

2 files changed

Lines changed: 43 additions & 8 deletions

File tree

src/media/outbound-attachment.test.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
// Outbound attachment tests cover media loading rules for outgoing messages.
2-
import { describe, expect, it, vi } from "vitest";
2+
import { afterEach, describe, expect, it, vi } from "vitest";
33

44
const loadWebMedia = vi.hoisted(() => vi.fn());
55
const markTrustedGeneratedHtmlPath = vi.hoisted(() => vi.fn());
66
const saveMediaBuffer = vi.hoisted(() => vi.fn());
7+
const rm = vi.hoisted(() => vi.fn(async () => {}));
8+
9+
vi.mock("node:fs/promises", () => ({
10+
rm,
11+
}));
712

813
vi.mock("./web-media.js", () => ({
914
loadWebMedia,
@@ -17,6 +22,10 @@ vi.mock("./store.js", () => ({
1722
const { resolveOutboundAttachmentFromBuffer, resolveOutboundAttachmentFromUrl } =
1823
await import("./outbound-attachment.js");
1924

25+
afterEach(() => {
26+
vi.clearAllMocks();
27+
});
28+
2029
describe("resolveOutboundAttachmentFromUrl", () => {
2130
it("preserves the loaded file name when staging outbound media", async () => {
2231
const buffer = Buffer.from("pdf");
@@ -59,9 +68,38 @@ describe("resolveOutboundAttachmentFromUrl", () => {
5968

6069
expect(markTrustedGeneratedHtmlPath).toHaveBeenCalledWith(
6170
"/tmp/media/outbound/report---uuid.html",
71+
buffer,
6272
);
6373
});
6474

75+
it("propagates a marker write failure and best-effort unlinks the staged file", async () => {
76+
const buffer = Buffer.from("<!doctype html><title>x</title>");
77+
loadWebMedia.mockResolvedValueOnce({
78+
buffer,
79+
contentType: "text/html",
80+
fileName: "report.html",
81+
trustedGeneratedHtmlSource: true,
82+
});
83+
saveMediaBuffer.mockResolvedValueOnce({
84+
path: "/tmp/media/outbound/report---uuid.html",
85+
contentType: "text/html",
86+
});
87+
88+
markTrustedGeneratedHtmlPath.mockReset();
89+
rm.mockReset();
90+
rm.mockResolvedValueOnce(undefined);
91+
const markerError = new Error("marker write failed");
92+
markTrustedGeneratedHtmlPath.mockRejectedValueOnce(markerError);
93+
94+
await expect(
95+
resolveOutboundAttachmentFromUrl("/tmp/openclaw/report.html", 1024),
96+
).rejects.toThrow(markerError);
97+
98+
expect(rm).toHaveBeenCalledWith("/tmp/media/outbound/report---uuid.html", {
99+
force: true,
100+
});
101+
});
102+
65103
it("does not mark untrusted outbound HTML staging", async () => {
66104
const buffer = Buffer.from("<!doctype html><title>x</title>");
67105
loadWebMedia.mockResolvedValueOnce({

src/media/outbound-attachment.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// Outbound attachment helpers prepare media attachments for channel delivery.
2-
import { logVerbose } from "../globals.js";
3-
import { formatErrorMessage } from "../infra/errors.js";
2+
import { rm } from "node:fs/promises";
43
import { buildOutboundMediaLoadOptions, type OutboundMediaAccess } from "./load-options.js";
54
import { saveMediaBuffer } from "./store.js";
65
import { loadWebMedia, markTrustedGeneratedHtmlPath } from "./web-media.js";
@@ -38,12 +37,10 @@ export async function resolveOutboundAttachmentFromUrl(
3837
// the staged file is treated as an arbitrary outbound HTML and rejected.
3938
if (media.trustedGeneratedHtmlSource) {
4039
try {
41-
await markTrustedGeneratedHtmlPath(saved.path);
40+
await markTrustedGeneratedHtmlPath(saved.path, media.buffer);
4241
} catch (err) {
43-
// best-effort: marker write is non-fatal — if the staged file vanished we'd reject at the gate anyway
44-
logVerbose(
45-
`outbound-attachment: failed to mark trusted-generated HTML at ${saved.path}: ${formatErrorMessage(err)}`,
46-
);
42+
await rm(saved.path, { force: true }).catch(() => {});
43+
throw err;
4744
}
4845
}
4946
return { path: saved.path, contentType: saved.contentType };

0 commit comments

Comments
 (0)