Skip to content

Commit ad2d13c

Browse files
committed
fix(discord): preserve thread reply file attachments
1 parent 6aaf235 commit ad2d13c

9 files changed

Lines changed: 572 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ Docs: https://docs.openclaw.ai
3838
- Gateway/diagnostics: add startup phase spans, active work labels, stale terminal bridge markers, and default sync-I/O tracing in `pnpm gateway:watch` so slow Gateway turns are easier to attribute from logs and stability diagnostics.
3939
- Plugins/loader: preserve real compiled plugin module evaluation errors on the native fast path instead of treating every thrown `.js` module as a source-transform fallback miss. Thanks @vincentkoc.
4040
- QA/Mantis: add `pnpm openclaw qa mantis slack-desktop-smoke` to run Slack live QA inside a Crabbox VNC desktop, open Slack Web, and capture desktop screenshots beside the Slack QA artifacts.
41+
- QA/Mantis: add an opt-in Discord thread attachment before/after scenario that creates a real thread, calls `message.thread-reply` with `filePath`, and captures baseline/candidate screenshot evidence.
42+
- Discord: preserve `filePath` and `path` attachments when replying to a thread with the message tool.
4143
- QA/Mantis: add visual desktop tasks with Crabbox MP4 recording, screenshot capture, and optional image-understanding assertions, and preserve video artifacts in Mantis before/after reports.
4244
- QA/WhatsApp: add `pnpm openclaw qa whatsapp` for live DM canary and pairing-gate coverage using two pre-linked WhatsApp Web sessions from the QA credential pool.
4345
- QA/Mantis: pass the runtime env through desktop-browser Crabbox and artifact-copy child commands, so embedded Mantis callers can provide Crabbox credentials without mutating the parent process. Thanks @vincentkoc.

docs/concepts/mantis.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,23 @@ directory, installs dependencies, builds each ref, runs the scenario with
8989
and `mantis-report.md`. For the first Discord scenario, a successful verification
9090
means baseline status is `fail` and candidate status is `pass`.
9191

92+
The second Discord before/after probe targets thread attachments:
93+
94+
```bash
95+
pnpm openclaw qa mantis run \
96+
--transport discord \
97+
--scenario discord-thread-reply-filepath-attachment \
98+
--baseline <bug-ref> \
99+
--candidate <fix-ref> \
100+
--output-dir .artifacts/qa-e2e/mantis/local-discord-thread-attachment
101+
```
102+
103+
That scenario posts a parent message with the driver bot, creates a real Discord
104+
thread, calls OpenClaw's `message.thread-reply` action with a repo-local
105+
`filePath`, then polls the thread for the SUT reply and attachment filename. The
106+
baseline screenshot shows the reply with no attachment; the candidate screenshot
107+
shows the expected `mantis-thread-report.md` attachment.
108+
92109
The first VM/browser primitive is the desktop smoke:
93110

