Skip to content

T69 Self-Test Trust Domain Validation

T69 Self-Test Trust Domain Validation

Scope

T69 gives Verchestra a way to exercise its own production boundaries with production code while holding zero production authority and mutating zero active state. A disposable root is provisioned and proven disjoint from every guarded root before anything runs; the subject is composed only from test-only material; the active-state Sentinel Set is hashed before and after; cleanup either proves removal or the root enters an explicit quarantine; and the run leaves exactly one allowlisted, signed report.

The split follows the boundary the architecture already enforces. Adapters may not import sibling adapters, and the domain has to exercise exactly those siblings, so rules live in packages/application/src/self-test/, Node-bound facts in the new packages/self-test/ adapter, and the only place that constructs TEST-ONLY sibling instances is apps/vestra-cli/src/self-test-composition.ts. Ports return facts, never verdicts: a rule an adapter can answer is a rule nobody can unit-test.

53 cases across four suites, against a declared minimum of 35.

Suite Cases
tests/unit/self-test-rules.test.mjs 30
tests/unit/self-test-adapter.test.mjs 9
tests/security/self-test-escape.test.mjs 4
tests/fault-injection/self-test-composition-faults.test.mjs 10

Deterministic gates

Both gates ran on a clean checkout detached at the implementation revision, dispatched through full-validation.yml; each run’s artifact records the revision and the profile.

Command Result
pnpm gate:quick PASS — run 30725095158
pnpm gate:security PASS — run 30725100211
Profile Stages
gate:quick format:check, lint, complexity:check, typecheck, test:unit, test:agent-readiness
gate:security format:check, lint, complexity:check, typecheck, build, test:unit, test:architecture, test:qualification, test:security, test:fault

A first dispatch of gate:quick at the T4 revision failed, and the failure was a defect in the validation workflow rather than in T69: the candidate was fetched with --depth=1, which makes it a graft boundary, so the ancestry proof this contract requires of every post-T68 report could not succeed and the resolver derived T68 complete; T68a next inside the runner. That is recorded in #177 and repaired in #178; the runs above are from the repaired workflow. It is stated here rather than omitted because the first failure is part of this task’s real evidence trail.

Adequacy matrix

Anchored in .specs/features/self-test/spec.md.

Criterion Requirement Assertion
TST-01 No alias, symlink, junction, or normalized form of a disposable root overlaps a guarded root; an overlapping candidate fails closed self-test-rules.test.mjs — device-and-inode identity, containment in both directions, a link-chain hop through a guarded root, a sibling sharing only a path prefix admitted, and missing device or inode facts failing closed rather than assuming disjoint. self-test-escape.test.mjs — a real junction (win32) or directory symlink (POSIX) into guarded state is exposed in linkChain by the adapter and refused by the rule. The orchestrator test proves an overlapping root is never mutated: zero cleanup and zero quarantine calls, because overlap means the root may be the production state this domain exists to protect
TST-02 Production keys, identities, policies, stores, and credentials are rejected with a distinct error; a test-only identity is constructed rather than borrowed self-test-rules.test.mjs — non-test material rejected with VES_SELFTEST_PRODUCTION_MATERIAL naming the material. self-test-escape.test.mjs and self-test-composition-faults.test.mjs — the subject never runs when production material is present, and the composed subject only ever receives testOnly: true material generated per run and never persisted
TST-03 Profiles come from a closed registry declaring identity, resources, limits, cleanup policy, and report schema; unknown profiles fail closed self-test-rules.test.mjs — all four sealed ids resolve with bounded fixture and duration limits and the single remove-or-quarantine cleanup policy; an unknown id fails naming the registry. There is no registration API, so the enum stays exactly the four values the qualified support-bundle contract admits. self-test-composition-faults.test.mjs — an unknown profile provisions nothing at all
TST-04 The Sentinel Set hashed before execution is byte-identical after it; any mutated, added, or removed sentinel fails the run and quarantines the root self-test-rules.test.mjs — order-independent comparison, mutated/added/removed each named, duplicate ids rejected as malformed facts. self-test-adapter.test.mjs — digests track content and absence is itself a fact. self-test-composition-faults.test.mjs — a scenario that rewrites a sentinel and a scenario that deletes one both fail the run and leave the root quarantined on disk
TST-05 A root cleanup cannot prove removed enters quarantine through an explicit state machine, never a silent leak; quarantine failure fails closed self-test-rules.test.mjs — legal path, terminal states, and illegal transitions naming both states; unproven cleanup quarantines; a quarantine that cannot prove itself raises VES_SELFTEST_QUARANTINE_FAILED. self-test-adapter.test.mjs — removal is only claimed when the root demonstrably no longer exists, proven against a genuinely undeletable root, and quarantine renames aside with a reason marker
TST-06 The report contains only the allowlisted self_test.* fields, passes the prohibited-content scanner, and is signed by the test-domain identity self-test-rules.test.mjs — unknown and missing fields rejected, prohibited content classes rejected while registered VES_ codes pass, profile and verdict enums closed. self-test-composition-faults.test.mjs — the sealed report verifies against its trust root, carries exactly the seven allowlisted fields, is bound to the code-registry digest so it cannot be replayed under a registry admitting different codes, and an unregistered failure code fails before sealing

