Merge master and preserve native handoff recovery

Keep upstream provider-specific admission while retaining mutable handoff target identity. Integrate the current base so normal pull-request CI can run.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-30 22:47:36 -05:00
commit 5b7b6aaeab
346 files changed
+16702 -1039

No files matched your search

@@ -1,7 +1,7 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createHash } from 'node:crypto';
import { validateReleaseRun, selectReleaseArtifacts, validateReleaseImageBase, validateImageBindings, decodeReleaseJson } from '../prepare-runner-release-image.mjs';
import { validateReleaseRun, selectReleaseArtifacts, validateReleaseImageBase, validateImageBindings, decodeReleaseJson, decodeReleaseKimiClosures } from '../prepare-runner-release-image.mjs';
const source = 'a'.repeat(40);
const run = { head_sha: source, conclusion: 'success', path: '.github/workflows/release-smoke.yml', repository: { full_name: 'paperclipai/paperclip' } };
@@ -81,3 +81,16 @@ test('frozen release JSON must be the bounded requested source file', () => {
assert.throws(() => decodeReleaseJson({ ...file, ...patch }, path));
}
});
test('frozen Kimi closure data is decoded without executing release source', () => {
const path = 'packages/paperclip-runner/src/drivers/acpx/kimi-closure-pins.ts';
const source = `throw new Error("must not execute");\nexport const KIMI_DISTRIBUTION_CLOSURE_SHA256 = Object.freeze({ "darwin-arm64": "${'a'.repeat(64)}", "linux-x64": "${'b'.repeat(64)}", });`;
const bytes = Buffer.from(source), file = { type: 'file', path, encoding: 'base64', size: bytes.length, content: bytes.toString('base64') };
assert.deepEqual(decodeReleaseKimiClosures(file, path), { 'darwin-arm64': 'a'.repeat(64), 'linux-x64': 'b'.repeat(64) });
for (const patch of [{ path: 'other.ts' }, { type: 'symlink' }, { size: bytes.length + 1 }, { size: 1024 * 1024 + 1 },
{ content: Buffer.from(source.replace('linux-x64', 'linux-arm64')).toString('base64') }]) {
assert.throws(() => decodeReleaseKimiClosures({ ...file, ...patch }, path));
}
const duplicate = Buffer.from(source + source);
assert.throws(() => decodeReleaseKimiClosures({ ...file, content: duplicate.toString('base64'), size: duplicate.length }, path));
});
@@ -1,6 +1,6 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
@@ -553,6 +553,136 @@ test("shared-database and unsharded chat runs retain one serial partition", () =
{ PAPERCLIP_TEST_DATABASE_URL: "" }).chatVitestShards, [{ index: 0, count: 1 }]);
});
test("default chat isolation covers six partitions without dropping native suites", () => {
for (const args of [[], ["--mode", "general"]]) {
for (const workflow of ["PR", "Release", ""]) {
const env = { GITHUB_WORKFLOW: workflow, PAPERCLIP_TEST_DATABASE_URL: "" };
const plan = dryRunJson(args, env);
const full = dryRunJson(["--mode", "general", "--group", "general-server",
"--shard-index", "0", "--shard-count", "1"], env);
assert.deepEqual(plan.chatVitestShards, Array.from({ length: 6 }, (_, index) => ({ index, count: 6 })));
assert.ok(!plan.selectedGeneralServerSuites.includes(chatSuitePath));
assert.equal(new Set(plan.selectedGeneralServerSuites).size, plan.selectedGeneralServerSuites.length);
assert.deepEqual([...plan.selectedGeneralServerSuites, chatSuitePath].sort(), full.selectedGeneralServerSuites.sort());
}
}
});
test("default chat isolation leaves explicit groups and shared databases unchanged", () => {
for (const args of [[], ["--mode", "general"]]) {
const shared = dryRunJson(args, { PAPERCLIP_TEST_DATABASE_URL: "postgresql://shared/test", GITHUB_WORKFLOW: "PR" });
assert.equal(shared.chatVitestShards, null);
assert.equal(shared.selectedGeneralServerSuites, null);
}
const full = dryRunJson(["--mode", "general", "--group", "general-server",
"--shard-index", "0", "--shard-count", "1"], { PAPERCLIP_TEST_DATABASE_URL: "", GITHUB_WORKFLOW: "PR" });
assert.equal(full.chatVitestShards, null);
assert.ok(full.selectedGeneralServerSuites.includes(chatSuitePath));
assert.ok(full.selectedGeneralServerSuites.includes(nativeRunnerSuitePath));
assert.deepEqual(dryRunJson(["--mode", "general", "--group", "general-chat"],
{ PAPERCLIP_TEST_DATABASE_URL: "" }).chatVitestShards, [{ index: 0, count: 1 }]);
assert.equal(dryRunJson(["--mode", "serialized"], { PAPERCLIP_TEST_DATABASE_URL: "" }).chatVitestShards, null);
});
function defaultLauncher({ shared = false, failureIndex = null } = {}) {
const directory = mkdtempSync(path.join(os.tmpdir(), "default-chat-launch-"));
const file = path.join(repoRoot, chatSuitePath);
const lines = readFileSync(file, "utf8").split("\n")
.flatMap((line, index) => /^\s*it\(/.test(line) ? [index + 1] : []).slice(0, 6);
assert.equal(lines.length, 6);
const cases = lines.map(line => ({ file, projectName: "@paperclipai/server",
name: `default launcher fixture ${line}`, location: { line } }));
const receipt = path.join(directory, "events.ndjson");
const active = path.join(directory, "active");
const fakePnpm = path.join(directory, "pnpm");
try {
writeFileSync(fakePnpm, `#!${process.execPath}
const fs = require('node:fs');
const args = process.argv.slice(2);
const tests = ${JSON.stringify(cases)};
const selected = args.flatMap(arg => {
const match = arg.match(/chat-channels\\.integration\\.test\\.ts:(\\d+)$/);
return match ? [Number(match[1])] : [];
});
if (args.includes('list')) {
const output = args.find(arg => arg.startsWith('--json=')).slice(7);
fs.writeFileSync(output, JSON.stringify(selected.length
? tests.filter(test => selected.includes(test.location.line)) : tests));
} else {
const entry = { args, selected, home: process.env.PAPERCLIP_HOME,
config: process.env.PAPERCLIP_CONFIG, instance: process.env.PAPERCLIP_INSTANCE_ID,
tmpdir: process.env.TMPDIR };
const record = edge => fs.appendFileSync(${JSON.stringify(receipt)}, JSON.stringify({ ...entry, edge }) + '\\n');
record('begin');
if (fs.existsSync(${JSON.stringify(active)})) process.exit(91);
fs.writeFileSync(${JSON.stringify(active)}, 'running');
setTimeout(() => {
record('end');
fs.unlinkSync(${JSON.stringify(active)});
process.exit(selected.includes(${JSON.stringify(failureIndex === null ? null : lines[failureIndex])}) ? 7 : 0);
}, selected.length ? 200 : 0);
}
`);
chmodSync(fakePnpm, 0o755);
const result = spawnSync(process.execPath, [script, "--mode", "general"], {
cwd: repoRoot, encoding: "utf8", timeout: 30_000,
env: workflowEnv({ GITHUB_WORKFLOW: "PR", PATH: `${directory}${path.delimiter}${process.env.PATH}`,
PAPERCLIP_TEST_DATABASE_URL: shared ? "postgresql://shared/test" : "", PAPERCLIP_CI_PROFILE: "false" }),
});
const events = existsSync(receipt) ? readFileSync(receipt, "utf8").trim().split("\n").map(line => JSON.parse(line)) : [];
return { result, events, lines };
} finally {
rmSync(directory, { recursive: true, force: true });
}
}
test("default chat isolation executes every partition sequentially before other groups", { skip: process.platform === "win32" }, () => {
const { result, events, lines } = defaultLauncher();
assert.equal(result.status, 0, result.stdout + result.stderr);
const chat = events.filter(event => event.selected.length > 0);
assert.deepEqual(chat.map(event => event.edge), Array.from({ length: 6 }, () => ["begin", "end"]).flat());
const starts = chat.filter(event => event.edge === "begin");
assert.deepEqual(starts.flatMap(event => event.selected).sort((a, b) => a - b), lines);
for (const event of starts) {
assert.ok(event.args.includes("--no-file-parallelism"));
assert.deepEqual(event.args.filter(arg => arg.startsWith("--maxWorkers")), ["--maxWorkers=1"]);
}
for (const field of ["home", "config", "instance", "tmpdir"]) {
assert.equal(new Set(starts.map(event => event[field])).size, 6, `${field} must be isolated per child`);
}
assert.equal(events.findIndex(event => event.selected.length === 0), 12);
const server = events[12];
assert.ok(server.args.includes("@paperclipai/server"));
const excluded = server.args.flatMap((arg, index) => arg === "--exclude" ? [server.args[index + 1]] : []);
assert.equal(excluded.filter(file => file === chatSuitePath.replace(/^server\//, "")).length, 1);
for (const file of [nativeRunnerSuitePath, usageDeliverySuitePath, dotRunnerSuitePath, restartRecoverySuitePath,
"server/src/services/native-runtime/plugin-runner-probe.test.ts"]) {
assert.ok(!excluded.includes(file.replace(/^server\//, "")), `default must retain ${file}`);
}
});
test("default chat isolation propagates failure without starting another partition or group", { skip: process.platform === "win32" }, () => {
const { result, events } = defaultLauncher({ failureIndex: 2 });
assert.equal(result.status, 1, result.stdout + result.stderr);
assert.match(result.stderr, /failed \(7\)/);
assert.equal(events.length, 6);
assert.ok(events.every(event => event.selected.length === 1));
assert.deepEqual(events.map(event => event.edge), ["begin", "end", "begin", "end", "begin", "end"]);
});
test("default chat isolation keeps shared-database execution monolithic and serial", { skip: process.platform === "win32" }, () => {
const { result, events } = defaultLauncher({ shared: true });
assert.equal(result.status, 0, result.stdout + result.stderr);
assert.ok(events.every(event => event.selected.length === 0));
assert.ok(events[0].args.includes("@paperclipai/server"));
assert.ok(events[0].args.includes("--no-file-parallelism"));
assert.deepEqual(events[0].args.filter(arg => arg.startsWith("--maxWorkers")), ["--maxWorkers=1"]);
const excluded = events[0].args.flatMap((arg, index) => arg === "--exclude" ? [events[0].args[index + 1]] : []);
assert.ok(!excluded.includes(chatSuitePath.replace(/^server\//, "")));
assert.ok(!excluded.includes(nativeRunnerSuitePath.replace(/^server\//, "")));
assert.deepEqual(events.map(event => event.edge), Array.from({ length: events.length / 2 }, () => ["begin", "end"]).flat());
});
test("a failed chat child fails the job after its sibling finishes", { skip: process.platform === "win32" }, () => {
const directory = mkdtempSync(path.join(os.tmpdir(), "chat-launch-proof-"));
+147
View File
@@ -0,0 +1,147 @@
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const filesystem = /^(?:node:)?fs(?:\/promises)?$/;
const readers = new Set(["readFile", "readFileSync", "existsSync", "stat", "statSync", "lstat", "lstatSync", "readdir", "readdirSync"]);
export function repositoryFileInputs(file, program) {
if (!program.body.some(node => node.type === "ImportDeclaration" && filesystem.test(node.source.value))) {
return { covered: [], inputs: [] };
}
const parents = new Map(), nodes = [], declarations = new Map(), constants = new Map(), imports = new Map();
const bind = (node) => {
if (!node) return;
if (node.type === "Identifier") declarations.set(node.name, (declarations.get(node.name) ?? 0) + 1);
else if (node.type === "ObjectPattern") node.properties.forEach(p => bind(p.value ?? p.argument));
else if (node.type === "ArrayPattern") node.elements.forEach(bind);
else if (node.type === "AssignmentPattern") bind(node.left);
else if (node.type === "RestElement") bind(node.argument);
};
function visit(node, parent) {
if (!node || typeof node !== "object" || !node.type) return;
parents.set(node, parent); nodes.push(node);
if (node.type === "ImportDeclaration") {
for (const spec of node.specifiers) {
bind(spec.local);
const source = filesystem.test(node.source.value) ? node.source.value.replace(/^node:/, "") : node.source.value;
imports.set(spec.local.name, { source, name: spec.type === "ImportSpecifier" ? spec.imported.name : "*" });
}
}
if (node.type === "VariableDeclarator") {
bind(node.id);
if (parent.kind === "const" && node.id.type === "Identifier") constants.set(node.id.name, node.init);
}
if (/Function/.test(node.type) || node.type === "ArrowFunctionExpression") {
bind(node.id); node.params?.forEach(bind);
}
if (node.type === "ClassDeclaration" || node.type === "ClassExpression") bind(node.id);
if (node.type === "CatchClause") bind(node.param);
if (node.type === "AssignmentExpression") bind(node.left);
if (node.type === "UpdateExpression") bind(node.argument);
for (const value of Object.values(node)) {
if (Array.isArray(value)) value.forEach(child => visit(child, node));
else if (value && typeof value === "object" && value.type) visit(value, node);
}
}
visit(program);
const tainted = new Set();
for (const node of nodes) {
if (node.type === "MetaProperty" && node.meta.name === "import" && node.property.name === "meta") {
const member = parents.get(node), use = parents.get(member);
if (member?.type !== "MemberExpression" || member.computed || !["url", "dirname", "filename"].includes(member.property.name)
|| (use?.type === "AssignmentExpression" && use.left === member)
|| use?.type === "UpdateExpression" || (use?.type === "UnaryExpression" && use.operator === "delete")) tainted.add("import.meta");
}
if (node.type !== "Identifier") continue;
const parent = parents.get(node);
const binding = imports.get(node.name);
if (binding?.name === "*" && /^(?:node:)?(?:path|url)$/.test(binding.source)
&& !parent?.type.startsWith("Import")) {
if (parent?.type !== "MemberExpression" || parent.object !== node
|| parents.get(parent)?.type !== "CallExpression" || parents.get(parent).callee !== parent) tainted.add(node.name);
}
if (node.name === "URL" && !(parent?.type === "NewExpression" && parent.callee === node)) tainted.add("URL");
}
const unique = (name) => declarations.get(name) === 1;
const member = (node, name) => node?.type === "MemberExpression" && !node.computed && node.property.name === name;
const importMeta = node => node?.type === "MetaProperty" && node.meta.name === "import" && node.property.name === "meta";
function imported(node, source, name) {
if (node?.type === "Identifier" && unique(node.name) && !tainted.has(node.name)) {
const entry = imports.get(node.name);
return entry && source.test(entry.source) && entry.name === name;
}
if (member(node, name) && node.object.type === "Identifier" && unique(node.object.name) && !tainted.has(node.object.name)) {
const entry = imports.get(node.object.name);
return entry && source.test(entry.source) && entry.name === "*";
}
return false;
}
function evaluate(node, seen = new Set()) {
if (!node) return undefined;
if (node.type === "Literal" && typeof node.value === "string") return node.value;
if (node.type === "TemplateLiteral" && !node.expressions.length) return node.quasis[0].value.cooked;
if (node.type === "Identifier" && unique(node.name) && constants.has(node.name) && !seen.has(node.name)) {
const value = evaluate(constants.get(node.name), new Set([...seen, node.name]));
return typeof value === "string" ? value : undefined;
}
if (node.type === "Identifier" && !declarations.has(node.name)) {
if (node.name === "__dirname") return path.dirname(file);
if (node.name === "__filename") return file;
}
if (node.type === "MemberExpression" && importMeta(node.object) && !node.computed && !tainted.has("import.meta")) {
if (node.property.name === "url") return pathToFileURL(file).href;
if (node.property.name === "dirname") return path.dirname(file);
if (node.property.name === "filename") return file;
}
if (node.type === "NewExpression" && node.callee.name === "URL" && !declarations.has("URL") && !tainted.has("URL") && node.arguments.length === 2) {
const args = node.arguments.map(arg => evaluate(arg, seen));
if (args.every(arg => typeof arg === "string")) {
try { return new URL(...args); } catch { return undefined; }
}
}
if (node.type === "CallExpression") {
const args = node.arguments.map(arg => evaluate(arg, seen));
if (imported(node.callee, /^(?:node:)?url$/, "fileURLToPath") && args.length === 1) {
try { return fileURLToPath(args[0]); } catch { return undefined; }
}
if (args.every(arg => typeof arg === "string")) {
for (const method of ["resolve", "join", "dirname"]) {
if (!imported(node.callee, /^(?:node:)?path$/, method)) continue;
if (!args.length || !path.isAbsolute(args[0]) || (method === "dirname" && args.length !== 1)) return undefined;
return path[method](...args);
}
}
}
return undefined;
}
const covered = new Set(), invalid = new Set(), inputs = new Map();
for (const node of nodes) {
if (node.type !== "Identifier" || !filesystem.test(imports.get(node.name)?.source ?? "")) continue;
const binding = imports.get(node.name), parent = parents.get(node);
if (parent?.type.startsWith("Import")) continue;
let target = node, method = binding.name;
if (!unique(node.name)) { invalid.add(binding.source); continue; }
if (method === "*" || method === "promises") {
target = parent;
if (method === "*" && member(target, "promises")) target = parents.get(target);
if (target?.type !== "MemberExpression" || target.computed) { invalid.add(binding.source); continue; }
method = target.property.name;
}
const call = parents.get(target);
if (!readers.has(method) || call?.type !== "CallExpression" || call.callee !== target) {
invalid.add(binding.source); continue;
}
let input = evaluate(call.arguments[0]);
if (input instanceof URL) {
try { input = fileURLToPath(input); } catch { input = undefined; }
}
if (typeof input !== "string" || !path.isAbsolute(input)) { invalid.add(binding.source); continue; }
covered.add(binding.source);
const values = inputs.get(binding.source) ?? new Set();
values.add(path.resolve(input)); inputs.set(binding.source, values);
}
return {
covered: [...covered].filter(source => !invalid.has(source)),
inputs: [...new Set([...inputs.values()].flatMap(values => [...values]))],
};
}
+54
View File
@@ -0,0 +1,54 @@
import { createHash } from "node:crypto";
import { readFileSync, realpathSync } from "node:fs";
import path from "node:path";
import { parseSync } from "oxc-parser";
const contracts = new Map([
["drizzle-orm/index.js", "96c38000daaf94419eddbfd168f7dc509962d6088664e96882979bf851e0dca5"],
["drizzle-orm/pg-core/index.js", "7a6f61b63d19ff5feb4403d8f4afdba90c25364d33769c7a4b3be507c0130357"],
]);
export function dependencyFingerprint(file, packageRoot) {
const files = new Map();
function visit(filename) {
filename = realpathSync(filename);
if (!filename.startsWith(`${packageRoot}${path.sep}`)) throw new Error("Dependency escapes reviewed package");
if (files.has(filename)) return;
const source = readFileSync(filename, "utf8"), ast = parseSync(filename, source);
if (ast.errors.length) throw new Error("Unparsed dependency");
const inspect = node => {
if (!node || typeof node !== "object") return;
if (node.type === "ImportExpression" || (node.type === "Identifier"
&& /^(require|createRequire|getBuiltinModule|eval|Function)$/.test(node.name))) throw new Error("Unreviewed runtime loader");
for (const value of Object.values(node)) {
if (Array.isArray(value)) value.forEach(inspect);
else if (value && typeof value === "object" && value.type) inspect(value);
}
};
inspect(ast.program);
files.set(filename, createHash("sha256").update(source).digest("hex"));
for (const node of ast.program.body) {
if (!node.source) continue;
const specifier = node.source.value;
if (!specifier.startsWith(".")) throw new Error("Unreviewed external import");
visit(path.resolve(path.dirname(filename), specifier));
}
}
visit(file);
return createHash("sha256").update(JSON.stringify([...files].map(([file, hash]) =>
[path.relative(packageRoot, file).split(path.sep).join("/"), hash]).sort())).digest("hex");
}
export function dependencyReviews() {
const cache = new Map();
return (file, relative) => {
if (!contracts.has(relative)) return false;
if (!cache.has(file)) {
try {
const packageRoot = file.slice(0, -relative.slice("drizzle-orm/".length).length - 1);
cache.set(file, dependencyFingerprint(file, packageRoot) === contracts.get(relative));
} catch { cache.set(file, false); }
}
return cache.get(file);
};
}
+1 -1
View File
@@ -59,7 +59,7 @@ export async function createTestSelection(plan, head) {
const graphs = [];
const changed = productInputs(plan.changed);
for (const project of projects) {
const graph = await selectProjectTests({ project, changed, onlyOwnChanges: true });
const graph = await selectProjectTests({ project, changed });
graphs.push(graph);
console.log(`${project}: ${graph.selected.length}/${graph.inventory.length} files, ${graph.alwaysRun.length} retained conservatively, ${graph.seconds.toFixed(1)}s`);
for (const owner of Object.values(owners)) if (owner[project] === null) owner[project] = graph.inventory;
+134
View File
@@ -0,0 +1,134 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import test from "node:test";
import { parseSync } from "oxc-parser";
import { repositoryFileInputs } from "./repository-file-inputs.mjs";
import { selectProjectTests, sourceDependencies } from "./vitest-selection-graph.mjs";
import { dependencyFingerprint, dependencyReviews } from "./reviewed-test-dependencies.mjs";
const file = "/repo/ui/src/contract.test.ts";
const analyze = source => repositoryFileInputs(file, parseSync(file, source).program);
test("data properties and erased types are not global loaders or mock bypasses", () => {
const data = sourceDependencies(file, 'import {vi} from "vitest"; const schema = {arguments: {}, fetch: false, Function: "name"}; type Factory = Function;');
assert.deepEqual(data.reasons, []);
const chained = sourceDependencies(file, 'import {z} from "zod"; const schema = z.object({message:z.literal("routes require auth; fetch input")}).refine(value => true);');
assert.deepEqual(chained.reasons, []);
for (const source of [
'fetch("https://example.invalid");',
'const load = globalThis.fetch; load("https://example.invalid");',
'const make = globalThis.Function; make("return 1");',
'const scope = globalThis; const make = scope.Function; make("return 1");',
'const {Function: make} = globalThis; make("return 1");',
'const {fetch: request} = globalThis; request("https://example.invalid");',
'import {vi as mocker} from "vitest"; mocker.mock("node:path", () => ({join: () => "/other.ts"}));',
'import {vi} from "vitest"; vi.mock("./input", function() { return arguments[0](); });',
'import {vi} from "vitest"; vi.importActual("./input");',
'const result = (fetch("https://example.invalid") as unknown);',
]) assert.ok(sourceDependencies(file, source).reasons.length, source);
});
test("reviewed SQL builders exclude drivers and revoke approval when imported source changes", t => {
const root = realpathSync("server/node_modules/drizzle-orm");
const review = dependencyReviews();
for (const entry of ["index.js", "pg-core/index.js"]) assert.equal(review(path.join(root, entry), `drizzle-orm/${entry}`), true);
assert.equal(review(path.join(root, "postgres-js/index.js"), "drizzle-orm/postgres-js/index.js"), false);
const fixture = realpathSync(mkdtempSync(path.join(tmpdir(), "ci-review-")));
t.after(() => rmSync(fixture, { recursive: true, force: true }));
const entry = path.join(fixture, "index.js"), leaf = path.join(fixture, "leaf.js");
writeFileSync(entry, 'export * from "./leaf.js";'); writeFileSync(leaf, 'export const value = 1;');
const before = dependencyFingerprint(entry, fixture);
writeFileSync(leaf, 'export const value = 2;');
assert.notEqual(dependencyFingerprint(entry, fixture), before);
assert.equal(dependencyReviews()(entry, "drizzle-orm/index.js"), false);
writeFileSync(leaf, 'import "node:fs";');
assert.throws(() => dependencyFingerprint(entry, fixture), /Unreviewed external import/);
for (const source of ['import("./child.js");', 'require("./child.js");', 'const load = require; load("./child.js");']) {
writeFileSync(leaf, source);
assert.throws(() => dependencyFingerprint(entry, fixture), /Unreviewed runtime loader/);
}
});
test("file input edges require module-relative paths and non-escaping read APIs", () => {
for (const source of [
'import {readFileSync as read} from "node:fs"; read(new URL("./leaf.ts", import.meta.url));',
'import fs from "node:fs"; import path from "node:path"; const root = path.dirname(import.meta.filename); const input = path.join(root, "leaf.ts"); fs.readFileSync(input);',
'import {promises as fs} from "node:fs"; import {fileURLToPath} from "node:url"; fs.readFile(fileURLToPath(new URL("./leaf.ts", import.meta.url)));',
]) {
assert.deepEqual(analyze(source), { covered: ["fs"], inputs: ["/repo/ui/src/leaf.ts"] });
}
for (const source of [
'import fs from "node:fs"; fs.readFileSync(process.env.INPUT);',
'import {readFileSync as known} from "node:fs"; import {readFileSync as unknown} from "fs"; known(new URL("./leaf.ts", import.meta.url)); unknown(process.env.INPUT);',
'import {readFileSync as known} from "fs"; import {readFileSync as unknown} from "node:fs"; known(new URL("./leaf.ts", import.meta.url)); unknown(process.env.INPUT);',
'import fs from "node:fs"; fs.readFileSync("src/leaf.ts");',
'import fs from "node:fs"; const read = fs.readFileSync; read(new URL("./leaf.ts", import.meta.url));',
'import {readFileSync as read} from "node:fs"; function load(read) { read(new URL("./leaf.ts", import.meta.url)); }',
'import fs from "node:fs"; const input = new URL("./leaf.ts", import.meta.url); function load(input) { fs.readFileSync(input); }',
'import fs from "node:fs"; let input = new URL("./leaf.ts", import.meta.url); fs.readFileSync(input);',
'import fs from "node:fs"; fs.writeFileSync(new URL("./leaf.ts", import.meta.url), "value");',
'import fs from "node:fs"; consume(fs); fs.readFileSync(new URL("./leaf.ts", import.meta.url));',
'import fs from "node:fs"; const URL = custom; fs.readFileSync(new URL("./leaf.ts", import.meta.url));',
'import fs from "node:fs"; fs.promises[method](new URL("./leaf.ts", import.meta.url));',
'import fs from "node:fs"; fs.readFileSync(new URL("https://example.invalid/source.ts"));',
'import fs from "node:fs"; const input = new URL("./leaf.ts", import.meta.url); fs.readFileSync(input);',
'import fs from "node:fs"; const input = new URL("./leaf.ts", import.meta.url); input.pathname = "/other.ts"; fs.readFileSync(input);',
'import fs from "node:fs"; import path from "node:path"; path.join = custom; fs.readFileSync(path.join(__dirname,"leaf.ts"));',
'import fs from "node:fs"; import path from "node:path"; mutate(path); fs.readFileSync(path.join(__dirname,"leaf.ts"));',
'import fs from "node:fs"; import.meta.url = "file:///other/test.ts"; fs.readFileSync(new URL("./leaf.ts", import.meta.url));',
'import fs from "node:fs"; const meta = import.meta; meta.url = "file:///other/test.ts"; fs.readFileSync(new URL("./leaf.ts", import.meta.url));',
]) assert.deepEqual(analyze(source).covered, [], source);
});
test("unchanged consumer workspaces follow cross-package imports and file inputs", async t => {
const root = realpathSync(mkdtempSync(path.join(tmpdir(), "ci-inputs-")));
t.after(() => rmSync(root, { recursive: true, force: true }));
symlinkSync(path.join(process.cwd(), "node_modules"), path.join(root, "node_modules"), "dir");
const write = (file, source) => {
mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
writeFileSync(path.join(root, file), source);
};
write("vitest.config.mjs", 'export default {test:{projects:["consumer", "independent"]}}');
for (const name of ["consumer", "independent"]) {
write(`${name}/vitest.config.mjs`, `export default {test:{name:"${name}",include:["*.test.ts"]}}`);
write(`${name}/unrelated.test.ts`, 'export const value = 1;');
}
write("producer/leaf.ts", "export const value = 1;");
write("producer/other.ts", "export const other = 1;");
write("consumer/import.test.ts", 'import {value} from "../producer/leaf"; console.log(value);');
write("consumer/read.test.ts", 'import {readFileSync} from "node:fs"; readFileSync(new URL("../producer/leaf.ts",import.meta.url));');
write("consumer/directory.test.ts", 'import {readdirSync} from "node:fs"; readdirSync(new URL("../producer/",import.meta.url));');
write("consumer/dynamic.test.ts", 'import {readFileSync} from "node:fs"; readFileSync(process.env.INPUT);');
write("consumer/mixed.test.ts", 'import {readFileSync as known} from "node:fs"; import {readFileSync as unknown} from "fs"; known(new URL("../producer/leaf.ts",import.meta.url)); unknown(process.env.INPUT);');
write("consumer/helper.ts", 'import {readFileSync} from "node:fs"; export const source = readFileSync(new URL("../producer/leaf.ts",import.meta.url));');
write("consumer/helper.test.ts", 'import {source} from "./helper"; console.log(source);');
write("consumer/outside.test.ts", 'import {readFileSync} from "node:fs"; readFileSync(new URL("/etc/hosts",import.meta.url));');
const git = (...args) => execFileSync("git", args, { cwd: root, stdio: "ignore" });
git("init", "-q"); git("add", ".");
const select = changed => selectProjectTests({ root, project: "consumer", changed });
const affected = await select(["producer/leaf.ts"]);
assert.deepEqual(affected.related, ["consumer/import.test.ts"]);
assert.deepEqual(affected.inputRelated, ["consumer/directory.test.ts", "consumer/helper.test.ts", "consumer/mixed.test.ts", "consumer/read.test.ts"]);
assert.deepEqual(affected.alwaysRun, ["consumer/dynamic.test.ts", "consumer/mixed.test.ts", "consumer/outside.test.ts"]);
assert.ok(!affected.selected.includes("consumer/unrelated.test.ts"));
const other = await select(["producer/other.ts"]);
assert.deepEqual(other.selected, ["consumer/directory.test.ts", "consumer/dynamic.test.ts", "consumer/mixed.test.ts", "consumer/outside.test.ts"]);
const independent = await selectProjectTests({ root, project: "independent", changed: ["producer/leaf.ts"] });
assert.deepEqual(independent.selected, [], "a project without imports or opaque inputs can drop out entirely");
write("producer/new.ts", "export const added = true;"); git("add", ".");
assert.ok((await select(["producer/new.ts"])).inputRelated.includes("consumer/directory.test.ts"));
symlinkSync("leaf.ts", path.join(root, "producer/link.ts"));
write("consumer/symlink.test.ts", 'import {readFileSync} from "node:fs"; readFileSync(new URL("../producer/link.ts",import.meta.url));');
git("add", ".");
assert.ok((await select(["producer/leaf.ts"])).inputRelated.includes("consumer/symlink.test.ts"));
rmSync(path.join(root, "producer/link.ts"));
symlinkSync("other.ts", path.join(root, "producer/link.ts"));
assert.ok((await select(["producer/link.ts"])).inputRelated.includes("consumer/symlink.test.ts"));
write("generated/unknown.ts", "export const generated = true;");
write("consumer/generated.test.ts", 'import {readdirSync} from "node:fs"; readdirSync(new URL("../generated/",import.meta.url));');
git("add", "consumer/generated.test.ts");
assert.ok((await select(["producer/other.ts"])).alwaysRun.includes("consumer/generated.test.ts"));
});
@@ -51,7 +51,9 @@ test("real graph retains direct and indirect filesystem, process and opaque depe
const select = (changed) => selectProjectTests({ root: directory, project: "fixture", changed });
const result = await select(["ui/src/leaf.ts"]);
assert.deepEqual(result.related, ["ui/src/import.test.ts"]);
for (const file of ["reader", "indirect", "process", "dynamic", "cjs", "builtin-loader", "opaque", "partial", "automock", "bypass", "spy", "remock", "arguments"]) {
assert.ok(result.inputRelated.includes("ui/src/reader.test.ts"), "literal file reads add the missing source dependency");
assert.ok(!result.alwaysRun.includes("ui/src/reader.test.ts"));
for (const file of ["indirect", "process", "dynamic", "cjs", "builtin-loader", "opaque", "partial", "automock", "bypass", "spy", "remock", "arguments"]) {
assert.ok(result.alwaysRun.includes(`ui/src/${file}.test.ts`), `${file} must never be dropped`);
}
assert.ok(!result.selected.includes("ui/src/independent.test.ts"));
@@ -73,6 +75,9 @@ test("real graph retains direct and indirect filesystem, process and opaque depe
assert.ok((await select(["ui/src/leaf.ts"])).alwaysRun.includes("ui/src/generated.test.ts"),
"generated inputs have no trustworthy source-to-output graph and retain their consumers");
write("ui/src/environment.ts", 'import {readFileSync} from "node:fs"; readFileSync(new URL("./leaf.ts", import.meta.url));');
const fileSetup = await select(["ui/src/leaf.ts"]);
assert.deepEqual(fileSetup.selected, fileSetup.inventory, "an explicit setup file read affects the entire project");
write("ui/src/environment.ts", 'import {readFileSync} from "node:fs"; readFileSync(process.env.INPUT);');
await assert.rejects(select(["ui/src/leaf.ts"]), /Opaque setup builtin/, "setup filesystem reads can affect every test outside the import graph");
mkdirSync(path.join(directory, "server/src/__tests__/helpers"), { recursive: true });
for (const file of ["server/src/__tests__/setup-supertest.ts", "server/src/__tests__/helpers/isolated-test-home.ts"]) {
+82 -24
View File
@@ -1,10 +1,12 @@
import { readFileSync, realpathSync } from "node:fs";
import { readFileSync, realpathSync, statSync } from "node:fs";
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import path from "node:path";
import { isBuiltin } from "node:module";
import { parseSync } from "oxc-parser";
import { createVitest } from "vitest/node";
import { repositoryFileInputs } from "./repository-file-inputs.mjs";
import { dependencyReviews } from "./reviewed-test-dependencies.mjs";
// Everything else is opaque by default. Do not add IO clients, module loaders,
// filesystem helpers or process launchers here to improve selection counts.
@@ -18,7 +20,8 @@ const purePackages = new Set([
"i18next", "react-i18next",
]);
const pureBuiltins = new Set(["assert", "assert/strict", "path", "path/posix", "path/win32",
"url", "util", "util/types", "buffer", "events", "string_decoder", "crypto"]);
"url", "util", "util/types", "buffer", "events", "string_decoder", "crypto",
"os", "timers", "timers/promises", "stream", "stream/promises", "stream/web", "async_hooks", "zlib"]);
// Reviewed setup performs only temporary-home IO and Supertest socket setup,
// not reads of arbitrary repository inputs. Pin contents, not just paths: any
// edit disables this exception until its effects are reviewed again. Continue
@@ -56,6 +59,15 @@ export function sourceDependencies(file, source) {
if (literal(node)) imports.add(node.value);
else reasons.add("dynamic module name");
};
const calleeName = (node) => {
if (node?.type === "Identifier") return node.name;
if (node?.type === "MetaProperty") return `${node.meta.name}.${node.property.name}`;
if (node?.type === "MemberExpression") {
const member = node.computed ? (literal(node.property) ? node.property.value : "") : node.property.name;
return `${calleeName(node.object)}.${member}`;
}
return "";
};
for (const statement of ast.program.body) {
const call = statement.type === "ExpressionStatement" ? statement.expression : null;
const callee = call?.type === "CallExpression" ? call.callee : null;
@@ -66,20 +78,30 @@ export function sourceDependencies(file, source) {
mocks.push(call.arguments[0].value);
}
}
function visit(node, parent = null) {
function visit(node, parent = null, grandparent = null) {
if (!node || typeof node !== "object") return;
if (node.type?.startsWith("TS") && !["TSImportEqualsDeclaration", "TSEnumDeclaration", "TSEnumMember",
"TSModuleDeclaration", "TSModuleBlock", "TSExportAssignment", "TSParameterProperty"].includes(node.type)) {
if (node.expression) visit(node.expression, node);
return;
}
const dataKey = parent && ((["Property", "ObjectProperty", "MethodDefinition", "PropertyDefinition"].includes(parent.type)
&& grandparent?.type !== "ObjectPattern" && parent.key === node && !parent.computed && !parent.shorthand)
|| (parent.type === "MemberExpression" && parent.property === node && !parent.computed
&& !/^(require|fetch|eval|Function|WebSocket|XMLHttpRequest|Worker)$/.test(node.name)));
if ((node.type === "ImportDeclaration" && node.importKind !== "type")
|| (["ExportNamedDeclaration", "ExportAllDeclaration"].includes(node.type) && node.exportKind !== "type")) {
if (node.source) importSource(node.source);
}
if (node.type === "ImportExpression") importSource(node.source);
if (node.type === "TSImportEqualsDeclaration") reasons.add("CommonJS import");
if (node.type === "Identifier" && /^(require|fetch|eval|Function|WebSocket|XMLHttpRequest|Worker)$/.test(node.name)) {
if (node.type === "Identifier" && !dataKey && /^(require|fetch|eval|Function|WebSocket|XMLHttpRequest|Worker)$/.test(node.name)) {
reasons.add("external input or dynamic code");
}
if ((node.type === "Identifier" && node.name === "getBuiltinModule")
|| (literal(node) && node.value === "getBuiltinModule")) reasons.add("dynamic builtin loader");
if ((node.type === "Identifier" && /^(importActual|unmock|doUnmock|doMock|arguments)$/.test(node.name))
if ((node.type === "Identifier" && (/^(importActual|unmock|doUnmock|doMock)$/.test(node.name)
|| (mockBindings.size > 0 && node.name === "arguments" && !dataKey)))
|| (literal(node) && /^(importActual|unmock|doUnmock|doMock)$/.test(node.value))) reasons.add("mock bypass");
if (node.type === "Identifier" && mockBindings.has(node.name) && parent
&& parent.type !== "ImportSpecifier"
@@ -87,7 +109,12 @@ export function sourceDependencies(file, source) {
reasons.add("escaped or dynamic mocking API");
}
if (node.type === "CallExpression") {
const name = source.slice(node.callee.start, node.callee.end);
const name = calleeName(node.callee);
if (node.callee.type === "MemberExpression" && mockBindings.has(node.callee.object.name)
&& /^(mock|doMock)$/.test(node.callee.property.name)
&& literal(node.arguments[0]) && /^(?:node:)?(?:fs(?:\/promises)?|path(?:\/posix|\/win32)?|url)$/.test(node.arguments[0].value)) {
reasons.add("mocked filesystem path resolution");
}
if (/^(?:vi|vitest)\.(?:mock|doMock|importActual|importMock)$/.test(name)) {
importSource(node.arguments[0]);
}
@@ -102,15 +129,15 @@ export function sourceDependencies(file, source) {
if (!viteGlob && /\b(?:fetch|eval|Function|glob|globSync)\b/.test(name)) reasons.add("external input or dynamic code");
}
for (const value of Object.values(node)) {
if (Array.isArray(value)) value.forEach((child) => visit(child, node));
else if (value && typeof value === "object" && value.type) visit(value, node);
if (Array.isArray(value)) value.forEach((child) => visit(child, node, parent));
else if (value && typeof value === "object" && value.type) visit(value, node, parent);
}
}
visit(ast.program);
return { imports: [...imports], reasons: [...reasons], mocks };
return { imports: [...imports], reasons: [...reasons], mocks, fileInputs: repositoryFileInputs(file, ast.program) };
}
export async function selectProjectTests({ root = process.cwd(), project, changed, onlyOwnChanges = false }) {
export async function selectProjectTests({ root = process.cwd(), project, changed }) {
root = realpathSync(root);
const started = performance.now();
const absoluteChanges = changed.map((file) => path.resolve(root, file));
@@ -123,22 +150,37 @@ export async function selectProjectTests({ root = process.cwd(), project, change
const inventory = await ctx.globTestSpecifications();
if (!inventory.length) throw new Error(`Empty inventory: ${project}`);
const relative = (spec) => path.relative(root, spec.moduleId).split(path.sep).join("/");
if (onlyOwnChanges && !absoluteChanges.some((file) => file.startsWith(`${owner.config.root}${path.sep}`))) {
// Keep coarse dependency coverage, but avoid spending a graph traversal
// on an unchanged consumer project merely to retain most of its IO tests.
const files = inventory.map(relative).sort();
return { project, inventory: files, selected: files, related: [], alwaysRun: files, coveredChanges: [],
opaqueModules: {}, retainedReason: "Unchanged project retains complete lane coverage", seconds: (performance.now() - started) / 1000 };
}
const tracked = new Set(execFileSync("git", ["ls-files", "--cached", "-z"], {
cwd: root, encoding: "utf8", maxBuffer: 16 * 1024 * 1024,
}).split("\0").filter(Boolean));
const audit = new Map();
const reviewedDependency = dependencyReviews();
const parsed = new Map();
const parse = (file) => {
if (!parsed.has(file)) parsed.set(file, sourceDependencies(file, readFileSync(file, "utf8")));
return parsed.get(file);
};
const fileInputs = new Map();
const declaredInputs = (file, source) => {
const paths = [];
try {
for (const input of source.fileInputs.inputs) {
const resolved = realpathSync(input);
const relative = path.relative(root, resolved).split(path.sep).join("/");
const logical = path.relative(root, input).split(path.sep).join("/");
const directory = statSync(resolved).isDirectory();
if (!inside(root, input) || !inside(root, resolved) || (input !== resolved && !tracked.has(logical)) || (directory
? ![...tracked].some(file => file.startsWith(`${relative}/`))
: !tracked.has(relative))) return [];
paths.push({ path: resolved, directory });
if (input !== resolved) paths.push({ path: input, directory });
}
} catch { return []; }
fileInputs.set(file, paths);
return source.fileInputs.covered;
};
const touchesInputs = (inputs, change) => inputs.some(input => change === input.path
|| (input.directory && change.startsWith(`${input.path}${path.sep}`)));
const resolve = async (specifier, importer) => {
try {
const resolved = await environment.pluginContainer.resolveId(specifier, importer);
@@ -153,12 +195,13 @@ export async function selectProjectTests({ root = process.cwd(), project, change
if (setup.has(file)) return;
setup.add(file);
const source = parse(file);
const coveredBuiltins = declaredInputs(file, source);
const review = reviewedSetup.get(path.relative(root, file).split(path.sep).join("/"));
const reviewed = review?.hash === createHash("sha256").update(readFileSync(file)).digest("hex");
if (!reviewed && source.reasons.length) throw new Error(`Opaque setup dependency: ${file}`);
for (const specifier of source.imports) {
if (isBuiltin(specifier)) {
if (!reviewed && !pureBuiltins.has(specifier.replace(/^node:/, ""))) throw new Error(`Opaque setup builtin: ${specifier}`);
if (!reviewed && !pureBuiltins.has(specifier.replace(/^node:/, "")) && !coveredBuiltins.includes(specifier.replace(/^node:/, ""))) throw new Error(`Opaque setup builtin: ${specifier}`);
continue;
}
if (reviewed && review.external.includes(specifier)) continue;
@@ -175,7 +218,8 @@ export async function selectProjectTests({ root = process.cwd(), project, change
else if (!resolved) throw new Error(`Unresolved setup module: ${file}`);
}
for (const file of owner.vite.config.configFileDependencies ?? []) setup.add(realpathSync(file));
const setupChanged = absoluteChanges.some((file) => setup.has(file));
const setupChanged = absoluteChanges.some(file => setup.has(file)
|| [...setup].some(module => touchesInputs(fileInputs.get(module) ?? [], file)));
const raw = await ctx.getRelevantTestSpecifications();
const modules = environment.moduleGraph.idToModuleMap;
const resolvedDependencies = new Map();
@@ -192,6 +236,7 @@ export async function selectProjectTests({ root = process.cwd(), project, change
for (const [id, module] of modules) {
if (!inside(root, id) || !/\.[cm]?[jt]sx?$/.test(id)) continue;
const source = parse(id);
const coveredBuiltins = declaredInputs(id, source);
const reasons = [...source.reasons];
if (!tracked.has(path.relative(root, id).split(path.sep).join("/"))) reasons.push("generated or untracked source");
const local = [];
@@ -203,7 +248,8 @@ export async function selectProjectTests({ root = process.cwd(), project, change
if (!Array.isArray(transformed?.deps)) reasons.push("unknown transform shape");
for (const dependencyId of [...(transformed?.deps ?? []), ...(transformed?.dynamicDeps ?? [])]) {
if (isBuiltin(dependencyId)) {
if (!pureBuiltins.has(dependencyId.replace(/^node:/, ""))) reasons.push(`builtin ${dependencyId}`);
if (!pureBuiltins.has(dependencyId.replace(/^node:/, ""))
&& !coveredBuiltins.some(source => source.replace(/^node:/, "") === dependencyId.replace(/^node:/, ""))) reasons.push(`builtin ${dependencyId}`);
continue;
}
const dependency = dependencyPath(dependencyId);
@@ -214,7 +260,8 @@ export async function selectProjectTests({ root = process.cwd(), project, change
if (!modules.get(dependency)?.transformResult) reasons.push(`untracked dependency ${path.relative(root, dependency)}`);
} else {
const external = dependency?.split(`${path.sep}node_modules${path.sep}`).at(-1).split(path.sep).join("/");
if (!dependency || !dependency.includes(`${path.sep}node_modules${path.sep}`) || !purePackages.has(packageName(external))) {
if (!dependency || !dependency.includes(`${path.sep}node_modules${path.sep}`)
|| (!purePackages.has(packageName(external)) && !reviewedDependency(dependency, external))) {
reasons.push(`opaque dependency ${external ? packageName(external) : dependencyId}`);
}
}
@@ -223,6 +270,8 @@ export async function selectProjectTests({ root = process.cwd(), project, change
if (reasons.length) audit.set(id, [...new Set(reasons)]);
}
const retained = [];
const inputRelated = [];
const retentionReasons = {};
for (const spec of inventory) {
const mocked = new Set();
// Honor only unconditional, explicitly supplied mock factories without an
@@ -241,19 +290,28 @@ export async function selectProjectTests({ root = process.cwd(), project, change
const file = queue.pop();
if (visited.has(file)) continue;
visited.add(file);
if (audit.has(file)) { opaque = true; break; }
if (absoluteChanges.some(change => touchesInputs(fileInputs.get(file) ?? [], change))) inputRelated.push(spec);
if (audit.has(file)) {
retentionReasons[relative(spec)] = { module: path.relative(root, file), reasons: audit.get(file) };
opaque = true; break;
}
for (const dependency of localImports.get(file) ?? []) if (!mocked.has(dependency)) queue.push(dependency);
}
if (opaque) retained.push(spec);
}
const selected = [...new Set([...raw, ...retained].map(relative))].sort();
const selected = [...new Set([...raw, ...inputRelated, ...retained].map(relative))].sort();
return {
project,
inventory: inventory.map(relative).sort(),
selected,
related: raw.map(relative).sort(),
inputRelated: [...new Set(inputRelated.map(relative))].sort(),
alwaysRun: retained.map(relative).sort(),
coveredChanges: changed.filter((file, index) => modules.has(absoluteChanges[index]) || setup.has(absoluteChanges[index])),
retentionReasons,
coveredChanges: changed.filter((file, index) => modules.has(absoluteChanges[index]) || setup.has(absoluteChanges[index])
|| [...fileInputs.values()].some(inputs => touchesInputs(inputs, absoluteChanges[index]))),
fileInputs: Object.fromEntries([...fileInputs].filter(([, inputs]) => inputs.length)
.map(([file, inputs]) => [path.relative(root, file), inputs.map(input => ({ ...input, path: path.relative(root, input.path) }))])),
opaqueModules: Object.fromEntries([...audit].map(([file, reasons]) => [path.relative(root, file), reasons])),
setupChanged,
seconds: (performance.now() - started) / 1000,
+17 -1
View File
@@ -61,6 +61,17 @@ export function decodeReleaseJson(file, path) {
return JSON.parse(bytes.toString('utf8'));
}
export function decodeReleaseKimiClosures(file, path) {
if (file?.type !== 'file' || file.path !== path || file.encoding !== 'base64'
|| !Number.isSafeInteger(file.size) || file.size < 1 || file.size > 1024 * 1024
|| typeof file.content !== 'string') throw Error('Invalid frozen Kimi closure source');
const bytes = Buffer.from(file.content, 'base64');
if (bytes.length !== file.size) throw Error('Frozen Kimi closure source size mismatch');
const matches = [...bytes.toString('utf8').matchAll(/export const KIMI_DISTRIBUTION_CLOSURE_SHA256 = Object\.freeze\(\{\s*"darwin-arm64": "([a-f0-9]{64})",\s*"linux-x64": "([a-f0-9]{64})",?\s*\}\);/g)];
if (matches.length !== 1) throw Error('Invalid frozen Kimi closure declaration');
return { 'darwin-arm64': matches[0][1], 'linux-x64': matches[0][2] };
}
const digestFile = async file => {
const hash = createHash('sha256');
for await (const chunk of createReadStream(file)) hash.update(chunk);
@@ -134,7 +145,12 @@ z.extractall(dest)
const path = `packages/paperclip-runner/${name}`;
return decodeReleaseJson(await api(`contents/${path}?ref=${source}`), path);
}));
verifyReleaseProviderPack(pack, source, 'linux-x64', { profiles, distributions });
let kimiClosures;
if (profiles.profiles.kimi && profiles.profiles.kimi.qualificationStatus !== 'pending') {
const path = 'packages/paperclip-runner/src/drivers/acpx/kimi-closure-pins.ts';
kimiClosures = decodeReleaseKimiClosures(await api(`contents/${path}?ref=${source}`), path);
}
verifyReleaseProviderPack(pack, source, 'linux-x64', { profiles, distributions, kimiClosures });
const packages = JSON.parse(await readFile(join(dirs[2], 'manifest.json'), 'utf8'));
if (packages.schema !== 'paperclip.runner.public-packages/v1' || packages.sourceRevision !== source) throw Error('Wrong public-package source');
const server = packages.archives?.filter(a => a.name === '@paperclipai/server');
+34
View File
@@ -0,0 +1,34 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { appendFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";
import { resolveStandardImageDigest } from "./standard-image-contract.mjs";
export async function reuseCanaryImage(sha, {
resolve = source => resolveStandardImageDigest(source, fetch, { missing: "return-null" }),
run = (command, args) => execFileSync(command, args, { stdio: "inherit" }),
} = {}) {
assert.match(sha ?? "", /^[a-f0-9]{40}$/);
const digest = await resolve(sha);
if (digest === null) return false;
assert.match(digest, /^sha256:[a-f0-9]{64}$/);
const image = `ghcr.io/paperclipai/paperclip@${digest}`;
await run("gh", ["attestation", "verify", `oci://${image}`,
"--repo", "paperclipai/paperclip", "--bundle-from-oci",
"--signer-workflow", "paperclipai/paperclip/.github/workflows/docker.yml",
"--source-ref", "refs/heads/master", "--source-digest", sha,
"--signer-digest", sha, "--deny-self-hosted-runners"]);
await run("docker", ["buildx", "imagetools", "create", "-t",
`ghcr.io/paperclipai/paperclip:sha-${sha.slice(0, 7)}`, image]);
return true;
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const reused = await reuseCanaryImage(process.env.GITHUB_SHA);
if (process.env.GITHUB_OUTPUT) await appendFile(process.env.GITHUB_OUTPUT, `reused=${reused}\n`);
const summary = reused
? "Reused the signed multi-platform master image. Skipped both image builds and the manifest merge."
: "No full-SHA master image exists. Building both platforms for this canary.";
console.log(summary);
if (process.env.GITHUB_STEP_SUMMARY) await appendFile(process.env.GITHUB_STEP_SUMMARY, `${summary}\n`);
}
+75
View File
@@ -0,0 +1,75 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { reuseCanaryImage } from "./reuse-canary-image.mjs";
const sha = "a".repeat(40);
const digest = `sha256:${"b".repeat(64)}`;
test("reuses only an exact-source, master-signed multi-platform image by digest", async () => {
const calls = [];
const reused = await reuseCanaryImage(sha, {
resolve: async source => { assert.equal(source, sha); return digest; },
run: async (...args) => calls.push(args),
});
assert.equal(reused, true);
assert.deepEqual(calls, [
["gh", ["attestation", "verify", `oci://ghcr.io/paperclipai/paperclip@${digest}`,
"--repo", "paperclipai/paperclip", "--bundle-from-oci",
"--signer-workflow", "paperclipai/paperclip/.github/workflows/docker.yml",
"--source-ref", "refs/heads/master", "--source-digest", sha,
"--signer-digest", sha, "--deny-self-hosted-runners"]],
["docker", ["buildx", "imagetools", "create", "-t",
"ghcr.io/paperclipai/paperclip:sha-aaaaaaa", `ghcr.io/paperclipai/paperclip@${digest}`]],
]);
});
test("a missing master image enables the fallback without retagging", async () => {
assert.equal(await reuseCanaryImage(sha, {
resolve: async () => null,
run: () => assert.fail("A missing image must not be promoted"),
}), false);
});
test("registry and provenance failures stop promotion rather than bypass verification", async () => {
await assert.rejects(reuseCanaryImage(sha, {
resolve: async () => { throw new Error("HTTP 403"); },
run: () => assert.fail("A registry failure must not be promoted"),
}), /HTTP 403/);
let commands = 0;
await assert.rejects(reuseCanaryImage(sha, {
resolve: async () => digest,
run: command => {
assert.equal(command, "gh");
commands++;
throw new Error("Untrusted source or signature");
},
}), /Untrusted source/);
assert.equal(commands, 1);
});
test("invalid source refs and digests cannot reach external commands", async () => {
for (const invalid of ["master", "a".repeat(7), "a".repeat(40) + "\n"]) {
await assert.rejects(reuseCanaryImage(invalid, {
resolve: () => assert.fail("Invalid source accepted"),
}));
}
await assert.rejects(reuseCanaryImage(sha, {
resolve: async () => "mutable:tag",
run: () => assert.fail("Invalid digest accepted"),
}));
});
test("canary reuse gates both native builds and still runs convergent promotion", () => {
const workflow = readFileSync(new URL("../.github/workflows/docker.yml", import.meta.url), "utf8");
const reuse = workflow.split(" reuse_canary_image:\n")[1].split(" build-and-push:\n")[0];
assert.match(reuse, /github.repository_id == '1170821064'.*startsWith\(github.ref, 'refs\/tags\/canary\/v'\)/);
assert.match(reuse, /node scripts\/reuse-canary-image.mjs/);
const build = workflow.split(" build-and-push:\n")[1].split(" merge-and-push:\n")[0];
assert.match(build, /needs: \[reuse_canary_image\]/);
assert.match(build, /!cancelled\(\).*result == 'skipped'.*result == 'success'.*outputs.reused == 'false'/);
assert.match(build, /platform: linux\/amd64/);
assert.match(build, /platform: linux\/arm64/);
const promotion = workflow.split(" promote_canary_channel:\n")[1];
assert.match(promotion, /!cancelled\(\).*needs.merge-and-push.result == 'success'.*outputs.reused == 'true'/);
});
+16 -7
View File
@@ -457,9 +457,13 @@ async function runVitest(args, label, testShard = null, exactFiles = false) {
});
}
async function runGeneralSuites(routeTests) {
async function runGeneralSuites(routeTests, chatShards) {
for (const shard of chatShards ?? []) {
await runVitest(["--project", "@paperclipai/server", ...serializedServerVitestArgs, chatSuite],
"chat integration test shard", shard);
}
for (const groupName of generalGroupNames) {
await runGeneralGroup(routeTests, groupName);
await runGeneralGroup(routeTests, groupName, null, null, chatShards ? [chatSuite] : []);
}
}
@@ -484,7 +488,7 @@ function chatShardWorkers(index, count) {
}));
}
async function runGeneralGroup(routeTests, groupName, shardIndex = null, shardCount = null) {
async function runGeneralGroup(routeTests, groupName, shardIndex = null, shardCount = null, additionalExcludedSuites = []) {
if (groupName === generalChatGroupName) {
// Each invocation gets its own temporary home and embedded PostgreSQL.
// Wait for both children even on failure so no sibling is abandoned.
@@ -504,7 +508,7 @@ async function runGeneralGroup(routeTests, groupName, shardIndex = null, shardCo
return;
}
if (generalServerGroups.includes(groupName)) {
const excludedSuites = excludedServerSuites(groupName);
const excludedSuites = [...excludedServerSuites(groupName), ...additionalExcludedSuites];
const files = generalServerTestFiles.filter((file) => !excludedSuites.includes(file));
if (shardCount !== null && shardCount > 1) {
const shardFiles = selectGeneralServerShard(
@@ -641,6 +645,9 @@ if (selection && options.dryRun) {
writeFileSync(1, JSON.stringify({ selection }));
process.exit(0);
}
const defaultChatShards = !selection && options.group === null && options.mode !== serializedModeName && !process.env.PAPERCLIP_TEST_DATABASE_URL
? Array.from({ length: 6 }, (_, index) => ({ index, count: 6 }))
: null;
if (options.dryRun) {
const serializedSuites =
options.mode === serializedModeName
@@ -659,7 +666,9 @@ if (options.dryRun) {
selectedSerializedSuites: serializedSuites.map((routeTest) => routeTest.repoPath),
generalServerSuiteCount: generalServerTestFiles.length,
selectedGeneralServerSuites:
options.mode === generalModeName && options.group === generalServerNativeRunnerGroupName
defaultChatShards
? generalServerTestFiles.filter(file => file !== chatSuite)
: options.mode === generalModeName && options.group === generalServerNativeRunnerGroupName
? nativeRunnerSuites
: options.mode === generalModeName &&
generalServerGroups.includes(options.group) &&
@@ -672,7 +681,7 @@ if (options.dryRun) {
)
: null,
chatVitestShards: options.group === generalChatGroupName
? chatShardWorkers(options.shardIndex, options.shardCount) : null,
? chatShardWorkers(options.shardIndex, options.shardCount) : defaultChatShards,
workspaceProjects:
options.group === generalWorkspacesAGroupName
? generalWorkspacesAProjects
@@ -709,7 +718,7 @@ try {
if (options.group) {
await runGeneralGroup(routeTests, options.group, options.shardIndex, options.shardCount);
} else {
await runGeneralSuites(routeTests);
await runGeneralSuites(routeTests, defaultChatShards);
}
}
+5 -1
View File
@@ -21,6 +21,7 @@ export function standardImageDigest(bytes) {
/** Read registry bytes directly; CLI stdout formatting is not artifact content. */
export async function resolveStandardImageDigest(sha, fetchImpl = fetch, {
sleep = ms => new Promise(resolve => setTimeout(resolve, ms)),
missing = "error",
} = {}) {
assert.match(sha ?? "", /^[a-f0-9]{40}$/);
const request = async (url, headers, consume, allowNotFound = false) => {
@@ -35,6 +36,7 @@ export async function resolveStandardImageDigest(sha, fetchImpl = fetch, {
}
if (!response.ok) {
await response.body?.cancel();
if (allowNotFound && response.status === 404 && missing === "return-null") return null;
const transient = [408, 425, 429].includes(response.status) || response.status >= 500 || (allowNotFound && response.status === 404);
assert.ok(transient && attempt < 5, `Registry lookup failed: HTTP ${response.status}`);
const retryAfter = Number(response.headers.get("retry-after"));
@@ -53,7 +55,7 @@ export async function resolveStandardImageDigest(sha, fetchImpl = fetch, {
};
const { token } = await request("https://ghcr.io/token?service=ghcr.io&scope=repository:paperclipai/paperclip:pull", {}, response => response.json());
assert.ok(typeof token === "string" && token, "Missing public pull token");
const { raw, expected } = await request(`https://ghcr.io/v2/paperclipai/paperclip/manifests/sha-${sha}`, {
const manifest = await request(`https://ghcr.io/v2/paperclipai/paperclip/manifests/sha-${sha}`, {
authorization: `Bearer ${token}`, accept: "application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json",
}, async response => {
assert.ok(response.body, "Standard image index has no body");
@@ -71,6 +73,8 @@ export async function resolveStandardImageDigest(sha, fetchImpl = fetch, {
} finally { await reader.cancel(); }
return { raw: Buffer.concat(chunks), expected: response.headers.get("docker-content-digest") };
}, true);
if (manifest === null) return null;
const { raw, expected } = manifest;
const digest = standardImageDigest(raw);
assert.equal(digest, expected, "Registry digest does not match the index bytes");
return digest;
+14
View File
@@ -77,3 +77,17 @@ test("permanent registry errors fail immediately and transient errors exhaust a
assert.equal(calls, expectedCalls);
}
});
test("optional lookup falls back only for a missing manifest", async () => {
for (const status of [401, 403, 404, 503]) {
let calls = 0;
const lookup = resolveStandardImageDigest("a".repeat(40), async url => {
if (url.includes("/token?")) return Response.json({ token: "public-fixture" });
calls++;
return new Response(null, { status });
}, { missing: "return-null", sleep: async () => {} });
if (status === 404) assert.equal(await lookup, null);
else await assert.rejects(lookup, /Registry lookup failed/);
assert.equal(calls, status === 503 ? 5 : 1);
}
});