返回 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 for (const path of ["scripts/test-windows-upgrade-startup.ps1", "scripts/windows-upgrade-ui-evidence.ps1", "desktop/internal/upgradefixture/fixture.go", "desktop/cmd/windows-upgrade-fixture/main.go"]) {
75 assert.equal(classifyPaths([path]).flags.packaging, true, path);
76 }
77 });
78
79 test("Windows shell ownership paths select the full builtin package", () => {
80 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"]) {
81 assert.equal(classifyPaths([path]).flags.windows_builtin, true, path);
82 }
83 assert.equal(classifyPaths(["internal/acp/e2e_test.go"]).flags.windows_builtin, false);
84 });
85
86 test("mixed changes cannot hide affected work and unknown paths fail closed", () => {
87 let result = classifyPaths(["README.md", "desktop/frontend/src/App.tsx"]);
88 assert.equal(result.flags.memory, true);
89 result = classifyPaths(["shared/new-loader.js"]);
90 assert.deepEqual(result.unknown, ["shared/new-loader.js"]);
91 for (const name of ["code", "desktop", "frontend", "browser", "memory", "memory_full", "electron", "native", "packaging"])
92 assert.equal(result.flags[name], true, name);
93 });
94
95 test("shared Node runtime changes validate every frontend artifact surface", () => {
96 const { flags } = classifyPaths([".node-version"]);
97 for (const name of ["code", "desktop", "frontend", "browser", "memory", "memory_full", "electron", "native", "packaging"])
98 assert.equal(flags[name], true, name);
99 });
100
101 test("release notes and full events are deterministic", () => {
102 assert.equal(classifyPaths(["release-notes/v1.md"]).flags.notes_only, true);
103 const notesPush = classifyPaths(["release-notes/v1.md"], { full: true }).flags;
104 assert.equal(notesPush.notes_only, true);
105 assert.equal(notesPush.desktop, false);
106 assert.equal(classifyPaths([]).flags.desktop, false);
107 const full = classifyPaths([], { full: true }).flags;
108 assert.equal(full.notes_only, false);
109 for (const name of ["code", "desktop", "frontend", "browser", "memory", "memory_full", "electron", "native", "packaging", "site", "sdk", "windows_builtin"])
110 assert.equal(full[name], true, name);
111 });
112
113 test("push and merge-base diffs retain deletions and renames", t => {
114 const root = mkdtempSync(path.join(os.tmpdir(), "reasonix-ci-paths-"));
115 t.after(() => rmSync(root, { recursive: true, force: true }));
116 mkdirSync(path.join(root, "desktop/frontend/src"), { recursive: true });
117 writeFileSync(path.join(root, "desktop/frontend/src/old.ts"), "old");
118 writeFileSync(path.join(root, "desktop/frontend/src/delete.ts"), "delete");
119 execFileSync("git", ["init"], { cwd: root });
120 execFileSync("git", ["add", "."], { cwd: root });
121 execFileSync("git", ["-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-m", "base"], { cwd: root });
122 const base = execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim();
123 execFileSync("git", ["mv", "desktop/frontend/src/old.ts", "desktop/frontend/src/new.ts"], { cwd: root });
124 execFileSync("git", ["rm", "desktop/frontend/src/delete.ts"], { cwd: root });
125 execFileSync("git", ["-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-am", "change"], { cwd: root });
126 const head = execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim();
127 const push = changedFiles({ base, head, mode: "push", cwd: root });
128 const pull = changedFiles({ base, head, mode: "pull_request", cwd: root });
129 for (const files of [push, pull]) {
130 assert.ok(files.includes("desktop/frontend/src/delete.ts"));
131 assert.ok(files.includes("desktop/frontend/src/new.ts"));
132 assert.equal(classifyPaths(files).flags.memory, true);
133 }
134 assert.deepEqual(changedFiles({ base: head, head, mode: "push", cwd: root }), []);
135 });
136
137 test("invalid diff identities fail instead of producing a skip", () => {
138 assert.throws(() => changedFiles({ base: "0".repeat(40), head: "HEAD" }), /non-zero/);
139 assert.throws(() => changedFiles({ base: "f".repeat(40), head: "HEAD" }));
140 });
141
142 test("CLI entry runs from paths with URL-significant characters", t => {
143 const root = mkdtempSync(path.join(os.tmpdir(), "reasonix-ci-entry-"));
144 t.after(() => rmSync(root, { recursive: true, force: true }));
145 const expectedNames = ["code", "desktop", "desktop_go", "frontend", "browser", "memory", "memory_full", "electron", "native", "packaging", "site", "sdk", "windows_builtin", "release_control", "notes_only"];
146 for (const directory of ["ordinary", "with space", "中文", "hash#directory", "literal%20directory"]) {
147 const target = path.join(root, directory, "ci-paths.mjs");
148 mkdirSync(path.dirname(target), { recursive: true });
149 copyFileSync(new URL("./ci-paths.mjs", import.meta.url), target);
150 const output = execFileSync(process.execPath, [target, "--full"], { encoding: "utf8" });
151 const values = Object.fromEntries(output.trim().split("\n").map(line => line.split("=")));
152 assert.deepEqual(Object.keys(values).sort(), [...expectedNames].sort(), directory);
153 for (const name of expectedNames) assert.equal(values[name], name === "notes_only" ? "false" : "true", `${directory}: ${name}`);
154 }
155 });
156
157 test("CLI writes GitHub output and module import stays side-effect free", t => {
158 const root = mkdtempSync(path.join(os.tmpdir(), "reasonix-ci-output-"));
159 t.after(() => rmSync(root, { recursive: true, force: true }));
160 const target = path.join(root, "hash#literal%20", "ci-paths.mjs");
161 const outputPath = path.join(root, "GitHub output.txt");
162 mkdirSync(path.dirname(target), { recursive: true });
163 copyFileSync(new URL("./ci-paths.mjs", import.meta.url), target);
164 const stdout = execFileSync(process.execPath, [target, "--full", "--github-output", outputPath], { encoding: "utf8" });
165 assert.equal(stdout, "");
166 const output = readFileSync(outputPath, "utf8");
167 assert.equal(output.trim().split("\n").length, 15);
168 assert.match(output, /^desktop=true$/m);
169 assert.match(output, /^notes_only=false$/m);
170
171 const imported = execFileSync(process.execPath, ["--input-type=module", "--eval", `import(${JSON.stringify(pathToFileURL(target).href)})`], { encoding: "utf8" });
172 assert.equal(imported, "");
173
174 const stdinImported = execFileSync(process.execPath, ["--input-type=module", "-"], {
175 input: `await import(${JSON.stringify(pathToFileURL(target).href)});`, encoding: "utf8",
176 });
177 assert.equal(stdinImported, "");
178 });
179
180 // site and sdk are in this list because the required aggregates accept a
181 // skipped site or a no-op sdk; a PR that edits the routing contract would
182 // otherwise satisfy those expectations without running either surface.
183 test("CI routing changes exercise their owning workflow surfaces", () => {
184 for (const path of [".github/workflows/ci.yml", "scripts/ci-paths.mjs"]) {
185 const flags = classifyPaths([path]).flags;
186 for (const name of ["desktop", "desktop_go", "frontend", "browser", "electron", "native", "packaging", "site", "sdk"])
187 assert.equal(flags[name], true, `${path}: ${name}`);
188 }
189 assert.equal(classifyPaths([".github/workflows/ci.yml"]).flags.memory, false);
190 for (const path of [".github/workflows/app-memory.yml", "scripts/ci-paths.mjs", "scripts/ci-paths.test.mjs"]) {
191 const flags = classifyPaths([path]).flags;
192 assert.equal(flags.memory, true, path);
193 assert.equal(flags.memory_full, true, path);
194 }
195 const memoryOnly = classifyPaths([".github/workflows/app-memory.yml"]).flags;
196 assert.equal(memoryOnly.packaging, false);
197 assert.equal(memoryOnly.sdk, false);
198 });
199
200 test("release control changes run focused contracts without selecting product suites", () => {
201 for (const path of [
202 ".github/workflows/release-candidate.yml",
203 ".github/workflows/release-candidate-verify.yml",
204 ".github/workflows/release-promote.yml",
205 "scripts/release-candidate.mjs",
206 "scripts/reconcile-release-publication.sh",
207 "scripts/reconcile-release-publication.test.mjs",
208 "scripts/check-release-public-access.sh",
209 "scripts/validate-release-control-plane.sh",
210 "scripts/verify-release-artifact-archive.mjs",
211 "scripts/desktop-release-artifacts.test.mjs",
212 "npm/publish-candidate.mjs",
213 ]) {
214 const flags = classifyPaths([path]).flags;
215 assert.equal(flags.release_control, true, path);
216 assert.equal(flags.code, false, path);
217 assert.equal(flags.desktop, false, path);
218 }
219 });
220
220 lines Plain Text