From 3260a0592ede1b36e50f70a588f9bd23d8f51d00 Mon Sep 17 00:00:00 2001 From: zhyongrui Date: Tue, 17 Mar 2026 09:02:44 +0000 Subject: [PATCH 1/2] feat: implement issue #130 --- .../parallels-discord-roundtrip/SKILL.md | 59 ------------------- docs/openclawcode/run-json-contract.md | 1 + src/commands/openclawcode.test.ts | 2 + src/commands/openclawcode.ts | 2 + 4 files changed, 5 insertions(+), 59 deletions(-) delete mode 100644 .agents/skills/parallels-discord-roundtrip/SKILL.md diff --git a/.agents/skills/parallels-discord-roundtrip/SKILL.md b/.agents/skills/parallels-discord-roundtrip/SKILL.md deleted file mode 100644 index 8fda0da1a2..0000000000 --- a/.agents/skills/parallels-discord-roundtrip/SKILL.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -name: parallels-discord-roundtrip -description: Run the macOS Parallels smoke harness with Discord end-to-end roundtrip verification, including guest send, host verification, host reply, and guest readback. ---- - -# Parallels Discord Roundtrip - -Use when macOS Parallels smoke must prove Discord two-way delivery end to end. - -## Goal - -Cover: - -- install on fresh macOS snapshot -- onboard + gateway health -- guest `message send` to Discord -- host sees that message on Discord -- host posts a new Discord message -- guest `message read` sees that new message - -## Inputs - -- host env var with Discord bot token -- Discord guild ID -- Discord channel ID -- `OPENAI_API_KEY` - -## Preferred run - -```bash -export OPENCLAW_PARALLELS_DISCORD_TOKEN="$( - ssh peters-mac-studio-1 'jq -r ".channels.discord.token" ~/.openclaw/openclaw.json' | tr -d '\n' -)" - -pnpm test:parallels:macos \ - --discord-token-env OPENCLAW_PARALLELS_DISCORD_TOKEN \ - --discord-guild-id 1456350064065904867 \ - --discord-channel-id 1456744319972282449 \ - --json -``` - -## Notes - -- Snapshot target: closest to `macOS 26.3.1 fresh`. -- Harness configures Discord inside the guest; no checked-in token/config. -- Use the `openclaw` wrapper for guest `message send/read`; `node openclaw.mjs message ...` does not expose the lazy message subcommands the same way. -- Write `channels.discord.guilds` in one JSON object (`--strict-json`), not dotted `config set channels.discord.guilds....` paths; numeric snowflakes get treated like array indexes. -- Avoid `prlctl enter` / expect for long Discord setup scripts; it line-wraps/corrupts long commands. Use `prlctl exec --current-user /bin/sh -lc ...` for the Discord config phase. -- Harness cleanup deletes the temporary Discord smoke messages at exit. -- Per-phase logs: `/tmp/openclaw-parallels-smoke.*` -- Machine summary: pass `--json` -- If roundtrip flakes, inspect `fresh.discord-roundtrip.log` and `discord-last-readback.json` in the run dir first. - -## Pass criteria - -- fresh lane or upgrade lane requested passes -- summary reports `discord=pass` for that lane -- guest outbound nonce appears in channel history -- host inbound nonce appears in `openclaw message read` output diff --git a/docs/openclawcode/run-json-contract.md b/docs/openclawcode/run-json-contract.md index b6a748a79f..959c604f0f 100644 --- a/docs/openclawcode/run-json-contract.md +++ b/docs/openclawcode/run-json-contract.md @@ -79,6 +79,7 @@ those nested objects. ### Change And Scope Signals - `buildSummary` +- `buildHasSignals` - `buildSummaryPresent` - `changedFiles` - `changedFilesPresent` diff --git a/src/commands/openclawcode.test.ts b/src/commands/openclawcode.test.ts index d37ef22985..3e951e5578 100644 --- a/src/commands/openclawcode.test.ts +++ b/src/commands/openclawcode.test.ts @@ -186,6 +186,7 @@ describe("openclawCodeRunCommand", () => { expect(payload.buildAttemptCount).toBe(1); expect(payload.verificationAttemptCount).toBe(1); expect(payload.buildSummary).toBe("Updated JSON output"); + expect(payload.buildHasSignals).toBe(true); expect(payload.buildSummaryPresent).toBe(true); expect(payload.changedFiles).toEqual([ "src/openclawcode/app/run-issue.ts", @@ -465,6 +466,7 @@ describe("openclawCodeRunCommand", () => { expect(payload.buildAttemptCount).toBe(1); expect(payload.verificationAttemptCount).toBe(1); expect(payload.buildSummary).toBeNull(); + expect(payload.buildHasSignals).toBe(false); expect(payload.buildSummaryPresent).toBe(false); expect(payload.changedFiles).toEqual([]); expect(payload.changedFilesPresent).toBe(false); diff --git a/src/commands/openclawcode.ts b/src/commands/openclawcode.ts index bb3ccc5c35..1f125066c8 100644 --- a/src/commands/openclawcode.ts +++ b/src/commands/openclawcode.ts @@ -1247,6 +1247,8 @@ function toWorkflowRunJson(run: WorkflowRun) { buildAttemptCount: run.attempts?.building ?? null, verificationAttemptCount: run.attempts?.verifying ?? null, buildSummary: run.buildResult?.summary ?? null, + buildHasSignals: + run.buildResult?.summary === true || (run.buildResult?.summary?.length ?? 0) > 0, buildSummaryPresent: (run.buildResult?.summary?.length ?? 0) > 0, changedFiles: run.buildResult?.changedFiles ?? [], changedFilesPresent: (run.buildResult?.changedFiles.length ?? 0) > 0, From 10a059fc66add90cb2f45eafa43f247323441723 Mon Sep 17 00:00:00 2001 From: zhyongrui Date: Tue, 17 Mar 2026 09:09:25 +0000 Subject: [PATCH 2/2] feat: implement issue #130 --- src/commands/openclawcode.test.ts | 38 +++++++++++++++++++++++++++++++ src/commands/openclawcode.ts | 21 ++++++++++++++--- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/commands/openclawcode.test.ts b/src/commands/openclawcode.test.ts index 3e951e5578..bb87e8125a 100644 --- a/src/commands/openclawcode.test.ts +++ b/src/commands/openclawcode.test.ts @@ -441,6 +441,44 @@ describe("openclawCodeRunCommand", () => { ); }); + it("treats a boolean build summary as a present build signal in JSON output", async () => { + const run = createRun(); + mocks.runIssueWorkflow.mockResolvedValue( + createRun({ + buildResult: { + ...run.buildResult!, + summary: true as never, + }, + }), + ); + + await openclawCodeRunCommand({ issue: "2", repoRoot: "/repo", json: true }, runtime); + + const payload = JSON.parse(runtime.log.mock.calls[0]?.[0] ?? "null"); + expect(payload.buildSummary).toBe(true); + expect(payload.buildHasSignals).toBe(true); + expect(payload.buildSummaryPresent).toBe(true); + }); + + it("treats a non-empty build summary entry list as present build signals in JSON output", async () => { + const run = createRun(); + mocks.runIssueWorkflow.mockResolvedValue( + createRun({ + buildResult: { + ...run.buildResult!, + summary: ["lint", "tests"] as never, + }, + }), + ); + + await openclawCodeRunCommand({ issue: "2", repoRoot: "/repo", json: true }, runtime); + + const payload = JSON.parse(runtime.log.mock.calls[0]?.[0] ?? "null"); + expect(payload.buildSummary).toEqual(["lint", "tests"]); + expect(payload.buildHasSignals).toBe(true); + expect(payload.buildSummaryPresent).toBe(true); + }); + it("prints empty top-level scope fields and blocks auto-merge when workflow data is missing", async () => { mocks.runIssueWorkflow.mockResolvedValue( createRun({ diff --git a/src/commands/openclawcode.ts b/src/commands/openclawcode.ts index 1f125066c8..0f7b91a4ac 100644 --- a/src/commands/openclawcode.ts +++ b/src/commands/openclawcode.ts @@ -793,6 +793,22 @@ function resolveDraftPullRequestDisposition(run: WorkflowRun): { }; } +function resolveBuildHasSignals(summary: unknown): boolean { + if (summary === true) { + return true; + } + if (typeof summary === "string") { + return summary.trim().length > 0; + } + if (Array.isArray(summary)) { + return summary.length > 0; + } + if (summary != null && typeof summary === "object") { + return Object.keys(summary).length > 0; + } + return false; +} + function resolveChangedFileListStable(run: WorkflowRun): boolean { const changedFiles = run.buildResult?.changedFiles; if (changedFiles == null) { @@ -1247,9 +1263,8 @@ function toWorkflowRunJson(run: WorkflowRun) { buildAttemptCount: run.attempts?.building ?? null, verificationAttemptCount: run.attempts?.verifying ?? null, buildSummary: run.buildResult?.summary ?? null, - buildHasSignals: - run.buildResult?.summary === true || (run.buildResult?.summary?.length ?? 0) > 0, - buildSummaryPresent: (run.buildResult?.summary?.length ?? 0) > 0, + buildHasSignals: resolveBuildHasSignals(run.buildResult?.summary), + buildSummaryPresent: resolveBuildHasSignals(run.buildResult?.summary), changedFiles: run.buildResult?.changedFiles ?? [], changedFilesPresent: (run.buildResult?.changedFiles.length ?? 0) > 0, changedFileListStable: resolveChangedFileListStable(run),