/** * vigiles — runner-agnostic helpers for harness tests / evals. * * `runHarnessTest` and `runEval` are plain async functions that return data, so * they already work inside any runner (node:test, vitest, jest, mocha). These * helpers remove the last bit of boilerplate without coupling to a runner: * * - `withHarness` — run a harness test and auto-clean the sandbox (try/finally), * so you don't leak temp dirs in `afterEach`. * - plain `assert*` helpers that throw — usable in every runner, including * node:test which has no `expect.extend`. * - `vigilesMatchers` — register with `expect.extend(vigilesMatchers)` for * `expect(...).toHaveCreated(...)` sugar. The signature is identical for * vitest and jest, so the same object supports both. */ import { type HarnessTestSpec, type HarnessTestResult, type ToolCall, type Trace } from "./harness-test.js"; import type { EvalReport, TriggerRateReport } from "./eval.js"; import type { HookRunResult, EgressAttempt } from "./run-hook.js"; import type { AnyHook, RawHookEvent } from "./core/hook-program.js"; import { type Check } from "./check.js"; import type { OutputContract } from "./core/spec.js"; import { type ParsedAgentResult } from "./adapters/claude-code/agent-result.js"; import { type BaselineFile, type DiffOptions } from "./eval-baseline.js"; export { compareArms } from "./stats.js"; export type { Comparison } from "./stats.js"; export { diffReports, toBaselineFile, parseBaselineFile, readBaseline, writeBaseline, formatBaselineDiff, diffToJUnit, } from "./eval-baseline.js"; export type { BaselineFile, BaselineDiff, MetricDiff, DiffStatus, DiffOptions, } from "./eval-baseline.js"; /** * Run a harness test, hand the result to `fn`, and always clean up the sandbox. * Returns whatever `fn` returns. Use this instead of calling `cleanup()` by * hand — it survives assertion failures. */ export declare function withHarness(spec: HarnessTestSpec, fn: (r: HarnessTestResult) => T | Promise): Promise; /** * Mark the current `*.harness.*` / `*.eval.*` SCRIPT as SKIPPED and exit, so * `vigiles test` / `vigiles eval` report a loud `⊘ SKIPPED` instead of a silent * `✓` — e.g. the deterministic tier when the `claude` CLI isn't installed. A skip * never fails the run. Exit 77 is the runner's `SKIP_EXIT_CODE` (run-scripts.ts). * * For standalone CLI-fallback scripts only — inside a runner (vitest / jest / * node:test) use that runner's own skip, not a process exit. */ export declare function skip(reason?: string): never; /** Assert the sandbox contains `path` (a hook/agent side-effect file). */ export declare function assertCreated(r: HarnessTestResult, path: string): void; /** Assert the sandbox does NOT contain `path` (e.g. a blocked action's output). */ export declare function assertNotCreated(r: HarnessTestResult, path: string): void; /** Assert the scripted model served at least `n` turns (e.g. a Stop hook forced more). */ export declare function assertServedTurns(r: HarnessTestResult, n: number): void; /** Assert a `runHook` result blocked (exit 2 / decision:block / permission:deny). */ export declare function assertHookBlocked(r: HookRunResult): void; /** Assert a `runHook` result allowed (did not block). */ export declare function assertHookAllowed(r: HookRunResult): void; /** * Assert a `runHook` result's text REACHED THE MODEL — the assertion a nudge * test needs, because "the hook printed it" is not "the model received it". * * Reads {@link HookRunResult.delivery}: passes only on `delivered`. Each other * verdict fails with its own cause — the five are different facts and a red * test must name the right one: * - `undelivered` — text went to a stream this event does not deliver (the * measured case: stderr at exit 0 on `PostToolUse`). Names the stream, the * bytes, and the cure. * - `silent` — nothing addressed to the model, or the hook never ran (its * condition did not match). * - `blocked` — a veto, which is not a nudge; use {@link assertHookBlocked}. * - `unknown` — the event has no row in the harness's capability table, so * delivery cannot be judged. FAILS rather than passes (`no-silent-skips`): * pass `capabilities` to `runHook`, or add the row. */ export declare function assertDelivered(r: HookRunResult): void; /** * Assert a COMPILED hook (a `vigiles/hook` program) denies an event — evaluated * in-process, no subprocess, no model. The cheapest way to test a gate's logic: * pass the hook's default export and a raw event. (For the wired-into-the-real-CLI * check, use {@link assertHookBlocked} over `runHook`.) */ export declare function assertHookDenies(hook: AnyHook, event: RawHookEvent): void; /** Assert a COMPILED hook allows an event (in-process). The twin of {@link assertHookDenies}. */ export declare function assertHookAllows(hook: AnyHook, event: RawHookEvent): void; /** * Assert a COMPILED **react** hook emits a `notice(…)` for an event, optionally * matching its message — the react-tier twin of {@link assertHookDenies}. * * A react hook can't block, so the gate assertions don't apply to it, and there * was no assertion that did. That left `notice()` a live trap: a probe built on * `execFileSync` (which returns stdout only) reported a perfectly healthy react * hook as DEAD — measured against three real hooks on 2026-08-03. Reading the * reaction in-process means no stream enters into it, which is why this * assertion did not change when delivery did: since 2026-09-07 a notice also * goes to stdout as `additionalContext` on an event the harness injects (see * `noticeDelivery`), so WHICH stream carries it now depends on the event — a * stream probe is even less answerable than before, and this one is unaffected. */ export declare function assertHookNotices(hook: AnyHook, event: RawHookEvent, matcher?: string | RegExp): void; /** * Assert a COMPILED **react** hook stays silent for an event — it returns * `nothing()` (a `"none"` reaction). The twin of {@link assertHookNotices}: together they pin both * halves of a react hook's behaviour — it fires when it should, and (the half * that actually regresses) it does NOT fire when it shouldn't. * * A `run(…)` reaction is not silent for this purpose: the hook still reacted. */ export declare function assertHookSilent(hook: AnyHook, event: RawHookEvent): void; /** Anything carrying recorded egress attempts (a runHook recordEgress result). */ interface HasEgress { readonly egress: readonly EgressAttempt[]; } /** The `host:port` strings a run attempted, e.g. `["registry.npmjs.org:443"]`. */ export declare function egressHosts(r: HasEgress): string[]; /** Assert the confined run made NO network egress attempt at all. */ export declare function assertNoEgress(r: HasEgress): void; /** * Assert every egress attempt went to an allowed host. `allowed` matches a host * (exact string or regex), or a specific `host:port`. Any attempt outside the * allowlist fails, naming the offender — exfil / unexpected-registry detection. */ export declare function assertEgressOnly(r: HasEgress, allowed: ReadonlyArray): void; /** * Anything carrying recorded file writes (a confined `runHook`/`runScript` * result). `filesWritten` is OPTIONAL on purpose: `undefined` means the run * never recorded writes at all, which is a different fact from `[]` ("recorded, * and it wrote nothing"). See {@link requireWrites}. */ interface HasWrites { readonly filesWritten?: readonly string[]; } /** Assert the run wrote NO file matching `pattern` (substring or regex). */ export declare function assertNoWrite(r: HasWrites, pattern: string | RegExp): void; /** Assert every file the run wrote matches one of `allowed` (substring or regex). */ export declare function assertWroteOnly(r: HasWrites, allowed: ReadonlyArray): void; /** * Assert the worker's output is a success result, and return its `value`. With a * `contract`, the value is validated against the success shape (a wrong/missing * field fails the assertion). A malformed or error result throws. */ export declare function assertAgentOk(output: string, contract?: OutputContract): Record; /** * Assert the worker's output is an error result, and return its `error`. The * railway's error track — proves the worker reported failure with rich detail * (not that it crashed or returned prose). A malformed or success result throws. */ export declare function assertAgentErr(output: string, contract?: OutputContract): Record; /** * Assert the parsed result satisfies `predicate` — the general form, for * checking rich detail (e.g. `(r) => r.kind === "ok" && r.value.files.length > 0`). */ export declare function assertAgentResult(output: string, predicate: (r: ParsedAgentResult) => boolean, contract?: OutputContract): void; /** * Did the agent invoke a tool whose name matches `name` (string = exact, * RegExp = test)? The predicate behind `assertToolUsed` / `assertToolNotUsed`. */ export declare function usedTool(trace: Trace, name: string | RegExp): boolean; /** How many tools matching `name` the agent invoked. Behind `assertToolCount`. */ export declare function toolCount(trace: Trace, name: string | RegExp): number; /** * Did the `Skill` tool resolve `skill` (e.g. `"superpowers:test-driven-development"`) * without error? The skill-activation predicate behind `assertSkillResolved`. */ export declare function skillResolved(trace: Trace, skill: string): boolean; /** * Did the agent invoke a tool matching `name` whose INPUT satisfies * `inputMatcher` — a tool-ARGUMENT predicate (DeepEval-style), e.g. an `Edit` * that targeted the right file. The predicate behind `assertToolUsedWith`. */ export declare function toolUsedWith(trace: Trace, name: string | RegExp, inputMatcher: (input: unknown) => boolean): boolean; /** * Does the agent's final answer (`trace.output`) contain `needle` (string = * substring, RegExp = test)? The output predicate behind `assertOutputContains` * — the DeepEval-style "what did the agent actually say" check. */ export declare function outputContains(trace: Trace, needle: string | RegExp): boolean; /** * Did ANY request the model received contain `needle` — searching the system * prompt and every message across all requests? The predicate that proves * injected context *reached the model*: a SessionStart hook's `additionalContext` * or a slash command's expansion. * * THIS IS A PROJECTION OF THE REQUEST, NOT THE REQUEST — state what it omits * before building a claim on it. Until 2026-09-10 this sentence was false: the * flattener read only `.text`, so the eight `ContentBlockParam` variants that * carry `content` were invisible, and a payload Claude Code had delivered inside * a `tool_result` read as "never arrived" (zernie/vigiles#231, a false finding). * It now covers every block type the pinned SDK union names, and serialises any * it does not. STILL OMITTED BY DESIGN: `tool_use` / `server_tool_use` inputs — * those are what the model SAID, not what it was TOLD, so a needle in a tool * argument must not read as delivery. See `flattenBlock` in mock-model.ts. Harness tier only — the eval tier drives the * real API, so its `modelRequests` (and this) is empty. Behind `assertRequestContains`. */ export declare function requestContains(trace: Trace, needle: string | RegExp): boolean; /** * Did a hook matching `name` fire? Matches against both the hook label * (`"PreToolUse:Edit"`) and the bare event (`"PreToolUse"`), so `/PreToolUse/` * or `"PreToolUse:Edit"` both work. The predicate behind `assertHookFired`. */ export declare function hookFired(trace: Trace, name: string | RegExp): boolean; /** Did a hook matching `name` fire AND block (exit ≠ 0 / outcome "error")? */ export declare function hookBlocked(trace: Trace, name: string | RegExp): boolean; /** * Assert the agent invoked a tool whose name matches `name` (string = exact, * RegExp = test) — e.g. a skill (`"Skill"`), an MCP tool (`/^mcp__github__/`), or * a subagent (`"Task"`). Needs `transcript: true`. The action invariant the * skill/MCP/command surfaces are really about. */ export declare function assertToolUsed(trace: Trace, name: string | RegExp): void; /** * Assert the agent did NOT invoke any tool matching `name` — the safety negative * (e.g. a destructive MCP tool was never called). "File unchanged" can pass by * accident; "the tool was never used" is the real invariant. Needs `transcript`. */ export declare function assertToolNotUsed(trace: Trace, name: string | RegExp): void; /** * Assert the `Skill` tool resolved `skill` (e.g. `"superpowers:test-driven-development"`) * without error — the correct skill-activation invariant, vs. grepping the body. */ export declare function assertSkillResolved(trace: Trace, skill: string): void; /** * Assert the agent invoked a tool matching `name` whose INPUT satisfies * `inputMatcher` — a tool-ARGUMENT invariant (DeepEval-style). Asserts not just * *that* a tool ran but *with what args*, e.g. an `Edit` that targeted the right * file: `assertToolUsedWith(r, "Edit", (i) => (i as { file_path?: string }) * .file_path === "src/x.ts")`. Needs `transcript`. */ export declare function assertToolUsedWith(trace: Trace, name: string | RegExp, inputMatcher: (input: unknown) => boolean, message?: string): void; /** Assert the agent's final answer contains `needle` (string substring / RegExp). */ export declare function assertOutputContains(trace: Trace, needle: string | RegExp): void; /** * Assert some request the model received contained `needle` — the "did the * injected context land" invariant (SessionStart `additionalContext`, slash * command expansion). Harness tier only; a zero-request trace fails with a hint * that the eval tier can't capture requests. */ export declare function assertRequestContains(trace: Trace, needle: string | RegExp): void; /** * Assert a hook matching `name` fired (and, with `{ blocked: true }`, that it * blocked) — the honest hook-firing check, recorded from the run's stream rather * than inferred from a marker file the hook had to write. Needs `transcript`. */ export declare function assertHookFired(trace: Trace, name: string | RegExp, opts?: { blocked?: boolean; }): void; /** * Assert how many tools matching `name` the agent invoked is within bounds — a * budget invariant (e.g. `{ max: 1 }` = "at most one Write", `{ exactly: 0 }` = * "never touched it"). Catches runaway loops and wasted work. Needs `transcript`. */ export declare function assertToolCount(trace: Trace, name: string | RegExp, bounds: { min?: number; max?: number; exactly?: number; }): void; /** * Assert the named tools occurred in this order (as a subsequence — gaps allowed) * — an ordering invariant. e.g. `["Read", "Edit"]` checks a Read came before an * Edit. For a stricter rule (every Edit preceded by a Read), use `assertToolCalls`. * Needs `transcript`. */ export declare function assertToolSequence(trace: Trace, names: ReadonlyArray): void; /** * The escape hatch: assert any custom invariant over the full list of tool calls * the agent made — for rules the helpers above don't express, e.g. "every Edit * was preceded by a Read of that file". Needs `transcript`. */ export declare function assertToolCalls(trace: Trace, predicate: (calls: readonly ToolCall[]) => boolean, message?: string): void; /** * Did `arm` succeed on EVERY trial for `metric` — τ-bench pass^k = 1? The * reliability predicate over an eval report (vs. `improvement`, which reads the * mean gap). Reads `report.arms[arm].stats[metric].passK`. */ export declare function reliable(report: EvalReport, arm: string, metric: string): boolean; /** * Assert `arm` passed `metric` on every trial (pass^k = 1) — the reliability * gate for a non-deterministic harness ("worked every time", not "on average"). */ export declare function assertReliable(report: EvalReport, opts: { arm: string; metric: string; }): void; /** The gap on `metric` between two arms (arm − baseline). */ export declare function improvement(report: EvalReport, baseline: string, arm: string, metric: string): number; /** * Did `arm` *significantly* beat `baseline` on `metric` — a positive gap whose * two-sided Welch t-test p-value is below `alpha` (default 0.05)? The grounded * upgrade over `improvement`: the noise floor is computed from the arms' spread, * not hand-fed. False when either arm/metric is missing. See `src/stats.ts`. */ export declare function significantlyBeats(report: EvalReport, baseline: string, arm: string, metric: string, alpha?: number): boolean; /** * Assert `arm` significantly beats `baseline` on `metric` (positive gap, p < α). * The statistical gate for a non-deterministic A/B — "the gap clears the noise", * with the noise floor computed, not supplied. The honest version of * `assertImproves(..., { by: se })`. */ export declare function assertSignificant(report: EvalReport, opts: { baseline: string; arm: string; metric: string; alpha?: number; }): void; /** * Assert `arm` beats `baseline` on `metric`. By default just a positive gap > `by` * (pass the combined se to clear the noise floor by hand). Pass `{ significant: * true }` to demand a Welch t-test at `alpha` instead — the computed noise floor. */ export declare function assertImproves(report: EvalReport, opts: { baseline: string; arm: string; metric: string; by?: number; significant?: boolean; alpha?: number; }): void; /** * Assert the current run has not *regressed* against a committed baseline — the * CI gate (Phase C). A regression is an arm×metric that moved **significantly in * the bad direction** vs. `baseline` (Welch t-test, so sampling noise doesn't * trip it; see `src/eval-baseline.ts`). Higher is better by default; list * `lowerIsBetter` metrics (cost/latency) to flip them. Load the baseline with * `readBaseline(path)` and record a fresh one with `writeBaseline(path, reports)`. */ export declare function assertNoRegression(current: EvalReport | readonly EvalReport[], baseline: BaselineFile, opts?: DiffOptions): void; /** * Assert a skill/behaviour triggered reliably — the gate for a skill's * *activation*, over a {@link TriggerRateReport} from `measureTriggerRate`. * `min` gates recall (fires when it should). With irrelevant prompts measured, * `maxFalsePositive` gates the precision side (does NOT fire when it shouldn't) * and `minPrecision` gates `firedRight / firedTotal` — so a too-broad * description that hijacks unrelated work fails, not just a too-narrow one. */ export declare function assertTriggerRate(report: TriggerRateReport, opts: { min?: number; maxFalsePositive?: number; minPrecision?: number; }): void; interface MatcherOutput { pass: boolean; message: () => string; } /** * Custom matchers compatible with both vitest and jest. Register once: * * import { expect } from "vitest"; // or "@jest/globals" * import { vigilesMatchers } from "vigiles"; * expect.extend(vigilesMatchers); * * expect(result).toHaveCreated("RESULT"); * expect(report).toBeatBaseline("vanilla", "gated", "caught"); */ export declare const vigilesMatchers: { toHaveCreated(received: HarnessTestResult, path: string): MatcherOutput; toBlock(received: HookRunResult): MatcherOutput; toBeatBaseline(received: EvalReport, baseline: string, arm: string, metric: string, by?: number): MatcherOutput; toPass(received: T, check: Check): MatcherOutput; toPassAll(received: T, checks: readonly Check[]): MatcherOutput; }; //# sourceMappingURL=harness-assert.d.ts.map