1// @effect-diagnostics nodeBuiltinImport:off - verifies native task caching in isolated subprocesses.
2import { describe, expect, it } from "vite-plus/test";
3import * as NodeChildProcess from "node:child_process";
4import * as NodeFSP from "node:fs/promises";
5import * as NodeOS from "node:os";
6import * as NodePath from "node:path";
7import * as NodeURL from "node:url";
8
9import { cachedTestTasks, matchesTestPath } from "./cached-tests.ts";
10
11describe("cachedTestTasks", () => {
12 it("selects every test for a checkout-root path", () => {
13 const root = NodePath.resolve(import.meta.dirname, "../..");
14 for (const path of [".", root]) {
15 expect(
16 matchesTestPath("apps/server/src/auth/utils.test.ts", [
17 NodePath.relative(root, NodePath.resolve(root, path)),
18 ]),
19 ).toBe(true);
20 }
21 expect(matchesTestPath("apps/server/src/auth/utils.test.ts", ["apps/server"])).toBe(true);
22 expect(matchesTestPath("apps/server-other/a.test.ts", ["apps/server"])).toBe(false);
23 });
24 it("keeps a file's cache task stable when unrelated tests are added", () => {
25 const first = cachedTestTasks(JSON.stringify({ files: ["src/b.test.ts"], groupSize: 1 }));
26 const second = cachedTestTasks(
27 JSON.stringify({ files: ["src/a.test.ts", "src/b.test.ts"], groupSize: 1 }),
28 );
29 const key = Object.keys(first).find((key) => key !== "test-cache")!;
30 expect(second[key]).toEqual(first[key]);
31 });
32
33 it("keeps shell metacharacters in filenames out of the command", () => {
34 const tasks = cachedTestTasks(
35 JSON.stringify({ files: ["src/a'$(touch injected).test.ts"], groupSize: 1 }),
36 );
37 const task = Object.entries(tasks).find(([key]) => key !== "test-cache")![1];
38 expect(task.command).toBe("vp test run 'src/a'\"'\"'$(touch injected).test.ts'");
39 });
40
41 it("rejects paths outside the checkout and empty groups", () => {
42 for (const request of [
43 { files: ["../elsewhere.test.ts"], groupSize: 1 },
44 { files: ["/absolute.test.ts"], groupSize: 1 },
45 { files: ["src/a.test.ts"], groupSize: 0 },
46 ]) {
47 expect(() => cachedTestTasks(JSON.stringify(request))).toThrow();
48 }
49 });
50
51 it("accepts spec files discovered by Vitest", () => {
52 const tasks = cachedTestTasks(JSON.stringify({ files: ["src/example.spec.ts"], groupSize: 1 }));
53 expect(
54 Object.values(tasks).some((task) => task.command === "vp test run 'src/example.spec.ts'"),
55 ).toBe(true);
56 });
57
58 it("does not add tasks without an explicit request", () => {
59 expect(cachedTestTasks(undefined)).toEqual({});
60 });
61
62 it("reuses individual results, invalidates filesystem inputs, and never caches failures", async () => {
63 const directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "cached-tests-"));
64 const root = NodePath.resolve(import.meta.dirname, "../..");
65 const run = (files = ["a.test.ts", "b.test.ts"], uncached: Array<string> = []) =>
66 new Promise<number | null>((resolve, reject) => {
67 const child = NodeChildProcess.spawn(
68 process.execPath,
69 [NodePath.join(root, "node_modules/vite-plus/dist/bin.js"), "run", "test-cache"],
70 {
71 cwd: directory,
72 env: {
73 ...process.env,
74 NODE_OPTIONS: "",
75 VP_REMOTE_CACHE: "off",
76 T3_CACHED_TESTS: JSON.stringify({ files, groupSize: 1, uncached }),
77 VITE_CACHE_PATH: NodePath.join(directory, "cache"),
78 PATH: `${NodePath.join(root, "node_modules/.bin")}${NodePath.delimiter}${process.env.PATH}`,
79 },
80 stdio: "ignore",
81 },
82 );
83 child.once("error", reject);
84 child.once("close", resolve);
85 });
86 const summary = async () => {
87 const value: { tasks: Array<{ result: Record<string, unknown> }> } = JSON.parse(
88 await NodeFSP.readFile(NodePath.join(directory, "cache/v18/last-summary.json"), "utf8"),
89 );
90 return value.tasks.filter((task) => "CacheHit" in task.result).length;
91 };
92 try {
93 await NodeFSP.mkdir(NodePath.join(directory, "node_modules"));
94 await NodeFSP.symlink(
95 NodePath.join(root, "node_modules/vite-plus"),
96 NodePath.join(directory, "node_modules/vite-plus"),
97 "dir",
98 );
99 await NodeFSP.writeFile(NodePath.join(directory, "package.json"), '{"type":"module"}');
100 await NodeFSP.writeFile(
101 NodePath.join(directory, "vite.config.ts"),
102 `import {cachedTestTasks} from ${JSON.stringify(NodeURL.pathToFileURL(NodePath.join(root, "scripts/lib/cached-tests.ts")).href)}; export default {run:{tasks:cachedTestTasks(process.env.T3_CACHED_TESTS, import.meta.dirname)},test:{include:["*.test.ts"]}};`,
103 );
104 await NodeFSP.writeFile(
105 NodePath.join(directory, "a.test.ts"),
106 'import {test,expect} from "vite-plus/test"; import {readFileSync} from "node:fs"; test("fixture",()=>expect(readFileSync("fixture.txt","utf8")).toMatch(/^hello/));',
107 );
108 await NodeFSP.writeFile(
109 NodePath.join(directory, "b.test.ts"),
110 'import {test,expect} from "vite-plus/test"; test("independent",()=>expect(1).toBe(1));',
111 );
112 await NodeFSP.writeFile(NodePath.join(directory, "fixture.txt"), "hello");
113 expect(await run()).toBe(0);
114 expect(await run()).toBe(0);
115 expect(await summary()).toBe(2);
116 expect(await run(["b.test.ts"])).toBe(0);
117 expect(await summary()).toBe(1);
118 expect(await run()).toBe(0);
119 expect(await summary()).toBe(2);
120 await NodeFSP.writeFile(NodePath.join(directory, "fixture.txt"), "hello changed");
121 expect(await run()).toBe(0);
122 expect(await summary()).toBe(1);
123 await NodeFSP.writeFile(NodePath.join(directory, "fixture.txt"), "broken");
124 expect(await run()).toBe(1);
125 expect(await run()).toBe(1);
126 expect(await summary()).toBe(1);
127 await NodeFSP.writeFile(NodePath.join(directory, "fixture.txt"), "hello again");
128 await NodeFSP.writeFile(
129 NodePath.join(directory, "b.test.ts"),
130 'import {test} from "vite-plus/test";import {writeFileSync} from "node:fs";test("report",()=>writeFileSync("report.json","{}"));',
131 );
132 expect(await run(undefined, ["b.test.ts"])).toBe(0);
133 await NodeFSP.unlink(NodePath.join(directory, "report.json"));
134 expect(await run(undefined, ["b.test.ts"])).toBe(0);
135 expect(await NodeFSP.readFile(NodePath.join(directory, "report.json"), "utf8")).toBe("{}");
136 } finally {
137 await NodeFSP.rm(directory, { recursive: true, force: true });
138 }
139 });
140});