1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
|
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;
cwd: string;
conversationId: string;
remoteConversationId: string;
}
// 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
// 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;
let fixture: Fixture;
beforeAll(async () => {
fixture = await new Promise<Fixture>((resolve, reject) => {
child = spawn("bun", ["packages/transport-http/src/text-file-probe.ts"], {
cwd: resolvePath(process.cwd(), "../backend"),
stdio: ["ignore", "pipe", "pipe"],
});
const timer = setTimeout(
() => reject(new Error("Text-file fixture startup timed out")),
15000,
);
let stderr = "";
child.stderr?.on("data", (chunk) => {
stderr += String(chunk);
});
child.once("error", reject);
child.once("exit", (code) => {
clearTimeout(timer);
reject(new Error(`Text-file fixture exited (${code}): ${stderr}`));
});
if (!child.stdout) {
reject(new Error("Missing fixture stdout"));
return;
}
const lines = createInterface({ input: child.stdout });
lines.once("line", (line) => {
clearTimeout(timer);
try {
resolve(JSON.parse(line) as Fixture);
} catch (error) {
reject(error);
}
});
});
}, 20000);
afterAll(async () => {
if (child && child.exitCode === null) {
await new Promise<void>((resolve) => {
const timer = setTimeout(() => child.kill("SIGKILL"), 3000);
child.once("exit", () => {
clearTimeout(timer);
resolve();
});
child.kill("SIGTERM");
});
}
});
it("renders only the saved reference, previews/expands Markdown, and rereads changed/deleted files on refresh and remount", async () => {
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);
let view = render(Markdown, { text: reply, textFiles: client });
try {
await screen.findByRole("heading", { name: "Saved step 1" });
expect(screen.getByRole("heading", { name: "Saved step 20" })).toBeInTheDocument();
expect(screen.queryByRole("heading", { name: "Saved step 10" })).not.toBeInTheDocument();
await fireEvent.click(screen.getByRole("button", { name: "Expand document" }));
expect(screen.getByRole("heading", { name: "Saved step 10" })).toBeInTheDocument();
expect(reply).not.toContain(saved);
await writeFile(fixture.path, "# Updated on disk\n\n**Fresh content**");
await fireEvent.click(screen.getByRole("button", { name: "Refresh" }));
await screen.findByRole("heading", { name: "Updated on disk" });
expect(view.container.querySelector("strong")?.textContent).toBe("Fresh content");
view.unmount();
view = render(Markdown, { text: reply, textFiles: client });
await screen.findByRole("heading", { name: "Updated on disk" });
await rm(fixture.path);
await fireEvent.click(screen.getByRole("button", { name: "Refresh" }));
expect(await screen.findByRole("alert")).toHaveTextContent("Text file not found");
await writeFile(fixture.path, "# Recreated plan");
await fireEvent.click(screen.getByRole("button", { name: "Retry" }));
await screen.findByRole("heading", { name: "Recreated plan" });
} finally {
view.unmount();
client.dispose();
}
});
it("does not read the same local path for an authoritative remote conversation", async () => {
const client = createTextFileClient(fixture.url, fixture.remoteConversationId, networkFetch);
const view = render(Markdown, {
text: `[Remote](text-file:${encodeURI(fixture.path)})`,
textFiles: client,
});
try {
expect(await screen.findByRole("alert")).toHaveTextContent(
"not available for remote computers",
);
expect(screen.queryByRole("heading", { name: "Recreated plan" })).not.toBeInTheDocument();
} finally {
view.unmount();
client.dispose();
}
});
});
|