Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 122 additions & 1 deletion cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ import {
import { verifyAttestation } from '../src/attest.js'
import { sha256Hex } from '../src/digest.js'
import { runMcpServer } from '../src/mcp.js'
import type { VerificationReport } from '../src/types.js'
import { localExecRunner, type ExecRunOutcome } from '../src/exec/dialect.js'
import { parseExecSuiteDocument } from '../src/suite-doc.js'
import type { Suite, VerificationReport } from '../src/types.js'

interface Flags {
/** Last value wins (single-valued reads). */
Expand Down Expand Up @@ -246,6 +248,121 @@ async function main(): Promise<number> {
return emit(report, suiteMarkdown(report), flags)
}

if (cmd === 'vitest') {
// vitest <suite.json|module.mjs> — run an `api.qa/vitest@1` artifact
// LOCALLY through the SAME shared subset harness the hosted verifier
// executes (AXP A.8.6.2 parity is by construction: one module, byte-
// identical, never a reimplementation). Local runs are ADVISORY (never
// attested); the definition of done is the SAME digest passing hosted.
//
// npx autonomous-qa vitest suite.json --target https://api.example
// [--env <name>] [--expect-digest sha256:<64hex>] [--seed <n>] [--json]
// npx autonomous-qa vitest dist/index.mjs --target https://api.example
// [--export suite] [--expect-digest sha256:<64hex>]
//
// Artifact KIND follows the card rule (A.8.5): a path ending `.mjs` (or
// `--module`) is a module artifact; anything else is a suite document.
// The digest gate is FAIL-CLOSED: with --expect-digest, bytes that do not
// hash to the pin never instantiate.
const file = rest[0]
if (!file) return die('vitest needs an artifact: a suite document (.json) or a module (.mjs)')
const text = readFileSync(file, 'utf8')
const digest = `sha256:${await sha256Hex(text)}`
const expect = flags.get('expect-digest')
if (expect !== undefined && expect !== digest) {
console.error(
`autonomous-qa: vitest artifact digest mismatch: expected ${expect}, ${file} hashes to ${digest}. ` +
'NOTHING was instantiated (digest fail-closed, A.8.6.3).',
)
return 1
}
const isModule = flags.has('module') || /\.mjs$/i.test(file)
const envName = flags.get('env') ?? 'public'

let doc: Suite | undefined
if (!isModule) {
try {
doc = parseExecSuiteDocument(text)
Comment on lines +283 to +285

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Apply the hosted row gate in the local CLI

For document artifacts, this path only calls parseExecSuiteDocument() and never applies gateVitestSuiteDocument(), while the hosted path rejects non-endpoint rows and any declarative method other than GET/HEAD. The later local verifySuite() call is consent mode and permits writes, so a document containing a POST row can mutate the target and pass locally even though hosted verification refuses the artifact before execution. Reuse the same document gate before starting either part of the local run.

Useful? React with 👍 / 👎.

} catch (err) {
console.error(`autonomous-qa: ${err instanceof Error ? err.message : String(err)}`)
return 1
}
if (!Object.hasOwn(doc.environments, envName)) {
return die(
`vitest: environment "${envName}" is not defined by the suite (it defines ${Object.keys(doc.environments).join(', ') || 'none'})`,
)
}
} else if (flags.has('env') && envName !== 'public') {
return die('vitest: a module artifact defines no environments — only the implicit "public" exists (A.8.6.4)')
}
const env = doc?.environments[envName]
const vars = { ...(env?.vars ?? {}) }
const target = flags.get('target') ?? (typeof vars.baseUrl === 'string' ? (vars.baseUrl as string) : undefined)
if (!target) return die('vitest needs a target: --target <origin> (or a string `baseUrl` var in the selected environment)')
const runSeed = seed ?? (Math.floor(Math.random() * 0xffffffff) >>> 0)

const outcome: ExecRunOutcome = await localExecRunner().run({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Run declarative rows before executable tests locally

The local CLI starts executable tests here and only runs declarative rows afterward, whereas observeVitestSuite() runs rows before calling execRunner.run(). For a sandbox suite whose executable test creates, updates, or deletes state observed by a row, local and hosted runs therefore inspect different target states and can produce opposite verdicts under the same digest and seed. Preserve the hosted ordering in the CLI before claiming local/hosted parity.

Useful? React with 👍 / 👎.

artifactKind: isModule ? 'module' : 'document',
testsSource: isModule ? text : doc!.tests!,
...(doc?.module !== undefined && { moduleSource: doc.module }),
...(flags.has('export') && { exportName: flags.get('export') }),
origin: new URL(/^https?:\/\//.test(target) ? target : `https://${target}`).origin,
vars,
environment: envName,
sandbox: env?.sandbox === true,
seed: runSeed,
declarativeRows: doc?.requirements.length ?? 0,
digest,
})

// Declarative rows (document form) keep unchanged suite@1 engine
// semantics — run them through the SAME verifySuite door the `suite` verb
// uses, folded into one exit code (A.8.6.5's one result set).
let rowsPassed = true
let rowLines: string[] = []
if (doc !== undefined && doc.requirements.length > 0) {
const rowSuite = JSON.stringify({
$type: 'Suite',
name: doc.name,
version: doc.version,
environments: doc.environments,
requirements: doc.requirements,
})
const rowReport = await verifySuite(rowSuite, envName, {
mode: 'local',
seed: runSeed,
target,
delayMs: isLocalTarget(target) ? 0 : 150,
})
rowsPassed = rowReport.passed
rowLines = rowReport.requirements.map(
(r) => ` ${r.verdict === 'pass' ? 'PASS' : 'FAIL'} row ${r.id}${r.verdict === 'pass' ? '' : ` — ${r.detail}`}`,
)
}

if (flags.get('json') === 'true') {
console.log(JSON.stringify({ runner: 'api.qa/vitest@1', digest, seed: runSeed, environment: envName, outcome, rowsPassed }, null, 2))
} else {
console.log(`# api.qa/vitest@1 — local run (advisory, never attested)`)
console.log(`artifact ${file} (${isModule ? 'module' : 'document'}), digest ${digest}`)
console.log(`target ${target}, environment "${envName}"${env?.sandbox === true ? ' (sandbox)' : ''}, seed ${runSeed}`)
for (const line of rowLines) console.log(line)
if (outcome.status === 'ran') {
for (const r of outcome.results) {
console.log(` ${r.status === 'pass' ? 'PASS' : 'FAIL'} ${r.name} (${r.durationMs} ms)${r.reason ? ` — ${r.reason}` : ''}`)
}
console.log(
`${outcome.results.filter((r) => r.status === 'pass').length}/${outcome.registered} tests passed ` +
`(${outcome.elapsedWallMs} ms wall, limits ${outcome.appliedLimits.wallMs} ms wall / ${outcome.appliedLimits.cpuMs} ms CPU)`,
)
} else {
console.log(`RUN ${outcome.status.toUpperCase()}: ${outcome.reason}`)
}
}
const testsPassed = outcome.status === 'ran' && outcome.results.every((r) => r.status === 'pass')
return testsPassed && rowsPassed ? 0 : 1
}

if (cmd === 'contract-diff') {
// contract-diff <openapi-spec> <target> (ax-gyh): the highest-value CI gate
// — "did this deploy break the published contract?". The spec is LOCAL (a
Expand Down Expand Up @@ -464,6 +581,10 @@ function usage(): string {
[--iteration-data <dataset.csv|.json>] run once per dataset row (data-driven)
[--target <target>] [--expect-digest <sha256>] [--seed <n>]
(target defaults to the selected environment's baseUrl var)
npx autonomous-qa vitest <suite.json|module.mjs> run an api.qa/vitest@1 executable suite LOCALLY
--target <origin> [--env <name>] [--export <n>] through the SAME shared subset harness the hosted
[--expect-digest <sha256:…>] [--seed <n>] verifier executes (local==hosted by construction);
[--module] [--json] EXITS NON-ZERO iff any test or row fails
npx autonomous-qa contract-diff <spec> <target> did this deploy break the published contract?
[--json] [--reporter ...] [--seed <n>] EXITS NON-ZERO on any breaking operation diff
(spec is the published OpenAPI file; target is fetched live, SSRF-gated)
Expand Down
44 changes: 44 additions & 0 deletions scripts/gen-vitest-subset.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env node
/**
* Regenerate `src/exec/vitest-subset-source.ts` from the canonical harness
* `src/exec/vitest-subset.mjs`.
*
* node scripts/gen-vitest-subset.mjs
*
* WHY A GENERATED STRING CONSTANT EXISTS AT ALL. AXP A.8.6.2 requires the
* hosted verifier and the local CLI to share ONE implementation of the
* `api.qa/vitest@1` subset. The hosted runner must inject the harness into a
* Worker Loader isolate's module map as SOURCE TEXT (an isolate takes strings,
* not in-memory objects), and a Worker has no filesystem to read the .mjs from
* — so the exact bytes are carried as a TypeScript string constant, exactly
* like `scripts/gen-assets.mjs` inlines the raster assets. The local runner
* instantiates the SAME constant via a `data:text/javascript` import, so both
* hosts execute byte-identical harness code by construction.
*
* The constant is committed; a normal build does NOT run this. A pinning test
* (test/vitest-subset.test.ts) fails when the generated bytes drift from the
* canonical file, naming this script as the fix.
*/

import { readFileSync, writeFileSync } from 'node:fs'

const canonicalUrl = new URL('../src/exec/vitest-subset.mjs', import.meta.url)
const outUrl = new URL('../src/exec/vitest-subset-source.ts', import.meta.url)

const source = readFileSync(canonicalUrl, 'utf8')

const generated = `/**
* GENERATED — do not edit. Regenerate with: node scripts/gen-vitest-subset.mjs
*
* The exact bytes of the canonical \`api.qa/vitest@1\` subset harness
* (src/exec/vitest-subset.mjs), carried as a string so the hosted Worker
* Loader runner can inject them into an isolate's module map and the local
* runner can instantiate them via a data: import — ONE implementation for
* both hosts (AXP A.8.6.2), pinned byte-identical by test/vitest-subset.test.ts.
*/

export const VITEST_SUBSET_SOURCE: string = ${JSON.stringify(source)}
`

writeFileSync(outUrl, generated)
console.log(`gen-vitest-subset: wrote ${outUrl.pathname} (${source.length} bytes of harness source)`)
7 changes: 6 additions & 1 deletion skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Build and verify AXP-conformant API surfaces (the agent-experience

# AXP — the agent-experience standard

Every API property ships a machine face that agents can discover, price, call, and **verify without asking anyone**. The standard is AXP (https://axp.org.ai, pinned spec `apis-ax-axp@2.2.0`); the verifier is api.qa (`autonomous-qa` on npm). This skill encodes how to build to it.
Every API property ships a machine face that agents can discover, price, call, and **verify without asking anyone**. The standard is AXP (https://axp.org.ai, pinned spec `apis-ax-axp@2.4.0`); the verifier is api.qa (`autonomous-qa` on npm). This skill encodes how to build to it.

## The doctrine (why, in one paragraph)

Expand All @@ -18,6 +18,11 @@ A published test suite converts **"trust us" into "run this."** It is the execut
3. **`/pricing`** — the machine-readable pricing document. `{"model":"free"}` where true — an agent must never have to ask a human what something costs; metered surfaces declare hard ceilings, offers, and the 402 boundary.
4. **`llms.txt`** — cross-linking the rest of your API family.

**Optional declared interfaces — additive only, and only where already true (AXP 0.6.0, Appendix A.8):** beside `http` and `mcp`, a card MAY declare further `interfaces.<name>` members. **Presence is the declaration**, and the rule is two-sided: omitting the key is *fully conformant* and the armed check `skip`s, while declaring it is judged **strictly** — a defective declaration **fails**, it does not skip. There is no value meaning "no"; a card that means no omits the key. Optional interfaces are **additive capabilities only**: none of them can ever relieve you of Clauses 1–7.

- `interfaces.digitalLink` — this origin's GS1 **Resolver Description File** at `/.well-known/gs1resolver`, so an agent which has never heard of GS1 learns from the card it already reads that this origin resolves GS1 keys. Declared, that file MUST answer 200, MUST validate against GS1's published description-file schema, and its `resolverRoot` MUST be this origin. **Since `apis-ax-axp@2.3.0` this is admission-pinned in declaration-armed form** (`check-digital-link-resolver`, `appliesWhen: { cardDeclares: "interfaces.digitalLink" }`) — so declare it only where the well-known already answers; a card that omits it passes as *not applicable*. AXP restates none of GS1's vocabulary and verifies none of GS1's resolution behaviour (linkType, RFC 9264 linksets, redirects) — that is GS1's standard and GS1's test suite.
- `interfaces.testSuite` — this origin's own digest-pinned conformance suite. Card seam `{ url?, package?, version?, export?, digest, environment?, runner? }`: at least one address, one `sha256:` digest as the **sole byte authority**. Two ratified dialects (AXP 0.7.0): `api.qa/suite@1` — declarative rows the verifier *interprets*, GET/HEAD-only with writes disabled, same-origin — and `api.qa/vitest@1` (Appendix A.8.6) — executable tests as a digest-pinned module, addressed by any of the three collapsed channels (A.8.6.6): string members inside the pinned suite document (`tests` + optional `module`), a natively served ES module at a versioned URL (e.g. `https://pkg.do/apis.vin@1.2.0/index.mjs` — the SDK case; an AXP package property MUST serve `.mjs` + `.d.ts` natively at immutable versioned URLs with `{package, version, digest}` provenance), or an npm `package@version` **identity assertion** over the served bytes (npm a verifiable mirror, never in the loop). A guaranteed vitest subset (`describe`/`it`/`expect` + async; imports closed to `vitest` / `suite:env` / `suite:module`; no node built-ins, snapshots, or mocking) the verifier *executes* in a fresh zero-authority isolate above a network floor (no metadata/link-local/private/verifier-internal destinations; **full external egress otherwise**), under a metered circuit-breaker deadline (default 300s wall / 60s CPU, billed; suite@1 keeps its fixed 20s), with seeded randomness and mutating verbs only against an environment the suite declares `sandbox: true`. The runner is a paid-tier capability — the paid tier, never scarcity, is its gate — so the remaining caps are abuse circuit-breakers, not rations: 1000 rows+tests combined, 1 MiB document, 4 MiB module artifact, 4 MiB output. Same file runs under local vitest and hosted api.qa — one shared harness, parity by construction. **Since `apis-ax-axp@2.4.0` this is admission-pinned in declaration-armed form** (`check-published-test-suite`) — declare it only where the artifact already answers at the declared pin; a card that omits it passes as *not applicable*.

Plus: **typed envelopes** (`OK / EMPTY / BLOCKED / OFFER` — three emptinesses never blend) and **the conneg law** on every dereferenceable address: extension forces (`.html/.json/.md`) → Accept infers (q-values, header-order ties) → client-class defaults on `*/*` (browser via Sec-Fetch-*, never UA sniffing → HTML; known agent UAs → markdown; everything else incl. bare curl → JSON). JSON faces are JSON-LD with resolvable `$context`. `Link rel="alternate"` advertises siblings. Never 406. HEAD mirrors GET.

## How to build (never hand-roll)
Expand Down
Loading
Loading