Files
tutti/tools/scripts/run-check-changed.test.mjs
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

497 lines
13 KiB
JavaScript

import assert from "node:assert/strict";
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
import test from "node:test";
import {
buildLaneInputFingerprint,
gitFingerprintMaxBuffer,
mergeLaneResults,
resolveRetryPushReady,
selectFailedOnlyLanes
} from "./run-check-changed-cache.mjs";
import {
isPackageBuildRelevant,
parseCliArgs,
printSummary,
runLanes,
selectExistingLintFiles
} from "./run-check-changed.mjs";
import {
isAgentActivityRuntimeBoundaryRelevant,
isRendererBoundaryRelevant
} from "./repository-checks.mjs";
import { createIsolatedGitEnvironment } from "./git-environment.mjs";
isolateProcessGitEnvironment(createIsolatedGitEnvironment(tmpdir()));
test("parseCliArgs rejects unknown and invalid options", () => {
assert.throws(() => parseCliArgs(["--push-reddy"]), /unknown option/u);
assert.throws(
() => parseCliArgs(["--max-parallel", "nope"]),
/positive integer/u
);
assert.throws(() => parseCliArgs(["--base"]), /requires a value/u);
});
test("parseCliArgs accepts pnpm separators and explicit values", () => {
assert.deepEqual(
parseCliArgs([
"--",
"--dry-run",
"--push-ready",
"--base",
"origin/trunk",
"--max-parallel",
"2",
"--tail-lines",
"40"
]),
{
baseRef: "origin/trunk",
dryRun: true,
failedOnly: false,
maxParallel: 2,
pushReady: true,
tailLines: 40,
verbose: false
}
);
});
test("package builds exclude test-only inputs", () => {
const manifestIsNotRelevant = () => false;
for (const file of [
"packages/agent/gui/controller.spec.ts",
"packages/agent/gui/vitest.config.ts",
"packages/agent/gui/vitest.shared.setup.ts",
"packages/agent/gui/package.json"
]) {
assert.equal(
isPackageBuildRelevant(file, manifestIsNotRelevant),
false,
file
);
}
assert.equal(
isPackageBuildRelevant(
"packages/agent/gui/index.ts",
manifestIsNotRelevant
),
true
);
assert.equal(
isPackageBuildRelevant("packages/agent/gui/package.json", () => true),
true
);
});
test("lane input fingerprints change only with their own inputs", () => {
const workspaceRoot = mkdtempSync(join(tmpdir(), "check-fingerprint-"));
runFixtureGit(workspaceRoot, ["init", "--quiet"]);
const sourcePath = join(workspaceRoot, "source.ts");
const helperPath = join(workspaceRoot, "helper.ts");
writeFileSync(sourcePath, "export const value = 1;\n");
writeFileSync(helperPath, "export const helper = 1;\n");
runFixtureGit(workspaceRoot, ["add", "source.ts", "helper.ts"]);
runFixtureGit(workspaceRoot, [
"-c",
"user.email=test@example.com",
"-c",
"user.name=Test",
"commit",
"--quiet",
"-m",
"init"
]);
const sourceLane = {
key: "source",
label: "source",
command: ["check", "source.ts"],
inputFiles: ["source.ts"]
};
const helperLane = {
key: "helper",
label: "helper",
command: ["check", "helper.ts"],
inputFiles: ["helper.ts"]
};
const initialSource = buildLaneInputFingerprint({
baseRef: "HEAD",
lane: sourceLane,
workspaceRoot
});
const initialHelper = buildLaneInputFingerprint({
baseRef: "HEAD",
lane: helperLane,
workspaceRoot
});
writeFileSync(sourcePath, "export const value = 2;\n");
const workingSource = buildLaneInputFingerprint({
baseRef: "HEAD",
lane: sourceLane,
workspaceRoot
});
const workingHelper = buildLaneInputFingerprint({
baseRef: "HEAD",
lane: helperLane,
workspaceRoot
});
assert.notEqual(workingSource, initialSource);
assert.equal(workingHelper, initialHelper);
runFixtureGit(workspaceRoot, ["add", "source.ts"]);
writeFileSync(sourcePath, "export const value = 3;\n");
const staged = buildLaneInputFingerprint({
baseRef: "HEAD",
lane: sourceLane,
workspaceRoot
});
runFixtureGit(workspaceRoot, ["reset", "--quiet"]);
const unstaged = buildLaneInputFingerprint({
baseRef: "HEAD",
lane: sourceLane,
workspaceRoot
});
assert.notEqual(staged, unstaged);
});
test("lane input fingerprints support diffs larger than the spawnSync default buffer", () => {
const workspaceRoot = mkdtempSync(join(tmpdir(), "check-large-fingerprint-"));
runFixtureGit(workspaceRoot, ["init", "--quiet"]);
const sourcePath = join(workspaceRoot, "source.txt");
writeFileSync(sourcePath, "initial\n");
runFixtureGit(workspaceRoot, ["add", "source.txt"]);
runFixtureGit(workspaceRoot, [
"-c",
"user.email=test@example.com",
"-c",
"user.name=Test",
"commit",
"--quiet",
"-m",
"init"
]);
writeFileSync(sourcePath, `${"x".repeat(2 * 1024 * 1024)}\n`);
const fingerprint = buildLaneInputFingerprint({
baseRef: "HEAD",
lane: {
key: "large-source",
label: "large-source",
command: ["check", "source.txt"],
inputFiles: ["source.txt"]
},
workspaceRoot
});
assert.match(fingerprint, /^[a-f0-9]{64}$/u);
});
test("lane fingerprint buffer covers repository-managed binary source assets", () => {
assert.equal(gitFingerprintMaxBuffer, 128 * 1024 * 1024);
});
test("failed-only reuses only passed lanes with unchanged inputs", () => {
const currentLanes = [
laneFixture("passed-unchanged", "same"),
laneFixture("failed-unchanged", "same"),
laneFixture("passed-changed", "after"),
laneFixture("new-lane", "new")
];
const summary = {
laneFingerprintVersion: 1,
results: [
resultFixture("passed-unchanged", "same", 0),
resultFixture("failed-unchanged", "same", 1),
resultFixture("passed-changed", "before", 0),
resultFixture("removed-lane", "same", 0)
]
};
const selection = selectFailedOnlyLanes(currentLanes, summary);
assert.deepEqual(
selection.lanesToRun.map((lane) => lane.key),
["failed-unchanged", "passed-changed", "new-lane"]
);
assert.deepEqual(
selection.reusedResults.map((result) => result.key),
["passed-unchanged"]
);
});
test("failed-only rejects summaries without lane fingerprints", () => {
assert.throws(
() => selectFailedOnlyLanes([], { results: [] }),
/legacy failed-lane state/u
);
});
test("failed-only inherits push-ready lanes from the previous run", () => {
assert.equal(resolveRetryPushReady(false, { pushReady: true }), true);
assert.equal(resolveRetryPushReady(false, { pushReady: false }), false);
assert.equal(resolveRetryPushReady(true, { pushReady: false }), true);
});
test("failed-only keeps reused results available for the next retry", () => {
const currentLanes = [
laneFixture("passed", "same"),
laneFixture("retried", "fixed")
];
const firstSelection = selectFailedOnlyLanes(currentLanes, {
laneFingerprintVersion: 1,
results: [
resultFixture("passed", "same", 0),
resultFixture("retried", "broken", 1)
]
});
const mergedResults = mergeLaneResults(
currentLanes,
[resultFixture("retried", "fixed", 0)],
firstSelection.reusedResults
);
const nextSelection = selectFailedOnlyLanes(currentLanes, {
laneFingerprintVersion: 1,
results: mergedResults
});
assert.deepEqual(nextSelection.lanesToRun, []);
assert.deepEqual(
nextSelection.reusedResults.map((result) => result.key),
["passed", "retried"]
);
});
test("renderer boundary lane covers renderer and checker changes", () => {
for (const file of [
"apps/desktop/src/renderer/src/features/workspace-workbench/services/coordinator.ts",
"tools/scripts/check-renderer-feature-boundaries.mjs",
"tools/scripts/check-renderer-feature-boundaries.test.mjs"
]) {
assert.equal(isRendererBoundaryRelevant(file), true, file);
}
assert.equal(
isRendererBoundaryRelevant("apps/desktop/src/main/index.ts"),
false
);
});
test("activity runtime boundary lane covers package, desktop adapter, and checker changes", () => {
for (const file of [
"packages/agent/gui/AgentGUI.tsx",
"packages/agent/activity-core/src/engine/engine.ts",
"apps/desktop/src/renderer/src/features/workspace-agent/services/runtime.ts",
"apps/desktop/src/renderer/src/features/workspace-workbench/ui/Agent.tsx",
"tools/scripts/check-agent-activity-runtime-boundaries.mjs",
"tools/scripts/check-agent-activity-runtime-boundaries.test.mjs"
]) {
assert.equal(isAgentActivityRuntimeBoundaryRelevant(file), true, file);
}
assert.equal(
isAgentActivityRuntimeBoundaryRelevant(
"apps/desktop/src/renderer/src/features/workspace-file-manager/file.ts"
),
false
);
});
test("selectExistingLintFiles drops deleted lint targets", () => {
const changedFiles = [
"packages/foo/src/live.ts",
"packages/foo/src/deleted.ts",
"packages/foo/README.md"
];
const lintFiles = selectExistingLintFiles(
changedFiles,
(file) => file !== "packages/foo/src/deleted.ts"
);
assert.deepEqual(lintFiles, ["packages/foo/src/live.ts"]);
});
test("selectExistingLintFiles keeps existing lintable paths", () => {
const changedFiles = [
"apps/desktop/src/main/index.ts",
"packages/foo/src/helper.mjs"
];
const lintFiles = selectExistingLintFiles(changedFiles, () => true);
assert.deepEqual(lintFiles, changedFiles);
});
test("runLanes preserves lane indexes without relying on outer scope", async () => {
const runDirectory = mkdtempSync(join(tmpdir(), "run-check-changed-"));
const lanes = [
{
key: "lane-b",
label: "lane-b",
command: [process.execPath, "-e", "setTimeout(() => {}, 10)"]
},
{
key: "lane-a",
label: "lane-a",
command: [process.execPath, "-e", ""]
}
];
const results = await runLanes(lanes, runDirectory);
assert.deepEqual(
results.map((result) => result.index),
[0, 1]
);
assert.deepEqual(
results.map((result) => result.key),
["lane-b", "lane-a"]
);
});
test("runLanes spawns Windows command shims through the shell", async () => {
if (process.platform !== "win32") {
return;
}
const runDirectory = mkdtempSync(join(tmpdir(), "run-check-changed-"));
const shimPath = join(runDirectory, "shim.cmd");
writeFileSync(shimPath, "@echo off\r\nexit /b 0\r\n");
const results = await runLanes(
[
{
key: "windows-shim",
label: "windows-shim",
command: [shimPath]
}
],
runDirectory
);
assert.deepEqual(
results.map((result) => result.exitCode),
[0]
);
});
test("runLanes serializes lanes that write the same generated assets", async () => {
const runDirectory = mkdtempSync(join(tmpdir(), "run-check-changed-"));
const markerPath = join(runDirectory, "marker.txt");
const firstProgram =
'const fs=require("node:fs"); fs.appendFileSync(process.argv[1], "first-start\\n"); setTimeout(() => { fs.appendFileSync(process.argv[1], "first-end\\n"); }, 30);';
const secondProgram =
'const fs=require("node:fs"); if (!fs.readFileSync(process.argv[1], "utf8").includes("first-end")) process.exit(1);';
const lanes = [
{
key: "generate:builtin-apps",
label: "generate:builtin-apps",
serialGroup: "tuttid-builtin-assets",
command: [process.execPath, "-e", firstProgram, markerPath]
},
{
key: "build:go",
label: "build:go",
serialGroup: "tuttid-builtin-assets",
command: [process.execPath, "-e", secondProgram, markerPath]
}
];
const results = await runLanes(lanes, runDirectory);
assert.deepEqual(
results.map((result) => result.exitCode),
[0, 0]
);
assert.equal(readFileSync(markerPath, "utf8"), "first-start\nfirst-end\n");
});
test("printSummary includes rerun hint for failures", () => {
const errors = [];
const originalError = console.error;
console.error = (...args) => {
errors.push(args.join(" "));
};
try {
printSummary(
[
{
command: ["pnpm", "lint"],
durationMs: 10,
exitCode: 1,
index: 0,
key: "lint:changed",
label: "lint:changed",
logPath: "/tmp/lint.log",
logPathRelative: ".tmp/check-runs/example/lint.log"
}
],
[
{
command: ["pnpm", "lint"],
durationMs: 10,
exitCode: 1,
index: 0,
key: "lint:changed",
label: "lint:changed",
logPath: "/tmp/lint.log",
logPathRelative: ".tmp/check-runs/example/lint.log"
}
],
10,
"/tmp/check-runs/example"
);
} finally {
console.error = originalError;
}
assert.match(
errors.at(-1) ?? "",
/Rerun failed lanes with: pnpm check:changed -- --failed-only/u
);
});
function runFixtureGit(workspaceRoot, args) {
const result = spawnSync("git", args, {
cwd: workspaceRoot,
encoding: "utf8",
env: createIsolatedGitEnvironment(workspaceRoot)
});
assert.equal(result.status, 0, result.stderr || result.stdout);
}
function laneFixture(key, inputFingerprint) {
return {
command: ["check", key],
inputFiles: [`${key}.ts`],
inputFingerprint,
key,
label: key
};
}
function resultFixture(key, inputFingerprint, exitCode) {
return {
...laneFixture(key, inputFingerprint),
durationMs: 10,
exitCode,
index: 0,
logPath: `/tmp/${key}.log`,
logPathRelative: `.tmp/${key}.log`
};
}
function isolateProcessGitEnvironment(nextEnvironment) {
for (const name of Object.keys(process.env)) {
if (!Object.hasOwn(nextEnvironment, name)) {
delete process.env[name];
}
}
Object.assign(process.env, nextEnvironment);
}