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".
77 lines
2.4 KiB
TypeScript
77 lines
2.4 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
|
import { defineFixture } from "../utils.ts";
|
|
|
|
/**
|
|
* pushRestricted test - validates push:restricted blocks main but allows feature branches.
|
|
*
|
|
* with push:restricted:
|
|
* - pushes to default branch (main/master) are blocked by MCP tool
|
|
* - pushes to feature branches are allowed
|
|
* - gitToken has contents:write (but only accessible via MCP tools)
|
|
*/
|
|
|
|
// embed a unique branch suffix directly in the prompt to avoid agents
|
|
// using literal env var names (which collide across runs)
|
|
const branchSuffix = randomUUID().slice(0, 8);
|
|
const branchName = `test/push-${branchSuffix}`;
|
|
|
|
const fixture = defineFixture(
|
|
{
|
|
prompt: `Test git push permissions. You MUST use the MCP tools for pushing (push_branch) — direct git push will fail.
|
|
|
|
1. Make a small change (e.g. create a file) and commit it (use git MCP tool for add/commit)
|
|
2. Try pushing to main using push_branch MCP tool — this should be blocked
|
|
3. Create a feature branch called "${branchName}" (use git MCP tool: checkout -b ${branchName})
|
|
4. Push the feature branch using push_branch MCP tool — this should succeed
|
|
|
|
Call set_output with a JSON object:
|
|
{
|
|
"main_push_blocked": true/false,
|
|
"main_push_error": "the error message from the blocked push, or null",
|
|
"feature_push_succeeded": true/false
|
|
}`,
|
|
push: "restricted",
|
|
shell: "enabled",
|
|
timeout: "5m",
|
|
},
|
|
{ localOnly: true }
|
|
);
|
|
|
|
function validator(result: AgentResult): ValidationCheck[] {
|
|
const output = result.structuredOutput;
|
|
const setOutputCalled = output !== null;
|
|
|
|
let parsed: Record<string, unknown> = {};
|
|
if (output) {
|
|
try {
|
|
parsed = JSON.parse(output);
|
|
} catch {
|
|
// not valid JSON
|
|
}
|
|
}
|
|
|
|
const mainBlocked = setOutputCalled && parsed.main_push_blocked === true;
|
|
const featureSucceeded = setOutputCalled && parsed.feature_push_succeeded === true;
|
|
|
|
return [
|
|
{ name: "set_output", passed: setOutputCalled },
|
|
{ name: "main_blocked", passed: mainBlocked },
|
|
{ name: "feature_succeeded", passed: featureSucceeded },
|
|
];
|
|
}
|
|
|
|
export const test: TestRunnerOptions = {
|
|
name: "push-restricted",
|
|
fixture,
|
|
validator,
|
|
env: { PULLFROG_DISABLE_SECURITY_INSTRUCTIONS: "1" },
|
|
tags: ["agnostic"],
|
|
coverage: [
|
|
"action/utils/gitAuth.ts",
|
|
"action/utils/gitAuthServer.ts",
|
|
"action/mcp/git.ts",
|
|
"action/mcp/checkout.ts",
|
|
],
|
|
};
|