@workglow/test-contract 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/README.md +232 -0
  2. package/dist/ai-provider/assertions/capabilityHonesty.d.ts +7 -0
  3. package/dist/ai-provider/assertions/dispose.d.ts +7 -0
  4. package/dist/ai-provider/assertions/inferAdvertisesRegistered.d.ts +15 -0
  5. package/dist/ai-provider/assertions/inferServesInferred.d.ts +20 -0
  6. package/dist/ai-provider/assertions/pricingMatchesModality.d.ts +36 -0
  7. package/dist/ai-provider/assertions/registryCoverage.d.ts +13 -0
  8. package/dist/ai-provider/assertions/sessionReuse.d.ts +7 -0
  9. package/dist/ai-provider/assertions/signalHonoring.d.ts +7 -0
  10. package/dist/ai-provider/assertions/structuredGeneration.d.ts +7 -0
  11. package/dist/ai-provider/assertions/textGenerationSmoke.d.ts +7 -0
  12. package/dist/ai-provider/assertions/toolCallAccumulator.d.ts +7 -0
  13. package/dist/ai-provider/assertions/toolCallMultiTurn.d.ts +7 -0
  14. package/dist/ai-provider/assertions/usageNormalization.d.ts +61 -0
  15. package/dist/ai-provider/fixtures.d.ts +8 -0
  16. package/dist/ai-provider/runAiProviderConformance.d.ts +7 -0
  17. package/dist/ai-provider/types.d.ts +75 -0
  18. package/dist/ai-provider.d.ts +25 -0
  19. package/dist/ai-provider.js +876 -0
  20. package/dist/ai-provider.js.map +26 -0
  21. package/dist/browser-context/ConformanceMockContext.d.ts +54 -0
  22. package/dist/browser-context/assertions/ariaRoundTrip.d.ts +7 -0
  23. package/dist/browser-context/assertions/capabilityHonesty.d.ts +7 -0
  24. package/dist/browser-context/assertions/itExpectFail.d.ts +8 -0
  25. package/dist/browser-context/assertions/networkIntrospection.d.ts +7 -0
  26. package/dist/browser-context/assertions/tabsLifecycle.d.ts +7 -0
  27. package/dist/browser-context/fixtures.d.ts +8 -0
  28. package/dist/browser-context/runIBrowserContextConformance.d.ts +7 -0
  29. package/dist/browser-context/types.d.ts +48 -0
  30. package/dist/browser-context.d.ts +18 -0
  31. package/dist/browser-context.js +567 -0
  32. package/dist/browser-context.js.map +17 -0
  33. package/dist/creditExhaustedSkip.d.ts +39 -0
  34. package/dist/creditExhaustedSkip.js +163 -0
  35. package/dist/creditExhaustedSkip.js.map +10 -0
  36. package/dist/entitlement-profile/assertions/denialShape.d.ts +13 -0
  37. package/dist/entitlement-profile/assertions/dispose.d.ts +16 -0
  38. package/dist/entitlement-profile/assertions/hierarchyHonoring.d.ts +11 -0
  39. package/dist/entitlement-profile/assertions/optionalNeverDenied.d.ts +11 -0
  40. package/dist/entitlement-profile/assertions/requestEntitlementShape.d.ts +12 -0
  41. package/dist/entitlement-profile/assertions/resourceScoping.d.ts +16 -0
  42. package/dist/entitlement-profile/assertions/subscribeGrant.d.ts +11 -0
  43. package/dist/entitlement-profile/assertions/subscribeReload.d.ts +11 -0
  44. package/dist/entitlement-profile/assertions/subscribeRevocation.d.ts +12 -0
  45. package/dist/entitlement-profile/assertions/surfaceCoverage.d.ts +7 -0
  46. package/dist/entitlement-profile/assertions/unsubscribeIdempotent.d.ts +7 -0
  47. package/dist/entitlement-profile/fixtures.d.ts +21 -0
  48. package/dist/entitlement-profile/runEntitlementProfileConformance.d.ts +7 -0
  49. package/dist/entitlement-profile/types.d.ts +45 -0
  50. package/dist/entitlement-profile.d.ts +23 -0
  51. package/dist/entitlement-profile.js +436 -0
  52. package/dist/entitlement-profile.js.map +22 -0
  53. package/dist/human-connector/MockHumanConnector.d.ts +29 -0
  54. package/dist/human-connector/assertions/abort.d.ts +7 -0
  55. package/dist/human-connector/assertions/capabilityHonesty.d.ts +7 -0
  56. package/dist/human-connector/assertions/concurrentIsolation.d.ts +7 -0
  57. package/dist/human-connector/assertions/multiTurnFollowUp.d.ts +7 -0
  58. package/dist/human-connector/assertions/notifyDisplayFastResolve.d.ts +7 -0
  59. package/dist/human-connector/assertions/roundtrip.d.ts +7 -0
  60. package/dist/human-connector/fixtures.d.ts +8 -0
  61. package/dist/human-connector/runHumanConnectorConformance.d.ts +9 -0
  62. package/dist/human-connector/types.d.ts +91 -0
  63. package/dist/human-connector.d.ts +19 -0
  64. package/dist/human-connector.js +843 -0
  65. package/dist/human-connector.js.map +20 -0
  66. package/dist/itExpectFail.d.ts +15 -0
  67. package/dist/itExpectFail.js +180 -0
  68. package/dist/itExpectFail.js.map +11 -0
  69. package/dist/storage-migrations/assertions/appliesAndRecords.d.ts +7 -0
  70. package/dist/storage-migrations/assertions/concurrentRunsSerialize.d.ts +18 -0
  71. package/dist/storage-migrations/assertions/ensureBookkeepingIdempotent.d.ts +7 -0
  72. package/dist/storage-migrations/assertions/failedMigrationLeavesNoPartialSchema.d.ts +14 -0
  73. package/dist/storage-migrations/assertions/failedMigrationNotRecorded.d.ts +7 -0
  74. package/dist/storage-migrations/assertions/idempotentRun.d.ts +7 -0
  75. package/dist/storage-migrations/assertions/incrementalApplication.d.ts +7 -0
  76. package/dist/storage-migrations/runMigrationRunnerContract.d.ts +9 -0
  77. package/dist/storage-migrations/types.d.ts +78 -0
  78. package/dist/storage-migrations.d.ts +18 -0
  79. package/dist/storage-migrations.js +473 -0
  80. package/dist/storage-migrations.js.map +20 -0
  81. package/dist/tabular-migrations/assertions/addAndDropIndex.d.ts +7 -0
  82. package/dist/tabular-migrations/assertions/addColumn.d.ts +7 -0
  83. package/dist/tabular-migrations/assertions/backfill.d.ts +7 -0
  84. package/dist/tabular-migrations/assertions/dropColumn.d.ts +7 -0
  85. package/dist/tabular-migrations/assertions/failedMigrationNotRecorded.d.ts +7 -0
  86. package/dist/tabular-migrations/assertions/freshDbFastPath.d.ts +7 -0
  87. package/dist/tabular-migrations/assertions/incrementalApplication.d.ts +7 -0
  88. package/dist/tabular-migrations/assertions/renameColumn.d.ts +7 -0
  89. package/dist/tabular-migrations/runTabularMigrationContract.d.ts +8 -0
  90. package/dist/tabular-migrations/types.d.ts +28 -0
  91. package/dist/tabular-migrations.d.ts +19 -0
  92. package/dist/tabular-migrations.js +257 -0
  93. package/dist/tabular-migrations.js.map +18 -0
  94. package/dist/tabular-storage/assertions/countMatchesQuery.d.ts +7 -0
  95. package/dist/tabular-storage/assertions/guardParity.d.ts +25 -0
  96. package/dist/tabular-storage/assertions/inListCriterion.d.ts +14 -0
  97. package/dist/tabular-storage/assertions/joinBoundedLeftRead.d.ts +29 -0
  98. package/dist/tabular-storage/assertions/joinPushdown.d.ts +13 -0
  99. package/dist/tabular-storage/assertions/notInListCriterion.d.ts +15 -0
  100. package/dist/tabular-storage/assertions/strategyParity.d.ts +31 -0
  101. package/dist/tabular-storage/assertions/subscribeToChanges.d.ts +21 -0
  102. package/dist/tabular-storage/assertions/vectorColumnFormat.d.ts +7 -0
  103. package/dist/tabular-storage/assertions/withConnectionTransaction.d.ts +7 -0
  104. package/dist/tabular-storage/assertions/withTransactionRollback.d.ts +7 -0
  105. package/dist/tabular-storage/fixtures.d.ts +33 -0
  106. package/dist/tabular-storage/joinFixtures.d.ts +66 -0
  107. package/dist/tabular-storage/runTabularJoinContract.d.ts +24 -0
  108. package/dist/tabular-storage/runTabularStorageContract.d.ts +9 -0
  109. package/dist/tabular-storage/types.d.ts +102 -0
  110. package/dist/tabular-storage.d.ts +25 -0
  111. package/dist/tabular-storage.js +1477 -0
  112. package/dist/tabular-storage.js.map +27 -0
  113. package/dist/vector-storage/assertions/dimensionValidation.d.ts +17 -0
  114. package/dist/vector-storage/assertions/legacyEncoding.d.ts +16 -0
  115. package/dist/vector-storage/assertions/metadataFilter.d.ts +16 -0
  116. package/dist/vector-storage/assertions/searchOptions.d.ts +16 -0
  117. package/dist/vector-storage/assertions/shared.d.ts +23 -0
  118. package/dist/vector-storage/assertions/similarityRanking.d.ts +18 -0
  119. package/dist/vector-storage/assertions/similaritySearchEvent.d.ts +16 -0
  120. package/dist/vector-storage/assertions/vectorRoundTrip.d.ts +17 -0
  121. package/dist/vector-storage/runVectorStorageContract.d.ts +20 -0
  122. package/dist/vector-storage/types.d.ts +87 -0
  123. package/dist/vector-storage.d.ts +19 -0
  124. package/dist/vector-storage.js +412 -0
  125. package/dist/vector-storage.js.map +21 -0
  126. package/dist/web-search/assertions/capabilityAgreement.d.ts +23 -0
  127. package/dist/web-search/assertions/credentialNaming.d.ts +21 -0
  128. package/dist/web-search/assertions/dateFiltering.d.ts +22 -0
  129. package/dist/web-search/assertions/domainNormalization.d.ts +24 -0
  130. package/dist/web-search/assertions/maxResultsCeiling.d.ts +22 -0
  131. package/dist/web-search/assertions/reportedQuery.d.ts +18 -0
  132. package/dist/web-search/assertions/signalThreading.d.ts +20 -0
  133. package/dist/web-search/fixtures.d.ts +40 -0
  134. package/dist/web-search/runWebSearchProviderConformance.d.ts +24 -0
  135. package/dist/web-search/types.d.ts +79 -0
  136. package/dist/web-search.d.ts +19 -0
  137. package/dist/web-search.js +560 -0
  138. package/dist/web-search.js.map +18 -0
  139. package/dist/worker-proxy/assertions/backlogOrdering.d.ts +7 -0
  140. package/dist/worker-proxy/assertions/disposeTerminatesWorker.d.ts +8 -0
  141. package/dist/worker-proxy/assertions/errorPropagation.d.ts +7 -0
  142. package/dist/worker-proxy/assertions/providerCallHelpers.d.ts +23 -0
  143. package/dist/worker-proxy/browserOnlyStub.d.ts +7 -0
  144. package/dist/worker-proxy/runWorkerProxyBoundary.d.ts +7 -0
  145. package/dist/worker-proxy/types.d.ts +47 -0
  146. package/dist/worker-proxy.d.ts +16 -0
  147. package/dist/worker-proxy.js +389 -0
  148. package/dist/worker-proxy.js.map +17 -0
  149. package/package.json +120 -0
