4952b28a8e
- Removed the mnemopi Bun preload file and added explicit `./setup` imports in the changed test suites. - Deleted the `RUN_EMBEDDINGS` flag path and shifted `MNEMOPI_NO_EMBEDDINGS` handling into per-suite before/after hooks. - Excluded `packages/mnemopi` from the fast parallel CI test bucket with a note about missing fastembed models.
380 lines
12 KiB
TypeScript
Executable File
380 lines
12 KiB
TypeScript
Executable File
#!/usr/bin/env bun
|
|
|
|
import * as fs from "node:fs/promises";
|
|
import * as path from "node:path";
|
|
|
|
type Mode =
|
|
| "all"
|
|
| "workspace"
|
|
| "native"
|
|
| "coding-agent-singleton"
|
|
| "coding-agent-ui"
|
|
| "coding-agent-runtime"
|
|
| "coding-agent-native"
|
|
| "coding-agent-heavy";
|
|
|
|
type CodingAgentBucket = "singleton" | "ui" | "runtime" | "native";
|
|
|
|
interface TestCommand {
|
|
label: string;
|
|
cwd: string;
|
|
command: string[];
|
|
}
|
|
|
|
type CodingAgentTestPartition = Record<CodingAgentBucket, string[]>;
|
|
|
|
const repoRoot = path.join(import.meta.dir, "..");
|
|
const args = process.argv.slice(2);
|
|
const isDryRun = args.includes("--dry-run");
|
|
const requestedMode = args.find(arg => !arg.startsWith("--")) ?? "all";
|
|
|
|
const validModes = new Set<Mode>([
|
|
"all",
|
|
"workspace",
|
|
"native",
|
|
"coding-agent-singleton",
|
|
"coding-agent-ui",
|
|
"coding-agent-runtime",
|
|
"coding-agent-native",
|
|
"coding-agent-heavy",
|
|
]);
|
|
|
|
const codingAgentBucketPlans: Record<CodingAgentBucket, { label: string; parallel: number }> = {
|
|
singleton: { label: "singleton/global-state bucket", parallel: 1 },
|
|
ui: { label: "UI/TUI bucket", parallel: 1 },
|
|
runtime: { label: "runtime/session bucket", parallel: 1 },
|
|
native: { label: "native/tooling/browser/unit bucket", parallel: 1 },
|
|
};
|
|
|
|
// Smaller workspace packages stay separate from native/TUI/integration suites so
|
|
// their short TS suites can run together. CI still downloads the Linux x64 native
|
|
// addon before this bucket: shared utility barrels may load native-backed modules.
|
|
// mnemopi is intentionally excluded — its embedding suites depend on a ~270MB
|
|
// fastembed model absent from CI runners, so they flake/time out under the parallel
|
|
// bucket; run `bun --cwd=packages/mnemopi test` locally instead.
|
|
const fastWorkspacePackages = [
|
|
"packages/hashline",
|
|
"packages/wire",
|
|
"packages/utils",
|
|
"packages/catalog",
|
|
"packages/ai",
|
|
"packages/snapcompact",
|
|
"packages/agent",
|
|
];
|
|
|
|
// These suites cover the native package, TUI/browser-ish behavior, local servers,
|
|
// or coding-agent-adjacent benchmark paths. Keep them low-concurrency and in jobs
|
|
// that have downloaded the Linux x64 native addon artifacts.
|
|
const nativeAndIntegrationPackages = [
|
|
"packages/natives",
|
|
"packages/tui",
|
|
"packages/collab-web",
|
|
"packages/typescript-edit-benchmark",
|
|
];
|
|
|
|
const codingAgentNativePathPatterns = [
|
|
/(^|\/)[^/]*(bash|native|browser|cmux|mnemopi|hindsight|memory)[^/]*\.test\.ts$/i,
|
|
/^test\/[^/]*(ask|gh|irc|task|eval|search|read|write|edit|ast|resolve|sqlite|web-search|fetch|image|ssh|tool)[^/]*\.test\.ts$/,
|
|
/^test\/core\/python-[^/]*\.test\.ts$/,
|
|
/^test\/core\/[^/]*executor[^/]*\.test\.ts$/,
|
|
/^test\/tools\/[^/]*(ask|gh|irc|task|eval|search|read|edit|ast|resolve|sqlite|web-search|fetch|image|ssh)[^/]*\.test\.ts$/,
|
|
/^test\/tools\/web-scrapers\//,
|
|
/^test\/web\//,
|
|
/^test\/ssh\//,
|
|
/^test\/tools\.test\.ts$/,
|
|
];
|
|
|
|
const codingAgentSingletonPathPatterns = [
|
|
/^test\/(settings|config|fast-mode-scope|autocomplete-max-visible)[^/]*\.test\.ts$/,
|
|
/^test\/[^/]*(singleton|global-state|fake-timer)[^/]*\.test\.ts$/,
|
|
];
|
|
|
|
const codingAgentUiPathPatterns = [
|
|
/^test\/modes\//,
|
|
/^test\/(interactive-mode|main-interactive|input-controller|streaming|status-line|keybindings|editor|hook|theme|setup-wizard|job-renderer|tool-args-reveal|tool-execution)[^/]*\.test\.ts$/,
|
|
/^src\/modes\/components\//,
|
|
];
|
|
|
|
const codingAgentRuntimePathPatterns = [
|
|
/^test\/agent-session[^/]*\.test\.ts$/,
|
|
/^test\/(acp|mcp|rpc|sdk)[^/]*\.test\.ts$/,
|
|
/^test\/(session|session-manager|task|collab|internal-urls)\//,
|
|
/^test\/session[^/]*\.test\.ts$/,
|
|
/^test\/session-manager[^/]*\.test\.ts$/,
|
|
/^test\/(extensions?|plugin|autolearn|skills|marketplace|oauth)[^/]*\.test\.ts$/,
|
|
/^test\/[^/]*oauth[^/]*\.test\.ts$/,
|
|
/^test\/(extensibility|discovery|tool-discovery|goals|marketplace)\//,
|
|
/^test\/(model|model-|model-registry|model-resolver|compaction)[^/]*\.test\.ts$/,
|
|
];
|
|
|
|
const codingAgentNativeContentMarkers = [
|
|
"@oh-my-pi/pi-natives",
|
|
"pi-natives",
|
|
"native",
|
|
"readImageMetadata",
|
|
"Bun.spawn",
|
|
"Bun.spawnSync",
|
|
"child_process",
|
|
"Bun.serve",
|
|
"new Worker",
|
|
"Worker(",
|
|
"puppeteer",
|
|
"bun:sqlite",
|
|
"Redis",
|
|
"redis",
|
|
"WebSocket",
|
|
];
|
|
|
|
const codingAgentSingletonContentMarkers = [
|
|
"Settings.init(",
|
|
"Settings.instance",
|
|
"resetSettingsForTest",
|
|
"setAgentDir(",
|
|
"setDefaultTabWidth(",
|
|
"vi.useFakeTimers(",
|
|
"vi.useRealTimers(",
|
|
"vi.stubEnv(",
|
|
"vi.unstubAllEnvs(",
|
|
];
|
|
|
|
const codingAgentSingletonContentPatterns = [
|
|
/(^|[^\w$.])(process\.env|Bun\.env)\.[A-Za-z0-9_]+\s*=/,
|
|
/(^|[^\w$.])(process\.env|Bun\.env)\[[^\]]+\]\s*=/,
|
|
/delete\s+(process\.env|Bun\.env)(\.[A-Za-z0-9_]+|\[[^\]]+\])/,
|
|
/Object\.assign\((process\.env|Bun\.env),/,
|
|
];
|
|
|
|
const codingAgentUiContentMarkers = [
|
|
"@oh-my-pi/pi-tui",
|
|
"InteractiveMode",
|
|
"InputController",
|
|
"StatusLine",
|
|
"ToolExecutionComponent",
|
|
"render(",
|
|
"renderToString",
|
|
];
|
|
|
|
const codingAgentRuntimeContentMarkers = [
|
|
"AgentSession",
|
|
"SessionManager",
|
|
"AuthStorage",
|
|
"Bun.sleep",
|
|
"setTimeout(",
|
|
];
|
|
|
|
let codingAgentTestPartitionPromise: Promise<CodingAgentTestPartition> | null = null;
|
|
|
|
function shellQuote(value: string): string {
|
|
if (/^[A-Za-z0-9_./:=@+-]+$/.test(value)) {
|
|
return value;
|
|
}
|
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
}
|
|
|
|
function workspaceTestCommand(pkg: string, parallel: number, smol = false): TestCommand {
|
|
return {
|
|
label: pkg,
|
|
cwd: pkg,
|
|
command: ["bun", ...(smol ? ["--smol"] : []), "test", `--parallel=${parallel}`, "--only-failures"],
|
|
};
|
|
}
|
|
|
|
async function collectTestsUnder(root: string, baseDir: string): Promise<string[]> {
|
|
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
const files: string[] = [];
|
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
const filePath = path.join(root, entry.name);
|
|
if (entry.isDirectory()) {
|
|
files.push(...(await collectTestsUnder(filePath, baseDir)));
|
|
continue;
|
|
}
|
|
if (!entry.isFile() || !entry.name.endsWith(".test.ts")) {
|
|
continue;
|
|
}
|
|
files.push(path.relative(baseDir, filePath).split(path.sep).join("/"));
|
|
}
|
|
return files;
|
|
}
|
|
|
|
function hasAnyMarker(content: string, markers: string[]): boolean {
|
|
return markers.some(marker => content.includes(marker));
|
|
}
|
|
|
|
function matchesAnyPath(testFile: string, patterns: RegExp[]): boolean {
|
|
return patterns.some(pattern => pattern.test(testFile));
|
|
}
|
|
|
|
function matchesAnyContentPattern(content: string, patterns: RegExp[]): boolean {
|
|
return patterns.some(pattern => pattern.test(content));
|
|
}
|
|
// Native/tooling tests are classified first because they need the lowest
|
|
// concurrency; all coding-agent buckets run with the native addon available in CI.
|
|
function classifyCodingAgentTest(testFile: string, content: string): CodingAgentBucket {
|
|
if (
|
|
matchesAnyPath(testFile, codingAgentNativePathPatterns) ||
|
|
hasAnyMarker(content, codingAgentNativeContentMarkers)
|
|
) {
|
|
return "native";
|
|
}
|
|
if (
|
|
matchesAnyPath(testFile, codingAgentUiPathPatterns) ||
|
|
hasAnyMarker(content, codingAgentUiContentMarkers)
|
|
) {
|
|
return "ui";
|
|
}
|
|
if (
|
|
matchesAnyPath(testFile, codingAgentSingletonPathPatterns) ||
|
|
hasAnyMarker(content, codingAgentSingletonContentMarkers) ||
|
|
matchesAnyContentPattern(content, codingAgentSingletonContentPatterns)
|
|
) {
|
|
return "singleton";
|
|
}
|
|
if (
|
|
matchesAnyPath(testFile, codingAgentRuntimePathPatterns) ||
|
|
hasAnyMarker(content, codingAgentRuntimeContentMarkers)
|
|
) {
|
|
return "runtime";
|
|
}
|
|
return "native";
|
|
}
|
|
|
|
async function getCodingAgentTestPartition(): Promise<CodingAgentTestPartition> {
|
|
codingAgentTestPartitionPromise ??= (async () => {
|
|
const codingAgentDir = path.join(repoRoot, "packages/coding-agent");
|
|
const testFiles = [
|
|
...(await collectTestsUnder(path.join(codingAgentDir, "test"), codingAgentDir)),
|
|
...(await collectTestsUnder(path.join(codingAgentDir, "src"), codingAgentDir)),
|
|
].sort();
|
|
const partition: CodingAgentTestPartition = {
|
|
singleton: [],
|
|
ui: [],
|
|
runtime: [],
|
|
native: [],
|
|
};
|
|
|
|
for (const testFile of testFiles) {
|
|
const content = await Bun.file(path.join(codingAgentDir, testFile)).text();
|
|
partition[classifyCodingAgentTest(testFile, content)].push(testFile);
|
|
}
|
|
|
|
return partition;
|
|
})();
|
|
return codingAgentTestPartitionPromise;
|
|
}
|
|
|
|
async function codingAgentTestCommand(bucket: CodingAgentBucket): Promise<TestCommand> {
|
|
const partition = await getCodingAgentTestPartition();
|
|
const testFiles = partition[bucket];
|
|
if (testFiles.length === 0) {
|
|
throw new Error(`No coding-agent ${bucket} tests matched`);
|
|
}
|
|
const plan = codingAgentBucketPlans[bucket];
|
|
return {
|
|
label: `packages/coding-agent (${plan.label}; ${testFiles.length} files; parallel=${plan.parallel})`,
|
|
cwd: "packages/coding-agent",
|
|
command: ["bun", "--smol", "test", `--parallel=${plan.parallel}`, "--only-failures", ...testFiles],
|
|
};
|
|
}
|
|
|
|
async function commandsForMode(mode: Mode): Promise<TestCommand[]> {
|
|
switch (mode) {
|
|
case "workspace":
|
|
return [
|
|
...fastWorkspacePackages.map(pkg => workspaceTestCommand(pkg, 8)),
|
|
{
|
|
label: "scripts",
|
|
cwd: ".",
|
|
command: ["bun", "test", "--parallel=4", "--only-failures", "scripts/ci-concurrency.test.ts"],
|
|
},
|
|
];
|
|
case "native":
|
|
return nativeAndIntegrationPackages.map(pkg => workspaceTestCommand(pkg, 1, true));
|
|
case "coding-agent-singleton":
|
|
return [await codingAgentTestCommand("singleton")];
|
|
case "coding-agent-ui":
|
|
return [await codingAgentTestCommand("ui")];
|
|
case "coding-agent-runtime":
|
|
return [await codingAgentTestCommand("runtime")];
|
|
case "coding-agent-native":
|
|
return [await codingAgentTestCommand("native")];
|
|
case "coding-agent-heavy":
|
|
return [
|
|
await codingAgentTestCommand("singleton"),
|
|
await codingAgentTestCommand("ui"),
|
|
await codingAgentTestCommand("runtime"),
|
|
await codingAgentTestCommand("native"),
|
|
];
|
|
case "all":
|
|
return [
|
|
...(await commandsForMode("workspace")),
|
|
...(await commandsForMode("native")),
|
|
...(await commandsForMode("coding-agent-heavy")),
|
|
];
|
|
}
|
|
}
|
|
|
|
// The omp-kata runner pods inject sccache S3 credentials (`AWS_*`) and config
|
|
// (`SCCACHE_*`) pod-wide via `envFrom`, GitHub Actions injects `GITHUB_TOKEN`,
|
|
// and a host may carry provider API keys. Any of these make env-sensitive code
|
|
// non-deterministic in tests — e.g. leaked AWS creds make `amazon-bedrock` look
|
|
// authenticated and win the provider startup fallback over `anthropic`. Run the
|
|
// suites in a hermetic environment with all credential / cloud-config variables
|
|
// stripped so resolution depends only on the test's own fixtures.
|
|
const SCRUBBED_ENV_PREFIXES = ["AWS_", "SCCACHE_", "GOOGLE_CLOUD_"];
|
|
const SCRUBBED_ENV_NAMES = new Set([
|
|
"RUSTC_WRAPPER",
|
|
"GITHUB_TOKEN",
|
|
"GH_TOKEN",
|
|
"COPILOT_GITHUB_TOKEN",
|
|
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
"ANTHROPIC_OAUTH_TOKEN",
|
|
"XAI_OAUTH_TOKEN",
|
|
]);
|
|
|
|
function isScrubbedEnvVar(key: string): boolean {
|
|
if (SCRUBBED_ENV_NAMES.has(key)) {
|
|
return true;
|
|
}
|
|
if (SCRUBBED_ENV_PREFIXES.some(prefix => key.startsWith(prefix))) {
|
|
return true;
|
|
}
|
|
// Any provider credential, e.g. ANTHROPIC_API_KEY / XAI_OAUTH_TOKEN / bedrock bearer.
|
|
return /_(API_KEY|OAUTH_TOKEN)$/.test(key) || key.includes("BEARER_TOKEN");
|
|
}
|
|
|
|
async function runTestCommand(testCommand: TestCommand): Promise<void> {
|
|
const cwd = path.join(repoRoot, testCommand.cwd);
|
|
const renderedCommand = testCommand.command.map(shellQuote).join(" ");
|
|
console.log(`\n==> ${testCommand.label}`);
|
|
console.log(`$ ${renderedCommand}`);
|
|
|
|
if (isDryRun) {
|
|
return;
|
|
}
|
|
|
|
const env: Record<string, string | undefined> = { ...Bun.env, GITHUB_ACTIONS: "" };
|
|
for (const key of Object.keys(env)) {
|
|
if (isScrubbedEnvVar(key)) {
|
|
delete env[key];
|
|
}
|
|
}
|
|
const proc = Bun.spawn(testCommand.command, {
|
|
cwd,
|
|
env,
|
|
stdout: "inherit",
|
|
stderr: "inherit",
|
|
});
|
|
const exitCode = await proc.exited;
|
|
if (exitCode !== 0) {
|
|
throw new Error(`${testCommand.label} failed with exit code ${exitCode}: ${renderedCommand}`);
|
|
}
|
|
}
|
|
|
|
if (!validModes.has(requestedMode as Mode)) {
|
|
throw new Error(`Unknown mode ${shellQuote(requestedMode)}. Expected one of: ${[...validModes].join(", ")}`);
|
|
}
|
|
|
|
for (const testCommand of await commandsForMode(requestedMode as Mode)) {
|
|
await runTestCommand(testCommand);
|
|
}
|