1// @effect-diagnostics nodeBuiltinImport:off
2import * as NodeFS from "node:fs";
3import * as NodeOS from "node:os";
4import * as NodePath from "node:path";
5import * as NodeChildProcess from "node:child_process";
6import { OrchestrationWorkflowFileError } from "@t3tools/contracts";
7import * as HostProcess from "@t3tools/shared/HostProcess";
8import { it as effectIt } from "@effect/vitest";
9import * as Effect from "effect/Effect";
10import { afterAll, assert, describe } from "vite-plus/test";
11import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
12import { readWorkflowScript } from "./workflowScriptQuery.ts";
13
14const root = NodePath.join(NodeOS.homedir(), ".claude", "projects", "__wf_script_test__");
15NodeFS.mkdirSync(root, { recursive: true });
16const scriptPath = NodePath.join(root, "run.js");
17NodeFS.writeFileSync(scriptPath, "export const meta = {};\n");
18const outside = NodePath.join(NodeOS.tmpdir(), "wf-outside.js");
19NodeFS.writeFileSync(outside, "evil\n");
20const link = NodePath.join(root, "sneaky.js");
21// Planted only where the host allows it; the escape test is skipped
22// otherwise rather than passing vacuously on "not-found".
23if (symlinksSupported) {
24 NodeFS.rmSync(link, { force: true });
25 NodeFS.symlinkSync(outside, link);
26 if (!NodeFS.lstatSync(link).isSymbolicLink()) {
27 throw new Error("test setup: sneaky.js must be a symlink");
28 }
29}
30
31afterAll(() => {
32 NodeFS.rmSync(root, { recursive: true, force: true });
33 NodeFS.rmSync(outside, { force: true });
34});
35
36describe("workflow script containment", () => {
37 effectIt.effect.skipIf(HostProcess.Platform.defaultValue() === "win32")(
38 "rejects a FIFO without waiting for a writer",
39 () =>
40 Effect.gen(function* () {
41 const fifo = NodePath.join(root, "pipe.js");
42 NodeChildProcess.execFileSync("mkfifo", [fifo]);
43 const error = yield* Effect.flip(readWorkflowScript({ scriptPath: fifo }));
44 assert.equal(error.reason, "not-regular-file");
45 }),
46 );
47
48 effectIt.effect("serves a real script under the projects root", () =>
49 Effect.gen(function* () {
50 const result = yield* readWorkflowScript({ scriptPath });
51 assert.include(result.contents, "export const meta");
52 assert.equal(result.truncated, false);
53 }),
54 );
55
56 effectIt.effect("rejects relative and non-js paths", () =>
57 Effect.gen(function* () {
58 const relative = yield* Effect.exit(readWorkflowScript({ scriptPath: "run.js" }));
59 assert.equal(relative._tag, "Failure");
60 const nonJs = yield* Effect.exit(
61 readWorkflowScript({ scriptPath: scriptPath.replace(".js", ".ts") }),
62 );
63 assert.equal(nonJs._tag, "Failure");
64 const error = yield* Effect.flip(readWorkflowScript({ scriptPath: "run.js" }));
65 assert.equal(error._tag, "OrchestrationGetWorkflowScriptError");
66 assert.equal(error.scriptPath, "run.js");
67 assert.equal(error.reason, "invalid-path");
68 assert.equal(error.message, "Workflow scripts must be absolute .js paths.");
69 assert.instanceOf(error.cause, OrchestrationWorkflowFileError);
70 }),
71 );
72
73 effectIt.effect.skipIf(!symlinksSupported)(
74 "rejects paths outside the root and symlink escapes",
75 () =>
76 Effect.gen(function* () {
77 const escaped = yield* Effect.exit(readWorkflowScript({ scriptPath: outside }));
78 assert.equal(escaped._tag, "Failure");
79 // A symlink INSIDE the root pointing outside must fail specifically on
80 // realpath re-containment — a "not-found" would mean the link was
81 // never exercised and the assertion proves nothing.
82 const sneaky = yield* Effect.exit(
83 readWorkflowScript({ scriptPath: link }).pipe(
84 Effect.flip,
85 Effect.map((error) => error.reason),
86 ),
87 );
88 assert.equal(sneaky._tag, "Success");
89 if (sneaky._tag === "Success") {
90 assert.equal(sneaky.value, "outside-root");
91 }
92 }),
93 );
94});