Discrimination sensor

Thirteen mutations, each targeting behavior rather than formatting, applied in place and reverted after the affected suites ran. Application was verified per mutation; a pattern that did not match would have been reported as not-applied rather than counted.

# Criterion Mutation Result
M1 TST-01 Path containment never reports overlap KILLED (4 failing)
M2 TST-01 Overlap ignores the real path and link chain KILLED (1)
M3 TST-02 Production material passes as test-only KILLED (3)
M4 TST-03 An unknown profile resolves instead of failing closed KILLED (3)
M5 TST-04 A mutated sentinel digest is not reported KILLED (4)
M6 TST-05 Any quarantine transition is legal KILLED (2)
M7 TST-05 A failed quarantine is treated as successful KILLED (1)
M8 TST-06 The report allowlist accepts unknown fields KILLED (1)
M9 TST-06 Prohibited report content passes the scanner KILLED (2)
M10 TST-01 The adapter hides symlink and junction hops KILLED (1)
M11 TST-05 Cleanup claims removal without proving it KILLED (1)
M12 TST-01 Fixtures may escape the disposable root KILLED (1)
M13 TST-06 Unregistered failure codes reach evidence KILLED (1)

M11 survived its first run: every case passed with the post-removal check deleted, because rm genuinely succeeded in each of them, so nothing was asserting that the claim was checked rather than assumed. The suite was strengthened rather than the mutation dropped — a root is made genuinely undeletable (a process working directory on Windows, a read-only parent on POSIX) and the reported facts are asserted against reality, with the case failing loudly if the arrangement itself did not hold. The mutation then died, and the campaign closed at 13 killed, 0 survived with a clean rerun of all four suites.

Non-shallow checks

  • The overlap rule is decided from facts an adapter cannot editorialize: canonical path, resolved real path, device id, inode id, and the full link chain. The rule never touches a filesystem, so every verdict is unit-testable and every fact is separately observable.
  • An overlapping root is refused before any mutation, including quarantine. Quarantining an overlapping root would mean writing a marker into what might be production state.
  • The four profile ids are not a convenience enum: they are the values the already-qualified support-bundle evidence contract admits for self_test.profile (T57). Crash-recovery in T71 is a mode inside full rather than a fifth id, so no qualified surface reopens.
  • The report is bound to the digest of the code registry it was validated against, and a negative assertion proves a swapped binding is refused, so a report cannot be replayed as though it came from a registry admitting different codes.
  • No JSON schema was added for the report. schema-registry.test.mjs seals the schema list and test:contract does not run in gate:security, so a schema added here would break a gate this task never runs. The allowlist stays in TypeScript until T72.
  • The adapter takes its key material from node:crypto rather than the evidence package, because evidence is a sibling adapter it must not import. The architecture rule shaped the design instead of being worked around.

Verdict

T69 is complete. Six of six acceptance criteria have file-and-assertion evidence, 53 cases exceed the declared minimum of 35, both declared gates pass on the implementation revision through externally dispatched runs, and thirteen of thirteen mutations are killed with none surviving.

What this does not claim: no Self-Test profile is composed into the public CLI, which still exposes only init. T70 adds the smoke and workspace profiles, T71 the full, fault, and approved-driver profiles, and T72 the doctor --deep verb and signed report surface. Independent verification and human review are established outside this file, at the pull request it names.