1f4c3031be
* ci: filter test matrices by per-test coverage globs to cut LLM spend every test in `crossagent/`, `agnostic/`, and every provider entry now declares a `coverage: string[]` of repo-relative globs. the new `changes` job runs `paths-filter` for a docs-only short-circuit, then pipes the changed-file list into `action/test/matrix.ts`, which intersects each entry's coverage against the diff and emits filtered `agents`, `agnostic`, `flagships`, and `aliases` matrices. main pushes and `workflow_dispatch` set `FULL=1` to run everything as a stale-glob safety net. retires `changed-agents.sh` and the `MODE=flagships` branch in `list-aliases.ts` in favor of one consistent model. * ci(matrix): switch test discovery to dep-free static parsing the GHA `changes` job has no `node_modules` installed. the previous dynamic-import path pulled the test files transitively through `utils.ts` -> `agents/index.ts` -> `@actions/core`, which exploded with ERR_MODULE_NOT_FOUND. parse the test files via regex instead so matrix.ts stays zero-dep — the chain (matrix -> coverage / providers / list-aliases / models) imports only node builtins and relative TS files. * ci(matrix): address PR #730 review feedback - drop dangling `action/mcp/toolFiltering.ts` glob from `nobash`, `restricted`, `tokenExfil` (file doesn't exist; `.test.ts` does, but the runtime tooling lives in `mcp/shell.ts` and `agents/{claude,opencode}.ts`, both already covered). - drop unused `coverageForProvider` export and its `byName` map from `providers.ts` (matrix.ts builds its own lookup inline). - derive the active agent list from `agents/index.ts` via the same dep-free regex tactic as `parseTestFile` instead of hardcoding `["claude", "opencode"]` — adding a new harness file now wires it into the dynamic matrix automatically. - treat `coverage: []` as `coverage: undefined` in `shouldRun` so an accidentally-empty array doesn't silently skip CI on every PR. - add `action/utils/activity.ts` and `action/mcp/selectMode.ts` to the `timeout` test's coverage — the activity-timeout enforcement path was the original reason the test exists. - ungate the `root` job (lint/format/typecheck/vitest). it's a required status check on `main`, so gating it on `code == 'true'` would make docs-only PRs unmergeable (skipped jobs don't satisfy required-check rules). the real LLM savings come from skipping the four matrices, not from skipping `root`. - harden the four matrix-job `if:` guards from `outputs.matrix && ...` to `outputs.matrix != '' && ...` — explicit > implicit short-circuit. - document `expandBraces`'s flat-only support so a future author isn't surprised by `{a,{b,c}}` not expanding. - fix awkward sentence in `wiki/action-tests.md` "CI Cost Filtering".
107 lines
4.2 KiB
TypeScript
107 lines
4.2 KiB
TypeScript
/**
|
|
* emits a JSON array of { slug, agent, name } entries for one of two CI matrix
|
|
* jobs. `agent` mirrors the harness the runtime would pick in production
|
|
* (anthropic/* → claude, everything else → opencode).
|
|
*
|
|
* MODE=aliases (default) — every alias minus pruned passthroughs. consumed by
|
|
* `models-live`, which runs the cheap top-level CLI smoke per alias
|
|
* (`action/test/model-smoke.ts`) to validate resolution + auth.
|
|
*
|
|
* MODE=flagships — one standard-tier model per provider. consumed by
|
|
* `providers-live`, which runs the full harness smoke
|
|
* (`pnpm runtest smoke <agent>`) to validate provider-class tool-calling
|
|
* (e.g. Gemini schema sanitizer, OpenAI tool-call format). flagship slugs
|
|
* live in `providers.ts` alongside their per-provider coverage globs.
|
|
*
|
|
* passthrough pruning (aliases mode): openrouter/* aliases and keyed opencode/*
|
|
* aliases are routing-layer wrappers around models we already smoke-test
|
|
* directly. running every passthrough burns CI minutes without catching
|
|
* anything new — slug-drift is covered by the `models-catalog` job. one canary
|
|
* per routing layer proves the routing surface (auth, tool-call translation)
|
|
* is alive; set INCLUDE_PASSTHROUGHS=1 to bypass for full validation.
|
|
*
|
|
* usage:
|
|
* node action/test/list-aliases.ts
|
|
* MODE=flagships node action/test/list-aliases.ts
|
|
* MATRIX_FILTER=gemini node action/test/list-aliases.ts
|
|
* INCLUDE_PASSTHROUGHS=1 node action/test/list-aliases.ts
|
|
*
|
|
* NOTE: the per-PR-precision matrix lives in `matrix.ts`, which calls into
|
|
* this file. raw invocation here emits the unfiltered matrix.
|
|
*/
|
|
import { modelAliases } from "../models.ts";
|
|
import { providers } from "./providers.ts";
|
|
|
|
const ROUTING_CANARIES = new Set(["openrouter/claude-sonnet", "opencode/claude-sonnet"]);
|
|
|
|
function isPrunablePassthrough(alias: (typeof modelAliases)[number]): boolean {
|
|
if (ROUTING_CANARIES.has(alias.slug)) return false;
|
|
if (alias.provider === "openrouter") return true;
|
|
// routing slugs (bedrock/byok) need a per-run env var to pick the actual
|
|
// model — there's no generic smoke test, so prune from both matrices.
|
|
if (alias.routing) return true;
|
|
// opencode FREE models (big-pickle, mimo-v2-pro-free, minimax-m2.5-free)
|
|
// are unique to opencode and used in prod — keep them. only prune the keyed
|
|
// mirrors.
|
|
return alias.provider === "opencode" && !alias.isFree;
|
|
}
|
|
|
|
export type MatrixEntry = {
|
|
slug: string;
|
|
agent: string;
|
|
name: string;
|
|
};
|
|
|
|
function toMatrixEntry(alias: (typeof modelAliases)[number]): MatrixEntry {
|
|
return {
|
|
slug: alias.slug,
|
|
agent: alias.slug.startsWith("anthropic/") ? "claude" : "opencode",
|
|
// readable display name (GHA renders slashes awkwardly in matrix job titles)
|
|
name: alias.slug.replace("/", "-"),
|
|
};
|
|
}
|
|
|
|
const aliasBySlug = new Map(modelAliases.map((a) => [a.slug, a]));
|
|
|
|
export function buildAliasMatrix(opts: {
|
|
filter?: string;
|
|
includePassthroughs?: boolean;
|
|
}): MatrixEntry[] {
|
|
const filter = opts.filter ?? "";
|
|
const includePassthroughs = opts.includePassthroughs ?? false;
|
|
return modelAliases
|
|
.filter((alias) => {
|
|
if (filter && !alias.slug.toLowerCase().includes(filter)) return false;
|
|
if (!includePassthroughs && isPrunablePassthrough(alias)) return false;
|
|
return true;
|
|
})
|
|
.map(toMatrixEntry);
|
|
}
|
|
|
|
export function buildFlagshipMatrix(opts: { filter?: string }): MatrixEntry[] {
|
|
const filter = opts.filter ?? "";
|
|
return providers
|
|
.map((p) => {
|
|
const alias = aliasBySlug.get(p.flagship);
|
|
if (!alias) {
|
|
throw new Error(
|
|
`list-aliases: flagship "${p.flagship}" missing from modelAliases — update providers.ts`
|
|
);
|
|
}
|
|
return alias;
|
|
})
|
|
.filter((alias) => !filter || alias.slug.toLowerCase().includes(filter))
|
|
.map(toMatrixEntry);
|
|
}
|
|
|
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
const mode = process.env.MODE === "flagships" ? "flagships" : "aliases";
|
|
const filter = process.env.MATRIX_FILTER?.trim().toLowerCase() ?? "";
|
|
const includePassthroughs = process.env.INCLUDE_PASSTHROUGHS === "1";
|
|
const matrix =
|
|
mode === "flagships"
|
|
? buildFlagshipMatrix({ filter })
|
|
: buildAliasMatrix({ filter, includePassthroughs });
|
|
process.stdout.write(JSON.stringify(matrix));
|
|
}
|