summaryrefslogtreecommitdiffhomepage
path: root/scripts/text-file-live.test.ts
blob: 33056163fdd5f2630c3ae9bd34f18bc4721eb741 (plain)
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();
    }
  });
});