返回 DeepSeek-Reasonix
ci-workflow.test.mjs
根目录 / scripts / ci-workflow.test.mjs
1 import assert from "node:assert/strict";
2 import { mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
3 import { spawnSync } from "node:child_process";
4 import os from "node:os";
5 import path from "node:path";
6 import vm from "node:vm";
7 import test from "node:test";
8 import { groups as windowsDesktopGroups, testArgs as windowsDesktopTestArgs } from "./desktop-windows-go-tests.mjs";
9
10 const workflow = name => readFileSync(new URL(`../.github/workflows/${name}.yml`, import.meta.url), "utf8");
11 function job(source, name) {
12 const body = source.match(new RegExp(`\\n ${name}:\\n([\\s\\S]*?)(?=\\n [a-z][a-z0-9-]*:|$)`))?.[1];
13 assert.ok(body, name);
14 return body;
15 }
16 function condition(body, context) {
17 const expression = body.match(/^ if: (.+)$/m)[1].replace(/^\$\{\{\s*|\s*\}\}$/g, "")
18 .replace(/needs\.([a-z][a-z0-9-]*)/g, 'needs["$1"]');
19 return vm.runInNewContext(expression, { always: () => true, cancelled: () => false, ...context });
20 }
21 function shellStep(body, name) {
22 return body.split(` - name: ${name}\n`)[1].match(/ run: \|\n((?: .*\n|\n)+)/)[1]
23 .replace(/^ /gm, "");
24 }
25 const ci = workflow("ci");
26 const release = workflow("release-desktop");
27 const promote = workflow("release-promote");
28 const appMemory = workflow("app-memory");
29
30 test("Windows PR verifies credential aliases before full push CI", () => {
31 assert.match(ci, /name: test \(Windows credential ACL identity\)[\s\S]*?runner\.os == 'Windows' && github\.event_name == 'pull_request'[\s\S]*?go test -timeout=2m -run '\^TestCredentialAccessRepairsLegacyCredentialDeny\|\^TestRepairLegacyCredentialDenyMatchesFileAcrossPathAliases\$' \.\/internal\/config \.\/internal\/winaclresidue/);
32 });
33
34 test("release candidate verification cannot mutate repository contents before approval", () => {
35 assert.match(job(promote, "preflight"), /permissions:\n actions: read\n attestations: read\n contents: read/);
36 assert.match(job(promote, "authorize"), /environment: release[\s\S]*permissions:\n contents: read/);
37 assert.match(job(promote, "activate"), /permissions:\n contents: write/);
38 });
39
40 test("cancelled CI stops expensive workers but keeps result aggregation", () => {
41 for (const name of ["test", "windows-control", "windows-isolated", "race", "sdk", "desktop-prepare",
42 "desktop-frontend", "desktop-browser-group", "desktop-go", "desktop-go-race", "desktop-macos",
43 "desktop-windows", "desktop-windows-go-group", "desktop-windows-package", "lint-code", "site", "coverage", "prune-go-cache"]) {
44 assert.equal(condition(job(ci, name), { cancelled: () => true }), false, name);
45 }
46 for (const name of ["root", "lint", "desktop", "desktop-browser", "desktop-windows-go"])
47 assert.equal(condition(job(ci, name), { cancelled: () => true }), true, name);
48 });
49
50 test("packaging changes run native installer acceptance before merge", () => {
51 assert.match(job(ci, "desktop-prepare"), /REASONIX_COMMIT: \$\{\{ github.sha \}\}/,
52 "prepared frontend must budget the full source identity used by native packaging");
53 const body = job(ci, "desktop-windows-package");
54 assert.doesNotMatch(ci, /performance-benchmark\.mjs/);
55 const diagnostic = workflow("diagnostic-overhead");
56 assert.match(diagnostic, /schedule:/);
57 assert.match(diagnostic, /workflow_dispatch:/);
58 assert.match(diagnostic, /desktop\/electron\/\*\*/);
59 assert.match(diagnostic, /desktop\/frontend\/\*\*/);
60 assert.match(diagnostic, /run: pnpm install --frozen-lockfile/);
61 assert.match(diagnostic, /run: node electron\/scripts\/performance-benchmark\.mjs/);
62 assert.match(diagnostic, /if-no-files-found: error/);
63 assert.doesNotMatch(diagnostic, /continue-on-error/);
64 for (const event of ["pull_request", "push"]) {
65 for (const packaging of ["true", "false", ""]) {
66 const context = { github: { event_name: event }, needs: {
67 "desktop-prepare": { result: "success" }, changes: { outputs: { packaging, notes_only: "false" } },
68 } };
69 assert.equal(condition(body, context), event === "push" || packaging !== "false");
70 const aggregate = job(ci, "desktop").match(/PACKAGE_REQUIRED: \$\{\{ (.+) \}\}/)[1];
71 assert.equal(vm.runInNewContext(aggregate, context), condition(body, context));
72 }
73 }
74 });
75
76 test("notes pushes preserve required ancestor CI while code pushes cancel obsolete runs", () => {
77 const dir = mkdtempSync(path.join(os.tmpdir(), "reasonix-ci-cancel-"));
78 const git = (...args) => {
79 const result = spawnSync("git", args, { cwd: dir, encoding: "utf8" });
80 assert.equal(result.status, 0, result.stderr);
81 return result.stdout.trim();
82 };
83 try {
84 git("init", "-q");
85 git("config", "user.name", "test");
86 git("config", "user.email", "test@example.invalid");
87 const commit = (file, content) => {
88 writeFileSync(path.join(dir, file), content);
89 git("add", "."); git("commit", "-qm", "fixture");
90 return git("rev-parse", "HEAD");
91 };
92 const old = commit("code", "old");
93 const code = commit("code", "current");
94 mkdirSync(path.join(dir, "release-notes"));
95 const notes = commit("release-notes/record", "first");
96 const head = commit("release-notes/record", "reviewed");
97 const run = candidate => {
98 const script = `set -euo pipefail
99 sleep() { :; }
100 gh() {
101 if [ "$2" = "-X" ]; then printf '%s\\n' "$4" >> "$CANCEL_LOG";
102 elif [[ "$2" == *workflows/ci.yml/runs* ]]; then printf '%s\\n' "$ACTIVE_RUNS";
103 else echo completed; fi
104 }
105 ${shellStep(job(workflow("supersede-ci"), "cancel-superseded"), "Cancel CI runs this push supersedes")}`;
106 const log = path.join(dir, "cancel-log");
107 writeFileSync(log, "");
108 const result = spawnSync("bash", ["-c", script], { cwd: dir, encoding: "utf8", env: {
109 ...process.env, GITHUB_REPOSITORY: "example/repo", GITHUB_SHA: candidate, CANCEL_LOG: log,
110 ACTIVE_RUNS: `11 ${old}\n12 ${code}\n13 ${notes}\n14 ${head}`,
111 } });
112 assert.equal(result.status, 0, result.stderr);
113 return readFileSync(log, "utf8").trim().split("\n");
114 };
115 assert.deepEqual(run(head), ["repos/example/repo/actions/runs/11/cancel"]);
116 const next = commit("code", "next");
117 assert.deepEqual(run(next), [11, 12, 13, 14].map(id => `repos/example/repo/actions/runs/${id}/cancel`));
118 const group = ci.match(/ group: (ci-.+)/)[1];
119 assert.match(group, /github.event_name == 'push' && github.sha \|\| github.ref/);
120 } finally {
121 rmSync(dir, { recursive: true, force: true });
122 }
123 });
124
125 test("Certum signing survives skipped ancestor gates but requires successful inputs", () => {
126 const body = job(release, "windows-sign");
127 // A status function is required to override GitHub's implicit success(),
128 // which otherwise propagates a skipped standalone/orchestrator ancestor.
129 assert.match(body, /if:.*always\(\)/);
130 const context = {
131 needs: { resolve: { result: "success" }, "windows-build": { result: "success" }, "signing-contract": { result: "success" } },
132 github: { repository: "esengine/DeepSeek-Reasonix" },
133 inputs: { desktop_manual_only: false },
134 };
135 assert.equal(condition(body, context), true);
136 assert.equal(condition(body, { ...context, cancelled: () => true }), false);
137 for (const name of Object.keys(context.needs)) {
138 for (const result of ["failure", "skipped", "cancelled"]) {
139 assert.equal(condition(body, { ...context, needs: { ...context.needs, [name]: { result } } }), false);
140 }
141 }
142 assert.equal(condition(body, { ...context, inputs: { desktop_manual_only: true } }), false);
143 assert.equal(condition(body, { ...context, github: { repository: "example/fork" } }), false);
144 });
145
146 test("Windows full runs use the partitioned suite without a duplicate module sweep", () => {
147 const body = job(ci, "test");
148 const enabled = (name, os, event, run = "true") => {
149 const step = body.split(` - name: ${name}\n`)[1].split(/\n - /)[0];
150 const expression = step.match(/^ if: (.+)$/m)[1];
151 return vm.runInNewContext(expression, {
152 env: { RUN_STEPS: run }, runner: { os }, github: { event_name: event },
153 });
154 };
155 for (const event of ["pull_request", "push", "workflow_dispatch"]) {
156 for (const os of ["Linux", "macOS", "Windows"]) {
157 assert.equal(enabled("test", os, event), os === "Linux" || (os === "macOS" && event !== "pull_request"));
158 assert.equal(enabled("test (full)", os, event), os === "Windows" && event !== "pull_request");
159 assert.equal(enabled("test (Windows smoke)", os, event), os === "Windows" && event === "pull_request");
160 assert.equal(enabled("test", os, event, "false"), false);
161 assert.equal(enabled("test (full)", os, event, "false"), false);
162 }
163 }
164 assert.match(body, /run: node scripts\/windows-go-tests\.mjs full/);
165 assert.match(job(ci, "windows-isolated"), /group: \[acp, agent, boot, bot, serve, session, worktree\]/);
166 assert.match(job(ci, "windows-control"), /run: node scripts\/windows-go-tests\.mjs control/);
167 });
168
169 test("App memory workflow tiers pull requests and keeps full scheduled coverage", t => {
170 assert.match(appMemory, /schedule:\n - cron: "17 3 \* \* \*"/);
171 assert.match(appMemory, /\[ "\$EVENT_NAME" = workflow_dispatch \] \|\| \[ "\$EVENT_NAME" = schedule \]/);
172 assert.match(appMemory, /matrix:\n shard: \$\{\{ fromJSON\(needs\.changes\.outputs\.memory_shards\) \}\}/);
173 assert.match(appMemory, /REASONIX_APP_MEMORY_PROFILE: \$\{\{ needs\.changes\.outputs\.memory_profile \}\}/);
174 const script = shellStep(job(appMemory, "changes"), "Select memory profile");
175 const root = mkdtempSync(path.join(os.tmpdir(), "reasonix-memory-workflow-"));
176 t.after(() => rmSync(root, { recursive: true, force: true }));
177 let index = 0;
178 const run = env => {
179 const output = path.join(root, `output-${index++}`);
180 const result = spawnSync("bash", ["-e", "-c", script], {
181 env: { ...process.env, GITHUB_OUTPUT: output, GITHUB_STEP_SUMMARY: path.join(root, "summary"), ...env }, encoding: "utf8",
182 });
183 return { ...result, workflowOutput: result.status === 0 ? readFileSync(output, "utf8") : "" };
184 };
185 for (const [env, expected] of [
186 [{ EVENT_NAME: "pull_request", MEMORY: "true", MEMORY_FULL: "false" }, "profile=short\nshards=[1]\n"],
187 [{ EVENT_NAME: "pull_request", MEMORY: "true", MEMORY_FULL: "true" }, "profile=full\nshards=[1,2,3]\n"],
188 [{ EVENT_NAME: "push", MEMORY: "true", MEMORY_FULL: "false" }, "profile=full\nshards=[1,2,3]\n"],
189 [{ EVENT_NAME: "pull_request", MEMORY: "false", MEMORY_FULL: "false" }, "profile=off\nshards=[1]\n"],
190 ]) {
191 const result = run(env);
192 assert.equal(result.status, 0, result.stderr);
193 assert.equal(result.workflowOutput, expected);
194 }
195 });
196
197 test("macOS signing diagnostics require protected main and cannot publish", () => {
198 const source = workflow("macos-signing-check");
199 const verify = job(source, "verify");
200 const github = { repository: "esengine/DeepSeek-Reasonix", ref: "refs/heads/main-v2", ref_protected: true };
201 assert.equal(condition(verify, { github }), true);
202 for (const changed of [{ repository: "fork/Reasonix" }, { ref: "refs/tags/v1.0.0" }, { ref_protected: false }]) {
203 assert.equal(condition(verify, { github: { ...github, ...changed } }), false);
204 }
205 assert.match(verify, /environment: release/);
206 assert.match(verify, /ref: \$\{\{ github.sha \}\}/);
207 assert.match(source, /permissions:\n contents: read\n/);
208 assert.doesNotMatch(source, /: write|secrets\.(R2_|SIGNPATH_|MINISIGN_|NPM_)/);
209 assert.match(verify, /HAS_APPLE_CERT: "true"/);
210 assert.match(verify, /scripts\/desktop-build.sh darwin\/universal v0.0.0-signing-check stable/);
211 assert.match(verify, /path: \$\{\{ runner.temp \}\}\/apple-notarization\/\*\.json/);
212 assert.match(verify, /if: always\(\)/);
213 });
214
215 test("required desktop aggregate rejects every failed, cancelled or unexpectedly skipped child", () => {
216 const script = shellStep(job(ci, "desktop"), "Verify desktop validation jobs");
217 const success = { CHANGES_RESULT: "success", PREPARE_REQUIRED: "true", NATIVE_REQUIRED: "true", FRONTEND_REQUIRED: "true", BROWSER_REQUIRED: "true",
218 PACKAGE_REQUIRED: "true", PREPARE_RESULT: "success", GO_RESULT: "success", GO_RACE_RESULT: "success", FRONTEND_RESULT: "success", BROWSER_RESULT: "success",
219 MACOS_RESULT: "success", WINDOWS_RESULT: "success", WINDOWS_GO_RESULT: "success", PACKAGE_RESULT: "success" };
220 const run = env => spawnSync("bash", ["-e", "-c", script], { env: { ...process.env, ...env } }).status;
221 assert.equal(run(success), 0);
222 for (const key of ["PREPARE_RESULT", "GO_RESULT", "GO_RACE_RESULT", "FRONTEND_RESULT", "BROWSER_RESULT", "CHANGES_RESULT",
223 "MACOS_RESULT", "WINDOWS_RESULT", "WINDOWS_GO_RESULT", "PACKAGE_RESULT"]) {
224 for (const value of ["failure", "cancelled", "skipped", ""]) assert.notEqual(run({ ...success, [key]: value }), 0, `${key}=${value}`);
225 }
226 // A pull request that cannot affect the desktop module: every child skips
227 // except the browser and Windows Go aggregates, which validate their groups.
228 assert.equal(run({ ...success, PREPARE_REQUIRED: "false", NATIVE_REQUIRED: "false", FRONTEND_REQUIRED: "false", BROWSER_REQUIRED: "false",
229 PACKAGE_REQUIRED: "false", PREPARE_RESULT: "skipped", GO_RESULT: "skipped", GO_RACE_RESULT: "skipped", FRONTEND_RESULT: "skipped",
230 BROWSER_RESULT: "success", MACOS_RESULT: "skipped", WINDOWS_RESULT: "skipped", WINDOWS_GO_RESULT: "success", PACKAGE_RESULT: "skipped" }), 0);
231 // A pull request unrelated to packaging must skip it.
232 assert.equal(run({ ...success, PACKAGE_REQUIRED: "false", PACKAGE_RESULT: "skipped" }), 0);
233 assert.notEqual(run({ ...success, PACKAGE_REQUIRED: "false", PACKAGE_RESULT: "success" }), 0);
234 assert.equal(run({ ...success, FRONTEND_REQUIRED: "false", BROWSER_REQUIRED: "false", FRONTEND_RESULT: "skipped", BROWSER_RESULT: "success" }), 0);
235 const browserScript = shellStep(job(ci, "desktop-browser"), "Verify desktop browser groups");
236 const browser = spawnSync("bash", ["-e", "-c", browserScript], { env: { ...process.env,
237 CHANGES_RESULT: "success", SHOULD_RUN: "false", PREPARE_RESULT: "skipped", GROUP_RESULT: "skipped" } });
238 assert.equal(browser.status, 0, "an unneeded browser aggregate succeeds after validating skipped groups");
239 assert.notEqual(run({ ...success, BROWSER_REQUIRED: "false", BROWSER_RESULT: "skipped" }), 0);
240 assert.notEqual(run({ ...success, NATIVE_REQUIRED: "false", WINDOWS_GO_RESULT: "skipped" }), 0);
241 });
242
243 test("required lint aggregates code lint and the deduplicated frontend suite", () => {
244 const body = job(ci, "lint");
245 const script = shellStep(body, "Verify lint and frontend validation jobs");
246 const success = { CHANGES_RESULT: "success", LINT_CODE_RESULT: "success", LINT_CODE_REQUIRED: "true",
247 RELEASE_CONTROL_RESULT: "success", RELEASE_CONTROL_REQUIRED: "true",
248 PREPARE_RESULT: "success", FRONTEND_RESULT: "success", FRONTEND_REQUIRED: "true" };
249 const run = env => spawnSync("bash", ["-e", "-c", script], { env: { ...process.env, ...env } }).status;
250 assert.equal(run(success), 0);
251 for (const key of ["CHANGES_RESULT", "LINT_CODE_RESULT", "RELEASE_CONTROL_RESULT", "PREPARE_RESULT", "FRONTEND_RESULT"])
252 for (const value of ["failure", "cancelled", "skipped", ""]) assert.notEqual(run({ ...success, [key]: value }), 0, `${key}=${value}`);
253 assert.equal(run({ ...success, LINT_CODE_REQUIRED: "false", LINT_CODE_RESULT: "skipped",
254 RELEASE_CONTROL_REQUIRED: "false", RELEASE_CONTROL_RESULT: "skipped",
255 FRONTEND_REQUIRED: "false", PREPARE_RESULT: "skipped", FRONTEND_RESULT: "skipped" }), 0);
256 assert.equal(run({ ...success, FRONTEND_REQUIRED: "false", PREPARE_RESULT: "success", FRONTEND_RESULT: "skipped" }), 0);
257 assert.doesNotMatch(job(ci, "lint-code"), /test:motion/);
258 assert.match(body, /needs: \[changes, lint-code, release-control, desktop-prepare, desktop-frontend\]/);
259 });
260
261 test("required root aggregate covers the jobs the per-OS test legs do not", () => {
262 const body = job(ci, "root");
263 const script = shellStep(body, "Verify root validation jobs");
264 const success = { CHANGES_RESULT: "success", CODE_REQUIRED: "true", SITE_REQUIRED: "true", COVERAGE_REQUIRED: "true",
265 CONTROL_RESULT: "success", ISOLATED_RESULT: "success", SDK_RESULT: "success", SITE_RESULT: "success", COVERAGE_RESULT: "success" };
266 const run = env => spawnSync("bash", ["-e", "-c", script], { env: { ...process.env, ...env } }).status;
267 assert.equal(run(success), 0);
268 for (const key of ["CHANGES_RESULT", "CONTROL_RESULT", "ISOLATED_RESULT", "SDK_RESULT", "SITE_RESULT", "COVERAGE_RESULT"])
269 for (const value of ["failure", "cancelled", "skipped", ""]) assert.notEqual(run({ ...success, [key]: value }), 0, `${key}=${value}`);
270 // A pull request unrelated to code or site: the internally-gated jobs still
271 // report success, the skippable ones must actually be skipped.
272 assert.equal(run({ ...success, CODE_REQUIRED: "false", SITE_REQUIRED: "false", COVERAGE_REQUIRED: "false",
273 ISOLATED_RESULT: "skipped", SITE_RESULT: "skipped", COVERAGE_RESULT: "skipped" }), 0);
274 // Coverage is push-only; a pull request that ran it is a routing defect.
275 assert.notEqual(run({ ...success, COVERAGE_REQUIRED: "false", COVERAGE_RESULT: "success" }), 0);
276 assert.match(body, /needs: \[changes, windows-control, windows-isolated, sdk, site, coverage\]/);
277 // govulncheck sets continue-on-error, so needs.*.result is success even when
278 // it fails; aggregating it would be a tautology that reads like coverage.
279 assert.match(job(ci, "govulncheck"), /continue-on-error: true/);
280 assert.doesNotMatch(body, /GOVULN/);
281 });
282
283 // The gap that let a failing desktop-windows-go merge was a job nobody had
284 // wired into a required aggregate. Keep that unrepeatable: every job must be
285 // reachable from a required check, or be named here with a reason.
286 test("every ci job is reachable from a required aggregate", () => {
287 const required = ["test", "race", "lint", "desktop", "root"];
288 const advisory = {
289 changes: "asserted by line one of every aggregate",
290 "ci-metrics": "reports queue and stage timing; failure must not block merges",
291 "prune-go-cache": "push-only cache housekeeping; cannot report on a pull request",
292 govulncheck: "continue-on-error by design — stdlib advisories precede Go patch releases",
293 };
294 const jobs = ci.slice(ci.indexOf("\njobs:\n")); // `on:` also nests two-space keys
295 const names = [...jobs.matchAll(/^ {2}([a-z][a-z0-9-]*):$/gm)].map(match => match[1]);
296 assert.ok(names.length > 20, `expected the full job list, got ${names.length}`);
297 // A bracketed list may wrap across lines, so consume up to its closing ].
298 const edges = new Map(names.map(name => [name, (job(ci, name).match(/^ {4}needs:\s*(\[[^\]]*\]|\S.*)$/m)?.[1] ?? "")
299 .replace(/[[\]]/g, "").split(",").map(entry => entry.trim()).filter(Boolean)]));
300 const reachable = new Set(required);
301 for (const name of required) for (const dependency of edges.get(name) ?? []) reachable.add(dependency);
302 for (let size = 0; size !== reachable.size;) {
303 size = reachable.size;
304 for (const name of [...reachable]) for (const dependency of edges.get(name) ?? []) reachable.add(dependency);
305 }
306 for (const name of names) {
307 if (reachable.has(name)) continue;
308 assert.ok(advisory[name], `${name} is gated by no required check and is not declared advisory`);
309 }
310 for (const name of Object.keys(advisory))
311 assert.ok(names.includes(name), `${name} is declared advisory but no longer exists`);
312 });
313
314 test("reuse skips only build work and still gates every publisher on validation", () => {
315 const context = {
316 inputs: { preflight_artifact_prefix: "desktop-123-1-preflight", orchestrated: true, signing_preflight_verified: true, signing_preflight: false, production_signing_smoke: false },
317 needs: { resolve: { result: "success" }, "signing-contract": { result: "success" }, "mac-universal-intel": { result: "skipped" }, "windows-build": { result: "skipped" }, "windows-sign": { result: "skipped" }, "windows-runtime-acceptance": { result: "skipped" }, build: { result: "skipped" } },
318 };
319 assert.equal(condition(job(release, "build"), context), false);
320 assert.equal(condition(job(release, "publish"), context), true);
321 for (const key of ["resolve", "signing-contract", "mac-universal-intel", "windows-build", "windows-sign", "build"]) {
322 for (const result of ["failure", "cancelled"]) {
323 const changed = structuredClone(context);
324 changed.needs[key].result = result;
325 assert.equal(condition(job(release, "publish"), changed), false, `${key}=${result}`);
326 }
327 }
328 for (const key of ["orchestrated", "signing_preflight_verified"]) {
329 assert.equal(condition(job(release, "publish"), { ...context, inputs: { ...context.inputs, [key]: false } }), false);
330 }
331 for (const key of ["signing_preflight", "production_signing_smoke"]) {
332 assert.equal(condition(job(release, "publish"), { ...context, inputs: { ...context.inputs, [key]: true } }), false);
333 }
334 const fresh = structuredClone(context);
335 fresh.inputs.preflight_artifact_prefix = "";
336 assert.equal(condition(job(release, "build"), fresh), true);
337 assert.equal(condition(job(release, "publish"), fresh), false);
338 fresh.needs.build.result = "success";
339 fresh.needs["windows-build"].result = "success";
340 fresh.needs["mac-universal-intel"].result = "success";
341 assert.equal(condition(job(release, "publish"), fresh), false, "unsigned Windows bundles cannot publish");
342 fresh.needs["windows-sign"].result = "success";
343 assert.equal(condition(job(release, "publish"), fresh), false, "signed Windows installers must pass native runtime acceptance");
344 fresh.needs["windows-runtime-acceptance"].result = "success";
345 assert.equal(condition(job(release, "publish"), fresh), true);
346 });
347
348 test("Certum signing preserves native builds and gates publication and attestation", () => {
349 const packageJob = job(ci, "desktop-windows-package");
350 assert.match(packageJob, /test-windows-installer-startup\.ps1/);
351 assert.match(packageJob, /ExpectedVersion v0\.0\.0-ci/);
352 const windowsBuild = job(release, "windows-build");
353 const signer = job(release, "windows-sign");
354 assert.match(windowsBuild, /runner: windows-latest, platform: windows\/amd64/);
355 assert.match(windowsBuild, /runner: windows-11-arm, platform: windows\/arm64/);
356 assert.match(windowsBuild, /Smoke-test packaged Electron startup/);
357 assert.match(windowsBuild, /Upload Windows signing inputs/);
358 assert.match(job(ci, 'test'), /test-windows-installer-startup\.test\.ps1/);
359 assert.match(signer, /needs: \[resolve, windows-build, signing-contract\]/);
360 assert.match(signer, /runs-on: windows-2022/);
361 assert.match(signer, /ref: \$\{\{ github.workflow_sha \}\}/);
362 assert.equal(signer.match(/setup-certum/g)?.length, 1, "both architectures share one Certum session");
363 assert.match(signer, /Finalize amd64 in the shared Certum session/);
364 assert.match(signer, /Finalize arm64 in the shared Certum session/);
365 assert.equal(signer.match(/finalize-windows-signed-candidate\.sh/g)?.length, 2);
366 assert.ok(signer.indexOf("Finalize amd64 in the shared Certum session")
367 < signer.indexOf("name: ${{ needs.resolve.outputs.artifact_prefix }}-windows-amd64"));
368 assert.ok(signer.indexOf("Finalize arm64 in the shared Certum session")
369 < signer.indexOf("name: ${{ needs.resolve.outputs.artifact_prefix }}-windows-arm64"));
370 assert.ok(!release.includes("secrets.SIGNPATH_API_TOKEN"));
371 const runtimeAcceptance = job(release, "windows-runtime-acceptance");
372 assert.match(runtimeAcceptance, /runner: windows-latest, arch: amd64/);
373 assert.match(runtimeAcceptance, /runner: windows-11-arm, arch: arm64/);
374 assert.match(runtimeAcceptance, /test-windows-installer-startup\.ps1/);
375 assert.match(runtimeAcceptance, /ExpectedVersion "\$\{\{ needs\.resolve\.outputs\.version \}\}"/);
376 const attestation = job(release, "attest-signing-contract");
377 assert.ok(!attestation.includes("gh api --method"), "GITHUB_TOKEN cannot mutate repository variables");
378 assert.match(attestation, /uses: actions\/upload-artifact@v7/);
379 assert.match(attestation, /verified-contract\.json/);
380 assert.match(attestation, /gh variable set/);
381 const context = { github: { repository: "esengine/DeepSeek-Reasonix" }, inputs: { signing_preflight: true, orchestrated: false },
382 needs: { "signing-contract": { result: "success" }, build: { result: "success" }, "windows-build": { result: "success" }, "windows-sign": { result: "success" }, "windows-runtime-acceptance": { result: "success" } } };
383 assert.equal(condition(attestation, context), true);
384 for (const key of ["windows-build", "windows-sign", "windows-runtime-acceptance"]) {
385 for (const result of ["failure", "cancelled", "skipped"]) {
386 assert.equal(condition(attestation, { ...context, needs: { ...context.needs, [key]: { result } } }), false);
387 }
388 }
389 });
390
391 test("reuse never moves artifact verification past public mutation or trusts candidate scripts", () => {
392 const publisher = job(release, "publish");
393 assert.ok(publisher.indexOf("Verify complete signed artifact handoff") < publisher.indexOf("name: Publish GitHub release"));
394 assert.ok(publisher.includes("node release-control/scripts/desktop-release-artifacts.mjs collect"));
395 assert.ok(publisher.includes("ref: ${{ github.workflow_sha }}"));
396 assert.ok(!publisher.includes("merge-multiple: true"));
397 const stable = workflow("release-stable");
398 assert.ok(job(stable, "desktop").includes("preflight_artifact_prefix: ${{ needs.signpath-preflight.outputs.artifact_prefix }}"));
399 for (const name of ["desktop", "cli", "npm"]) assert.ok(job(stable, name).includes("needs: [authorize, signpath-preflight]"));
400 });
401
402 test("all desktop consumers verify the prepared build and reject a failed preparation", () => {
403 const context = { github: { event_name: "pull_request" },
404 needs: { changes: { outputs: { desktop: "true" } }, "desktop-prepare": { result: "success" } } };
405 const aggregate = job(ci, "desktop");
406 const verifications = ci.match(/artifact-identity\.mjs verify/g)?.length ?? 0;
407 assert.equal(ci.match(/--attempt "\$\{\{ needs\.desktop-prepare\.outputs\.producer_attempt \}\}"/g)?.length, verifications);
408 assert.equal(ci.match(/test -n "\$\{\{ needs\.desktop-prepare\.outputs\.producer_attempt \}\}"/g)?.length, verifications);
409 for (const [name, variant] of [
410 ["desktop-go", "stable"], ["desktop-frontend", "stable"], ["desktop-browser-group", "stable"],
411 ["desktop-macos", "stable"], ["desktop-windows", "canary"], ["desktop-windows-go-group", "stable"],
412 ]) {
413 const body = job(ci, name);
414 if (["desktop-go", "desktop-frontend"].includes(name)) assert.ok(aggregate.includes(name));
415 assert.ok(body.includes("needs: [changes, desktop-prepare]"));
416 assert.ok(body.includes(`name: \${{ needs.desktop-prepare.outputs.${variant}_artifact_name }}`));
417 assert.ok(body.includes(`--shell electron --channel ${variant}`));
418 assert.ok(body.includes('test -n "${{ needs.desktop-prepare.outputs.producer_attempt }}"'));
419 assert.ok(body.includes('--attempt "${{ needs.desktop-prepare.outputs.producer_attempt }}"'));
420 assert.doesNotMatch(body, /artifact-identity\.mjs verify[^]*?--attempt "\$GITHUB_RUN_ATTEMPT"/);
421 assert.ok(!body.includes("pnpm --dir frontend build"));
422 assert.equal(condition(body, context), true);
423 assert.equal(condition(body, { ...context, needs: { ...context.needs, "desktop-prepare": { result: "failure" } } }), false);
424 }
425 for (const name of ["desktop-windows", "desktop-windows-package"]) {
426 const body = job(ci, name);
427 assert.match(body, /REASONIX_PACKAGE_REUSE_FRONTEND: "1"/);
428 assert.match(body, /REASONIX_FRONTEND_PNPM_VERSION="\$\(pnpm --version\)"\n\s+export REASONIX_FRONTEND_PNPM_VERSION/);
429 assert.match(body, /canary_artifact_name/);
430 }
431 assert.match(job(ci, "desktop-macos"), /REASONIX_FRONTEND_PNPM_VERSION="\$\(pnpm --version\)"\n\s+export REASONIX_FRONTEND_PNPM_VERSION/);
432 const prepare = job(ci, "desktop-prepare");
433 assert.match(prepare, /producer_attempt: \$\{\{ steps\.artifact-identity\.outputs\.attempt \}\}/);
434 assert.match(prepare, /id: artifact-identity\n\s+run: echo "attempt=\$GITHUB_RUN_ATTEMPT" >> "\$GITHUB_OUTPUT"/);
435 assert.match(prepare, /stable_artifact_name: desktop-frontend-stable-\$\{\{ github\.run_id \}\}-\$\{\{ steps\.artifact-identity\.outputs\.attempt \}\}/);
436 assert.equal(prepare.match(/desktop\/frontend\/sourcemaps\/\$\{\{ github\.sha \}\}/g)?.length, 2);
437 });
438
439 test("browser matrix preserves five entry points and fails closed through desktop-browser", () => {
440 const groups = job(ci, "desktop-browser-group");
441 assert.match(groups, /max-parallel: 2/);
442 assert.match(groups, /fail-fast: false/);
443 assert.match(groups, /group: \[app-settings-motion, transcript\]/);
444 assert.match(groups, /REASONIX_TRANSCRIPT_MODE=native-scrollbar REASONIX_LAYOUT_ARTIFACTS="\$evidence\/native-scrollbar"/);
445 assert.match(groups, /REASONIX_TRANSCRIPT_MODE=headless-reader REASONIX_LAYOUT_ARTIFACTS="\$evidence\/headless-reader"/);
446 assert.doesNotMatch(groups, /group: \[app-settings, motion, transcript\]/);
447 for (const command of ["test:app-browser", "test:settings-browser", "test:motion-browser", "test:transcript-browser", "test:transcript-reader-browser"])
448 assert.equal(ci.match(new RegExp(`pnpm --dir frontend ${command}(?:\\s|$)`, "g"))?.length, 1, command);
449 const summary = job(ci, "desktop-browser");
450 assert.match(summary, /needs: \[changes, desktop-prepare, desktop-browser-group\]/);
451 const script = shellStep(summary, "Verify desktop browser groups");
452 const run = env => spawnSync("bash", ["-e", "-c", script], { env: { ...process.env, ...env } }).status;
453 assert.equal(run({ CHANGES_RESULT: "success", SHOULD_RUN: "true", PREPARE_RESULT: "success", GROUP_RESULT: "success" }), 0);
454 for (const result of ["failure", "cancelled", "skipped", ""])
455 assert.notEqual(run({ CHANGES_RESULT: "success", SHOULD_RUN: "true", PREPARE_RESULT: "success", GROUP_RESULT: result }), 0);
456 assert.equal(run({ CHANGES_RESULT: "success", SHOULD_RUN: "false", PREPARE_RESULT: "success", GROUP_RESULT: "skipped" }), 0);
457 });
458
459 test("Desktop race uses every verified partition and one shared cache writer", () => {
460 const body = job(ci, "desktop-go-race");
461 assert.deepEqual(body.match(/group: \[([^\]]+)\]/)[1].split(",").map(value => value.trim()), windowsDesktopGroups);
462 assert.match(body, /fail-fast: false/);
463 assert.match(body, /run: node \.\.\/scripts\/desktop-windows-go-tests\.mjs \$\{\{ matrix.group \}\} --race/);
464 for (const group of windowsDesktopGroups) {
465 const args = windowsDesktopTestArgs(group, true);
466 assert.deepEqual(args.filter(arg => arg !== "-race"), windowsDesktopTestArgs(group));
467 assert.equal(args.filter(arg => arg === "-race").length, 1);
468 }
469 assert.match(body, /matrix.group == 'A-B' && steps.gocache.outputs.key/);
470 assert.match(job(ci, "desktop"), /GO_RACE_RESULT: \$\{\{ needs.desktop-go-race.result \}\}/);
471 });
472
473 test("installer evidence excludes running payloads and cache files on every publisher", () => {
474 for (const body of [job(ci, "desktop-windows-package"), job(release, "windows-runtime-acceptance")]) {
475 const upload = body.match(/name: Upload (?:signed )?Windows installer acceptance evidence\n([\s\S]*?)(?=\n - |$)/)?.[1];
476 assert.ok(upload);
477 for (const extension of ["json", "png", "log"])
478 assert.ok(upload.includes(`reasonix-installer-acceptance/**/*.${extension}`));
479 for (const excluded of ["installed/**", "**/cache/**"])
480 assert.ok(upload.includes(`!\${{ runner.temp }}/reasonix-installer-acceptance/${excluded}`));
481 }
482 });
483
484 test("Windows desktop Go partitions tests without verbose JSON cache overhead", () => {
485 const windowsGo = job(ci, "desktop-windows-go-group");
486 const context = { github: { event_name: "pull_request" }, needs: {
487 "desktop-prepare": { result: "success" }, changes: { outputs: { native: "true" } },
488 } };
489 assert.equal(condition(windowsGo, context), true);
490 assert.equal(condition(windowsGo, { ...context, cancelled: () => true }), false,
491 "superseded Windows workers must release the workflow concurrency slot");
492 assert.match(windowsGo, /run: node \.\.\/scripts\/desktop-windows-go-tests\.mjs \$\{\{ matrix.group \}\}/);
493 const commands = windowsDesktopGroups.map(group => {
494 const args = windowsDesktopTestArgs(group);
495 assert.equal(args[0], "test");
496 assert.equal(args.at(-1), "./...");
497 assert.ok(!args.some(arg => arg.startsWith("-timeout") || arg === "-json" || arg === "-v"));
498 return { run: args.includes("-run") ? args[args.indexOf("-run") + 1] : undefined,
499 skip: args.includes("-skip") ? args[args.indexOf("-skip") + 1] : undefined };
500 });
501 assert.equal(commands.length, windowsDesktopGroups.length);
502 // Include non-test entry points and every possible first suffix character.
503 // The complement group retains names outside the selected ranges.
504 const names = ["Example", "ExampleSession", "FuzzSession", "Test"];
505 for (let code = 0; code <= 127; code++) names.push(`Test${String.fromCharCode(code)}Session`);
506 names.push("Test会话", "TestΩSession", "TestWindowsTerminalProcessConPTYSmoke");
507 for (const name of names) {
508 const owners = commands.filter(command =>
509 (!command.run || new RegExp(command.run).test(name))
510 && (!command.skip || !new RegExp(command.skip).test(name)));
511 if (name === "TestWindowsTerminalProcessConPTYSmoke") {
512 assert.equal(owners.length, 0, `${name} must be isolated from the correctness partition`);
513 continue;
514 }
515 assert.equal(owners.length, 1, `${name} must run in exactly one group`);
516 }
517 assert.doesNotMatch(windowsGo, /go test -json/);
518 assert.doesNotMatch(windowsGo, /go-test-timing/);
519 assert.doesNotMatch(windowsGo, /go test -run ['"]?\^\$/);
520
521 const groups = windowsGo.match(/group: \[([^\]]+)\]/)[1].split(",").map(value => value.trim());
522 assert.deepEqual(groups, windowsDesktopGroups);
523 assert.match(windowsGo, /fail-fast: false/);
524
525 assert.match(windowsGo, /name: probe \(Windows ConPTY host integration\)[\s\S]*?continue-on-error: true[\s\S]*?run: go test -run '\^TestWindowsTerminalProcessConPTYSmoke\$' \./);
526 assert.match(windowsGo, /name: probe \(Windows ConPTY host integration\)\n\s+if: matrix.group == 'T-Z'/);
527 assert.match(windowsGo, /name: test \(vendored systray identity\)\n\s+if: matrix.group == 'T-Z'/);
528 assert.match(windowsGo, /steps\.conpty-smoke\.outcome == 'failure'/);
529 });
530
531 test("Windows desktop Go aggregate rejects incomplete matrix results", () => {
532 const summary = job(ci, "desktop-windows-go");
533 assert.match(summary, /needs: \[changes, desktop-prepare, desktop-windows-go-group\]/);
534 const script = shellStep(summary, "Verify Windows desktop Go groups");
535 const success = { CHANGES_RESULT: "success", SHOULD_RUN: "true", PREPARE_RESULT: "success", GROUP_RESULT: "success" };
536 const run = patch => spawnSync("bash", ["-e", "-c", script], { env: { ...process.env, ...success, ...patch } }).status;
537 assert.equal(run({}), 0);
538 for (const key of ["CHANGES_RESULT", "PREPARE_RESULT", "GROUP_RESULT"])
539 for (const result of ["failure", "cancelled", "skipped", ""])
540 assert.notEqual(run({ [key]: result }), 0, `${key}=${result}`);
541 assert.equal(run({ SHOULD_RUN: "false", PREPARE_RESULT: "skipped", GROUP_RESULT: "skipped" }), 0);
542 assert.notEqual(run({ SHOULD_RUN: "false" }), 0);
543 });
544
544 lines Plain Text