From 6e6d8f42a714ab1470a782b493301d6a3274b673 Mon Sep 17 00:00:00 2001 From: 0x4007 <4975670+0x4007@users.noreply.github.com> Date: Fri, 13 Nov 2026 04:39:55 +0000 Subject: [PATCH 1/2] fix(test): scope replay artifacts and route Darwin fixtures --- .github/workflows/deno-deploy.yml | 2 +- scripts/responses-conformance-codex.ts | 120 ++++++++++++++++++++++ scripts/test-runner-prelaunch.test.ts | 22 +++- scripts/test-runner.ts | 35 ++++--- scripts/test-scope.test.ts | 68 +++++++++++++ scripts/test-scope.ts | 133 +++++++++++++++++++++++-- 6 files changed, 355 insertions(+), 25 deletions(-) create mode 100644 scripts/responses-conformance-codex.ts diff --git a/.github/workflows/deno-deploy.yml b/.github/workflows/deno-deploy.yml index 76080b0d0..7be286619 100644 --- a/.github/workflows/deno-deploy.yml +++ b/.github/workflows/deno-deploy.yml @@ -443,7 +443,7 @@ jobs: shell: bash run: | set -euo pipefail - bun --no-env-file --no-install run test tests/gpg-dsh.test.ts "--native-kv-binding=$(cat "$RUNNER_TEMP/uos-native-kv/receipt.json")" + bun --no-env-file --no-install scripts/test-runner.ts darwin "--native-kv-binding=$(cat "$RUNNER_TEMP/uos-native-kv/receipt.json")" verify-artifact: needs: [scope, validate, native, mac-containment, darwin-native] diff --git a/scripts/responses-conformance-codex.ts b/scripts/responses-conformance-codex.ts new file mode 100644 index 000000000..c831c453c --- /dev/null +++ b/scripts/responses-conformance-codex.ts @@ -0,0 +1,120 @@ +import assert from "node:assert/strict"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { isAbsolute, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { runCodexConformance } from "../tests/helpers/responses-conformance-client.ts"; +import { initializeTestNativeKv, verifiedTestNativeKvBinding, type TestNativeKvBinding } from "./test-native-kv.ts"; +import { testNetworkArgv } from "./test-network.ts"; + +type Options = Readonly<{ + executable: string; + expectedSha256: string; + expectedVersion: string; + captureDirectory: string; + nativeBinding: TestNativeKvBinding; +}>; + +const parseOptions = (args: readonly string[]): Options => { + const names = ["codex-executable", "codex-sha256", "codex-version", "capture-directory", "native-kv-binding"]; + const values = new Map(); + for (const argument of args) { + const match = /^--([^=]+)=(.+)$/.exec(argument); + if (!match || !names.includes(match[1]) || values.has(match[1])) throw new Error("Pass each supported --name=value option exactly once"); + values.set(match[1], match[2]); + } + const required = (name: string): string => { + const value = values.get(name); + if (!value) throw new Error("Missing --" + name); + return value; + }; + const executable = required("codex-executable"); + const captureDirectory = required("capture-directory"); + assert(isAbsolute(executable) && isAbsolute(captureDirectory), "Executable and fresh capture directory must be absolute"); + const expectedSha256 = required("codex-sha256"); + const expectedVersion = required("codex-version"); + assert(/^[a-f0-9]{64}$/.test(expectedSha256), "An independently pinned Codex SHA256 is required"); + assert(/^\d+\.\d+\.\d+$/.test(expectedVersion), "An exact Codex CLI version is required"); + return { executable, expectedSha256, expectedVersion, captureDirectory, nativeBinding: verifiedTestNativeKvBinding(required("native-kv-binding")) }; +}; + +async function replay(options: Options): Promise { + initializeTestNativeKv(options.nativeBinding); + const { startResponsesConformanceFixture } = await import("../tests/helpers/responses-conformance-fixture.ts"); + const fixture = await startResponsesConformanceFixture({ holdToolDelta: false, captureDirectory: join(options.captureDirectory, "gateway") }); + try { + const native = await runCodexConformance({ + executable: options.executable, + expectedSha256: options.expectedSha256, + expectedVersion: options.expectedVersion, + endpoint: fixture.endpoint, + token: fixture.token, + captureDirectory: join(options.captureDirectory, "native"), + onTextDelta: (event) => { + fixture.acknowledge(2, event); + }, + }); + if (fixture.terminals.length < 2) await fixture.journal.wait((entry) => entry.kind === "request_terminal" && fixture.terminals.length === 2); + assert.equal(fixture.calls.length, 2, "One tool turn and one coherent continuation must dispatch exactly twice"); + assert.equal(fixture.terminals.length, 2); + await fixture.save(native); + } finally { + await fixture.stop(); + } + const reopened = fixture.verifyReopenedLedger(); + assert.equal(reopened.outcome, "verified"); + console.log( + JSON.stringify({ + kind: "responses_conformance_codex", + status: "passed", + captureDirectory: options.captureDirectory, + dispatches: fixture.calls.length, + durableReopen: reopened.outcome, + }) + ); +} + +async function isolated(options: Options, args: readonly string[]): Promise { + // mkdir without recursive reuse preserves every earlier transcript and database. + await mkdir(options.captureDirectory, { mode: 0o700 }); + const command = testNetworkArgv([process.execPath, "--no-env-file", "--no-install", fileURLToPath(import.meta.url), "--fixture-child", ...args]); + const env: Record = { PATH: process.env.PATH ?? "/usr/bin:/bin", TMPDIR: options.captureDirectory, BUN_RUNTIME_TRANSPILER_CACHE_PATH: "0" }; + if (process.env.HOME) env.HOME = process.env.HOME; + const child = Bun.spawn(command, { env, stdin: "ignore", stdout: "pipe", stderr: "pipe" }); + const stop = (): void => { + if (child.exitCode === null) child.kill("SIGTERM"); + }; + process.once("SIGINT", stop); + process.once("SIGTERM", stop); + const timer = setTimeout(stop, 60_000); + try { + const [code, stdout, stderr] = await Promise.all([child.exited, new Response(child.stdout).text(), new Response(child.stderr).text()]); + await Promise.all([writeFile(join(options.captureDirectory, "stdout.log"), stdout), writeFile(join(options.captureDirectory, "stderr.log"), stderr)]); + assert.equal(code, 0, "Synthetic native replay failed; inspect its capture directory"); + const raw: unknown = JSON.parse(await readFile(join(options.captureDirectory, "gateway/raw.json"), "utf8")); + assert(raw && typeof raw === "object" && "storage" in raw, "Durable source fixture evidence is required"); + console.log(stdout.trim()); + } finally { + clearTimeout(timer); + process.off("SIGINT", stop); + process.off("SIGTERM", stop); + if (child.exitCode === null) { + child.kill("SIGKILL"); + await child.exited; + } + } +} + +if (import.meta.main) { + try { + assert.equal(Bun.version, "1.4.0", "Use the prepared pinned Bun runtime"); + const args = process.argv.slice(2); + const child = args[0] === "--fixture-child"; + if (child) args.shift(); + const options = parseOptions(args); + if (child) await replay(options); + else await isolated(options, args); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; + } +} diff --git a/scripts/test-runner-prelaunch.test.ts b/scripts/test-runner-prelaunch.test.ts index bfe5f9b9b..fe60e207e 100644 --- a/scripts/test-runner-prelaunch.test.ts +++ b/scripts/test-runner-prelaunch.test.ts @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { test } from "bun:test"; import { realpathSync } from "node:fs"; import { fileURLToPath } from "node:url"; -import { testFileArgv } from "./test-runner.ts"; +import { groupsFor, testFileArgv, testFileSupportsPlatform } from "./test-runner.ts"; import { liveDestination } from "./test-network.ts"; const controller = realpathSync(fileURLToPath(new URL("../tests/private-bun-copy-confinement.test.ts", import.meta.url))); @@ -24,3 +24,23 @@ test("a live-policy request cannot opt the trusted controller out of its network const policy = { live: liveDestination("http://127.0.0.1:12345") }; assert.deepEqual(testFileArgv(controller, command, policy, "darwin", wrapped), wrapped(command)); }); + +test("the affected Darwin segment retains real private-copy execution without widening named files", () => { + const path = "tests/private-bun-copy-confinement.test.ts"; + const selection = { paths: [path], documentationOnly: false, affectedTests: [path], runAll: false, reason: "selected" as const }; + assert.equal(testFileSupportsPlatform(path, "linux"), false); + assert.equal(testFileSupportsPlatform(path, "darwin"), true); + assert.deepEqual(groupsFor("darwin", [], "linux", selection), []); + assert.deepEqual( + groupsFor("darwin", [], "darwin", selection).flatMap((group) => group.files), + [path] + ); + assert.deepEqual( + groupsFor("main", ["tests/gpg-dsh.test.ts"], "darwin", selection).flatMap((group) => group.files), + ["tests/gpg-dsh.test.ts"] + ); + assert.equal( + groupsFor("main", [], "linux", selection).some((group) => group.files.includes(path)), + false + ); +}); diff --git a/scripts/test-runner.ts b/scripts/test-runner.ts index e0f2694b0..f3bedbae1 100644 --- a/scripts/test-runner.ts +++ b/scripts/test-runner.ts @@ -4,7 +4,7 @@ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; import { fileURLToPath } from "node:url"; import { LIVE_E2E_DEFAULT_BASE_URL, liveDestination, testNetworkArgv, type TestNetworkPolicy } from "./test-network.ts"; import { createTestNativeKvPreload, prepareTestNativeKv, verifiedTestNativeKvBinding, type TestNativeKvBinding } from "./test-native-kv.ts"; -import { detectTestSelection, formatScopeMessage, testRequiresNativeKv } from "./test-scope.ts"; +import { detectTestSelection, formatScopeMessage, testRequiresNativeKv, type TestSelection } from "./test-scope.ts"; const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); let interrupted = false; @@ -69,6 +69,9 @@ if (!TEST_SELECTION.runAll && TEST_SELECTION.affectedTests.length === 0) { process.exit(0); } type Group = Readonly<{ profile: string; files: readonly string[]; environment: readonly string[]; network?: "live" }>; +export const testFileSupportsPlatform = (path: string, platform: NodeJS.Platform = process.platform): boolean => + (path !== "tests/private-bun-copy-confinement.test.ts" || platform === "darwin") && + (path !== "tests/native-authority-workerd.test.ts" || platform === "linux"); const filesBelow = (directory: string): string[] => { const found: string[] = []; for (const entry of readdirSync(join(ROOT, directory), { withFileTypes: true })) { @@ -94,16 +97,15 @@ const quality = (): Group => ], ["PATH"] ); -const allGroupsFor = (segment: string, explicitFiles: readonly string[]): Group[] => { +const allGroupsFor = (segment: string, explicitFiles: readonly string[], platform: NodeJS.Platform): Group[] => { switch (segment) { + case "darwin": + return platform === "darwin" ? [group("main", ["tests/gpg-dsh.test.ts", "tests/private-bun-copy-confinement.test.ts"], PROVIDER_ENV)] : []; case "main": return [ group( "main", - filesBelow("tests").filter( - (path) => - !SPECIAL_MAIN_FILES.has(path.split("/").at(-1) ?? "") && (path !== "tests/native-authority-workerd.test.ts" || process.platform === "linux") - ), + filesBelow("tests").filter((path) => !SPECIAL_MAIN_FILES.has(path.split("/").at(-1) ?? "") && testFileSupportsPlatform(path, platform)), PROVIDER_ENV ), group( @@ -120,7 +122,7 @@ const allGroupsFor = (segment: string, explicitFiles: readonly string[]): Group[ case "vps": return [ group("vps", [ - ...(process.platform === "linux" ? ["ops/tests/vps-release.test.ts"] : []), + ...(platform === "linux" ? ["ops/tests/vps-release.test.ts"] : []), "ops/tests/release-retention.test.ts", "ops/tests/vps-release-driver.test.ts", "ops/tests/vps-release-recovery.test.ts", @@ -148,7 +150,7 @@ const allGroupsFor = (segment: string, explicitFiles: readonly string[]): Group[ ["SENTINEL_REPLAY_KEY"] ), group("sentinel-upstream", ["tests/sentinel-upstream-providers.test.ts"], ["SENTINEL_REPLAY_KEY"]), - ...(process.platform === "linux" + ...(platform === "linux" ? [group("sentinel-consumer", ["tests/sentinel-replay-consumer.test.ts"], ["DENO_DIR", "DENO_NO_UPDATE_CHECK", "PATH", "HOME"])] : []), ]; @@ -169,10 +171,15 @@ const segmentHints = new Map([ ["tests/live-default-model.e2e.ts", "e2e"], ["tests/vps-deploy-guards.test.ts", "deploy-guards"], ]); -export const groupsFor = (segment: string, explicitFiles: readonly string[]): Group[] => { - const groups = allGroupsFor(segment, explicitFiles); - if (explicitFiles.length === 0 && !TEST_SELECTION.runAll) { - const affected = new Set(TEST_SELECTION.affectedTests); +export const groupsFor = ( + segment: string, + explicitFiles: readonly string[], + platform: NodeJS.Platform = process.platform, + selection: TestSelection = TEST_SELECTION +): Group[] => { + const groups = allGroupsFor(segment, explicitFiles, platform); + if (explicitFiles.length === 0 && !selection.runAll) { + const affected = new Set(selection.affectedTests); return groups.map((item) => ({ ...item, files: item.files.filter((path) => affected.has(path)) })).filter((item) => item.files.length !== 0); } if (explicitFiles.length === 0 || segment === "files") return groups; @@ -361,7 +368,9 @@ const portableGroups = (groups: readonly Group[]): Group[] => groups .map((item) => ({ ...item, - files: item.files.filter((path) => !PORTABLE_EXCLUDED.some((pattern) => pattern.test(path)) && !testRequiresNativeKv(path)), + files: item.files.filter( + (path) => testFileSupportsPlatform(path) && !PORTABLE_EXCLUDED.some((pattern) => pattern.test(path)) && !testRequiresNativeKv(path) + ), })) .filter((item) => item.files.length !== 0); const resolveNativeTestArtifacts = async (needsNativeKv: boolean, nativeBinding?: string) => { diff --git a/scripts/test-scope.test.ts b/scripts/test-scope.test.ts index af20e1f93..46560c895 100644 --- a/scripts/test-scope.test.ts +++ b/scripts/test-scope.test.ts @@ -880,3 +880,71 @@ describe("private-copy trusted controller source selection", () => { expect(selectAffectedTestFiles([producer], [controller + ".other"], create())).toEqual([]); }); }); + +describe("separate research and CMake component inputs", () => { + const create = (reader?: string) => { + const root = mkdtempSync(join(fixture, "component-scope-")); + for (const path of ["src", "tests", "docs/research", "cpp"]) mkdirSync(join(root, path), { recursive: true }); + writeFileSync(join(root, "docs/research/selector-fixture.json"), "{}\n"); + writeFileSync(join(root, "docs/research/selector-fixture.py"), "print('fixture')\n"); + writeFileSync(join(root, "cpp/selector-fixture.ts"), "export const fixture = 1;\n"); + if (reader) writeFileSync(join(root, "src/reader.ts"), reader); + writeFileSync(join(root, "tests/reader.test.ts"), "import '../src/reader.ts';\n"); + return root; + }; + test("unconsumed research JSON and Python are documentation, while real config stays conservative", () => { + const root = create(); + expect(classifyChangedPaths(["docs/research/selector-fixture.json", "docs/research/selector-fixture.py"], root).documentationOnly).toBe(true); + expect(classifyChangedPaths(["package.json"], root).documentationOnly).toBe(false); + expect(classifyChangedPaths(["docs/openapi.json"], root).documentationOnly).toBe(false); + }); + test("real imports and literal file reads preserve research runtime inputs, including deleted data", () => { + const json = "../docs/research/selector-fixture.json"; + const python = "../docs/research/selector-fixture.py"; + const imported = create(`import value from ${JSON.stringify(json)};\nexport {value};\n`); + expect(classifyChangedPaths(["docs/research/selector-fixture.json"], imported).documentationOnly).toBe(false); + const read = create(`import {readFileSync} from 'node:fs';\nreadFileSync(new URL(${JSON.stringify(python)},import.meta.url));\n`); + expect(classifyChangedPaths(["docs/research/selector-fixture.py"], read).documentationOnly).toBe(false); + rmSync(join(read, "docs/research/selector-fixture.py")); + expect(classifyChangedPaths(["docs/research/selector-fixture.py"], read).documentationOnly).toBe(false); + }); + test("a quoted fixture program is not an executing runtime reference", () => { + const program = "readFileSync(new URL('../docs/research/selector-fixture.json',import.meta.url));"; + const root = create(`export const example = ${JSON.stringify(program)};\n`); + expect(classifyChangedPaths(["docs/research/selector-fixture.json"], root).documentationOnly).toBe(true); + }); + test("a repository-root URL is not a component input, while an explicit component directory remains one", () => { + const relativeRoot = "../"; + const root = create(`export const root = new URL(${JSON.stringify(relativeRoot)},import.meta.url);\n`); + expect(classifyChangedPaths(["docs/research/selector-fixture.json"], root).documentationOnly).toBe(true); + const directory = "../docs/research/"; + const reader = create(`export const data = new URL(${JSON.stringify(directory)},import.meta.url);\n`); + expect(classifyChangedPaths(["docs/research/selector-fixture.json"], reader).documentationOnly).toBe(false); + }); + test("standalone CPP stays out of Bun gates until an actual Bun consumer is introduced", () => { + const path = "cpp/selector-fixture.ts"; + const native = "cpp/src/selector-fixture.cpp"; + const selection: TestSelection = { paths: [native], documentationOnly: false, affectedTests: [], runAll: false, reason: "no-dependent-tests" }; + expect(ciScopeForSelection(selection, create(), {}).needsTypeScriptValidate).toBe(false); + expect(formatScopeMessage(selection, "bun test")).toContain("committed CMake/CTest presets"); + const adapter = { ...selection, paths: ["cpp/tests/selector-fixture.ts"] }; + expect(ciScopeForSelection(adapter, create(), {}).needsTypeScriptValidate).toBe(true); + expect(formatScopeMessage(adapter, "bun test")).toContain("strict no-emit checks"); + const specifier = "../cpp/selector-fixture.ts"; + const root = create(`import {fixture} from ${JSON.stringify(specifier)};\nexport {fixture};\n`); + const affectedTests = selectAffectedTestFiles([path], ["tests/reader.test.ts"], root); + expect(affectedTests).toEqual(["tests/reader.test.ts"]); + expect(ciScopeForSelection({ ...selection, paths: [path], affectedTests, reason: "selected" }, root, {}).needsTypeScriptValidate).toBe(true); + const literal = create(`import {readFileSync} from 'node:fs';\nreadFileSync(new URL(${JSON.stringify("../" + native)},import.meta.url));\n`); + expect(ciScopeForSelection(selection, literal, {}).needsTypeScriptValidate).toBe(true); + }); + test("an affected private-copy controller selects the actual Darwin job", () => { + const path = "tests/private-bun-copy-confinement.test.ts"; + expect( + ciScopeForSelection({ paths: [path], documentationOnly: false, affectedTests: [path], runAll: false, reason: "selected" }, create(), {}) + ).toMatchObject({ + needsDarwinTests: true, + needsDarwinNative: true, + }); + }); +}); diff --git a/scripts/test-scope.ts b/scripts/test-scope.ts index 85e80a057..c9cbc7e01 100644 --- a/scripts/test-scope.ts +++ b/scripts/test-scope.ts @@ -1,5 +1,5 @@ import { spawnSync } from "node:child_process"; -import { existsSync, readFileSync, statSync } from "node:fs"; +import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; import { dirname, extname, isAbsolute, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -16,6 +16,7 @@ const ALIASES = new Map([ const UNTRACKED_ROOTS = ["src/", "scripts/", "tests/", "ops/", "benchmarks/", "tools/", "static/", "rust/"]; const PRIVATE_COPY_CONTROLLER = "tests/private-bun-copy-confinement.test.ts"; const PRIVATE_COPY_PRODUCT = "tools/cloudflare/private-bun-copy.ts"; +const RESEARCH_ARTIFACT = /^docs\/research\/.+\.(?:json|py)$/i; const WORKER_RUNTIME_TEST = "tests/native-authority-workerd.test.ts"; const WORKER_RUNTIME_INPUT = /^(?:tests\/fixtures\/native-authority\/|tools\/worker-test-runtime\/|scripts\/prepare-worker-test-runtime\.ts$)/; const WORKER_RUNTIME_FIXTURES = ["tests/fixtures/native-authority/fixture-worker.mjs", "tests/fixtures/native-authority/bun-client.ts"]; @@ -117,10 +118,14 @@ export const RUST_NATIVE_ONLY_PACKAGES = [ ] as const; const WORKFLOW_CONTRACT_PATH = ".github/workflows/deno-deploy.yml"; -export const classifyChangedPaths = (paths: readonly string[]): ChangeScope => { +const classifyPaths = (paths: readonly string[], separate: ReadonlySet): ChangeScope => { const normalized = uniqueSorted(paths.map(normalizePath).filter(Boolean)); - return { paths: normalized, documentationOnly: normalized.length > 0 && normalized.every(isDocumentationPath) }; + return { + paths: normalized, + documentationOnly: normalized.length > 0 && normalized.every((path) => isDocumentationPath(path) || (RESEARCH_ARTIFACT.test(path) && separate.has(path))), + }; }; +export const classifyChangedPaths = (paths: readonly string[], root = ROOT): ChangeScope => classifyPaths(paths, separateComponentPaths(paths, root)); const resolveImportedPath = (from: string, specifier: string, root: string): string | null => { const clean = specifier.split(/[?#]/, 1)[0] ?? ""; @@ -175,6 +180,101 @@ const testTouchesChangedPath = (testPath: string, changedPaths: ReadonlySet { + const files: string[] = []; + const visit = (directory: string): void => { + if (!existsSync(directory)) return; + for (const entry of readdirSync(directory, { withFileTypes: true })) { + if (["node_modules", ".git", ".data", ".codex-worktrees", "vendor", "target"].includes(entry.name)) continue; + const path = resolve(directory, entry.name); + if (entry.isDirectory()) visit(path); + else if (entry.isFile() && SOURCE_FILE.test(entry.name)) files.push(path); + } + }; + for (const entry of readdirSync(root, { withFileTypes: true })) { + if (entry.isFile() && SOURCE_FILE.test(entry.name)) files.push(resolve(root, entry.name)); + } + for (const directory of ["src", "scripts", "ops", "tests", "tools", "benchmarks", "static", ".github"]) visit(resolve(root, directory)); + return files; +}; + +const commentEnd = (source: string, index: number): number => { + const pair = source.slice(index, index + 2); + if (pair !== "//" && pair !== "/*") return index; + const close = pair === "//" ? "\n" : "*/"; + const end = source.indexOf(close, index + 2); + return end < 0 ? source.length : end + close.length; +}; +const runtimeLiteralPrefix = (source: string, index: number): boolean => { + let end = index; + while (end > 0 && /\s/.test(source[end - 1])) end--; + if (source[end - 1] === "(") end--; + while (end > 0 && /\s/.test(source[end - 1])) end--; + let start = end; + while (start > 0 && /[\w.]/.test(source[start - 1])) start--; + const name = source.slice(start, end).split(".").at(-1); + return name !== undefined && ["from", "import", "require", "URL", "file", "readFile", "readFileSync", "readTextFile", "readTextFileSync"].includes(name); +}; +const literalReferences = (file: string, root: string): string[] => { + const source = readFileSync(file, "utf8"); + const references: string[] = []; + let index = 0; + while (index < source.length) { + const afterComment = commentEnd(source, index); + if (afterComment !== index) { + index = afterComment; + continue; + } + const quote = source[index]; + if (!["'", '"', "`"].includes(quote)) { + index++; + continue; + } + const start = index++; + let value = ""; + while (index < source.length && source[index] !== quote) { + if (source[index] === "\\") index++; + value += source[index++]; + } + index++; + if (runtimeLiteralPrefix(source, start)) references.push(resolve(dirname(file), value), resolve(root, value)); + } + return references; +}; +const referencesComponent = (reference: string, target: string, component: string): boolean => + reference === target || (!extname(reference) && (reference === component || reference.startsWith(component + "/")) && target.startsWith(reference + "/")); + +/** Research records and the CMake pilot are separate unless a Bun consumer reaches them. */ +const separateComponentPaths = (paths: readonly string[], root: string): ReadonlySet => { + const candidates = uniqueSorted(paths.map(normalizePath).filter((path) => RESEARCH_ARTIFACT.test(path) || path.startsWith("cpp/"))); + if (!candidates.length) return new Set(); + const targets = candidates.map((path) => ({ + path, + absolute: resolve(root, path), + component: resolve(root, path.startsWith("cpp/") ? "cpp" : "docs/research"), + })); + const consumed = new Set(); + const visited = new Set(); + const pending = runtimeSourceFiles(root); + while (pending.length) { + const file = pending.pop(); + if (!file || visited.has(file) || !SOURCE_FILE.test(file)) continue; + visited.add(file); + const dependencies = dependenciesOf(file, root); + const references = literalReferences(file, root); + for (const target of targets) { + if ( + file === target.absolute || + dependencies.includes(target.absolute) || + references.some((reference) => referencesComponent(reference, target.absolute, target.component)) + ) + consumed.add(target.path); + } + pending.push(...dependencies); + } + return new Set(candidates.filter((path) => !consumed.has(path))); +}; + export const selectAffectedTestFiles = (changedPaths: readonly string[], candidateTests: readonly string[], root = ROOT): string[] => { const changed = new Set(changedPaths.map(normalizePath)); // The Node harness reads/bundles these files instead of importing them into Bun. @@ -228,16 +328,19 @@ const candidateTestFiles = (changedPaths: readonly string[]): string[] => export const detectTestSelection = (): TestSelection => { const paths = changedPathsFromGit(); if (paths === null) return { paths: [], documentationOnly: false, affectedTests: [], runAll: true, reason: "no-base" }; - const scope = classifyChangedPaths(paths); + const separate = separateComponentPaths(paths, ROOT); + const scope = classifyPaths(paths, separate); if (scope.paths.length === 0) return { ...scope, affectedTests: [], runAll: true, reason: "no-changes" }; if (scope.documentationOnly) return { ...scope, affectedTests: [], runAll: false, reason: "documentation-only" }; - const changedCode = scope.paths.filter((path) => !isDocumentationPath(path) && !isRustPath(path) && path !== WORKFLOW_CONTRACT_PATH); + const changedCode = scope.paths.filter((path) => !isDocumentationPath(path) && !isRustPath(path) && !separate.has(path) && path !== WORKFLOW_CONTRACT_PATH); const candidates = candidateTestFiles(scope.paths); const dependencyScoped = changedCode.every((path) => SOURCE_FILE.test(path) || isTestPath(path)); // The workflow is a readFile dependency of this contract test, not a TS import. const workflowTests = scope.paths.includes(WORKFLOW_CONTRACT_PATH) ? ["scripts/test-scope.test.ts"] : []; - const affectedTests = uniqueSorted([...(dependencyScoped ? selectAffectedTestFiles(changedCode, candidates) : candidates), ...workflowTests]); + // Retain the existing explicit Darwin signing gate for its native/profile inputs. + const darwinTests = scope.paths.some((path) => DARWIN_PATH.test(path)) && candidates.includes("tests/gpg-dsh.test.ts") ? ["tests/gpg-dsh.test.ts"] : []; + const affectedTests = uniqueSorted([...(dependencyScoped ? selectAffectedTestFiles(changedCode, candidates) : candidates), ...workflowTests, ...darwinTests]); if (affectedTests.length === 0) return { ...scope, affectedTests, runAll: false, reason: "no-dependent-tests" }; return { ...scope, affectedTests, runAll: false, reason: dependencyScoped ? "selected" : "config" }; }; @@ -275,14 +378,16 @@ export const ciScopeForSelection = (selection: TestSelection, root = ROOT, envir const rustPaths = selection.paths.filter(isRustGatePath); const nativeOnly = rustPaths.length > 0 && rustPaths.every((path) => RUST_NATIVE_ONLY_PACKAGES.some((name) => path.startsWith(`rust/crates/${name}/`))); const needsRustWasm = needsRustNative && (completeCheckSet || selection.runAll || !nativeOnly); - const hasTypeScriptWork = selection.paths.some((path) => !isDocumentationPath(path) && !isRustPath(path)); + const separate = separateComponentPaths(selection.paths, root); + const hasTypeScriptWork = selection.paths.some((path) => SOURCE_FILE.test(path) || (!isDocumentationPath(path) && !isRustPath(path) && !separate.has(path))); const needsNativeKv = selection.runAll || selection.paths.some((path) => NATIVE_PATH.test(path)) || (!docsOnly && selection.affectedTests.some((path) => testRequiresNativeKv(path, root))); const needsArtifact = completeCheckSet || (!docsOnly && selection.paths.some((path) => RELEASE_PATH.test(path))); const needsWorkerRuntime = workerRuntimeTestExists(root) && (selection.runAll || selection.affectedTests.includes(WORKER_RUNTIME_TEST)); - const needsDarwinTests = !docsOnly && (selection.runAll || selection.paths.some((path) => DARWIN_PATH.test(path))); + const needsDarwinTests = + !docsOnly && (selection.runAll || selection.paths.some((path) => DARWIN_PATH.test(path)) || selection.affectedTests.includes(PRIVATE_COPY_CONTROLLER)); return { ...selection, completeCheckSet, @@ -332,11 +437,19 @@ export const formatScopeMessage = (selection: TestSelection, command: string): s "Required checks for this change:", " - Format only the changed files.", " - Run `git diff --check`.", - " - Do not run unrelated tests, lint, Knip, native builds, or deployment suites.", + ...(selection.paths.some((path) => SOURCE_FILE.test(path)) + ? [" - Retain changed-source lint, formatting, and strict no-emit checks; do not run unrelated test suites."] + : [" - Do not run unrelated tests, lint, Knip, native builds, or deployment suites."]), "Override only for an explicitly requested test run: TEST_SCOPE_FORCE=1", ...(selection.paths.some(isRustGatePath) ? ["Rust checks are separate: bun --no-env-file --no-install scripts/rust-verify.ts", "This Bun test selection is not Rust verification."] : []), + ...(selection.paths.some((path) => path.startsWith("cpp/")) + ? [ + "C++ checks are separate: run the committed CMake/CTest presets and required local receipts.", + "This Bun test selection does not validate the C++ pilot.", + ] + : []), "Exit code: 0", ].join("\n"); }; @@ -360,5 +473,5 @@ if (import.meta.main) { const selection = detectTestSelection(); if (selection.runAll) process.exit(10); console.log(formatScopeMessage(selection, "bun run verify")); - process.exit(selection.affectedTests.length === 0 ? 0 : 10); + process.exit(selection.affectedTests.length === 0 && !selection.paths.some((path) => SOURCE_FILE.test(path)) ? 0 : 10); } From 6a1d579e82c878abc6b6605265f0af9a476997fb Mon Sep 17 00:00:00 2001 From: 0x4007 <4975670+0x4007@users.noreply.github.com> Date: Fri, 13 Nov 2026 04:46:02 +0000 Subject: [PATCH 2/2] fix(git): ignore local env backups and preserve templates --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index 8a5e68b16..2dae86175 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,11 @@ .env +.env.* +!.env.example +!.env.sample +!.env.template +!.env.*.example +!.env.*.sample +!.env.*.template .data/ .codex-worktrees/ .kv-migration/