1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { expect, it } from "@effect/vitest";
3import { DEFAULT_TERMINAL_ID, ThreadId } from "@t3tools/contracts";
4import * as ScriptProgress from "@t3tools/provider-core/server/ScriptProgress";
5import * as PtyAdapter from "@t3tools/shared/PtyAdapter";
6import * as Deferred from "effect/Deferred";
7import * as Effect from "effect/Effect";
8import * as FileSystem from "effect/FileSystem";
9import * as Layer from "effect/Layer";
10import * as Path from "effect/Path";
11import * as Stream from "effect/Stream";
12import * as ProcessRunner from "../processRunner.ts";
13import * as Manager from "./Manager.ts";
14import * as NodePtyAdapter from "./NodePtyAdapter.ts";
15
16it.live(
17 "injects progress after terminal filtering and keeps concurrent shells and their children isolated",
18 () =>
19 Effect.gen(function* () {
20 const fs = yield* FileSystem.FileSystem;
21 const path = yield* Path.Path;
22 const directory = yield* fs.makeTempDirectoryScoped({ prefix: "t3-progress-terminal-" });
23 const preload = path.join(directory, "progress preload.mjs");
24 const userPreload = path.join(directory, "user.mjs");
25 const script = path.join(directory, "probe.mjs");
26 yield* fs.writeFileString(preload, "globalThis.progressPreloaded = true;");
27 yield* fs.writeFileString(userPreload, "globalThis.userPreloaded = true;");
28 yield* fs.writeFileString(
29 script,
30 `
31 import { execFileSync } from 'node:child_process';
32 const child = execFileSync(process.execPath, ['--input-type=module', '-e',
33 'console.log(JSON.stringify({ token: process.env.T3_SCRIPT_PROGRESS_TOKEN, nativeThread: process.env.CODEX_THREAD_ID, progress: globalThis.progressPreloaded, user: globalThis.userPreloaded }))'
34 ], { encoding: 'utf8' });
35 console.log('PROGRESS_RESULT:' + child.trim());
36 `,
37 );
38 const originalOptions = process.env.NODE_OPTIONS;
39 const completed = new Map([
40 ["progress-terminal-one", yield* Deferred.make<string>()],
41 ["progress-terminal-two", yield* Deferred.make<string>()],
42 ]);
43 const output = new Map<string, string>();
44 const manager = yield* Manager.makeWithOptions({
45 logsDir: path.join(directory, "logs"),
46 ptyAdapter: yield* PtyAdapter.PtyAdapter,
47 shellResolver: () => "/bin/sh",
48 env: { ...process.env, NODE_OPTIONS: `--import=${JSON.stringify(userPreload)}` },
49 subprocessInspector: () =>
50 Effect.succeed({ hasRunningSubprocess: false, childCommand: null, processIds: [] }),
51 }).pipe(
52 Effect.provideService(ScriptProgress.ScriptProgress, {
53 environmentForThread: (threadId) =>
54 Effect.succeed({
55 NODE_OPTIONS: `--import=${JSON.stringify(preload)}`,
56 T3_SCRIPT_PROGRESS_ENDPOINT: "http://127.0.0.1:1/progress",
57 T3_SCRIPT_PROGRESS_TOKEN: threadId,
58 }),
59 subscribe: () => Stream.empty,
60 }),
61 );
62 const unsubscribe = yield* manager.subscribe((event) => {
63 if (event.type !== "output") return Effect.void;
64 const data = (output.get(event.threadId) ?? "") + event.data;
65 output.set(event.threadId, data);
66 const result = /PROGRESS_RESULT:([^\r\n]+)[\r\n]/.exec(data);
67 const done = completed.get(event.threadId);
68 return result && done
69 ? Deferred.succeed(done, result[1]!).pipe(Effect.asVoid)
70 : Effect.void;
71 });
72 yield* Effect.addFinalizer(() => Effect.sync(unsubscribe));
73 yield* Effect.all(
74 [...completed].map(([threadId, done]) =>
75 Effect.gen(function* () {
76 yield* manager.open({
77 threadId,
78 terminalId: DEFAULT_TERMINAL_ID,
79 cwd: directory,
80 cols: 120,
81 rows: 24,
82 env: { CODEX_THREAD_ID: "native-child" },
83 });
84 const quote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
85 yield* manager.write({
86 threadId,
87 terminalId: DEFAULT_TERMINAL_ID,
88 data: `${quote(process.execPath)} ${quote(script)}\n`,
89 });
90 const result = JSON.parse(yield* Deferred.await(done));
91 expect(result).toEqual({
92 token: ThreadId.make(threadId),
93 nativeThread: "native-child",
94 progress: true,
95 user: true,
96 });
97 yield* manager.close({ threadId });
98 }),
99 ),
100 { concurrency: "unbounded" },
101 );
102 expect(process.env.NODE_OPTIONS).toBe(originalOptions);
103 }).pipe(
104 Effect.timeout("15 seconds"),
105 Effect.scoped,
106 Effect.provide(
107 Layer.mergeAll(
108 NodeServices.layer,
109 NodePtyAdapter.layer.pipe(Layer.provide(NodeServices.layer)),
110 ProcessRunner.layer.pipe(Layer.provide(NodeServices.layer)),
111 ),
112 ),
113 ),
114);