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
|
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 { fireEvent, render, screen } from "@testing-library/svelte";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { createTextFileClient, Markdown } from "../src/features/markdown";
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.
// 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;
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, fetch);
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, fetch);
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();
}
});
});
|