From b666d77df8cf4da6ae40fb5637268926f02cc44d Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Sat, 24 Oct 2026 18:47:01 +0900 Subject: test: run frontend verification natively under Bun Make Bun the explicit Vitest runtime. Detect cyclic tool input before slow native pretty serialization, preserve shared references, and use native abort signals in the live jsdom probe. 2587 tests repeatedly green and2 real HTTP/UI tests pass; Bun-run typecheck, lint and production build pass without NODE_OPTIONS. --- backend-handoff.md | 18 +++++++++ notes/text-file-embeds-implementation.md | 60 +++++++++++++++++++++++++++- package.json | 7 ++-- scripts/text-file-environment.ts | 19 +++++++++ scripts/text-file-live.test.ts | 20 ++-------- src/features/chat/logic/tool-summary.test.ts | 15 +++++++ src/features/chat/logic/tool-summary.ts | 19 ++++++++- 7 files changed, 136 insertions(+), 22 deletions(-) create mode 100644 scripts/text-file-environment.ts 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 = { 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]"; } -- cgit v1.2.3