94111
```bash

docs/concepts/qa-e2e-automation.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,13 @@ The full CLI reference, profile/scenario catalog, env vars, and artifact layout
113113

114114
The scenarios cover transport behavior that unit tests cannot prove end to end: mention gating, allow-bot policies, allowlists, top-level and threaded replies, DM routing, reaction handling, inbound edit suppression, restart replay dedupe, homeserver interruption recovery, approval metadata delivery, media handling, and Matrix E2EE bootstrap/recovery/verification flows. The E2EE CLI profile also drives `openclaw matrix encryption setup` and verification commands through the same disposable homeserver before checking gateway replies.
115115

116+
Discord also has Mantis-only opt-in scenarios for bug reproduction. Use
117+
`--scenario discord-status-reactions-tool-only` for the explicit status reaction
118+
timeline, or `--scenario discord-thread-reply-filepath-attachment` to create a
119+
real Discord thread and verify that `message.thread-reply` preserves a
120+
`filePath` attachment. These scenarios stay out of the default live Discord lane
121+
because they are before/after repro probes rather than broad smoke coverage.
122+
116123
CI uses the same command surface in `.github/workflows/qa-live-transports-convex.yml`. Scheduled and default manual runs execute the fast Matrix profile with live frontier credentials, `--fast`, and `OPENCLAW_QA_MATRIX_NO_REPLY_WINDOW_MS=3000`. Manual `matrix_profile=all` fans out into the five profile shards so the exhaustive catalog can run in parallel while keeping one artifact directory per shard.
117124

118125
For transport-real Telegram, Discord, and Slack smoke lanes:

extensions/discord/src/actions/handle-action.guild-admin.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ import {
1919

2020
type Ctx = Pick<
2121
ChannelMessageActionContext,
22-
"action" | "params" | "cfg" | "accountId" | "requesterSenderId" | "mediaLocalRoots"
22+
| "action"
23+
| "params"
24+
| "cfg"
25+
| "accountId"
26+
| "requesterSenderId"
27+
| "mediaLocalRoots"
28+
| "mediaReadFile"
2329
>;
2430

2531
export async function tryHandleDiscordMessageActionGuildAdmin(params: {
@@ -365,7 +371,10 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
365371
const content = readStringParam(actionParams, "message", {
366372
required: true,
367373
});
368-
const mediaUrl = readStringParam(actionParams, "media", { trim: false });
374+
const mediaUrl =
375+
readStringParam(actionParams, "media", { trim: false }) ??
376+
readStringParam(actionParams, "path", { trim: false }) ??
377+
readStringParam(actionParams, "filePath", { trim: false });
369378
const replyTo = readStringParam(actionParams, "replyTo");
370379

371380
// `message.thread-reply` (tool) uses `threadId`, while the CLI historically used `to`/`channelId`.
@@ -383,6 +392,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
383392
replyTo: replyTo ?? undefined,
384393
},
385394
cfg,
395+
{ mediaLocalRoots: ctx.mediaLocalRoots, mediaReadFile: ctx.mediaReadFile },
386396
);
387397
}
388398

extensions/discord/src/actions/handle-action.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,38 @@ describe("handleDiscordMessageAction", () => {
216216
expect(handleDiscordActionMock).not.toHaveBeenCalled();
217217
});
218218

219+
it("maps thread-reply filePath to Discord threadReply with media read context", async () => {
220+
const mediaReadFile = vi.fn(async () => Buffer.from("report"));
221+
222+
await handleDiscordMessageAction({
223+
action: "thread-reply",
224+
params: {
225+
threadId: "thread-123",
226+
message: "thread update",
227+
filePath: "/tmp/agent-root/report.md",
228+
},
229+
cfg: {
230+
channels: { discord: { token: "tok", actions: { threads: true } } },
231+
} as OpenClawConfig,
232+
mediaLocalRoots: ["/tmp/agent-root"],
233+
mediaReadFile,
234+
});
235+
236+
expect(handleDiscordActionMock).toHaveBeenCalledWith(
237+
expect.objectContaining({
238+
action: "threadReply",
239+
channelId: "thread-123",
240+
content: "thread update",
241+
mediaUrl: "/tmp/agent-root/report.md",
242+
}),
243+
expect.any(Object),
244+
{
245+
mediaLocalRoots: ["/tmp/agent-root"],
246+
mediaReadFile,
247+
},
248+
);
249+
});
250+
219251
it("forwards top-level components on sends", async () => {
220252
const components = { blocks: [{ type: "text", text: "Pick one" }] };
221253

extensions/qa-lab/src/live-transports/discord/discord-live.runtime.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,11 @@ describe("discord live qa runtime", () => {
250250
expect(
251251
__testing.findScenario(["discord-status-reactions-tool-only"]).map((scenario) => scenario.id),
252252
).toEqual(["discord-status-reactions-tool-only"]);
253+
expect(
254+
__testing
255+
.findScenario(["discord-thread-reply-filepath-attachment"])
256+
.map((scenario) => scenario.id),
257+
).toEqual(["discord-thread-reply-filepath-attachment"]);
253258
});
254259

255260
it("collects the status reaction sequence across timeline snapshots", () => {
@@ -323,6 +328,21 @@ describe("discord live qa runtime", () => {
323328
expect(html).toContain("Seen: 👀 → 🤔");
324329
});
325330

331+
it("renders a human-readable thread attachment artifact", () => {
332+
const html = __testing.renderDiscordThreadReplyAttachmentHtml({
333+
attachmentFilenames: [],
334+
expectedAttachmentFilename: "mantis-thread-report.md",
335+
messageContent: "Mantis thread attachment reply",
336+
scenarioTitle: "Discord thread reply preserves filePath attachment",
337+
status: "fail",
338+
threadName: "mantis-thread-filepath-1234",
339+
});
340+
341+
expect(html).toContain("Attachment missing");
342+
expect(html).toContain("No attachments on the SUT thread reply");
343+
expect(html).toContain("mantis-thread-report.md");
344+
});
345+
326346
it("waits for the Discord account to become connected, not just running", async () => {
327347
vi.useFakeTimers();
328348
try {

0 commit comments

Comments
 (0)