// src/tck/contracts.ts import type { CapabilityLimits, EvidenceLevel, FeatureRegistry, FeatureState, Limitation } from '../backend-contract/capabilities' import type { BleCentralBackend } from '../backend-contract/backend' import type { CleanupRecord, NormalizedBleError } from '../backend-contract/errors' import type { ChooserRequest, WebChooser } from '../backend-contract/host/web' import type { AdapterSelection, BackendIdentity, BackendProvider, HostKind } from '../backend-contract/identity' import type { BorrowedBytes, PeerId, SerializableRecord } from '../backend-contract/primitives' import type { ManagerRestorationCapability, RestorationAdoptionRequest } from '../backend-contract/restoration' import type { IpcClientTransport, IpcRouteRequest } from '../ipc/protocol' /** * A production TCK fixture is an adapter around a backend's public contract. * It deliberately exposes observations instead of deterministic-backend state: * the same fixture shape is usable with virtual, mocked-boundary, and radio * backed implementations. */ export interface BackendTckFactory< Attachment extends string, Identity extends BackendIdentity, Backend extends BleCentralBackend > { readonly backendId: string readonly provider: BackendProvider readonly selection: AdapterSelection /** Known-unlisted adapter selection used to prove stale-target rejection. */ readonly staleSelection: AdapterSelection /** * Opt-in for providers whose host owns one attachment-scoped event sink. The * standard runner otherwise always creates one fixture for every base scenario. */ readonly providerOnlyIdentityScenarios?: boolean /** First-party suites supplied by the factory when a caller requests the standard run. */ readonly defaultFeatureSuites?: readonly TckFeatureSuite[] create(context: TckFixtureContext): Promise> } export interface TckFixtureContext { readonly scenarioId: TckScenarioId } export interface TckRunOptions { readonly proofScope: 'deterministic' /** * First-party deterministic-boundary registrations execute only the base * scenarios for which their boundary exposes every required control. */ readonly baseScenarioIds?: readonly TckScenarioId[] } /** * Deterministic environment controls consumed by runner-owned public-contract * scenarios. They can change the test boundary, but cannot return facts, * receipts, or proof labels. */ export interface TckScenarioController { readonly availableActions: readonly TckControllerAction[] now(): number settle(promise: Promise): Promise flush(): Promise perform(action: TckControllerAction, input: SerializableRecord): Promise } /** * Inputs a deterministic boundary must expose for the standard connection-controls observer. * The runner performs the public connection, RSSI, and MTU calls itself; this adapter supplies * only the host's valid request parameter and therefore cannot manufacture feature facts. */ export interface TckConnectionControlsScenarioAdapter { readonly requestedMtu: number } /** * Provider-owned restoration wiring for the standard restoration observer. The adapter supplies * a real manager capability, a concrete adoption request, and an environment action; the runner * owns all adoption calls and derives every fact from their public results. */ export interface TckRestorationScenarioAdapter< Attachment extends string, Identity extends BackendIdentity > { createCapability( clientId: import('../backend-contract/primitives').ClientId ): ManagerRestorationCapability createRequest(identity: Identity): RestorationAdoptionRequest seedJournal(controller: TckScenarioController): Promise } /** Typed browser-boundary inputs for the runner-owned chooser vertical slice. */ export interface TckWebChooserScenarioAdapter { readonly chooser: WebChooser readonly request: ChooserRequest /** Authoritative deterministic oracle for the peer returned by a cancelled chooser completion. */ readonly expectedSelectedPeerId: PeerId readonly expectedReadValue: BorrowedBytes readonly expectedInitialNotificationValue: BorrowedBytes } /** * Typed desktop-webview inputs for the runner-owned transport event-sink scenario. The adapter * supplies one already-attached transport plus the host's own valid request/response traffic; the * runner owns every subscribe, invoke, and acknowledge call and derives its fact from public results. */ export interface TckIpcTransportScenarioAdapter { /** Attached transport whose event sink the host bound once, on the attach request. */ readonly transport: IpcClientTransport /** * Host-valid request/response traffic the runner replays between event deliveries. Only route * requests belong here: a bootstrap request legitimately rebinds the event sink, and a release * request ends the attachment that owns it, so neither can prove the invariant under test. */ readonly routeRequests: readonly IpcRouteRequest[] } /** Typed deterministic-boundary inputs for feature scenarios that the standard runner observes. */ export interface TckFeatureScenarioAdapters> { readonly connectionControls?: TckConnectionControlsScenarioAdapter readonly ipcTransport?: TckIpcTransportScenarioAdapter readonly restoration?: TckRestorationScenarioAdapter readonly webChooser?: TckWebChooserScenarioAdapter readonly security?: TckSecurityScenarioAdapter } /** Host-specific peer/lifecycle controls for the runner-owned security scenario. */ export interface TckSecurityScenarioAdapter { readonly peerId: string readonly customCeremonySupported: boolean readonly supportsAlreadyUnpaired: boolean /** False when the host exposes state/pair but its compiled public API cannot cancel pairing. */ readonly supportsCancellation?: boolean /** False when the host has no public durable-unpair API; the scenario records that limitation. */ readonly supportsUnpair?: boolean readonly prepareCancellation?: () => void } export interface BackendTckFixture< Attachment extends string, Identity extends BackendIdentity, Backend extends BleCentralBackend > { readonly backend: Backend /** Deterministic environment inputs only; this boundary cannot submit facts or receipts. */ readonly controller: TckScenarioController /** Optional only when this fixture registers a feature scenario requiring typed host wiring. */ readonly featureScenarioAdapters?: TckFeatureScenarioAdapters dispose(): Promise } export type TckControllerAction = | 'queue-advertisement' | 'emit-notification' | 'resolve-chooser' | 'queue-operation-completion' | 'advance-time' | 'force-disconnect' | 'trigger-services-changed' | 'inject-att-error' | 'inject-unsubscribe-failure' | 'set-adapter-state' | 'reload-renderer' | 'seed-restoration-journal' | 'emit-ipc-event' export type TckProofScope = 'deterministic' /** * A deterministic receipt proves conformance only. It can never represent a * live-radio observation or elevate a capability's published support claim. */ export interface TckProofLabel { readonly scope: TckProofScope readonly claim: 'deterministic-conformance' readonly receiptId: string } export type TckScenarioId = | 'identity.provider-loadability-and-adapter-availability' | 'identity.adapter-selection-and-unique-instance' | 'identity.valid-all-axis-negotiation' | 'identity.version-skew-and-malformed-offers' | 'capability.truth-limits-evidence-and-binding' | 'adapter.atomic-snapshot-and-watch' | 'scan.owner-join-authority-and-signature' | 'scan.fairness-abort-deadline-and-final-cleanup' | 'connection.lease-joins-borrowing-transfer-and-revocation' | 'connection.two-client-arbitration' | 'connection.rssi-and-att-mtu-capability-contract' | 'gatt.descriptor-discovery-read-write' | 'gatt.discovery-complete-paths-and-services-changed' | 'gatt.reads-descriptors-write-policy-and-dispatched-cancellation' | 'gatt.maximum-write-length-boundaries' | 'gatt.long-write-partial-failure' | 'gatt.long-write-cancellation' | 'gatt.long-write-disconnect' | 'subscription.enable-ready-shared-cccd-and-fanout' | 'subscription.pre-ready-overflow-controls-and-late-quarantine' | 'restoration.provider-journal-adoption-and-rejection' | 'electron.trusted-sender-envelope-generations-and-quotas' | 'lifecycle.destroy-idempotency-admission-and-exact-settlement' | 'diagnostics.trace-redaction-and-resource-counters' | 'scenario.scan-connect-discover-read-notify-destroy' | 'web.unsupported-capabilities-reject-and-remain-honest' | 'web.chooser-connect-discover-read-notify-destroy' | 'ipc.event-sink-survives-request-response-traffic' | 'security.state-pair-cancel-unpair' /** One authority for the runner-owned Web unsupported-capability scenario. */ export const WEB_UNSUPPORTED_CAPABILITIES_TCK_SCENARIO_ID = 'web.unsupported-capabilities-reject-and-remain-honest' satisfies TckScenarioId /** One authority for the Web chooser scenario consumed by its scenario, registry, and registration. */ export const WEB_CHOOSER_TCK_SCENARIO_ID = 'web.chooser-connect-discover-read-notify-destroy' satisfies TckScenarioId /** One authority for the Web chooser feature suite consumed by its registry and registration. */ export const WEB_CHOOSER_TCK_SUITE_ID = 'web-chooser-discovery' export type TckFactId = | 'provider-loadability-separate-from-adapter-availability' | 'adapter-selection-rejects-ambiguous-or-stale-target' | 'backend-instance-id-is-unique' | 'all-applicable-version-axes-negotiate-highest-overlap' | 'skew-malformed-and-post-attachment-offers-reject-without-live-radio-resources' | 'capability-state-is-runtime-truth' | 'capability-limits-evidence-and-tck-binding-validate' | 'deterministic-proof-never-claims-live-support' | 'adapter-watch-is-atomic-with-initial-snapshot' | 'adapter-watch-orders-snapshot-before-transition' | 'scan-owner-remains-physical-authority' | 'scan-join-requires-authorized-identical-semantics' | 'scan-consumer-release-is-fair-and-isolated' | 'scan-abort-and-deadline-close-ingress' | 'scan-stop-resolves-before-final-physical-release' | 'scan-no-late-observation-after-stop' | 'connection-leases-are-owner-scoped' | 'connection-borrowing-cannot-destroy-or-cancel-owner-work' | 'connection-transfer-and-revocation-are-authenticated' | 'connection-lifecycle-peer-loss-is-generation-bound' | 'connection-lifecycle-requested-disconnect-is-distinct' | 'connection-lifecycle-stream-cleans-up' | 'connection-second-client-arbitrates-without-stealing-link' | 'connection-rssi-is-measured-or-explicitly-unavailable' | 'connection-att-mtu-is-negotiated-or-explicitly-unavailable' | 'connection-priority-request-outcome-is-not-observed-parameters' | 'connection-observation-is-bound-to-generation' | 'connection-phy-truth-is-explicit' | 'connection-parameters-truth-is-explicit' | 'connection-subrate-truth-is-explicit' | 'gatt-write-readiness-truth-is-explicit' | 'gatt-descriptor-discovery-read-write-crosses-boundary' | 'gatt-discovery-returns-complete-occurrence-safe-paths' | 'gatt-services-changed-invalidates-database-generation' | 'gatt-stale-path-rejects-before-dispatch' | 'gatt-read-and-descriptor-return-owned-bytes' | 'gatt-write-policy-and-uncertain-dispatched-commit-are-exact' | 'gatt-operation-queue-is-fair-and-bounded' | 'gatt-maximum-write-length-observation-is-current-and-bounded' | 'gatt-long-write-receipt-reports-partial-failure' | 'gatt-long-write-cancellation-stops-following-chunks' | 'gatt-long-write-disconnect-stops-following-chunks' | 'subscription-no-value-before-ready' | 'subscription-shares-physical-cccd-with-consumer-refcount' | 'subscription-fanout-is-consumer-isolated' | 'subscription-overflow-quota-order-and-one-terminal-are-exact' | 'subscription-no-late-value-after-removal' | 'restoration-journal-is-provider-owned-and-bounded' | 'restoration-adoption-is-verified-and-exactly-once' | 'restoration-rejection-is-non-consuming' | 'electron-sender-and-envelope-are-validated-before-backend-work' | 'electron-generation-and-client-quotas-isolate-renderers' | 'destroy-closes-admission-and-is-idempotent' | 'destroy-settles-each-operation-once' | 'operation-cancellation-and-destroy-leave-zero-residual-resources' | 'resource-counters-return-to-zero-without-underflow' | 'trace-is-ordered-bounded-and-redacted' | 'vertical-slice-preserves-scan-and-cleans-up' | 'web-unsupported-capabilities-reject-and-report-runtime-truth' | 'web-chooser-vertical-slice-preserves-selection-and-cleans-up' | 'ipc-event-sink-survives-request-response-traffic' | 'security-state-distinguishes-unbonded' | 'security-pairing-is-terminal-and-idempotent' | 'security-custom-challenge-is-bounded' | 'security-pairing-cancellation-cleans-up' | 'security-unpair-is-explicit' export interface TckScenarioDefinition { readonly id: TckScenarioId readonly execution: 'base' | 'feature' readonly requiredFacts: readonly TckFactId[] /** Every control must be declared by the fixture before runner execution. */ readonly requiredControllerActions: readonly TckControllerAction[] } export interface TckFact { readonly id: TckFactId readonly holds: boolean readonly detail: SerializableRecord } export interface TckScenarioReceipt { readonly scenarioId: TckScenarioId readonly proof: TckProofLabel readonly facts: readonly TckFact[] readonly error: NormalizedBleError | null } export interface TckFeatureSuite { readonly suiteId: string /** Feature-only scenario definitions this suite is authorized to require. */ readonly scenarioIds: readonly TckScenarioId[] } /** Immutable typed Web chooser suite authority. */ export const WEB_CHOOSER_TCK_FEATURE_SUITE = Object.freeze({ suiteId: WEB_CHOOSER_TCK_SUITE_ID, scenarioIds: Object.freeze< readonly [typeof WEB_CHOOSER_TCK_SCENARIO_ID, typeof WEB_UNSUPPORTED_CAPABILITIES_TCK_SCENARIO_ID] >([WEB_CHOOSER_TCK_SCENARIO_ID, WEB_UNSUPPORTED_CAPABILITIES_TCK_SCENARIO_ID]) }) satisfies TckFeatureSuite /** One authority for the desktop webview transport scenario consumed by its scenario, registry, and registration. */ export const IPC_TRANSPORT_TCK_SCENARIO_ID = 'ipc.event-sink-survives-request-response-traffic' satisfies TckScenarioId /** One authority for the desktop webview transport feature suite consumed by its registry and registration. */ export const IPC_TRANSPORT_TCK_SUITE_ID = 'ipc-transport-event-sink' /** Immutable typed desktop webview transport suite authority. */ export const IPC_TRANSPORT_TCK_FEATURE_SUITE = Object.freeze({ suiteId: IPC_TRANSPORT_TCK_SUITE_ID, scenarioIds: Object.freeze([IPC_TRANSPORT_TCK_SCENARIO_ID]) }) satisfies TckFeatureSuite export type RegisteredFeature = FeatureRegistry['registrations'][number] /** Immutable registration/evidence authority selected for feature execution. */ export interface TckFeatureBinding { readonly featureId: string readonly state: FeatureState readonly selectedSchemaMinimum: number readonly selectedSchemaMaximum: number readonly implementationOrigin: RegisteredFeature['implementationOrigin'] readonly suiteId: string readonly requiredScenarioIds: readonly TckScenarioId[] readonly contractMinimum: number readonly contractMaximum: number /** Registry evidence is supplied by the backend author, not by the TCK runner. */ readonly evidenceVerification: 'author-declared' readonly receiptId: string readonly evidenceLevel: EvidenceLevel readonly implementationVersion: string readonly sourceDigest: string readonly evidenceScenarioIds: readonly string[] readonly limitations: readonly Limitation[] readonly limits: CapabilityLimits } /** Runtime-observed authority shared by every fixture in one TCK run. */ export interface TckRuntimeIdentity { readonly registeredBackendId: string readonly registeredPlatformId: string readonly providerId: string readonly hostKind: HostKind readonly implementationVersion: string readonly selectedAdapterId: string } export interface TckRunReport { readonly backendId: string readonly identity: TckRuntimeIdentity /** Public receipts were constructed from runner-controlled scenario evidence. */ readonly verification: 'runner-controlled' readonly proofScope: TckProofScope readonly baseScenarioIds: readonly TckScenarioId[] readonly featureSuiteIds: readonly string[] readonly featureBindings: readonly TckFeatureBinding[] readonly receipts: readonly TckScenarioReceipt[] } export class TckAssertionError extends Error { constructor( readonly scenarioId: TckScenarioId, readonly message: string, options?: ErrorOptions ) { super(`${scenarioId}: ${message}`, options) this.name = 'TckAssertionError' } }