Skip to content

Commit cb88b94

Browse files
feat(check): run checks live and report Vapi Evals statuses
`npm run check -- <check>|--all` now runs each built target payload in the check's run org: one POST /eval/simulation/run per target, at most three at once, judged by the same strict verdict as `npm run sim` (the create/poll/cancel/hydrate loop moves into simRunExecute, which both share). - Budget and cancellation: each run's deadline is the earlier of its timeoutMinutes and --budget-minutes; no run starts with under five minutes left; the deadline, SIGINT and SIGTERM cancel in-flight runs. - Keys: VAPI_CHECK_TOKENS, then .env.<runOrg>, then VAPI_PRIVATE_API_KEY when every selected check runs in one org. --refresh-bindings runs the same read-only bindings pull promotion uses. - Selection: --changed-since <ref> runs only checks affected by changes since the merge base (the org's files and state, the run org's, the config, the engine, and the check's own paths). - Statuses (when GITHUB_TOKEN, GITHUB_REPOSITORY and HEAD_SHA are set): `Vapi Evals / <check> / <target>` goes pending with the run's link, then success/failure/error; --all runs post the aggregate `Vapi Evals` from a finally block, so config errors and exceptions report too. - Report: a markdown summary (also appended to $GITHUB_STEP_SUMMARY) with failing evaluations' expected vs extracted values and "unmocked tool called" notices from transcripts, plus --json. No PR comments. - Runs send User-Agent vapi-gitops-check/<version>. Exit codes: 0 passed, 1 failed, 2 config or build error, 3 incomplete. Refs TEST-141 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 1a7b576 commit cb88b94

16 files changed

Lines changed: 2035 additions & 158 deletions

‎AGENTS.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ If setup reports the org is "already set up locally", do not delete anything to
9090
| Push with new resources | `npm run push -- <org> --allow-new-files` — bypass orphan-YAML gate. **AI agents**: do NOT auto-pass this flag; confirm with the human first (see push section below) |
9191
| Test a call | `npm run call -- <org> -a <assistant-name>` or `-s <squad-name>` |
9292
| Run a simulation suite | `npm run sim -- <org> --suite <name> --target <assistant-name>` |
93-
| Build PR-check payloads offline | `npm run check -- <check> --dry-run` (or `--all`); `--print-payload` writes the JSON |
93+
| Run PR simulation checks | `npm run check -- <check>` (or `--all`); `--dry-run` builds the payloads offline, `--print-payload` writes them |
9494
| Migrate a legacy state file | `npm run migrate` — one-shot, all orgs; required once after upgrading to the hash-store engine |
9595

9696
---
@@ -904,7 +904,8 @@ npm run validate -- <org> # Lint resources locally (fai
904904
npm run audit -- <org> # Read-only drift detector: orphan YAML, state ghosts, content-identical clusters, sibling base-slugs, dashboard orphans, inline model.tools. Exit 1 on findings.
905905
npm run audit -- <org> --type assistants # Scope audit to a single resource type
906906
npm run sim -- <org> --suite <name> --target <name> # Run a simulation suite against an assistant/squad (exit 0 pass, 1 fail, 3 incomplete; --timeout <min>)
907-
npm run check -- <check> --dry-run # Build a vapi-checks.yml check's inline run payload offline (exit 0 built, 2 config/build error; --print-payload [dir])
907+
npm run check -- <check> # Run a vapi-checks.yml check inline against the local files (exit 0 pass, 1 fail, 2 config/build error, 3 incomplete; --changed-since <ref>, --budget-minutes <n>, --json <path>)
908+
npm run check -- <check> --dry-run # Build the check's inline run payloads offline, no key needed (--print-payload [dir])
908909
npm run rollback -- <org> --to <ISO-timestamp> # Re-apply a snapshot taken before a push
909910
npm run rollback -- <org> --list # List available snapshots
910911

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ Every command works in two modes:
104104
| `npm run rollback` | — | `npm run rollback -- <org> --list` or `--to <ISO>` | Restore from a snapshot in `.vapi-state.<org>.snapshots/` (one is written before every push/apply). |
105105
| `npm run call` | ✅ | `npm run call -- <org> -a <name>` or `-s <squad>` | Start an interactive WebSocket call against an assistant or squad. |
106106
| `npm run sim` | — | `npm run sim -- <org> --suite <name> --target <name> [--timeout <min>]` | Run a simulation suite (or specific simulations) against a deployed assistant/squad. Prints the run link; exits 0 passed, 1 failed, 3 incomplete (timeout, Ctrl-C, missing results). |
107-
| `npm run check` | — | `npm run check -- <check>\|--all --dry-run [--print-payload [dir]]` | Build each `vapi-checks.yml` check target's inline simulation-run payload from the files on disk, with its tools, handoffs, judges and personalities. Offline: no API key, nothing sent. `--print-payload` writes the JSON (default `tmp/check-payloads/`). Exits 0 when every payload builds, 2 on a config or build error. |
107+
| `npm run check` | — | `npm run check -- <check>\|--all [--dry-run] [--changed-since <ref>] [--budget-minutes <n>] [--json <path>]` | Run the `vapi-checks.yml` simulation checks against the files on disk: each target is built inline (tools, handoffs, judges, personalities; tools mocked fail-closed, servers dead-ended) and run in one simulation run per target, so nothing is deployed. Posts `Vapi Evals` commit statuses when run by the PR workflow. `--dry-run` builds the payloads offline (no key, nothing sent; `--print-payload` writes them). Exits 0 passed, 1 failed, 2 config or build error, 3 incomplete (timeout, interrupt, budget, billing). |
108108
| `npm run migrate` | — | `npm run migrate` | One-time, all orgs at once: slim legacy state files to pure `name → uuid` and seed the per-developer `.vapi-state-hash/` baseline store from the old hashes. Required once after upgrading to the hash-store engine — `pull`/`push`/`apply` refuse legacy-shaped state until it runs. Idempotent. |
109109
| `npm run build` | — | — | Type-check the codebase (`tsc --noEmit`). |
110110
| `npm test` | — | — | Run regression tests (`node:test`). |

‎src/check-build.ts‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Build every target payload of one check from the files under rootDir.
2+
// Shared by the dry run and the live run, so both test the same body.
3+
4+
import { existsSync, readFileSync } from "node:fs";
5+
import { join } from "node:path";
6+
import type { CheckDefinition, CheckTarget } from "./check-config.ts";
7+
import type { CheckPayloadResult } from "./check-payload.ts";
8+
import { checkPayloadBuild } from "./check-payload.ts";
9+
import { promotionBindingsResolve, promotionStateParse } from "./promotion.ts";
10+
import { orgResourcesRead } from "./resource-parse.ts";
11+
import type { StateFile } from "./types.ts";
12+
13+
export interface CheckJob {
14+
check: CheckDefinition;
15+
target: CheckTarget;
16+
// `<check> / <type>/<id>`
17+
label: string;
18+
result: CheckPayloadResult;
19+
}
20+
21+
export function checkTargetLabel(target: CheckTarget): string {
22+
return `${target.type}/${target.id}`;
23+
}
24+
25+
function stateRead(root: string, org: string, warnings: string[]): StateFile {
26+
const path = join(root, `.vapi-state.${org}.json`);
27+
if (!existsSync(path)) {
28+
warnings.push(
29+
`no .vapi-state.${org}.json: references by UUID and credential bindings can't resolve`,
30+
);
31+
return promotionStateParse("{}");
32+
}
33+
return promotionStateParse(readFileSync(path, "utf8"));
34+
}
35+
36+
export async function checkJobsBuild(
37+
root: string,
38+
check: CheckDefinition,
39+
): Promise<CheckJob[]> {
40+
const job = (target: CheckTarget, result: CheckPayloadResult): CheckJob => ({
41+
check,
42+
target,
43+
label: `${check.name} / ${checkTargetLabel(target)}`,
44+
result,
45+
});
46+
if (!existsSync(join(root, "resources", check.org))) {
47+
return check.targets.map((target) =>
48+
job(target, {
49+
errors: [`resources/${check.org}/ does not exist`],
50+
warnings: [],
51+
bytes: 0,
52+
}),
53+
);
54+
}
55+
const warnings: string[] = [];
56+
const resources = await orgResourcesRead(root, check.org);
57+
const sourceState = stateRead(root, check.org, warnings);
58+
const runState =
59+
check.runOrg === check.org
60+
? sourceState
61+
: stateRead(root, check.runOrg, warnings);
62+
const bindingsResolved = await promotionBindingsResolve(
63+
root,
64+
check.org,
65+
check.runOrg,
66+
sourceState,
67+
);
68+
return check.targets.map((target) => {
69+
const result = checkPayloadBuild({
70+
check,
71+
target,
72+
resources,
73+
sourceState,
74+
runState,
75+
bindingsResolved,
76+
});
77+
result.warnings.unshift(...warnings);
78+
return job(target, result);
79+
});
80+
}

0 commit comments

Comments
 (0)