summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--backend-handoff.md18
-rw-r--r--notes/text-file-embeds-implementation.md60
-rw-r--r--package.json7
-rw-r--r--scripts/text-file-environment.ts19
-rw-r--r--scripts/text-file-live.test.ts20
-rw-r--r--src/features/chat/logic/tool-summary.test.ts15
-rw-r--r--src/features/chat/logic/tool-summary.ts19
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]";
}