Documentation
¶
Overview ¶
Package audittest holds the canonical list of audit emissions the core must produce, plus the recording sink the contract tests drive it with.
Why a shared package instead of one list per test: the audit seam (ext.AuditSink) is the one thing an embedding distribution relies on to know WHO read WHICH historical rows. Before #945 the wiring covered the CLI's query/recover and the MCP tools only, while ext/audit.go's own contract named the shim, reconstruct and the console — and no test pinned any of it, so a refactor could drop an emission with CI green. Required below is that contract, machine-checked.
The list is split by Owner rather than exercised from one place because each surface is only drivable from inside its own package (unexported handlers, in-package fakes) or needs a live MySQL. Every owner's contract test calls CheckCoverage with the pairs it actually observed, so a dropped emission fails that owner's test and an emission nobody exercises fails as uncovered.
This package is imported by tests only; it is never linked into a binary.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var Required = []Requirement{ { Pair: Pair{Surface: "cli", Action: "query.run"}, Owner: OwnerCLI, Why: "reading indexed row images (before/after) from the command line", }, { Pair: Pair{Surface: "cli", Action: "recover.generate"}, Owner: OwnerCLI, Why: "producing a reversal script — the mutation artifact recover exists to record", }, { Pair: Pair{Surface: "cli", Action: "recover.cascade"}, Owner: OwnerCLI, Why: "producing a reversal script that also SYNTHESIZES cascade-deleted child rows", }, { Pair: Pair{Surface: "cli", Action: "reconstruct.run"}, Owner: OwnerCLI, Why: "reading a baseline snapshot's rows and folding deltas onto them (point-in-time row state)", }, { Pair: Pair{Surface: "cli", Action: "verify.explain"}, Owner: OwnerCLI, Why: "the only verify output that prints row-level data (the differing rows of a mismatch)", }, { Pair: Pair{Surface: "cli", Action: "drill.run"}, Owner: OwnerCLI, Why: "materializing historical row state into an operator-provided scratch server (a rehearsed restore)", }, { Pair: Pair{Surface: "mcp", Action: "query.run"}, Owner: OwnerMCP, Why: "an LLM client reading indexed row images", }, { Pair: Pair{Surface: "mcp", Action: "recover.generate"}, Owner: OwnerMCP, Why: "an LLM client generating a reversal script", }, { Pair: Pair{Surface: "mcp", Action: "recover.cascade"}, Owner: OwnerMCP, Why: "an LLM client generating a cascade reversal script with SYNTHESIZED child rows", }, { Pair: Pair{Surface: "mcp", Action: "reconstruct.row"}, Owner: OwnerMCPIntegration, Why: "an LLM client reading point-in-time row state (baseline + deltas)", }, { Pair: Pair{Surface: "console", Action: "reconstruct.row"}, Owner: OwnerMCPIntegration, Why: "the console's /mcp endpoint mounts the same reconstruct handler with Surface console", }, { Pair: Pair{Surface: "shim", Action: "timetravel.query"}, Owner: OwnerShim, Why: "network time-travel reads (_flashback/_snapshot/_diff) by an authenticated tenant", }, { Pair: Pair{Surface: "shim", Action: "timetravel.query"}, Owner: OwnerPGShim, Why: "the PostgreSQL wire front-end serves the same row images outside HandleQuery", }, { Pair: Pair{Surface: "console", Action: "query.run"}, Owner: OwnerConsoleUnit, Why: "reading indexed row images from the web console", }, { Pair: Pair{Surface: "console", Action: "recover.generate"}, Owner: OwnerConsoleUnit, Why: "generating a reversal script from the web console", }, { Pair: Pair{Surface: "console", Action: "verify.explain"}, Owner: OwnerConsoleUnit, Why: "the console's row-level mismatch drill-down", }, { Pair: Pair{Surface: "console", Action: "authz.denied"}, Owner: OwnerConsoleUnit, Why: "an authorization refusal — one of the two console events that are not data reads", }, { Pair: Pair{Surface: "console", Action: "profile.denied"}, Owner: OwnerConsoleUnit, Why: "a data-profile refusal: an unknown profile, or a surface that cannot honor redaction", }, { Pair: Pair{Surface: "console", Action: "reconstruct.run"}, Owner: OwnerConsoleIntegration, Why: "point-in-time row state served by the console's Time-travel tab", }, { Pair: Pair{Surface: "console", Action: "recover.cascade"}, Owner: OwnerConsoleIntegration, Why: "cascade reversal (with synthesized child rows) served by the console", }, { Pair: Pair{Surface: "console", Action: "sql.run"}, Owner: OwnerConsoleUnit, Why: "a free-form SQL statement executed over the archive/baseline Parquet by the console's SQL panel (#1177)", }, }
Required is the exact set of audit emissions the core guarantees, keyed by (pair, owner) — one pair may appear under two owners when two independent serving layers emit it (shim/timetravel.query: the MySQL command loop and the PostgreSQL front-end). Adding a surface means adding a row here AND exercising it in that owner's contract test; removing one means deleting the row and correcting the ext/audit.go docstring, which enumerates the same set in prose.
Functions ¶
func CheckCoverage ¶
CheckCoverage fails t unless the observed pairs cover the pairs owner is required to emit: every requirement seen at least once, and no observed pair that Required does not list anywhere.
Know exactly what that proves (#1123). At-least-once catches a DELETED emission for a pair; it is structurally blind to a NEW unaudited mode of a command that already has an audited one (reconstruct --baseline-only slipped through exactly this way). And the undeclared arm fails only on an undeclared emission FROM AN EXERCISED PATH — an emission on a handler no contract case drives, or a pair declared for a different owner, is invisible here. TestAuditRecordCallSitesAccounted (callsites_test.go) is the source-level backstop for the call-site half of that blindness; a new MODE reusing an existing call site still needs a hand-added emission and contract case.
Types ¶
type Owner ¶
type Owner string
Owner names the contract test responsible for exercising a requirement. It is the test's location, not a taxonomy of its own — the surface vocabulary lives in ext.AuditEvent.Surface.
const ( // OwnerCLI: internal/cli/audit_contract_integration_test.go. The CLI // commands connect through a real DSN (config.Connect), so their contract // test needs the integration MySQL — which CI runs on every pull request // as a required check, not only on tagged refs. OwnerCLI Owner = "internal/cli (integration)" // OwnerConsoleUnit: internal/console/audit_contract_test.go — the HTTP // handlers that a sqlmock index or a stub controller can serve. OwnerConsoleUnit Owner = "internal/console (unit)" // OwnerConsoleIntegration: internal/console/audit_contract_integration_test.go // — the endpoints that need a real index plus a baseline snapshot. OwnerConsoleIntegration Owner = "internal/console (integration)" // OwnerShim: internal/shim/audit_contract_test.go — Handler.HandleQuery // driven with the package's injected fetcher/resolver fakes. OwnerShim Owner = "internal/shim (unit)" // OwnerMCP: internal/mcptools/audit_contract_test.go — the tool handlers // over a sqlmock index. OwnerMCP Owner = "internal/mcptools (unit)" // OwnerMCPIntegration: internal/mcptools/reconstruct_integration_test.go — // the reconstruct tool needs a real index plus a baseline Parquet, so its // contract case lives in the integration tier (which CI runs on every // pull request as a required check). OwnerMCPIntegration Owner = "internal/mcptools (integration)" // OwnerPGShim: internal/pgshim/audit_contract_integration_test.go — the // PostgreSQL wire front-end drives a real pgx client against a seeded // index, the same real-code-path posture as the other owners. OwnerPGShim Owner = "internal/pgshim (integration)" )
type Pair ¶
Pair identifies one audit emission: the ext.AuditEvent Surface and Action an operator (or an embedding distribution's sink) can rely on seeing.
func RequiredFor ¶
RequiredFor returns the pairs owner must exercise.
type Recorder ¶
type Recorder struct {
// contains filtered or unexported fields
}
Recorder is an ext.AuditSink that keeps every event for assertions. Safe for concurrent use, as the AuditSink contract demands.
func Install ¶
Install registers a Recorder as the process-wide audit sink and removes it when the test ends.
The sink is process-wide, so a test that installs one must NOT call t.Parallel() — two parallel tests would observe each other's events. The swap itself is atomic (ext keeps the sink behind an atomic.Pointer), so driving a surface from another goroutine is race-safe.
func (*Recorder) Events ¶
func (r *Recorder) Events() []ext.AuditEvent
Events returns a copy of everything recorded so far.
type Requirement ¶
type Requirement struct {
Pair
Owner Owner
// Why states what the operator loses if this emission disappears.
Why string
}
Requirement is one emission the core must make, and where it is pinned.