返回 DeepSeek-Reasonix
ci-paths.test.mjs
根目录 / scripts / ci-paths.test.mjs
1 import assert from "node:assert/strict";
2 import { execFileSync } from "node:child_process";
3 import { copyFileSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4 import os from "node:os";
5 import path from "node:path";
6 import test from "node:test";
7 import { pathToFileURL } from "node:url";
8 import { changedFiles, classifyPaths } from "./ci-paths.mjs";
9
10 test("explicit documentation skips build surfaces", () => {
11 for (const path of ["README.md", "docs/guide.md", "desktop/AGENTS.md", "desktop/README.md", "desktop/electron/README.md"]) {
12 const { flags, unknown } = classifyPaths([path]);
13 assert.equal(flags.desktop, false, path);
14 assert.equal(flags.memory, false, path);
15 assert.deepEqual(unknown, [], path);
16 }
17 });
18
19 test("frontend inputs select frontend, browser, memory and native validation", () => {
20 for (const path of ["desktop/frontend/src/components/Button.tsx", "desktop/frontend/public/help.md", "desktop/frontend/vite.config.ts", "desktop/pnpm-lock.yaml"]) {
21 const { flags } = classifyPaths([path]);
22 for (const name of ["desktop", "frontend", "browser", "memory", "native"]) assert.equal(flags[name], true, `${path}: ${name}`);
23 assert.equal(flags.memory_full, false, path);
24 }
25 });
26
27 test("App lifecycle and memory protocol inputs select the full memory screen", () => {
28 for (const path of [
29 "desktop/frontend/src/App.tsx",
30 "desktop/frontend/src/AppRuntime.tsx",
31 "desktop/frontend/src/app-runtime/AppRuntime.tsx",
32 "desktop/frontend/src/app-shell/AppShell.tsx",
33 "desktop/frontend/src/components/Transcript.tsx",
34 "desktop/frontend/src/components/TranscriptCards.tsx",
35 "desktop/frontend/src/lib/useController.ts",
36 "desktop/frontend/src/lib/useControllerProfileCommands.ts",
37 "desktop/frontend/src/lib/subscriptionScope.ts",
38 "desktop/frontend/src/lib/useNavigationSurface.ts",
39 "desktop/frontend/src/lib/navigationSurfaceTransition.ts",
40 "desktop/frontend/src/lib/keyedResource.ts",
41 "desktop/frontend/src/lib/fileResource.ts",
42 "desktop/frontend/src/lib/useWorkspaceChangesResource.ts",
43 "desktop/frontend/src/lib/mcpServerLifecycle.ts",
44 "desktop/frontend/src/lib/fileNavigationLifetime.ts",
45 "desktop/frontend/src/lib/bridge.ts",
46 "desktop/frontend/src/lib/bridgeBenchFixtures.ts",
47 "desktop/frontend/src/lib/bridgeHistoryFixtures.ts",
48 "desktop/frontend/bench/app-memory.mjs",
49 "desktop/frontend/bench/app-browser.mjs",
50 "desktop/frontend/bench/app-page-actions.mjs",
51 ]) {
52 const { flags } = classifyPaths([path]);
53 assert.equal(flags.memory, true, path);
54 assert.equal(flags.memory_full, true, path);
55 }
56 });
57
58 test("Go, Electron and packaging inputs stay on their owning surfaces", () => {
59 const uninstaller = classifyPaths(["scripts/check-windows-uninstaller.mjs"]).flags;
60 assert.equal(uninstaller.packaging, true);
61 assert.equal(uninstaller.native, true);
62 assert.equal(uninstaller.memory, false);
63 let flags = classifyPaths(["internal/control/controller.go"]).flags;
64 assert.equal(flags.code, true);
65 assert.equal(flags.desktop_go, true);
66 assert.equal(flags.frontend, false);
67 assert.equal(flags.memory, false);
68 flags = classifyPaths(["desktop/electron/src/main/window.ts"]).flags;
69 assert.equal(flags.electron, true);
70 assert.equal(flags.frontend, false);
71 flags = classifyPaths(["desktop/packaging/package.mjs"]).flags;
72 assert.equal(flags.packaging, true);
73 assert.equal(flags.browser, false);
74 });
75
76 test("Windows shell ownership paths select the full builtin package", () => {
77 for (const path of ["internal/tool/builtin/workspace.go", "internal/tool/tool.go", "internal/sandbox/sandbox.go", "internal/permission/permission.go", "internal/permissionpreset/preset.go"]) {
78 assert.equal(classifyPaths([path]).flags.windows_builtin, true, path);
79 }
80 assert.equal(classifyPaths(["internal/acp/e2e_test.go"]).flags.windows_builtin, false);
81 });
82
83 test("mixed changes cannot hide affected work and unknown paths fail closed", () => {
84 let result = classifyPaths(["README.md", "desktop/frontend/src/App.tsx"]);
85 assert.equal(result.flags.memory, true);
86 result = classifyPaths(["shared/new-loader.js"]);
87 assert.deepEqual(result.unknown, ["shared/new-loader.js"]);
88 for (const name of ["code", "desktop", "frontend", "browser", "memory", "memory_full", "electron", "native", "packaging"])
89 assert.equal(result.flags[name], true, name);
90 });
91
92 test("release notes and full events are deterministic", () => {
93 assert.equal(classifyPaths(["release-notes/v1.md"]).flags.notes_only, true);
94 const notesPush = classifyPaths(["release-notes/v1.md"], { full: true }).flags;
95 assert.equal(notesPush.notes_only, true);
96 assert.equal(notesPush.desktop, false);
97 assert.equal(classifyPaths([]).flags.desktop, false);
98 const full = classifyPaths([], { full: true }).flags;
99 assert.equal(full.notes_only, false);
100 for (const name of ["code", "desktop", "frontend", "browser", "memory", "memory_full", "electron", "native", "packaging", "site", "sdk", "windows_builtin"])
101 assert.equal(full[name], true, name);
102 });
103
104 test("push and merge-base diffs retain deletions and renames", t => {
105 const root = mkdtempSync(path.join(os.tmpdir(), "reasonix-ci-paths-"));
106 t.after(() => rmSync(root, { recursive: true, force: true }));
107 mkdirSync(path.join(root, "desktop/frontend/src"), { recursive: true });
108 writeFileSync(path.join(root, "desktop/frontend/src/old.ts"), "old");
109 writeFileSync(path.join(root, "desktop/frontend/src/delete.ts"), "delete");
110 execFileSync("git", ["init"], { cwd: root });
111 execFileSync("git", ["add", "."], { cwd: root });
112 execFileSync("git", ["-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-m", "base"], { cwd: root });
113 const base = execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim();
114 execFileSync("git", ["mv", "desktop/frontend/src/old.ts", "desktop/frontend/src/new.ts"], { cwd: root });
115 execFileSync("git", ["rm", "desktop/frontend/src/delete.ts"], { cwd: root });
116 execFileSync("git", ["-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-am", "change"], { cwd: root });
117 const head = execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim();
118 const push = changedFiles({ base, head, mode: "push", cwd: root });
119 const pull = changedFiles({ base, head, mode: "pull_request", cwd: root });
120 for (const files of [push, pull]) {
121 assert.ok(files.includes("desktop/frontend/src/delete.ts"));
122 assert.ok(files.includes("desktop/frontend/src/new.ts"));
123 assert.equal(classifyPaths(files).flags.memory, true);
124 }
125 assert.deepEqual(changedFiles({ base: head, head, mode: "push", cwd: root }), []);
126 });
127
128 test("invalid diff identities fail instead of producing a skip", () => {
129 assert.throws(() => changedFiles({ base: "0".repeat(40), head: "HEAD" }), /non-zero/);
130 assert.throws(() => changedFiles({ base: "f".repeat(40), head: "HEAD" }));
131 });
132
133 test("CLI entry runs from paths with URL-significant characters", t => {
134 const root = mkdtempSync(path.join(os.tmpdir(), "reasonix-ci-entry-"));
135 t.after(() => rmSync(root, { recursive: true, force: true }));
136 const expectedNames = ["code", "desktop", "desktop_go", "frontend", "browser", "memory", "memory_full", "electron", "native", "packaging", "site", "sdk", "windows_builtin", "release_control", "notes_only"];
137 for (const directory of ["ordinary", "with space", "中文", "hash#directory", "literal%20directory"]) {
138 const target = path.join(root, directory, "ci-paths.mjs");
139 mkdirSync(path.dirname(target), { recursive: true });
140 copyFileSync(new URL("./ci-paths.mjs", import.meta.url), target);
141 const output = execFileSync(process.execPath, [target, "--full"], { encoding: "utf8" });
142 const values = Object.fromEntries(output.trim().split("\n").map(line => line.split("=")));
143 assert.deepEqual(Object.keys(values).sort(), [...expectedNames].sort(), directory);
144 for (const name of expectedNames) assert.equal(values[name], name === "notes_only" ? "false" : "true", `${directory}: ${name}`);
145 }
146 });
147
148 test("CLI writes GitHub output and module import stays side-effect free", t => {
149 const root = mkdtempSync(path.join(os.tmpdir(), "reasonix-ci-output-"));
150 t.after(() => rmSync(root, { recursive: true, force: true }));
151 const target = path.join(root, "hash#literal%20", "ci-paths.mjs");
152 const outputPath = path.join(root, "GitHub output.txt");
153 mkdirSync(path.dirname(target), { recursive: true });
154 copyFileSync(new URL("./ci-paths.mjs", import.meta.url), target);
155 const stdout = execFileSync(process.execPath, [target, "--full", "--github-output", outputPath], { encoding: "utf8" });
156 assert.equal(stdout, "");
157 const output = readFileSync(outputPath, "utf8");
158 assert.equal(output.trim().split("\n").length, 15);
159 assert.match(output, /^desktop=true$/m);
160 assert.match(output, /^notes_only=false$/m);
161
162 const imported = execFileSync(process.execPath, ["--input-type=module", "--eval", `import(${JSON.stringify(pathToFileURL(target).href)})`], { encoding: "utf8" });
163 assert.equal(imported, "");
164
165 const stdinImported = execFileSync(process.execPath, ["--input-type=module", "-"], {
166 input: `await import(${JSON.stringify(pathToFileURL(target).href)});`, encoding: "utf8",
167 });
168 assert.equal(stdinImported, "");
169 });
170
171 // site and sdk are in this list because the required aggregates accept a
172 // skipped site or a no-op sdk; a PR that edits the routing contract would
173 // otherwise satisfy those expectations without running either surface.
174 test("CI routing changes exercise their owning workflow surfaces", () => {
175 for (const path of [".github/workflows/ci.yml", "scripts/ci-paths.mjs"]) {
176 const flags = classifyPaths([path]).flags;
177 for (const name of ["desktop", "desktop_go", "frontend", "browser", "electron", "native", "packaging", "site", "sdk"])
178 assert.equal(flags[name], true, `${path}: ${name}`);
179 }
180 assert.equal(classifyPaths([".github/workflows/ci.yml"]).flags.memory, false);
181 for (const path of [".github/workflows/app-memory.yml", "scripts/ci-paths.mjs", "scripts/ci-paths.test.mjs"]) {
182 const flags = classifyPaths([path]).flags;
183 assert.equal(flags.memory, true, path);
184 assert.equal(flags.memory_full, true, path);
185 }
186 const memoryOnly = classifyPaths([".github/workflows/app-memory.yml"]).flags;
187 assert.equal(memoryOnly.packaging, false);
188 assert.equal(memoryOnly.sdk, false);
189 });
190
191 test("release control changes run focused contracts without selecting product suites", () => {
192 for (const path of [
193 ".github/workflows/release-candidate.yml",
194 ".github/workflows/release-candidate-verify.yml",
195 ".github/workflows/release-promote.yml",
196 ".github/workflows/pages.yml",
197 "scripts/release-candidate.mjs",
198 "scripts/verify-release-artifact-archive.mjs",
199 "scripts/desktop-release-artifacts.test.mjs",
200 "npm/publish-candidate.mjs",
201 ]) {
202 const flags = classifyPaths([path]).flags;
203 assert.equal(flags.release_control, true, path);
204 assert.equal(flags.code, false, path);
205 assert.equal(flags.desktop, false, path);
206 }
207 });
208
208 lines Plain Text