diff options
| -rw-r--r-- | backend-handoff.md | 18 | ||||
| -rw-r--r-- | notes/text-file-embeds-implementation.md | 60 | ||||
| -rw-r--r-- | package.json | 7 | ||||
| -rw-r--r-- | scripts/text-file-environment.ts | 19 | ||||
| -rw-r--r-- | scripts/text-file-live.test.ts | 20 | ||||
| -rw-r--r-- | src/features/chat/logic/tool-summary.test.ts | 15 | ||||
| -rw-r--r-- | src/features/chat/logic/tool-summary.ts | 19 |
7 files changed, 136 insertions, 22 deletions
diff --git a/backend-handoff.md b/backend-handoff.md index acae2f6..d1d0756 100644 --- a/backend-handoff.md +++ b/backend-handoff.md @@ -677,3 +677,21 @@ green; standard external-Claude bundle has existing missing @dispatch/kernel iss No merge/push/deployment/shared restart. Full report: `notes/text-file-embeds-implementation.md`. Human graphical desktop/mobile check remains; no cross-repo contract asks open for this slice. + +### Bun-native verification correction (2026-10-24) + +Supersedes the Node-backed verification paragraph above: both repository test +scripts now explicitly launch Vitest with Bun (`bun --bun .../vitest.mjs`). +`bun run test` passes under Bun with no NODE_OPTIONS: frontend2587 passed/2 +opt-in skipped repeatedly; backend4278 passed/9 existing SSH skips twice; +native Bun tests190 passed. `bun run test:text-file-live` passes2 against the +real ephemeral HTTP transport. Bun-run typecheck/lint/build pass (FE309 files, +backend754); existing build advisories/external-Claude wrapper limitation remain. + +Corrected pre-existing Bun test assumptions (Bun.serve spy instead of global +replacement, directory ordering, homedir caching). Added prompt cycle detection +in tool-call summary serialization plus shared-reference regression test; the +Bun timeout is fixed, not increased. Live probe uses a native-signal jsdom +environment instead of Node-only utility APIs. No production wire/feature +semantics changed, no subagents/deployment/merge/push. Full final verification +is in the Bun-native follow-up of notes/text-file-embeds-implementation.md. diff --git a/notes/text-file-embeds-implementation.md b/notes/text-file-embeds-implementation.md index 349f67e..2f23a41 100644 --- a/notes/text-file-embeds-implementation.md +++ b/notes/text-file-embeds-implementation.md @@ -175,7 +175,7 @@ their existing **AS-IS bypass** contract; they do not receive appended guidance. The initial instructions in already-running/persisted conversations are not retroactively changed, and this task did not redeploy/restart the shared service. -## Verification +## Initial verification (superseded by Bun-native follow-up below) `bun install` completed inside both feature worktrees before build/testing; frontend was refreshed after backend declarations were built. All work is on @@ -234,3 +234,61 @@ commits, visually confirm block styling/responsiveness on desktop/mobile, expand and collapse a longer Markdown plan, and click a remote-image consent button. No screenshots, graphical-browser layout check, AI-provider-generated live reply, remote file support, watchers, snapshots or editor/attachment redesign are claimed. + +## Bun-native verification follow-up — 2026-10-24 + +This is the **authoritative final verification**, superseding the initial +Node-backed Vitest results above. `bun run` previously respected Vitest's +`#!/usr/bin/env node` launcher. Both repositories now explicitly execute +`bun --bun node_modules/vitest/vitest.mjs` in `test` and `test:watch`, so ordinary +`bun run test` uses Bun itself, including its workers. No `NODE_OPTIONS` is needed; +all following runs explicitly unset it. Runtime: **Bun 1.4.3**. + +Bun-native runs exposed existing test assumptions which have been corrected, not +skipped and not hidden with larger timeouts: + +- HTTP/WS listener tests now spy on **Bun.serve only** and restore it afterward, + instead of redefining the non-configurable global `Bun` object. The fallback + global stub remains only for non-Bun supplemental runners. +- The image migration test sorts a directory listing before comparing its + contents; native filesystem enumeration order is not a contract. +- The local-image home-expansion test uses a path relative to the actual OS home + into its temporary fixture; it no longer assumes mutating HOME changes Bun's + cached homedir result. No real home-directory file is created. +- The frontend cyclic tool-input fallback was genuinely slow under Bun's pretty + JSON serializer. A local ancestor-stack replacer detects cycles before native + recursion, preserving the existing fallback text. Added a regression case for + nested cycles and **shared non-cyclic references** so valid JSON isn't rejected. + The original timeout-prone test now completes promptly; no timeout was raised. +- The opt-in live UI/network test no longer depends on Node-only + `transferableAbortController`. Its scoped jsdom environment retains the + runtime's matching native AbortController/AbortSignal pair, so actual Bun fetch + and cancellation are exercised. The production adapter remains unchanged. + +Final commands/results: + +| Worktree | Command | Result | +| --- | --- | --- | +| frontend | `bun run test` | **2587 passed / 2 opt-in skipped**, repeated green (130 passed files / one skipped) | +| frontend | `bun run test:text-file-live` | **2 passed**, real Svelte + ephemeral Bun HTTP + temp disk files | +| frontend | `bun --bun run typecheck` | **0 errors / 0 warnings** | +| frontend | `bun --bun run check` | **309 files clean**, no warnings/infos | +| frontend | `bun --bun run build` | **passed**, existing initial-value/large-chunk advisories remain | +| backend | `bun run test` | **4278 passed / 9 skipped**, twice (221 passed files / one skipped) | +| backend | `bun run test:bun` | **190 passed / 0 failed** | +| backend | `bun --bun run typecheck` | **passed** | +| backend | `bun --bun run check` | **754 files clean**, no warnings/infos | +| backend | `bun build ... --compile` (server and CLI) | **both passed**, ignored worktree-local binaries | + +The nine backend skips are the existing SSH integration suite; an additional +Bun-only test previously skipped under Node now runs, hence 4278 rather than +4277 passing. The frontend count increases by one for the new shared-reference +regression test. The two frontend live tests are intentionally opt-in but were +run separately and passed. No tests were excluded to obtain green results. + +Dependencies were refreshed using `bun install` inside both feature worktrees. +The external-Claude standard-build wrapper limitation described above remains; +no external/main checkout edits, installs, service restarts, merge or push were +performed in this verification follow-up. No subagents were used. Native probe +processes and temporary fixture files were cleaned up. Browser visual validation +remains manual as documented above. diff --git a/package.json b/package.json index 28e239c..f2012ba 100644 --- a/package.json +++ b/package.json @@ -8,10 +8,11 @@ "build": "vite build", "preview": "vite preview", "typecheck": "svelte-check --tsconfig ./tsconfig.json", - "test": "vitest run --passWithNoTests", - "test:watch": "vitest", + "test": "bun --bun node_modules/vitest/vitest.mjs run --passWithNoTests", + "test:watch": "bun --bun node_modules/vitest/vitest.mjs", "check": "biome check .", - "check:fix": "biome check --write ." + "check:fix": "biome check --write .", + "test:text-file-live": "TEXT_FILE_LIVE_PROBE=1 bun --bun node_modules/vitest/vitest.mjs run scripts/text-file-live.test.ts --environment ./scripts/text-file-environment.ts" }, "dependencies": { "@dispatch/layout-contract": "file:../backend/packages/layout-contract", diff --git a/scripts/text-file-environment.ts b/scripts/text-file-environment.ts new file mode 100644 index 0000000..6507be2 --- /dev/null +++ b/scripts/text-file-environment.ts @@ -0,0 +1,19 @@ +import type { Environment } from "vitest/environments"; +import { builtinEnvironments } from "vitest/environments"; + +/** Real runtime fetch and AbortSignal must belong to the same implementation. + * jsdom supplies DOM APIs but replaces these globals with an incompatible realm. + * Keep the runtime's pair for this opt-in real-network component probe only. + */ +export default { + name: "text-file-jsdom", + transformMode: "web", + async setup(global, options) { + const controller = global.AbortController; + const signal = global.AbortSignal; + const environment = await builtinEnvironments.jsdom.setup(global, options); + global.AbortController = controller; + global.AbortSignal = signal; + return environment; + }, +} satisfies Environment; diff --git a/scripts/text-file-live.test.ts b/scripts/text-file-live.test.ts index 18d1cb6..3305616 100644 --- a/scripts/text-file-live.test.ts +++ b/scripts/text-file-live.test.ts @@ -2,24 +2,10 @@ import { type ChildProcess, spawn } from "node:child_process"; import { rm, writeFile } from "node:fs/promises"; import { resolve as resolvePath } from "node:path"; import { createInterface } from "node:readline"; -import { transferableAbortController } from "node:util"; import { fireEvent, render, screen } from "@testing-library/svelte"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { createTextFileClient, Markdown } from "../src/features/markdown"; -// Bridge only the outer fetch edge's jsdom signal to Node's native signal. -const networkFetch: typeof fetch = async (input, init) => { - const controller = transferableAbortController(); - const abort = () => controller.abort(); - init?.signal?.addEventListener("abort", abort, { once: true }); - if (init?.signal?.aborted) controller.abort(); - try { - return await fetch(input, { ...init, signal: controller.signal }); - } finally { - init?.signal?.removeEventListener("abort", abort); - } -}; - interface Fixture { url: string; path: string; @@ -30,7 +16,7 @@ interface Fixture { // Opt-in real cross-repo UI/network probe. Owns an ephemeral loopback server and // temporary files only; never connects to or restarts the deployed application. -// TEXT_FILE_LIVE_PROBE=1 bunx vitest run scripts/text-file-live.test.ts +// bun run test:text-file-live // The backend must be installed and typechecked first (workspace public dists). describe.skipIf(process.env.TEXT_FILE_LIVE_PROBE !== "1")("live saved-file chat display", () => { let child: ChildProcess; @@ -86,7 +72,7 @@ describe.skipIf(process.env.TEXT_FILE_LIVE_PROBE !== "1")("live saved-file chat const saved = Array.from({ length: 20 }, (_, i) => `## Saved step ${i + 1}`).join("\n"); await writeFile(fixture.path, saved); const reply = "I saved the implementation plan.\n\n[Plan](text-file:plan%20100%25.md)"; - const client = createTextFileClient(fixture.url, fixture.conversationId, networkFetch); + const client = createTextFileClient(fixture.url, fixture.conversationId, fetch); let view = render(Markdown, { text: reply, textFiles: client }); try { await screen.findByRole("heading", { name: "Saved step 1" }); @@ -115,7 +101,7 @@ describe.skipIf(process.env.TEXT_FILE_LIVE_PROBE !== "1")("live saved-file chat }); it("does not read the same local path for an authoritative remote conversation", async () => { - const client = createTextFileClient(fixture.url, fixture.remoteConversationId, networkFetch); + const client = createTextFileClient(fixture.url, fixture.remoteConversationId, fetch); const view = render(Markdown, { text: `[Remote](text-file:${encodeURI(fixture.path)})`, textFiles: client, diff --git a/src/features/chat/logic/tool-summary.test.ts b/src/features/chat/logic/tool-summary.test.ts index b6c7e0b..c0caa0d 100644 --- a/src/features/chat/logic/tool-summary.test.ts +++ b/src/features/chat/logic/tool-summary.test.ts @@ -311,6 +311,21 @@ describe("tool summaries", () => { }); expect(() => view("mcp", { integer: 1n })).not.toThrow(); }); + it("rejects ancestor cycles but preserves shared non-cyclic tool values", () => { + const nested: Record<string, unknown> = { name: "nested" }; + const root = { nested }; + nested.parent = root; + expect(view("mcp", { payload: root }).callDetail).toEqual({ + kind: "fields", + fields: [{ key: "payload", value: "[unserializable input]", multiline: false }], + }); + const shared = { name: "shared" }; + const payload = { first: shared, second: shared, values: [shared, shared] }; + const detail = view("mcp", { payload }).callDetail; + expect(detail.kind).toBe("fields"); + if (detail.kind !== "fields") throw new Error("Expected fields"); + expect(detail.fields[0]?.value).toBe(JSON.stringify(payload, null, 2)); + }); it("caps and whitespace-collapses header strings but retains full detail", () => { const command = ` echo\n${"x".repeat(300)}`; const card = view("run_shell", { command }); diff --git a/src/features/chat/logic/tool-summary.ts b/src/features/chat/logic/tool-summary.ts index f6d1569..2380adc 100644 --- a/src/features/chat/logic/tool-summary.ts +++ b/src/features/chat/logic/tool-summary.ts @@ -74,7 +74,24 @@ function asArgs(input: unknown): Args { function serialize(input: unknown, pretty = false): string { if (typeof input === "string") return input; try { - return JSON.stringify(input, null, pretty ? 2 : undefined) ?? String(input); + // Detect ancestor cycles before the native serializer recurses. Bun's + // pretty JSON cycle detection can otherwise consume seconds of UI time. + // Track ancestors, not every visited object: shared non-cyclic values are valid. + const ancestors: object[] = []; + return ( + JSON.stringify( + input, + function (this: unknown, _key, value: unknown) { + if (value !== null && typeof value === "object") { + while (ancestors.length && ancestors.at(-1) !== this) ancestors.pop(); + if (ancestors.includes(value)) throw new TypeError("Circular tool input"); + ancestors.push(value); + } + return value; + }, + pretty ? 2 : undefined, + ) ?? String(input) + ); } catch { return "[unserializable input]"; } |
