1import { describe, expect, vi } from "vite-plus/test";
2import { it } from "@effect/vitest";
3import {
4 EnvironmentId,
5 ThreadId,
6 type ScriptProgressEvent,
7 type ScriptProgressStream,
8} from "@t3tools/contracts";
9import * as Effect from "effect/Effect";
10import * as Layer from "effect/Layer";
11import * as Stream from "effect/Stream";
12import { Atom, AtomRegistry } from "effect/reactivity";
13
14import { EnvironmentRegistry } from "../connection/registry.ts";
15import {
16 applyScriptProgressEvent,
17 createScriptProgressEnvironmentAtoms,
18} from "./scriptProgress.ts";
19
20const source = vi.hoisted(() => ({ stream: undefined as unknown }));
21vi.mock("../rpc/client.ts", async (original) => ({
22 ...(await original<typeof import("../rpc/client.ts")>()),
23 subscribe: () => source.stream,
24}));
25
26const build: ScriptProgressStream = {
27 id: "build",
28 pid: 42,
29 label: "build.ts",
30 status: "connected",
31 summary: [{ text: "Building", value: 4, total: 10, detail: "4/10" }],
32};
33
34describe("script progress summaries", () => {
35 it.effect("preserves every protocol event when RPC delivers a chunk in one atom update", () =>
36 Effect.scoped(
37 Effect.gen(function* () {
38 const zero = [{ text: "Building", value: 0, total: 10, detail: "0/10" }];
39 const events: ScriptProgressEvent[] = [
40 { type: "snapshot", streams: [] },
41 { type: "upsert", stream: build },
42 {
43 type: "progress",
44 id: build.id,
45 event: [-1, { k: 1, t: "Building", v: 4, e: 10 }, [1]],
46 summary: build.summary,
47 },
48 { type: "progress", id: build.id, event: [0, { k: 1, v: 0 }], summary: zero },
49 ];
50 source.stream = Stream.fromArray(events);
51 const runtime = Atom.runtime(
52 Layer.succeed(EnvironmentRegistry, {
53 followStream: (_id, stream) => stream,
54 } as EnvironmentRegistry["Service"]),
55 );
56 const progress = createScriptProgressEnvironmentAtoms(runtime);
57 const registry = AtomRegistry.make();
58 yield* Effect.addFinalizer(() => Effect.sync(() => registry.dispose()));
59 const result = yield* AtomRegistry.getResult(
60 registry,
61 progress.watch({
62 environmentId: EnvironmentId.make("chunk-proof"),
63 input: { threadId: ThreadId.make("thread") },
64 }),
65 );
66 expect(result.events).toEqual(events);
67 expect(result.streams).toEqual([{ ...build, summary: zero }]);
68 }),
69 ),
70 );
71
72 it("replaces a prior connection's streams with the reconnect snapshot", () => {
73 expect(applyScriptProgressEvent([build], { type: "snapshot", streams: [] })).toEqual([]);
74 });
75
76 it("keeps independent scripts and accepts progress resets to zero", () => {
77 const other = { ...build, id: "other", pid: 43 };
78 const summaries = applyScriptProgressEvent([build], { type: "upsert", stream: other });
79 const zero = [{ text: "Building", value: 0, total: 10, detail: "0/10" }];
80
81 expect(
82 applyScriptProgressEvent(summaries, {
83 type: "progress",
84 id: build.id,
85 event: [0, { k: 1, v: 0 }],
86 summary: zero,
87 }),
88 ).toEqual([{ ...build, summary: zero }, other]);
89 });
90
91 it("preserves the last summary when disconnected without reporting success", () => {
92 const disconnected = { ...build, status: "disconnected" as const };
93 const summaries = applyScriptProgressEvent([build], {
94 type: "upsert",
95 stream: disconnected,
96 });
97
98 expect(summaries).toEqual([disconnected]);
99 expect(applyScriptProgressEvent(summaries, { type: "remove", id: build.id })).toEqual([]);
100 });
101});