diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c4c649..ec32ac3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: - main pull_request: + workflow_dispatch: permissions: contents: read diff --git a/tests/conformance-authority-mutations.spec.ts b/tests/conformance-authority-mutations.spec.ts new file mode 100644 index 0000000..ab6948e --- /dev/null +++ b/tests/conformance-authority-mutations.spec.ts @@ -0,0 +1,997 @@ +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { describe, expect, test } from 'vitest'; + +import { + aggregateConformanceEvidence, + readAssertionRegistry, +} from '../scripts/conformance-contract.mjs'; +import type { + AssertionEntry, + CaveAssertionEntry, + CaveAssertionEngine, + ConformanceSummary, + PlatformEvidence, +} from '../scripts/conformance-contract.d.mts'; + +const workspaceRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const REGISTRY = readAssertionRegistry( + resolve( + workspaceRoot, + 'conformance/client-v1-cross-repository-assertions.json', + ), +); +const PLATFORMS = ['darwin-arm64', 'linux-x64', 'win32-x64'] as const; +const SHA_A = 'a'.repeat(64); +const SHA_B = 'b'.repeat(64); +const SHA_REGISTRY = 'e'.repeat(64); +const COMMIT_A = 'a'.repeat(40); +const COMMIT_B = 'b'.repeat(40); +const COMMIT_C = 'c'.repeat(40); +const COMMIT_D = 'd'.repeat(40); +const COMMIT_E = 'e'.repeat(40); +const RAN_AT = '2026-08-28T23:30:00.000Z'; +const CAVE_ASSERTION_IDS = ['cave.one', 'cave.two']; + +function coverageFailures( + entries: readonly { id: string }[], + expected: readonly string[], +): string[] { + const counts = new Map(); + for (const entry of entries) { + if (entry.id === 'harness.assertion-coverage') continue; + counts.set(entry.id, (counts.get(entry.id) ?? 0) + 1); + } + const failures: string[] = []; + for (const id of expected) { + const count = counts.get(id) ?? 0; + if (count === 0) failures.push(`missing ${id}`); + if (count > 1) failures.push(`duplicate ${id}`); + } + for (const id of counts.keys()) { + if (!expected.includes(id)) failures.push(`unexpected ${id}`); + } + return failures; +} + +function summarize( + entries: readonly { result: string }[], +): ConformanceSummary { + const passed = entries.filter(({ result }) => result === 'pass').length; + const failed = entries.filter(({ result }) => result === 'fail').length; + const skipped = entries.filter(({ result }) => result === 'skip').length; + return { + total: entries.length, + passed, + failed, + skipped, + status: failed > 0 ? 'failed' : 'passed', + }; +} + +interface EngineOptions { + blindToMissing?: boolean; + countSkipAsPass?: boolean; + reportExtraSkips?: number; + corruptRenderedSummary?: boolean; +} + +function createCaveEngine(options: EngineOptions = {}): CaveAssertionEngine { + const summarizeConformance = ( + entries: readonly { result: string }[], + ): ConformanceSummary => { + const summary = summarize(entries); + let passed = summary.passed; + let skipped = summary.skipped; + if (options.countSkipAsPass === true) { + passed += skipped; + skipped = 0; + } + if (typeof options.reportExtraSkips === 'number') { + skipped += options.reportExtraSkips; + } + return { ...summary, passed, skipped }; + }; + const checkAssertionCoverage = ( + entries: readonly { id: string }[], + expected: readonly string[], + ): string[] => { + const failures = coverageFailures(entries, expected); + if (options.blindToMissing === true) { + return failures.filter((failure) => !failure.startsWith('missing ')); + } + return failures; + }; + const renderConformanceRecord: CaveAssertionEngine['renderConformanceRecord'] = + (entries, context) => { + const record: PlatformEvidence['caveRecord'] = { + harness: 'scripts/client-v1-conformance.mjs', + issues: [ + 'OpenCoven/coven-cave#4832', + 'OpenCoven/coven-cave#4838', + ], + scope: 'cave-only', + ranAt: context.ranAt, + caveVersion: context.caveVersion, + commit: context.commit, + platform: context.platform, + nodeVersion: process.version, + includeTtl: context.includeTtl, + authorityTakeover: { ...context.authorityTakeover }, + notCovered: [...context.notCovered], + findings: context.findings.map((finding) => ({ ...finding })), + summary: summarizeConformance(entries), + assertions: entries.map((entry) => ({ ...entry })), + }; + if (options.corruptRenderedSummary === true) { + record.summary = { ...record.summary, failed: 1 }; + } + return record; + }; + return { + COVERAGE_ASSERTION_ID: 'harness.assertion-coverage', + FINDINGS: [ + { + id: 'cave-finding', + measured: 'safe aggregate fixture', + says: 'safe aggregate fixture', + severity: 'documentation', + where: 'docs/client-v1.md', + why: 'fixture', + }, + ], + NOT_COVERED: [ + 'The SDK and Chat halves are covered only by the cross-repository envelope.', + ], + expectedAssertionIds: () => [...CAVE_ASSERTION_IDS], + checkAssertionCoverage, + summarizeConformance, + renderConformanceRecord, + }; +} + +function passingAssertions(ids: readonly string[]): AssertionEntry[] { + return ids.map((id) => ({ + id, + result: 'pass', + diagnosticId: `${id}.passed`, + })); +} + +function caveAssertionEntries(): CaveAssertionEntry[] { + const entries: CaveAssertionEntry[] = CAVE_ASSERTION_IDS.map((id) => ({ + id, + result: 'pass', + detail: '', + })); + entries.push({ + id: 'harness.assertion-coverage', + result: 'pass', + detail: 'complete', + }); + return entries; +} + +function chatAssertionIdsFor(platform: string): string[] { + const platformIds = + REGISTRY.chat.platforms[platform as keyof typeof REGISTRY.chat.platforms]; + if (platformIds === undefined) { + throw new Error(`no chat registry entries for ${platform}`); + } + return [...REGISTRY.chat.common, ...platformIds]; +} + +function createPlatformEvidence( + platform: (typeof PLATFORMS)[number], +): PlatformEvidence { + const [os, arch] = platform.split('-') as [string, string]; + const caveAssertions = caveAssertionEntries(); + return { + schemaVersion: 1, + issue: 'OpenCoven/sdk#38', + platform, + ranAt: RAN_AT, + environment: { + os, + arch, + nodeVersion: 'v24.18.1', + packageManagerVersion: 'pnpm@10.34.0', + }, + releases: { + cave: '0.3.9', + coven: '0.1.0', + }, + commits: { + cave: COMMIT_A, + coven: COMMIT_B, + sdk: COMMIT_C, + chat: COMMIT_D, + }, + digests: { + caveAssertionEngine: SHA_A, + caveContractFixture: SHA_B, + hpkeVectors: 'c'.repeat(64), + consumerLock: 'd'.repeat(64), + assertionRegistry: SHA_REGISTRY, + sdkTarballs: [ + { packageName: '@opencoven/sdk-core', sha256: '1'.repeat(64) }, + { packageName: '@opencoven/cave-client', sha256: '2'.repeat(64) }, + { packageName: '@opencoven/coven-client', sha256: '3'.repeat(64) }, + { packageName: '@opencoven/sdk', sha256: '4'.repeat(64) }, + ], + }, + caveRecord: { + harness: 'scripts/client-v1-conformance.mjs', + issues: [ + 'OpenCoven/coven-cave#4832', + 'OpenCoven/coven-cave#4838', + ], + scope: 'cave-only', + ranAt: RAN_AT, + caveVersion: '0.3.9', + commit: COMMIT_A, + platform, + nodeVersion: 'v24.18.1', + includeTtl: true, + authorityTakeover: { + authorityMode: 'enforce', + discoveryVersion: 2, + mechanism: 'hpke-bound-v1', + }, + notCovered: ['The SDK and Chat halves are covered only by the cross-repository envelope.'], + findings: [ + { + id: 'cave-finding', + measured: 'safe aggregate fixture', + says: 'safe aggregate fixture', + severity: 'documentation', + where: 'docs/client-v1.md', + why: 'fixture', + }, + ], + summary: summarize(caveAssertions), + assertions: caveAssertions, + }, + sdkAssertions: passingAssertions(REGISTRY.sdk), + chatAssertions: passingAssertions(chatAssertionIdsFor(platform)), + coverage: { + cave: true, + coven: true, + sdk: true, + chat: true, + }, + notCovered: [], + isolation: { + strategy: 'process-owned-temporary-roots', + network: 'loopback-only', + sourceCheckoutDependency: false, + workspaceLinkDependency: false, + retainedPrivatePaths: false, + retainedSocketHandles: false, + roots: [ + { id: 'cave-home', ownershipVerified: true, removedAfterRun: true }, + { id: 'coven-home', ownershipVerified: true, removedAfterRun: true }, + { id: 'consumer-home', ownershipVerified: true, removedAfterRun: true }, + { + id: 'native-credential-store', + ownershipVerified: true, + removedAfterRun: true, + }, + ], + operatorState: [ + { id: 'cave-home', beforeSha256: SHA_A, afterSha256: SHA_A }, + { id: 'coven-home', beforeSha256: SHA_B, afterSha256: SHA_B }, + { + id: 'native-credential-store', + beforeSha256: 'c'.repeat(64), + afterSha256: 'c'.repeat(64), + }, + { + id: 'projects', + beforeSha256: 'd'.repeat(64), + afterSha256: 'd'.repeat(64), + }, + ], + }, + }; +} + +function createRecords(): PlatformEvidence[] { + return PLATFORMS.map(createPlatformEvidence); +} + +function aggregate(records: PlatformEvidence[], engine = createCaveEngine()) { + return aggregateConformanceEvidence({ + caveEngine: engine, + caveEngineSha256: SHA_A, + assertionRegistrySha256: SHA_REGISTRY, + canonicalPlatforms: PLATFORMS, + registry: REGISTRY, + platformRecords: records, + }); +} + +function recordFor( + records: PlatformEvidence[], + platform: string, +): PlatformEvidence { + const record = records.find((entry) => entry.platform === platform); + if (record === undefined) { + throw new Error(`fixture is missing the ${platform} record`); + } + return record; +} + +function dropAssertion( + record: PlatformEvidence, + list: 'sdkAssertions' | 'chatAssertions', + id: string, +): void { + record[list] = record[list].filter((entry) => entry.id !== id); +} + +function markAssertion( + record: PlatformEvidence, + list: 'sdkAssertions' | 'chatAssertions', + id: string, + result: 'fail' | 'skip', +): void { + const entry = record[list].find((candidate) => candidate.id === id); + if (entry === undefined) { + throw new Error(`fixture is missing the ${id} assertion`); + } + entry.result = result; +} + +function swapAdjacent(entries: { id: string }[]): void { + const [first, second] = entries; + if (first === undefined || second === undefined) { + throw new Error('fixture list is too short to reorder'); + } + entries[0] = second; + entries[1] = first; +} + +interface AuthorityDefect { + name: string; + expected: string; + defect: (records: PlatformEvidence[]) => void; +} + +function journeyDefects(): AuthorityDefect[] { + return [ + { + name: 'authority accepted a wrong pairing secret', + expected: 'coverage: missing sdk.cave.pairing.wrong-secret-refused', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'sdkAssertions', 'sdk.cave.pairing.wrong-secret-refused'); + }, + }, + { + name: 'authority accepted a replayed exchange', + expected: 'coverage: missing sdk.cave.pairing.replay-refused', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'sdkAssertions', 'sdk.cave.pairing.replay-refused'); + }, + }, + { + name: 'authority hid a pairing denial', + expected: 'coverage: missing sdk.cave.pairing.denied', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'sdkAssertions', 'sdk.cave.pairing.denied'); + }, + }, + { + name: 'authority hid a pairing expiry', + expected: 'coverage: missing sdk.cave.pairing.expired', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'sdkAssertions', 'sdk.cave.pairing.expired'); + }, + }, + { + name: 'authority ignored the shared failure budget', + expected: 'coverage: missing sdk.cave.pairing.shared-failure-budget', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'sdkAssertions', 'sdk.cave.pairing.shared-failure-budget'); + }, + }, + { + name: 'authority ignored the pairing rate limit', + expected: 'coverage: missing sdk.cave.pairing.rate-limit', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'sdkAssertions', 'sdk.cave.pairing.rate-limit'); + }, + }, + { + name: 'authority exchanged the pairing twice', + expected: 'coverage: duplicate sdk.cave.pairing.exchange-once', + defect: (records) => { + const record = recordFor(records, 'darwin-arm64'); + const entry = record.sdkAssertions.find( + (candidate) => candidate.id === 'sdk.cave.pairing.exchange-once', + ); + if (entry === undefined) { + throw new Error('fixture is missing the exchange-once assertion'); + } + record.sdkAssertions.push({ ...entry }); + }, + }, + { + name: 'authority skipped the missing Content-Length control case', + expected: + 'coverage: missing sdk.cave.exchange.missing-content-length-refused', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'sdkAssertions', 'sdk.cave.exchange.missing-content-length-refused'); + }, + }, + { + name: 'authority skipped the Content-Length: 0 exchange case', + expected: + 'coverage: missing sdk.cave.exchange.content-length-zero-accepted', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'sdkAssertions', 'sdk.cave.exchange.content-length-zero-accepted'); + }, + }, + { + name: 'authority collapsed proxy rejections into the Client v1 envelope', + expected: 'coverage: missing sdk.cave.proxy-rejection.distinct-envelope', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'sdkAssertions', 'sdk.cave.proxy-rejection.distinct-envelope'); + }, + }, + { + name: 'authority trusted a stale discovery record', + expected: 'coverage: missing sdk.cave.discovery.stale-record-refused', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'sdkAssertions', 'sdk.cave.discovery.stale-record-refused'); + }, + }, + { + name: 'authority trusted a replaced Cave instance', + expected: 'coverage: missing sdk.cave.discovery.replaced-instance-refused', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'sdkAssertions', 'sdk.cave.discovery.replaced-instance-refused'); + }, + }, + { + name: 'authority accepted a malformed cursor', + expected: 'coverage: missing sdk.cave.cursor.malformed-refused', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'sdkAssertions', 'sdk.cave.cursor.malformed-refused'); + }, + }, + { + name: 'authority accepted a non-canonical cursor', + expected: 'coverage: missing sdk.cave.cursor.noncanonical-refused', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'sdkAssertions', 'sdk.cave.cursor.noncanonical-refused'); + }, + }, + { + name: 'authority lost the reconcile_required case', + expected: 'coverage: missing sdk.cave.cursor.reconcile-required', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'sdkAssertions', 'sdk.cave.cursor.reconcile-required'); + }, + }, + { + name: 'authority served reads with a revoked credential', + expected: 'coverage: missing sdk.cave.revocation.familiars-refused', + defect: (records) => { + const record = recordFor(records, 'darwin-arm64'); + for (const id of [ + 'sdk.cave.revocation.familiars-refused', + 'sdk.cave.revocation.projects-refused', + 'sdk.cave.revocation.conversations-refused', + 'sdk.cave.revocation.conversation-refused', + 'sdk.cave.revocation.messages-refused', + ]) { + dropAssertion(record, 'sdkAssertions', id); + } + }, + }, + { + name: 'authority connected without a bounded deadline', + expected: 'coverage: missing sdk.deadline.connect-bounded', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'sdkAssertions', 'sdk.deadline.connect-bounded'); + }, + }, + { + name: 'authority moved the bearer out of the native SecretStore', + expected: 'coverage: missing sdk.cave.credential.native-store-required', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'sdkAssertions', 'sdk.cave.credential.native-store-required'); + }, + }, + { + name: 'authority did not reuse the credential after restart', + expected: 'coverage: missing sdk.cave.credential.restart-reused', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'sdkAssertions', 'sdk.cave.credential.restart-reused'); + }, + }, + { + name: 'authority failed open without the native keychain', + expected: + '"sdk.native.keychain-missing-fails-closed" did not pass', + defect: (records) => { + markAssertion( + recordFor(records, 'darwin-arm64'), + 'sdkAssertions', + 'sdk.native.keychain-missing-fails-closed', + 'fail', + ); + }, + }, + { + name: 'authority skipped the trust-binding failure case', + expected: + '"sdk.native.trust-binding-missing-fails-closed" did not pass', + defect: (records) => { + markAssertion( + recordFor(records, 'darwin-arm64'), + 'sdkAssertions', + 'sdk.native.trust-binding-missing-fails-closed', + 'skip', + ); + }, + }, + { + name: 'authority reported an unplanned assertion', + expected: 'coverage: unexpected sdk.cave.pairing.unplanned', + defect: (records) => { + recordFor(records, 'darwin-arm64').sdkAssertions.push({ + id: 'sdk.cave.pairing.unplanned', + result: 'pass', + diagnosticId: 'sdk.cave.pairing.unplanned.passed', + }); + }, + }, + { + name: 'authority reordered the SDK assertions', + expected: + 'darwin-arm64 SDK assertion order does not match the authoritative registry', + defect: (records) => { + swapAdjacent(recordFor(records, 'darwin-arm64').sdkAssertions); + }, + }, + { + name: 'authority reordered the Chat assertions', + expected: + 'darwin-arm64 Chat assertion order does not match the authoritative registry', + defect: (records) => { + swapAdjacent(recordFor(records, 'darwin-arm64').chatAssertions); + }, + }, + { + name: 'authority reordered its own Cave assertions', + expected: + 'darwin-arm64 Cave assertion order does not match the authoritative registry', + defect: (records) => { + swapAdjacent(recordFor(records, 'darwin-arm64').caveRecord.assertions); + }, + }, + { + name: 'consumer never inspected the live Unix peer identity', + expected: 'coverage: missing chat.coven.unix.connected-peer-identity', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'chatAssertions', 'chat.coven.unix.connected-peer-identity'); + }, + }, + { + name: 'consumer accepted a malicious COVEN_HOME', + expected: 'coverage: missing chat.coven.unix.malicious-home-refused', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'chatAssertions', 'chat.coven.unix.malicious-home-refused'); + }, + }, + { + name: 'consumer accepted a wrong socket peer UID', + expected: 'coverage: missing chat.coven.unix.wrong-peer-uid-refused', + defect: (records) => { + dropAssertion(recordFor(records, 'linux-x64'), 'chatAssertions', 'chat.coven.unix.wrong-peer-uid-refused'); + }, + }, + { + name: 'consumer accepted a constructed Windows pipe', + expected: 'coverage: missing chat.coven.windows.constructed-pipe-refused', + defect: (records) => { + dropAssertion(recordFor(records, 'win32-x64'), 'chatAssertions', 'chat.coven.windows.constructed-pipe-refused'); + }, + }, + { + name: 'consumer accepted a foreign Windows pipe', + expected: 'coverage: missing chat.coven.windows.foreign-pipe-refused', + defect: (records) => { + dropAssertion(recordFor(records, 'win32-x64'), 'chatAssertions', 'chat.coven.windows.foreign-pipe-refused'); + }, + }, + { + name: 'consumer skipped executable trust validation', + expected: 'coverage: missing chat.coven.executable.trusted', + defect: (records) => { + dropAssertion(recordFor(records, 'win32-x64'), 'chatAssertions', 'chat.coven.executable.trusted'); + }, + }, + { + name: 'consumer swallowed structured daemon errors', + expected: 'coverage: missing chat.coven.structured-errors-preserved', + defect: (records) => { + dropAssertion(recordFor(records, 'linux-x64'), 'chatAssertions', 'chat.coven.structured-errors-preserved'); + }, + }, + { + name: 'consumer retained prompts in evidence', + expected: 'coverage: missing chat.evidence.no-prompts', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'chatAssertions', 'chat.evidence.no-prompts'); + }, + }, + { + name: 'consumer retained message bodies in evidence', + expected: 'coverage: missing chat.evidence.no-message-bodies', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'chatAssertions', 'chat.evidence.no-message-bodies'); + }, + }, + { + name: 'consumer retained attachments in evidence', + expected: 'coverage: missing chat.evidence.no-attachments', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'chatAssertions', 'chat.evidence.no-attachments'); + }, + }, + { + name: 'consumer retained command output in evidence', + expected: 'coverage: missing chat.evidence.no-command-output', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'chatAssertions', 'chat.evidence.no-command-output'); + }, + }, + { + name: 'consumer repaired credentials automatically', + expected: 'coverage: missing chat.cave.restart.no-automatic-repairing', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'chatAssertions', 'chat.cave.restart.no-automatic-repairing'); + }, + }, + { + name: 'consumer kept stale state after Cave replacement', + expected: 'coverage: missing chat.cave.replacement.stale-state-refused', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'chatAssertions', 'chat.cave.replacement.stale-state-refused'); + }, + }, + { + name: 'consumer re-paired instead of reusing the credential', + expected: 'coverage: missing chat.cave.restart.credential-reused', + defect: (records) => { + dropAssertion(recordFor(records, 'darwin-arm64'), 'chatAssertions', 'chat.cave.restart.credential-reused'); + }, + }, + ]; +} + +function authorityBindingDefects(): AuthorityDefect[] { + return [ + { + name: 'authority downgraded takeover to observe mode', + expected: 'did not include the authority-takeover proof', + defect: (records) => { + recordFor(records, 'darwin-arm64').caveRecord.authorityTakeover.authorityMode = 'observe'; + }, + }, + { + name: 'authority downgraded discovery to version 1', + expected: 'did not include the authority-takeover proof', + defect: (records) => { + recordFor(records, 'darwin-arm64').caveRecord.authorityTakeover.discoveryVersion = 1; + }, + }, + { + name: 'authority swapped the hpke-bound mechanism', + expected: 'did not include the authority-takeover proof', + defect: (records) => { + recordFor(records, 'darwin-arm64').caveRecord.authorityTakeover.mechanism = 'bearer-v1'; + }, + }, + { + name: 'authority dropped the TTL assertions', + expected: 'did not include the TTL assertions', + defect: (records) => { + recordFor(records, 'darwin-arm64').caveRecord.includeTtl = false; + }, + }, + { + name: 'record ran a different Cave assertion engine', + expected: + 'Cave assertion engine digest does not match the loaded engine', + defect: (records) => { + recordFor(records, 'darwin-arm64').digests.caveAssertionEngine = SHA_B; + }, + }, + { + name: 'record aggregated against a different registry', + expected: + 'assertion registry digest does not match the committed registry', + defect: (records) => { + recordFor(records, 'darwin-arm64').digests.assertionRegistry = SHA_B; + }, + }, + { + name: 'platform recorded a different SDK commit', + expected: 'linux-x64 commits do not match darwin-arm64', + defect: (records) => { + recordFor(records, 'linux-x64').commits.sdk = COMMIT_E; + }, + }, + { + name: 'platform packed different SDK tarballs', + expected: 'win32-x64 digests do not match darwin-arm64', + defect: (records) => { + const tarballs = recordFor(records, 'win32-x64').digests.sdkTarballs; + const first = tarballs[0]; + if (first === undefined) { + throw new Error('fixture is missing the first tarball'); + } + first.sha256 = 'f'.repeat(64); + }, + }, + ]; +} + +function isolationDefects(): AuthorityDefect[] { + return [ + { + name: 'harness mutated the operator Cave home', + expected: 'operator state "cave-home" changed', + defect: (records) => { + const state = recordFor(records, 'darwin-arm64').isolation.operatorState[0]; + if (state === undefined) { + throw new Error('fixture is missing the cave-home operator state'); + } + state.afterSha256 = SHA_B; + }, + }, + { + name: 'harness mutated operator projects', + expected: 'operator state "projects" changed', + defect: (records) => { + const states = recordFor(records, 'darwin-arm64').isolation.operatorState; + const projects = states.find((entry) => entry.id === 'projects'); + if (projects === undefined) { + throw new Error('fixture is missing the projects operator state'); + } + projects.afterSha256 = SHA_B; + }, + }, + { + name: 'harness left the temporary Coven home behind', + expected: 'isolation root "coven-home" was not owned and removed', + defect: (records) => { + const roots = recordFor(records, 'darwin-arm64').isolation.roots; + const covenHome = roots.find((entry) => entry.id === 'coven-home'); + if (covenHome === undefined) { + throw new Error('fixture is missing the coven-home root'); + } + covenHome.removedAfterRun = false; + }, + }, + { + name: 'harness retained socket handles', + expected: 'retained socket handles', + defect: (records) => { + recordFor(records, 'darwin-arm64').isolation.retainedSocketHandles = true; + }, + }, + { + name: 'harness retained private filesystem paths', + expected: 'retained private filesystem paths', + defect: (records) => { + recordFor(records, 'darwin-arm64').isolation.retainedPrivatePaths = true; + }, + }, + { + name: 'harness left the loopback-only network boundary', + expected: 'isolation network was not loopback-only', + defect: (records) => { + recordFor(records, 'darwin-arm64').isolation.network = 'lan'; + }, + }, + { + name: 'harness linked the consumer to the SDK workspace', + expected: 'used a workspace-link dependency', + defect: (records) => { + recordFor(records, 'darwin-arm64').isolation.workspaceLinkDependency = true; + }, + }, + { + name: 'harness built against the source checkout', + expected: 'used a source-checkout dependency', + defect: (records) => { + recordFor(records, 'darwin-arm64').isolation.sourceCheckoutDependency = true; + }, + }, + { + name: 'harness reordered the isolation roots', + expected: + 'darwin-arm64 isolation roots assertion order does not match the authoritative registry', + defect: (records) => { + swapAdjacent(recordFor(records, 'darwin-arm64').isolation.roots); + }, + }, + { + name: 'harness reordered the operator-state proofs', + expected: + 'darwin-arm64 operator state assertion order does not match the authoritative registry', + defect: (records) => { + swapAdjacent(recordFor(records, 'darwin-arm64').isolation.operatorState); + }, + }, + ]; +} + +describe('representative authority-defect mutations', () => { + test('the unmutated three-platform candidate aggregates cleanly', () => { + const result = aggregate(createRecords()); + expect(result.summary.status).toBe('passed'); + expect(result.summary.failed).toBe(0); + expect(result.summary.skipped).toBe(0); + expect(result.summary.caveAssertions).toBe( + PLATFORMS.length * (CAVE_ASSERTION_IDS.length + 1), + ); + expect(result.summary.sdkAssertions).toBe(REGISTRY.sdk.length * 3); + expect(result.summary.chatAssertions).toBe( + PLATFORMS.reduce( + (total, platform) => total + chatAssertionIdsFor(platform).length, + 0, + ), + ); + expect(result.platforms.map(({ platform }) => platform)).toEqual(PLATFORMS); + }); + + test('input order does not change the aggregate bytes', () => { + const forward = aggregate(createRecords()); + const reversed = aggregate(createRecords().reverse()); + expect(JSON.stringify(reversed)).toBe(JSON.stringify(forward)); + }); + + test.each(journeyDefects())('$name', ({ expected, defect }) => { + const records = createRecords(); + defect(records); + expect(() => aggregate(records)).toThrow(expected); + }); + + test.each(authorityBindingDefects())('$name', ({ expected, defect }) => { + const records = createRecords(); + defect(records); + expect(() => aggregate(records)).toThrow(expected); + }); + + test.each(isolationDefects())('$name', ({ expected, defect }) => { + const records = createRecords(); + defect(records); + expect(() => aggregate(records)).toThrow(expected); + }); + + test('skips are never counted as passes anywhere in a record', () => { + const records = createRecords(); + const record = recordFor(records, 'darwin-arm64'); + for (const entry of record.caveRecord.assertions) entry.result = 'skip'; + for (const entry of record.sdkAssertions) entry.result = 'skip'; + for (const entry of record.chatAssertions) entry.result = 'skip'; + expect(() => aggregate(records)).toThrow( + 'darwin-arm64 Cave assertion "cave.one" did not pass', + ); + }); + + describe('opaque Cave-record helpers at aggregation time', () => { + test('rejects non-boolean TTL flags', () => { + const records = createRecords(); + const record = recordFor(records, 'darwin-arm64'); + const caveRecord = record.caveRecord as unknown as Record; + caveRecord.includeTtl = 'yes'; + expect(() => aggregate(records)).toThrow( + 'darwin-arm64 Cave record includeTtl must be a boolean', + ); + }); + + test('rejects non-integer discovery versions', () => { + const records = createRecords(); + const record = recordFor(records, 'darwin-arm64'); + record.caveRecord.authorityTakeover.discoveryVersion = 2.5; + expect(() => aggregate(records)).toThrow( + 'darwin-arm64 Cave record discoveryVersion must be an integer', + ); + }); + + test('rejects non-string Cave assertion details', () => { + const records = createRecords(); + const record = recordFor(records, 'darwin-arm64'); + const entry = record.caveRecord.assertions[0]; + if (entry === undefined) { + throw new Error('fixture is missing the first Cave assertion'); + } + (entry as unknown as { detail: unknown }).detail = 7; + expect(() => aggregate(records)).toThrow( + 'darwin-arm64 Cave record assertions[0].detail must be a string', + ); + }); + + test('rejects unknown Cave assertion results', () => { + const records = createRecords(); + const record = recordFor(records, 'darwin-arm64'); + const entry = record.caveRecord.assertions[0]; + if (entry === undefined) { + throw new Error('fixture is missing the first Cave assertion'); + } + (entry as { result: string }).result = 'unknown'; + expect(() => aggregate(records)).toThrow( + 'darwin-arm64 Cave record assertions[0].result must be pass, fail, or skip', + ); + }); + + test('rejects records whose platform contradicts its environment', () => { + const records = createRecords(); + recordFor(records, 'darwin-arm64').environment = { + os: 'linux', + arch: 'x64', + nodeVersion: 'v24.18.1', + packageManagerVersion: 'pnpm@10.34.0', + }; + expect(() => aggregate(records)).toThrow( + 'darwin-arm64 does not match its OS and architecture', + ); + }); + }); + + describe('defective assertion engines are still caught', () => { + test('a coverage check blind to missing assertions fails the order check', () => { + const records = createRecords(); + dropAssertion( + recordFor(records, 'darwin-arm64'), + 'sdkAssertions', + 'sdk.cave.pairing.wrong-secret-refused', + ); + expect(() => + aggregate(records, createCaveEngine({ blindToMissing: true })), + ).toThrow( + 'darwin-arm64 SDK assertion order does not match the authoritative registry', + ); + }); + + test('a summary that counts skips as passes fails the per-result check', () => { + const records = createRecords(); + markAssertion( + recordFor(records, 'darwin-arm64'), + 'chatAssertions', + 'chat.install.exact-sdk-tarballs', + 'skip', + ); + expect(() => + aggregate(records, createCaveEngine({ countSkipAsPass: true })), + ).toThrow( + 'darwin-arm64 Chat assertion "chat.install.exact-sdk-tarballs" did not pass', + ); + }); + + test('a summary that invents skips fails the complete-pass check', () => { + expect(() => + aggregate(createRecords(), createCaveEngine({ reportExtraSkips: 1 })), + ).toThrow('darwin-arm64 Cave assertion summary is not a complete pass'); + }); + + test('a renderer that rewrites the summary fails the authority comparison', () => { + expect(() => + aggregate( + createRecords(), + createCaveEngine({ corruptRenderedSummary: true }), + ), + ).toThrow( + 'darwin-arm64 Cave record does not match the authoritative renderer', + ); + }); + }); +}); diff --git a/tests/conformance-helper-boundaries.spec.ts b/tests/conformance-helper-boundaries.spec.ts new file mode 100644 index 0000000..43ba296 --- /dev/null +++ b/tests/conformance-helper-boundaries.spec.ts @@ -0,0 +1,788 @@ +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { describe, expect, test } from 'vitest'; + +import { + parseAssertionRegistry, + parsePlatformEvidence, + readAssertionRegistry, + scanConformanceEvidence, +} from '../scripts/conformance-contract.mjs'; + +const workspaceRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const SHA_A = 'a'.repeat(64); +const SHA_B = 'b'.repeat(64); +const SHA_REGISTRY = 'e'.repeat(64); +const COMMIT_A = 'a'.repeat(40); +const COMMIT_B = 'b'.repeat(40); +const COMMIT_C = 'c'.repeat(40); +const COMMIT_D = 'd'.repeat(40); + +function passingAssertion(id: string): { id: string; result: string; diagnosticId: string } { + return { id, result: 'pass', diagnosticId: `${id}.passed` }; +} + +function caveAssertions(): Array<{ id: string; result: string; detail: string }> { + return [ + { id: 'cave.one', result: 'pass', detail: '' }, + { id: 'cave.two', result: 'pass', detail: '' }, + { id: 'harness.assertion-coverage', result: 'pass', detail: 'complete' }, + ]; +} + +function validRecord(): Record { + const assertions = caveAssertions(); + return { + schemaVersion: 1, + issue: 'OpenCoven/sdk#38', + platform: 'darwin-arm64', + ranAt: '2026-08-28T23:30:00.000Z', + environment: { + os: 'darwin', + arch: 'arm64', + nodeVersion: 'v24.18.1', + packageManagerVersion: 'pnpm@10.34.0', + }, + releases: { cave: '0.3.9', coven: '0.1.0' }, + commits: { cave: COMMIT_A, coven: COMMIT_B, sdk: COMMIT_C, chat: COMMIT_D }, + digests: { + caveAssertionEngine: SHA_A, + caveContractFixture: SHA_B, + hpkeVectors: 'c'.repeat(64), + consumerLock: 'd'.repeat(64), + assertionRegistry: SHA_REGISTRY, + sdkTarballs: [ + { packageName: '@opencoven/sdk-core', sha256: '1'.repeat(64) }, + { packageName: '@opencoven/cave-client', sha256: '2'.repeat(64) }, + { packageName: '@opencoven/coven-client', sha256: '3'.repeat(64) }, + { packageName: '@opencoven/sdk', sha256: '4'.repeat(64) }, + ], + }, + caveRecord: { + harness: 'scripts/client-v1-conformance.mjs', + issues: ['OpenCoven/coven-cave#4832'], + scope: 'cave-only', + ranAt: '2026-08-28T23:30:00.000Z', + caveVersion: '0.3.9', + commit: COMMIT_A, + platform: 'darwin-arm64', + nodeVersion: 'v24.18.1', + includeTtl: true, + authorityTakeover: { + authorityMode: 'enforce', + discoveryVersion: 2, + mechanism: 'hpke-bound-v1', + }, + notCovered: ['The SDK and Chat halves are covered by the envelope.'], + findings: [], + summary: { total: 3, passed: 3, failed: 0, skipped: 0, status: 'passed' }, + assertions, + }, + sdkAssertions: [passingAssertion('sdk.one'), passingAssertion('sdk.two')], + chatAssertions: [passingAssertion('chat.common'), passingAssertion('chat.darwin')], + coverage: { cave: true, coven: true, sdk: true, chat: true }, + notCovered: [], + isolation: { + strategy: 'process-owned-temporary-roots', + network: 'loopback-only', + sourceCheckoutDependency: false, + workspaceLinkDependency: false, + retainedPrivatePaths: false, + retainedSocketHandles: false, + roots: [ + { id: 'cave-home', ownershipVerified: true, removedAfterRun: true }, + { id: 'coven-home', ownershipVerified: true, removedAfterRun: true }, + { id: 'consumer-home', ownershipVerified: true, removedAfterRun: true }, + { + id: 'native-credential-store', + ownershipVerified: true, + removedAfterRun: true, + }, + ], + operatorState: [ + { id: 'cave-home', beforeSha256: SHA_A, afterSha256: SHA_A }, + { id: 'coven-home', beforeSha256: SHA_B, afterSha256: SHA_B }, + { + id: 'native-credential-store', + beforeSha256: 'c'.repeat(64), + afterSha256: 'c'.repeat(64), + }, + { + id: 'projects', + beforeSha256: 'd'.repeat(64), + afterSha256: 'd'.repeat(64), + }, + ], + }, + }; +} + +function parseMutation( + mutate: (record: Record) => void, + source = 'record.json', +): () => unknown { + return () => { + const record = validRecord(); + mutate(record); + return parsePlatformEvidence(JSON.stringify(record), source); + }; +} + +function validRegistryText(): string { + return JSON.stringify({ + schemaVersion: 1, + cave: { + engine: 'scripts/client-v1-conformance.mjs', + requireIncludeTtl: true, + requireAuthorityTakeover: true, + }, + sdk: ['sdk.one', 'sdk.two'], + chat: { + common: ['chat.common'], + platforms: { + 'darwin-arm64': ['chat.darwin'], + 'linux-x64': ['chat.linux'], + 'win32-x64': ['chat.windows'], + }, + }, + }); +} + +describe('assertion helper boundaries', () => { + test('accepts a fully valid platform record', () => { + expect(() => + parsePlatformEvidence(JSON.stringify(validRecord()), 'valid.json'), + ).not.toThrow(); + }); + + describe('expectTimestamp via ranAt', () => { + test('rejects seconds-only UTC timestamps', () => { + expect(parseMutation((record) => { + record.ranAt = '2026-08-28T23:30:00Z'; + })).toThrow('canonical UTC ISO-8601 timestamp'); + }); + + test('rejects explicit zero offsets', () => { + expect(parseMutation((record) => { + record.ranAt = '2026-08-28T23:30:00.000+00:00'; + })).toThrow('canonical UTC ISO-8601 timestamp'); + }); + + test('rejects non-timestamps and impossible dates', () => { + expect(parseMutation((record) => { + record.ranAt = 'not-a-timestamp'; + })).toThrow('canonical UTC ISO-8601 timestamp'); + expect(parseMutation((record) => { + record.ranAt = '2026-13-01T00:00:00.000Z'; + })).toThrow('canonical UTC ISO-8601 timestamp'); + }); + + test('rejects oversized timestamp fields before parsing', () => { + expect(parseMutation((record) => { + record.ranAt = 'x'.repeat(33); + })).toThrow('exceeds the 32-byte limit'); + }); + }); + + describe('expectCommit via commits', () => { + test('rejects short, long, and uppercase commits', () => { + expect(parseMutation((record) => { + record.commits = { cave: 'a'.repeat(39), coven: COMMIT_B, sdk: COMMIT_C, chat: COMMIT_D }; + })).toThrow('commits.cave is not canonical'); + expect(parseMutation((record) => { + record.commits = { cave: 'a'.repeat(41), coven: COMMIT_B, sdk: COMMIT_C, chat: COMMIT_D }; + })).toThrow('commits.cave exceeds the 40-byte limit'); + expect(parseMutation((record) => { + record.commits = { cave: 'A'.repeat(40), coven: COMMIT_B, sdk: COMMIT_C, chat: COMMIT_D }; + })).toThrow('commits.cave is not canonical'); + }); + }); + + describe('expectSha256 via digests', () => { + test('rejects short and non-hex digests', () => { + expect(parseMutation((record) => { + const digests = record.digests as Record; + digests.consumerLock = 'a'.repeat(63); + })).toThrow('digests.consumerLock is not canonical'); + expect(parseMutation((record) => { + const digests = record.digests as Record; + digests.consumerLock = 'g'.repeat(64); + })).toThrow('digests.consumerLock is not canonical'); + }); + }); + + describe('expectTarballs via digests.sdkTarballs', () => { + test('rejects incomplete tarball sets', () => { + expect(parseMutation((record) => { + const digests = record.digests as Record; + digests.sdkTarballs = (digests.sdkTarballs as unknown[]).slice(0, 3); + })).toThrow('must contain the four canonical SDK tarballs'); + }); + + test('rejects non-canonical package order and names', () => { + expect(parseMutation((record) => { + const digests = record.digests as Record; + const tarballs = digests.sdkTarballs as Array<{ packageName: string; sha256: string }>; + const first = tarballs[0]; + const second = tarballs[1]; + if (first !== undefined && second !== undefined) { + tarballs[0] = second; + tarballs[1] = first; + } + })).toThrow('must use canonical package order'); + expect(parseMutation((record) => { + const digests = record.digests as Record; + const tarballs = digests.sdkTarballs as Array<{ packageName: string; sha256: string }>; + const first = tarballs[0]; + if (first !== undefined) first.packageName = '@opencoven/rogue'; + })).toThrow('must use canonical package order'); + }); + + test('rejects malformed tarball digests', () => { + expect(parseMutation((record) => { + const digests = record.digests as Record; + const tarballs = digests.sdkTarballs as Array<{ packageName: string; sha256: string }>; + const first = tarballs[0]; + if (first !== undefined) first.sha256 = '1'.repeat(63); + })).toThrow('sdkTarballs[0].sha256 is not canonical'); + }); + }); + + describe('expectCoverage via coverage', () => { + test('rejects missing, false, and extra scopes', () => { + expect(parseMutation((record) => { + record.coverage = { cave: true, coven: true, sdk: true }; + })).toThrow('coverage is missing required field "chat"'); + expect(parseMutation((record) => { + record.coverage = { cave: true, coven: true, sdk: true, chat: false }; + })).toThrow('coverage.chat must be true'); + expect(parseMutation((record) => { + record.coverage = { + cave: true, + coven: true, + sdk: true, + chat: true, + extra: true, + }; + })).toThrow('coverage has unexpected field "extra"'); + }); + }); + + describe('expectNotCovered via notCovered', () => { + test('never allows SDK or Chat to be declared not covered', () => { + expect(parseMutation((record) => { + record.notCovered = [ + { scopeId: 'sdk', diagnosticId: 'scope.sdk.not-covered' }, + ]; + })).toThrow('scopeId "sdk" is not allowlisted'); + expect(parseMutation((record) => { + record.notCovered = [ + { scopeId: 'chat', diagnosticId: 'scope.chat.not-covered' }, + ]; + })).toThrow('scopeId "chat" is not allowlisted'); + }); + + test('rejects unknown, duplicated, and non-canonical entries', () => { + expect(parseMutation((record) => { + record.notCovered = [ + { scopeId: 'pairing', diagnosticId: 'scope.pairing.not-covered' }, + ]; + })).toThrow('scopeId "pairing" is not allowlisted'); + expect(parseMutation((record) => { + record.notCovered = [ + { scopeId: 'write-apis', diagnosticId: 'scope.write-apis.not-covered' }, + { scopeId: 'write-apis', diagnosticId: 'scope.write-apis.repeat' }, + ]; + })).toThrow('duplicate scopeId "write-apis"'); + expect(parseMutation((record) => { + record.notCovered = [ + { scopeId: 'write-apis', diagnosticId: 'Not-Canonical' }, + ]; + })).toThrow('diagnosticId is not canonical'); + }); + + test('accepts the allowlisted non-release scopes', () => { + expect(parseMutation((record) => { + record.notCovered = [ + { scopeId: 'write-apis', diagnosticId: 'scope.write-apis.not-covered' }, + { scopeId: 'oauth-ui', diagnosticId: 'scope.oauth-ui.not-covered' }, + { scopeId: 'remote-peer', diagnosticId: 'scope.remote-peer.not-covered' }, + { + scopeId: 'cross-process-pairing', + diagnosticId: 'scope.cross-process-pairing.not-covered', + }, + ]; + })).not.toThrow(); + }); + }); + + describe('expectIsolation via isolation', () => { + test('rejects non-array roots and incomplete root entries', () => { + expect(parseMutation((record) => { + const isolation = record.isolation as Record; + isolation.roots = 'cave-home'; + })).toThrow('isolation.roots must be an array'); + expect(parseMutation((record) => { + const isolation = record.isolation as Record; + isolation.roots = [ + { id: 'cave-home', ownershipVerified: true }, + ]; + })).toThrow('roots[0] is missing required field "removedAfterRun"'); + }); + + test('rejects malformed operator-state digests', () => { + expect(parseMutation((record) => { + const isolation = record.isolation as Record; + isolation.operatorState = [ + { id: 'cave-home', beforeSha256: 'abc', afterSha256: 'abc' }, + ]; + })).toThrow('operatorState[0].beforeSha256 is not canonical'); + }); + + test('rejects unexpected isolation fields', () => { + expect(parseMutation((record) => { + const isolation = record.isolation as Record; + isolation.retainEverything = true; + })).toThrow('isolation has unexpected field "retainEverything"'); + }); + }); + + describe('expectCrossAssertion via assertion lists', () => { + test('rejects missing diagnostic IDs', () => { + expect(parseMutation((record) => { + record.sdkAssertions = [{ id: 'sdk.one', result: 'pass' }]; + })).toThrow('sdkAssertions[0] is missing required field "diagnosticId"'); + }); + + test('rejects unknown results and non-canonical IDs', () => { + expect(parseMutation((record) => { + record.sdkAssertions = [ + { id: 'sdk.one', result: 'unknown', diagnosticId: 'sdk.one.x' }, + ]; + })).toThrow('result must be pass, fail, or skip'); + expect(parseMutation((record) => { + record.sdkAssertions = [passingAssertion('SDK.ONE')]; + })).toThrow('sdkAssertions[0].id is not canonical'); + }); + + test('rejects oversized diagnostic IDs', () => { + expect(parseMutation((record) => { + record.sdkAssertions = [ + passingAssertion('sdk.one'), + { id: 'sdk.two', result: 'pass', diagnosticId: `sdk.two.${'x'.repeat(190)}` }, + ]; + })).toThrow('sdkAssertions[1].diagnosticId exceeds the 192-byte limit'); + }); + }); + + describe('expectAssertionArray bounds', () => { + test('rejects non-arrays and overlong assertion lists', () => { + expect(parseMutation((record) => { + record.sdkAssertions = { id: 'sdk.one' }; + })).toThrow('sdkAssertions must be an array'); + expect(parseMutation((record) => { + record.sdkAssertions = Array.from( + { length: 1_001 }, + () => passingAssertion('sdk.one'), + ); + })).toThrow('sdkAssertions exceeds the 1000-entry limit'); + }); + }); + + describe('expectString bounds and canonicality', () => { + test('rejects empty and oversized platform identifiers', () => { + expect(parseMutation((record) => { + record.platform = ''; + })).toThrow('platform must be a non-empty string'); + expect(parseMutation((record) => { + record.platform = 'x'.repeat(33); + })).toThrow('platform exceeds the 32-byte limit'); + }); + + test('rejects oversized release versions', () => { + expect(parseMutation((record) => { + record.releases = { cave: 'x'.repeat(65), coven: '0.1.0' }; + })).toThrow('releases.cave exceeds the 64-byte limit'); + }); + }); + + describe('expectBoolean', () => { + test('rejects non-boolean isolation flags', () => { + expect(parseMutation((record) => { + const isolation = record.isolation as Record; + isolation.workspaceLinkDependency = 'no'; + })).toThrow('isolation.workspaceLinkDependency must be a boolean'); + expect(parseMutation((record) => { + const isolation = record.isolation as Record; + isolation.retainedSocketHandles = 1; + })).toThrow('isolation.retainedSocketHandles must be a boolean'); + }); + }); + + describe('expectCaveRecord and expectExactObject', () => { + test('rejects array cave records', () => { + expect(parseMutation((record) => { + record.caveRecord = []; + })).toThrow('caveRecord must be a JSON object'); + }); + + test('rejects non-object environments and unexpected top-level fields', () => { + expect(parseMutation((record) => { + record.environment = []; + })).toThrow('environment must be a JSON object'); + expect(parseMutation((record) => { + record.platforms = ['darwin-arm64']; + })).toThrow('has unexpected field "platforms"'); + }); + }); + + describe('canonical runtime and issue bindings', () => { + test('rejects wrong issue, schema version, and toolchain identities', () => { + expect(parseMutation((record) => { + record.issue = 'OpenCoven/sdk#39'; + })).toThrow('issue must be "OpenCoven/sdk#38"'); + expect(parseMutation((record) => { + record.schemaVersion = 2; + })).toThrow('schemaVersion must be 1'); + expect(parseMutation((record) => { + const environment = record.environment as Record; + environment.nodeVersion = 'v22.11.0'; + })).toThrow('environment.nodeVersion is not canonical'); + expect(parseMutation((record) => { + const environment = record.environment as Record; + environment.packageManagerVersion = 'pnpm@10.0.0'; + })).toThrow('environment.packageManagerVersion is not canonical'); + }); + }); + + describe('duplicate JSON keys', () => { + test('rejects duplicated platform fields', () => { + const record = validRecord(); + const text = JSON.stringify(record).replace( + '"platform":"darwin-arm64"', + '"platform":"darwin-arm64","platform":"darwin-arm64"', + ); + expect(() => + parsePlatformEvidence(text, 'duplicate.json'), + ).toThrow('duplicate.json contains duplicate JSON object key "platform"'); + }); + }); +}); + +describe('assertion registry boundaries', () => { + test('accepts the valid registry text', () => { + expect(() => + parseAssertionRegistry(validRegistryText(), 'registry.json'), + ).not.toThrow(); + }); + + test('rejects schema, engine, and requirement drift', () => { + expect(() => + parseAssertionRegistry( + validRegistryText().replace('"schemaVersion":1', '"schemaVersion":2'), + 'registry.json', + ), + ).toThrow('registry.json.schemaVersion must be 1'); + expect(() => + parseAssertionRegistry( + validRegistryText().replace( + 'scripts/client-v1-conformance.mjs', + 'scripts/rogue-engine.mjs', + ), + 'registry.json', + ), + ).toThrow('registry.json.cave.engine must name Cave\'s authoritative harness'); + expect(() => + parseAssertionRegistry( + validRegistryText().replace('"requireIncludeTtl":true', '"requireIncludeTtl":false'), + 'registry.json', + ), + ).toThrow('registry.json.cave must require TTL and authority-takeover assertions'); + expect(() => + parseAssertionRegistry( + validRegistryText().replace( + '"requireAuthorityTakeover":true', + '"requireAuthorityTakeover":false', + ), + 'registry.json', + ), + ).toThrow('registry.json.cave must require TTL and authority-takeover assertions'); + }); + + test('rejects incomplete platform matrices', () => { + expect(() => + parseAssertionRegistry( + validRegistryText().replace( + /,"win32-x64":\["chat\.windows"\]/u, + '', + ), + 'registry.json', + ), + ).toThrow('registry.json.chat.platforms is missing required field "win32-x64"'); + }); + + test('rejects duplicate, non-canonical, and overlapping assertion IDs', () => { + expect(() => + parseAssertionRegistry( + validRegistryText().replace( + '"sdk":["sdk.one","sdk.two"]', + '"sdk":["sdk.one","sdk.one"]', + ), + 'registry.json', + ), + ).toThrow('duplicate assertion id "sdk.one"'); + expect(() => + parseAssertionRegistry( + validRegistryText().replace( + '"sdk":["sdk.one","sdk.two"]', + '"sdk":["sdk.one","SDK.TWO"]', + ), + 'registry.json', + ), + ).toThrow('non-canonical assertion id "SDK.TWO"'); + expect(() => + parseAssertionRegistry( + validRegistryText().replace( + '"darwin-arm64":["chat.darwin"]', + '"darwin-arm64":["chat.common"]', + ), + 'registry.json', + ), + ).toThrow('repeats common Chat assertion "chat.common" for darwin-arm64'); + }); + + test('rejects duplicate JSON keys and invalid JSON', () => { + expect(() => + parseAssertionRegistry( + validRegistryText().replace( + '"sdk":["sdk.one","sdk.two"]', + '"sdk":["sdk.one"],"sdk":["sdk.two"]', + ), + 'registry.json', + ), + ).toThrow('duplicate JSON object key "sdk"'); + expect(() => + parseAssertionRegistry('{not json', 'registry.json'), + ).toThrow('Cannot parse registry.json'); + }); +}); + +describe('evidence scan boundaries', () => { + describe('forbidden evidence fields', () => { + const forbiddenFields = [ + 'attachment', + 'attachments', + 'authorization', + 'bearer', + 'body', + 'commandOutput', + 'content', + 'credential', + 'credentialValue', + 'message', + 'messageBody', + 'pairingSecret', + 'prompt', + 'requestBody', + 'responseBody', + 'secret', + 'socketHandle', + 'stderr', + 'stdout', + ]; + + test.each(forbiddenFields)('rejects field %s', (field) => { + expect(() => + scanConformanceEvidence({ [field]: 'redacted' }), + ).toThrow(`forbidden evidence field "${field}"`); + expect(() => + scanConformanceEvidence({ nested: { [field.toUpperCase()]: 'redacted' } }), + ).toThrow(`forbidden evidence field "${field.toUpperCase()}"`); + }); + }); + + describe('secret-shaped values', () => { + test('rejects bearer tokens', () => { + expect(() => + scanConformanceEvidence({ detail: 'Bearer abcdefghijklmnop' }), + ).toThrow('possible secret'); + }); + + test('rejects authorization and pairing-secret headers with values', () => { + expect(() => + scanConformanceEvidence({ detail: 'authorization: "abcdefgh12345678"' }), + ).toThrow('possible secret'); + expect(() => + scanConformanceEvidence({ detail: 'x-coven-pairing-secret: abcdefgh12345678' }), + ).toThrow('possible secret'); + expect(() => + scanConformanceEvidence({ detail: 'X-COVEN-CAVE-TOKEN=abcdefgh12345678' }), + ).toThrow('possible secret'); + }); + + test('rejects private key material', () => { + expect(() => + scanConformanceEvidence({ detail: '-----BEGIN PRIVATE KEY-----' }), + ).toThrow('possible secret'); + expect(() => + scanConformanceEvidence({ detail: '-----BEGIN OPENSSH PRIVATE KEY-----' }), + ).toThrow('possible secret'); + expect(() => + scanConformanceEvidence({ detail: '-----BEGIN RSA PRIVATE KEY-----' }), + ).toThrow('possible secret'); + }); + + test('rejects JWT-shaped values', () => { + expect(() => + scanConformanceEvidence({ + detail: + 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', + }), + ).toThrow('possible secret'); + }); + + test('rejects provider token formats', () => { + expect(() => + scanConformanceEvidence({ detail: 'token ghp_abcdefghijklmnopqrstuvwxyz012345' }), + ).toThrow('possible secret'); + expect(() => + scanConformanceEvidence({ detail: 'token npm_aaaaaaaaaaaaaaaaaaaaaaaa' }), + ).toThrow('possible secret'); + }); + + test('rejects long credential-shaped runs', () => { + expect(() => + scanConformanceEvidence({ detail: `run ${'A'.repeat(43)}` }), + ).toThrow('possible secret'); + }); + + test('accepts ordinary API paths and header names without values', () => { + expect(() => + scanConformanceEvidence({ detail: 'Bearer' }), + ).not.toThrow(); + expect(() => + scanConformanceEvidence({ detail: 'authorization header omitted' }), + ).not.toThrow(); + }); + }); + + describe('private filesystem paths', () => { + const privatePaths = [ + '/Users/alice/.coven', + '/home/alice/.coven', + '/root/.coven', + '/private/var/scratch', + '/tmp/scratch', + '/var/folders/ab/scratch', + '/var/tmp/scratch', + '/Applications/Coven.app', + '/Library/Preferences', + '/System/Library', + '/Volumes/USB', + '/dev/console', + '/etc/passwd', + '/opt/toolchain', + '/run/user/501/coven.sock', + '/srv/evidence', + '/usr/local/bin', + 'C:\\Users\\alice\\AppData', + '\\\\fileserver\\share\\evidence.json', + ]; + + test.each(privatePaths)('rejects path %s', (path) => { + expect(() => scanConformanceEvidence({ detail: path })).toThrow( + 'private filesystem path', + ); + }); + + test('accepts repository-relative and API paths', () => { + expect(() => + scanConformanceEvidence({ + detail: 'docs/workflows/client-v1-cross-repository-conformance.md', + }), + ).not.toThrow(); + expect(() => + scanConformanceEvidence({ detail: '/api/client/v1/health' }), + ).not.toThrow(); + }); + }); + + describe('structural evidence bounds', () => { + test('rejects non-JSON values', () => { + expect(() => + scanConformanceEvidence({ detail: undefined }), + ).toThrow('contains a non-JSON value'); + expect(() => + scanConformanceEvidence({ detail: Number.NaN }), + ).toThrow('contains a non-JSON value'); + expect(() => + scanConformanceEvidence({ detail: new Date('2026-08-28T00:00:00.000Z') }), + ).toThrow('contains a non-JSON value'); + }); + + test('rejects depth beyond 32 levels', () => { + const root: Record = {}; + let current = root; + for (let level = 0; level < 34; level += 1) { + const next: Record = {}; + current.nested = next; + current = next; + } + expect(() => scanConformanceEvidence(root)).toThrow( + 'exceeds the 32-level depth limit', + ); + }); + + test('rejects structures beyond 50000 nodes', () => { + const items = Array.from({ length: 50_001 }, (_unused, index) => index); + expect(() => scanConformanceEvidence({ items })).toThrow( + 'exceeds the 50000-node limit', + ); + }); + + test('rejects strings beyond 16 KiB', () => { + expect(() => + scanConformanceEvidence({ detail: 'x'.repeat(16_385) }), + ).toThrow('string exceeds the 16384-byte evidence limit'); + }); + }); +}); + +describe('committed assertion registry', () => { + test('parses the shipped registry and covers the read-only journey', () => { + const registry = readAssertionRegistry( + resolve( + workspaceRoot, + 'conformance/client-v1-cross-repository-assertions.json', + ), + ); + expect(registry.schemaVersion).toBe(1); + expect(registry.cave.engine).toBe('scripts/client-v1-conformance.mjs'); + for (const id of [ + 'sdk.cave.pairing.wrong-secret-refused', + 'sdk.cave.pairing.replay-refused', + 'sdk.cave.exchange.missing-content-length-refused', + 'sdk.cave.revocation.messages-refused', + 'sdk.coven.structured-errors', + 'sdk.native.trust-binding-missing-fails-closed', + ]) { + expect(registry.sdk).toContain(id); + } + for (const id of [ + 'chat.coven.windows.constructed-pipe-refused', + 'chat.coven.windows.foreign-pipe-refused', + 'chat.native.windows-credential-manager.isolated', + ]) { + expect(registry.chat.platforms['win32-x64']).toContain(id); + } + for (const id of [ + 'chat.cave.bearer-never-enters-webview', + 'chat.coven.executable.trusted', + 'chat.evidence.no-command-output', + 'chat.native.trust-provider-unavailable-fails-closed', + ]) { + expect(registry.chat.common).toContain(id); + } + }); +});