package/README.md ADDED
@@ -0,0 +1,232 @@
1
+ # `@workglow/test-contract`
2
+
3
+ Parameterized test suites that exercise an interface contract against
4
+ every adapter that implements it. Each suite exports one function:
5
+
6
+ export function runXxxConformance(opts: { factory, capabilities, ... }): void;
7
+
8
+ An adapter writes a thin caller that supplies a factory and capability flags;
9
+ all behavioral assertions are inherited.
10
+
11
+ import { runTabularStorageContract } from "@workglow/test-contract/tabular-storage";
12
+
13
+ runTabularStorageContract({
14
+ name: "MyTabularStorage",
15
+ createStorage: async () => new MyTabularStorage(CompoundSchema, CompoundPrimaryKeyNames),
16
+ capabilities: { supportsSubscriptions: false, supportsVectorColumns: false,
17
+ supportsTransactions: true, supportsQuery: true },
18
+ });
19
+
20
+ ## Why a separate package
21
+
22
+ An adapter that implements one of these interfaces is usually not in this
23
+ repository. `@workglow/test` — the 441 concrete tests — is `private: true` and
24
+ 404s on npm, so while the suites lived inside it, every implementation
25
+ downstream inherited exactly zero assertions and re-derived the contract by
26
+ hand. This package is the suites and nothing else, published so they can be
27
+ taken.
28
+
29
+ Its dependency surface is deliberately small, and split one subpath per
30
+ contract: a tabular-storage adapter installs `@workglow/storage` and `vitest`,
31
+ and never loads `@workglow/ai` or `@workglow/browser-control` — those are
32
+ optional peers that only the suites needing them pull in.
33
+
34
+ Concrete test files (`*.test.ts`) stay in `packages/test/src/test/`. The
35
+ boundary is what makes the pattern obvious: everything here is reusable and
36
+ nothing here runs on its own.
37
+
38
+ Two pre-existing parameterized suites live with their concrete callers and
39
+ stay in place — moving them is unnecessary churn:
40
+
41
+ - `packages/test/src/test/job-queue/genericJobQueueTests.ts`
42
+ - `packages/test/src/test/storage-tabular/genericTabularStorageTests.ts`
43
+
44
+ Treat both as additional examples of the pattern.
45
+
46
+ ## Conventions
47
+
48
+ 1. **Entrypoint shape.**
49
+
50
+ export function runXxxConformance(opts: {
51
+ readonly name: string;
52
+ readonly skip?: boolean;
53
+ readonly timeout: number;
54
+ readonly factory: () => Promise<{ register, dispose, inspect }>;
55
+ readonly capabilities: Record<string, boolean>;
56
+ // ...contract-specific fields
57
+ }): void;
58
+
59
+ Defines a single top-level `describe.skipIf(opts.skip)`.
60
+
61
+ 2. **Factory shape.** `factory()` returns a fresh handle per top-level
62
+ `beforeAll`. The handle exposes:
63
+ - `register()` — install the provider/storage/queue and any model records.
64
+ - `dispose()` — release resources; called in `afterAll`.
65
+ - `inspect()` — optional whitebox handle for assertions that need to
66
+ observe internal state (session maps, disposable refs). Adapters that
67
+ don't expose internals return `{}`; assertions skip with a logged
68
+ warning instead of passing silently.
69
+
70
+ 3. **Capability flags drive `describe.skipIf(!cap)` blocks.** Never silently
71
+ skip on missing capability without a flag — the absence of a flag
72
+ indicates a contract gap, not a permitted variation.
73
+
74
+ 4. **Live-API tests honor existing preload + retry/timeout settings.** Do
75
+ not introduce new env vars from a contract suite.
76
+
77
+ 5. **Adapter shims are short.** A new adapter joining a contract suite
78
+ should be ~30 lines: imports, factory, capability flags, model IDs.
79
+
80
+ 6. **`dispose()` must be idempotent.** Conformance suites may call dispose
81
+ multiple times (once for the dispose assertion, once in `afterAll`).
82
+ Adapters whose underlying resource doesn't natively support repeated
83
+ dispose should guard with a flag.
84
+
85
+ ### Factory shape variants
86
+
87
+ The `register/dispose/inspect` factory documented above is one of two
88
+ legitimate shapes — used when an adapter is a long-lived global registration
89
+ (e.g. an AI provider). For contracts whose subject is heavyweight but
90
+ per-test state (e.g. browser contexts), prefer a `create/dispose` factory
91
+ where each top-level block instantiates its own subject:
92
+
93
+ factory: () => Promise<{
94
+ create: () => Promise<TSubject>;
95
+ dispose: (subject: TSubject) => Promise<void>;
96
+ }>
97
+
98
+ The principle is the same: a fresh handle per block, with no shared state
99
+ that block N can leak into block N+1. The methods on the handle are
100
+ contract-specific.
101
+
102
+ ## Worker-proxy pattern
103
+
104
+ Workers cross a `postMessage` boundary; the inline AiProvider conformance
105
+ assertions must also hold when a provider is registered via
106
+ `register({ worker })`. Each worker-capable adapter's shim invokes the suite
107
+ **three times**:
108
+
109
+ 1. `runAiProviderConformance({ name: "<Adapter> (inline)", factory: inlineFactory, ... })`
110
+ 2. `runAiProviderConformance({ name: "<Adapter> (worker)", factory: workerFactory, ... })`
111
+ 3. `runWorkerProxyBoundary({ name: "<Adapter>", factory: workerFactory, ... })`
112
+
113
+ The worker factory's `inspect()` returns `{}` — workers are opaque by
114
+ design. The inherited session-reuse and dispose blocks skip with their
115
+ existing logged-warning behavior; the boundary block adds three
116
+ worker-only assertions (dispose terminates worker, worker-side throw
117
+ surfaces with stack, postMessage handles concurrent streams independently).
118
+
119
+ Capability flags:
120
+
121
+ - `browserOnly: true` — entire boundary block emits a single skipped test.
122
+ Used for TF-MediaPipe until browser test infra arrives.
123
+ - `errorPropagation: false` — relaxes the throw-surfaces assertion to skip
124
+ the stack-frame check, asserting only a non-empty message.
125
+
126
+ ## Available suites
127
+
128
+ | Contract | Suite | Adapters |
129
+ | --------------------------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
130
+ | `AiProvider` | `@workglow/test-contract/ai-provider` | Anthropic, OpenAI, Gemini, Ollama, HF Inference, HF Transformers, LlamaCpp |
131
+ | `IMigrationRunner` | `@workglow/test-contract/storage-migrations` | Postgres, SQLite, IndexedDB |
132
+ | Tabular schema migrations | `@workglow/test-contract/tabular-migrations` | InMemory, IndexedDB, Postgres, SQLite, FsFolder |
133
+ | `ITabularStorage` | `@workglow/test-contract/tabular-storage` | InMemory, SharedInMemory, IndexedDB, Postgres, SQLite, DuckDB, Supabase, Cached, Telemetry, FsFolder |
134
+ | `ITabularStorage.join` | `@workglow/test-contract/tabular-storage` | InMemory, SharedInMemory, IndexedDB, Postgres, SQLite, DuckDB, Supabase, Cached, Telemetry, HttpProxy |
135
+ | `ITabularStorage` (rest of the surface) | `test/storage-tabular/genericTabularStorageTests` | InMemory, IndexedDB, Postgres, SQLite, Supabase, FsFolder, HuggingFace |
136
+ | `IQueueStorage` + `IRateLimiterStorage` | `test/job-queue/genericJobQueueTests` | InMemory, IndexedDB, Postgres, SQLite, Supabase |
137
+ | `IVectorStorage` | `@workglow/test-contract/vector-storage` | InMemory, SQLite, Postgres, IndexedDB, Scoped, Telemetry |
138
+ | `IEntitlementProfile` | `@workglow/test-contract/entitlement-profile` | Browser, Desktop, Server, Custom |
139
+ | `IBrowserContext` | `@workglow/test-contract/browser-context` | Mock, Playwright, BunWebView, Electron |
140
+ | `IHumanConnector` | `@workglow/test-contract/human-connector` | Mock, Mock (no followUp), McpElicitation, Prompt, Ink, RunEvent |
141
+ | `IWebSearchProvider` | `@workglow/test-contract/web-search` | Brave, Tavily, SearXNG, Anthropic, OpenAI, OpenRouter, Gemini |
142
+ | Worker-proxy parity | `@workglow/test-contract/worker-proxy` | _harness only — no adapters wired yet_ |
143
+
144
+ ## Billing failures: skipped on CI, failed locally
145
+
146
+ Live provider suites run against real accounts, so "we ran out of money" is a
147
+ condition every one of them can hit. `@workglow/test-contract/credit-exhausted-skip` detects
148
+ it — 402s, `insufficient_quota`, `insufficient_credits`, DeepSeek's
149
+ `Insufficient Balance`, Anthropic's credit-balance error — and the `it` exported
150
+ from that module (which every conformance assertion imports) decides what to do
151
+ with it:
152
+
153
+ | where | behavior | why |
154
+ | ------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
155
+ | CI | skip | nobody watching a build can top an account up, and one exhausted key would turn every provider suite red for a change that touched no provider |
156
+ | anywhere else | fail | the developer running the suite IS the person who can act on it |
157
+
158
+ `CI` / `GITHUB_ACTIONS` select the branch; `WORKGLOW_CREDIT_EXHAUSTED_SKIP=1`
159
+ forces the skip locally and `=0` forces the failure on CI.
160
+
161
+ The detector reads the **message** as well as the numeric status, and that is
162
+ load-bearing rather than belt-and-braces: `classifyProviderError` rebuilds a
163
+ provider error as a `PermanentJobError` carrying the message and neither
164
+ `status` nor `cause`, so by the time a test body catches a DeepSeek 402 the
165
+ number survives only as the `402 Insufficient Balance` text the OpenAI SDK put
166
+ in the message. A status-only detector reads that as an ordinary permanent
167
+ failure — which is exactly how DeepSeek kept failing suites while every other
168
+ provider skipped. Message matching for a bare `402` is anchored to an
169
+ HTTP-shaped position (line start, or after a summary line's colon) so prose
170
+ that merely contains the number is not scavenged as a status.
171
+
172
+ Rate limits (429 + `rate_limit_exceeded`) are deliberately NOT this: they are
173
+ transient and the retry policy handles them.
174
+
175
+ ## How to add a new contract suite
176
+
177
+ 1. Pick a contract surface (an interface or abstract base class).
178
+ 2. Enumerate the behavioral invariants the contract implies but that aren't
179
+ currently asserted in any concrete test.
180
+ 3. Decide the capability matrix — which assertions are universal, which
181
+ are opt-in.
182
+ 4. Create `packages/test-contract/src/<contract-name>/` with `types.ts`,
183
+ `fixtures.ts`, `run<Contract>Conformance.ts`, and per-assertion files
184
+ under `assertions/`.
185
+ 5. Add `src/<contract-name>.ts` re-exporting all of it, and an
186
+ `exports["./<contract-name>"]` entry plus a `build-js` entrypoint in
187
+ `package.json` — a suite nobody can import is the problem this package
188
+ exists to fix.
189
+ 6. Write one shim caller per adapter under
190
+ `packages/test/src/test/<contract-name>/<Adapter>_Generic.integration.test.ts`.
191
+ 7. Add a row to the table above.
192
+
193
+ ## Two strategies, one contract
194
+
195
+ `runTabularJoinContract` is the one suite here whose subject has two
196
+ implementations of a single semantics — a pushed-down `JOIN` statement when
197
+ both tables share a connection, an application-side hash join otherwise — with
198
+ the planner, not the caller, choosing between them. That makes a disagreement
199
+ between them a wrong answer nothing reports, which changes with where the
200
+ right-hand storage happens to live.
201
+
202
+ So it runs three blocks rather than one. The behavioural cases run on whichever
203
+ path the pair takes. `join bounded left read` runs only on the hash path and
204
+ asserts the half of the docstring the pushdown gets from the database for free:
205
+ that a bounded join stops reading the left side early, and only when the joined
206
+ rows are already in their final order. `join strategy parity` runs only where
207
+ both paths are reachable, runs the same spec through each, and asserts they
208
+ agree — over specs derived from the fields of `JoinSpec`, so an option added
209
+ there fails to compile until it declares what the two strategies owe each
210
+ other.
211
+
212
+ What the two are held to depends on what the spec asked for, because `join`
213
+ promises `orderBy`, `limit` and `offset` apply to the joined rows and never what
214
+ order an unordered join arrives in. An unordered join is compared as a set; an
215
+ unordered join that is also windowed is compared by row count and membership.
216
+ Holding either to a shared order would assert the storage engine's row layout,
217
+ which SQLite and Postgres happen to share and DuckDB, being columnar, does not.
218
+
219
+ ## Roadmap
220
+
221
+ Future contract suites in priority order:
222
+
223
+ 1. Storage extensions (subscribeToChanges ordering, vector-dimension format,
224
+ putBulk round-trip count, deleteSearch streaming) — additions to the
225
+ existing `genericTabularStorageTests.ts`.
226
+ 2. Worker-proxy contract — harness shipped; per-adapter wiring deferred to a
227
+ follow-up PR (vitest-Node `Worker` polyfill + per-adapter
228
+ `WorkerManager` unregister-on-dispose required before HFT/LlamaCpp can
229
+ register inline + worker in the same test file).
230
+ 3. `IBrowserContext` — Playwright / Electron / BunWebView / CDP backends.
231
+ 4. `EntitlementProfile` — desktop / web / server profiles.
232
+ 5. `IHumanConnector` — IN PROGRESS — `MockHumanConnector` + `McpElicitationConnector`. App / Electron adapters add their own shim when introduced.
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Steven Roussey <sroussey@gmail.com>
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import type { AiProviderConformanceOpts } from "../types";
7
+ export declare function capabilityHonestyBlock(opts: AiProviderConformanceOpts): void;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Steven Roussey <sroussey@gmail.com>
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import type { AiProviderConformanceOpts, ConformanceHandle } from "../types";
7
+ export declare function disposeBlock(opts: AiProviderConformanceOpts, getHandle: () => ConformanceHandle): void;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Steven Roussey <sroussey@gmail.com>
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ /**
7
+ * A registered run-fn capability that `inferCapabilities` never returns is
8
+ * unreachable for any model whose record was populated by inference — the
9
+ * path the catalog exists to serve.
10
+ *
11
+ * The inferred sets are unioned across the provider's fixtures, so this
12
+ * answers "is this capability reachable at all" and says nothing about any one
13
+ * model; {@link assertInferServesInferred} is the per-model direction.
14
+ */
15
+ export declare function assertInferAdvertisesRegistered(name: string, registered: readonly (readonly string[])[], inferred: readonly (readonly string[])[]): void;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Steven Roussey <sroussey@gmail.com>
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ /** One fixture model id paired with the capabilities `inferCapabilities` returns for it. */
7
+ export interface InferredForModel {
8
+ readonly id: string;
9
+ readonly capabilities: readonly string[];
10
+ }
11
+ /**
12
+ * The per-model direction of {@link assertInferAdvertisesRegistered}: that one
13
+ * unions the inferred sets across a provider's fixtures, which answers "is this
14
+ * capability reachable at all" and passes a branch that advertises a capability
15
+ * no run-fn can serve *for that model*. Such a model clears
16
+ * `modelMeetsRequires` during selection and fails inside the provider instead,
17
+ * with the capability message it would have gotten replaced by whatever the
18
+ * provider throws.
19
+ */
20
+ export declare function assertInferServesInferred(name: string, registered: readonly (readonly string[])[], inferred: readonly InferredForModel[]): void;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Steven Roussey <sroussey@gmail.com>
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import type { ModelPricing } from "@workglow/ai";
7
+ /** One fixture model, its inferred capabilities, and the card its provider returns. */
8
+ export interface PricedModel {
9
+ readonly id: string;
10
+ readonly capabilities: readonly string[];
11
+ readonly pricing: ModelPricing | undefined;
12
+ }
13
+ /**
14
+ * A provider must not quote a per-token rate for a model its own inference says
15
+ * is not billed per token.
16
+ *
17
+ * This is the direction that catches a real fabricated number, and it is a hard
18
+ * assertion because there is no legitimate case for it: `grok-2-image-1212`
19
+ * resolved `grok-2`'s per-1M-token card through the pricing table's substring
20
+ * walk, and `gemini-2.5-flash-image-preview` took `gemini-2.5-flash`'s the same
21
+ * way. Both are image models by the provider's own catalogue.
22
+ *
23
+ * A free card is exempt: `input: 0` on a local model says "this costs nothing",
24
+ * which is true whatever the billing unit, and is how every local provider
25
+ * answers.
26
+ */
27
+ export declare function assertPricingMatchesModality(name: string, models: readonly PricedModel[], namedByTable?: readonly string[]): void;
28
+ /**
29
+ * The weaker direction: a token-billed model SHOULD carry a card.
30
+ *
31
+ * A ratchet rather than an assertion, deliberately. Real gaps exist today —
32
+ * `gemini-embedding-001` among them — and a hard assert would redden the build
33
+ * on a known one and get disabled, which costs more than the gap. Recording the
34
+ * set means it cannot grow while it shrinks deliberately.
35
+ */
36
+ export declare function assertPricedGapDoesNotGrow(name: string, models: readonly PricedModel[], knownUnpriced: readonly string[]): void;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Steven Roussey <sroussey@gmail.com>
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import type { AiProviderConformanceOpts } from "../types";
7
+ /**
8
+ * Capability-set conformance: every model in `opts.models` must be backed by
9
+ * a registry entry whose `serves` superset covers the model's declared
10
+ * `capabilities`. Each capability is also probed individually to confirm the
11
+ * provider can dispatch the bare capability via `getRunFnFor`.
12
+ */
13
+ export declare function registryCoverageBlock(opts: AiProviderConformanceOpts): void;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Steven Roussey <sroussey@gmail.com>
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import type { AiProviderConformanceOpts, ConformanceFixture, ConformanceHandle } from "../types";
7
+ export declare function sessionReuseBlock(opts: AiProviderConformanceOpts, fixture: ConformanceFixture, getHandle: () => ConformanceHandle): void;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Steven Roussey <sroussey@gmail.com>
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import type { AiProviderConformanceOpts, ConformanceFixture } from "../types";
7
+ export declare function signalHonoringBlock(opts: AiProviderConformanceOpts, fixture: ConformanceFixture): void;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Steven Roussey <sroussey@gmail.com>
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import type { AiProviderConformanceOpts, ConformanceFixture } from "../types";
7
+ export declare function structuredGenerationBlock(opts: AiProviderConformanceOpts, fixture: ConformanceFixture): void;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Steven Roussey <sroussey@gmail.com>
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import type { AiProviderConformanceOpts, ConformanceFixture } from "../types";
7
+ export declare function textGenerationSmokeBlock(opts: AiProviderConformanceOpts, fixture: ConformanceFixture): void;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Steven Roussey <sroussey@gmail.com>
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import type { AiProviderConformanceOpts, ConformanceFixture } from "../types";
7
+ export declare function toolCallAccumulatorBlock(opts: AiProviderConformanceOpts, fixture: ConformanceFixture): void;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Steven Roussey <sroussey@gmail.com>
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import type { AiProviderConformanceOpts, ConformanceFixture } from "../types";
7
+ export declare function toolCallMultiTurnBlock(opts: AiProviderConformanceOpts, fixture: ConformanceFixture): void;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Steven Roussey <sroussey@gmail.com>
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import type { Usage } from "@workglow/ai";
7
+ /**
8
+ * Shared contract for every provider's usage normalization.
9
+ *
10
+ * Each provider reads token counts off a different terminal frame, but they all
11
+ * have to land in {@link Usage} under one rule: **`undefined` means the provider
12
+ * did not report this figure, and never means zero.** Collapsing "not reported"
13
+ * to `0` silently understates spend — a model that billed no cached tokens and
14
+ * a model that says nothing about caching are different facts.
15
+ *
16
+ * A provider registers its mapper here rather than re-litigating the rule in its
17
+ * own suite, so a new provider inherits the same guarantees.
18
+ */
19
+ /** The full set of normalized counters, used to check untouched slots. */
20
+ declare const USAGE_COUNTER_FIELDS: readonly ["input", "output", "cached", "cacheWrite", "reasoning", "total"];
21
+ type UsageCounterField = (typeof USAGE_COUNTER_FIELDS)[number];
22
+ export interface UsageNormalizationCase {
23
+ /** What this frame represents, e.g. "a cache-hit completion". */
24
+ readonly name: string;
25
+ /** The provider-shaped payload handed to the mapper. */
26
+ readonly frame: unknown;
27
+ /** The exact expected result — compared with `toEqual`, so extra keys fail. */
28
+ readonly expected: Usage | undefined;
29
+ }
30
+ export interface UsageNormalizationOpts {
31
+ /** Provider name, used in test titles. */
32
+ readonly provider: string;
33
+ /** The provider's own mapper from a raw frame to {@link Usage}. */
34
+ readonly mapUsage: (raw: unknown) => Usage | undefined;
35
+ /** Provider-specific frames and their expected mappings. */
36
+ readonly cases: readonly UsageNormalizationCase[];
37
+ /**
38
+ * A minimal real frame reporting only `sparseReports`. Every counter outside
39
+ * that set must come back `undefined` — this is the zero-vs-absent guard.
40
+ */
41
+ readonly sparseFrame: unknown;
42
+ /** Counters the sparse frame actually reports. */
43
+ readonly sparseReports: readonly UsageCounterField[];
44
+ /**
45
+ * A frame in which the provider genuinely reports `0` for `sparseReports`.
46
+ * Proves the mapper does not discard a real zero along with the absent ones.
47
+ */
48
+ readonly zeroFrame: unknown;
49
+ }
50
+ /** Assert a mapped {@link Usage} carries only numbers and `undefined`. */
51
+ export declare function assertUsageShape(usage: Usage): void;
52
+ /**
53
+ * Assert the field relationships every provider's mapper must produce.
54
+ *
55
+ * The sum check only runs when the provider states a `total` — it is the
56
+ * provider's own arithmetic, so it is the one place the normalization can be
57
+ * checked against an external authority rather than against itself.
58
+ */
59
+ export declare function assertUsageDisjointness(usage: Usage): void;
60
+ export declare function usageNormalizationBlock(opts: UsageNormalizationOpts): void;
61
+ export {};
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Steven Roussey <sroussey@gmail.com>
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import type { ConformanceFixture } from "./types";
7
+ export declare const DEFAULT_CONFORMANCE_FIXTURE: ConformanceFixture;
8
+ export declare function resolveFixture(override: Partial<ConformanceFixture> | undefined): ConformanceFixture;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Steven Roussey <sroussey@gmail.com>
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import type { AiProviderConformanceOpts } from "./types";
7
+ export declare function runAiProviderConformance(opts: AiProviderConformanceOpts): void;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Steven Roussey <sroussey@gmail.com>
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import type { JsonSchema } from "@workglow/util/schema";
7
+ export interface AiProviderConformanceOpts {
8
+ readonly name: string;
9
+ readonly skip?: boolean;
10
+ readonly timeout: number;
11
+ readonly factory: () => Promise<ConformanceHandle>;
12
+ readonly capabilities: AiProviderCapabilities;
13
+ readonly models: AiProviderConformanceModels;
14
+ readonly fixture?: Partial<ConformanceFixture>;
15
+ /**
16
+ * Names of conformance assertions that are currently broken in this
17
+ * adapter. Each named assertion is wrapped in `it.fails` instead of `it`.
18
+ * Remove the entry once the adapter bug is fixed.
19
+ *
20
+ * Known names:
21
+ * "signal.nonStreaming"
22
+ * "signal.midStream"
23
+ * "session.reuse"
24
+ */
25
+ readonly expectedFailures?: ReadonlyArray<string>;
26
+ }
27
+ export interface ConformanceHandle {
28
+ readonly register: () => Promise<void>;
29
+ readonly dispose: () => Promise<void>;
30
+ readonly inspect: () => ProviderInspectionHandle;
31
+ /**
32
+ * Optional hook the conformance harness calls before the session-reuse
33
+ * block to release transient state (e.g. cached chat sessions) that prior
34
+ * blocks may have accumulated. Implementations must NOT tear down models
35
+ * or contexts — only short-lived per-call resources.
36
+ */
37
+ readonly releaseTransients?: () => Promise<void>;
38
+ }
39
+ export interface AiProviderCapabilities {
40
+ readonly streaming: boolean;
41
+ readonly tools: boolean;
42
+ readonly structured: boolean;
43
+ readonly embeddings: boolean;
44
+ readonly sessions: boolean;
45
+ readonly abortMidStream: boolean;
46
+ }
47
+ export interface AiProviderConformanceModels {
48
+ readonly textGeneration?: string;
49
+ readonly toolCalling?: string;
50
+ readonly structured?: string;
51
+ readonly embeddings?: string;
52
+ }
53
+ export interface ProviderInspectionHandle {
54
+ readonly sessionMap?: ReadonlyMap<string, unknown>;
55
+ readonly disposables?: ReadonlyArray<{
56
+ readonly alive: boolean;
57
+ }>;
58
+ }
59
+ export interface ConformanceFixture {
60
+ readonly textPrompt: string;
61
+ readonly weatherTool: {
62
+ readonly name: string;
63
+ readonly description: string;
64
+ readonly inputSchema: JsonSchema;
65
+ };
66
+ readonly weatherToolPrompt: string;
67
+ readonly multiTurnTranscript: ReadonlyArray<{
68
+ readonly role: "user" | "assistant" | "tool";
69
+ readonly text: string;
70
+ }>;
71
+ readonly structuredSchema: JsonSchema;
72
+ readonly structuredPrompt: string;
73
+ readonly maxTokens: number;
74
+ readonly abortGraceMs: number;
75
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Steven Roussey <sroussey@gmail.com>
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ /**
7
+ * Conformance suite for an AI provider adapter: what it advertises it can do
8
+ * is what it serves, over both the inline and worker registrations.
9
+ */
10
+ export * from "./ai-provider/fixtures";
11
+ export * from "./ai-provider/runAiProviderConformance";
12
+ export * from "./ai-provider/types";
13
+ export * from "./ai-provider/assertions/capabilityHonesty";
14
+ export * from "./ai-provider/assertions/dispose";
15
+ export * from "./ai-provider/assertions/inferAdvertisesRegistered";
16
+ export * from "./ai-provider/assertions/inferServesInferred";
17
+ export * from "./ai-provider/assertions/pricingMatchesModality";
18
+ export * from "./ai-provider/assertions/registryCoverage";
19
+ export * from "./ai-provider/assertions/sessionReuse";
20
+ export * from "./ai-provider/assertions/signalHonoring";
21
+ export * from "./ai-provider/assertions/structuredGeneration";
22
+ export * from "./ai-provider/assertions/textGenerationSmoke";
23
+ export * from "./ai-provider/assertions/toolCallAccumulator";
24
+ export * from "./ai-provider/assertions/toolCallMultiTurn";
25
+ export * from "./ai-provider/assertions/usageNormalization";