@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.
- package/README.md +232 -0
- package/dist/ai-provider/assertions/capabilityHonesty.d.ts +7 -0
- package/dist/ai-provider/assertions/dispose.d.ts +7 -0
- package/dist/ai-provider/assertions/inferAdvertisesRegistered.d.ts +15 -0
- package/dist/ai-provider/assertions/inferServesInferred.d.ts +20 -0
- package/dist/ai-provider/assertions/pricingMatchesModality.d.ts +36 -0
- package/dist/ai-provider/assertions/registryCoverage.d.ts +13 -0
- package/dist/ai-provider/assertions/sessionReuse.d.ts +7 -0
- package/dist/ai-provider/assertions/signalHonoring.d.ts +7 -0
- package/dist/ai-provider/assertions/structuredGeneration.d.ts +7 -0
- package/dist/ai-provider/assertions/textGenerationSmoke.d.ts +7 -0
- package/dist/ai-provider/assertions/toolCallAccumulator.d.ts +7 -0
- package/dist/ai-provider/assertions/toolCallMultiTurn.d.ts +7 -0
- package/dist/ai-provider/assertions/usageNormalization.d.ts +61 -0
- package/dist/ai-provider/fixtures.d.ts +8 -0
- package/dist/ai-provider/runAiProviderConformance.d.ts +7 -0
- package/dist/ai-provider/types.d.ts +75 -0
- package/dist/ai-provider.d.ts +25 -0
- package/dist/ai-provider.js +876 -0
- package/dist/ai-provider.js.map +26 -0
- package/dist/browser-context/ConformanceMockContext.d.ts +54 -0
- package/dist/browser-context/assertions/ariaRoundTrip.d.ts +7 -0
- package/dist/browser-context/assertions/capabilityHonesty.d.ts +7 -0
- package/dist/browser-context/assertions/itExpectFail.d.ts +8 -0
- package/dist/browser-context/assertions/networkIntrospection.d.ts +7 -0
- package/dist/browser-context/assertions/tabsLifecycle.d.ts +7 -0
- package/dist/browser-context/fixtures.d.ts +8 -0
- package/dist/browser-context/runIBrowserContextConformance.d.ts +7 -0
- package/dist/browser-context/types.d.ts +48 -0
- package/dist/browser-context.d.ts +18 -0
- package/dist/browser-context.js +567 -0
- package/dist/browser-context.js.map +17 -0
- package/dist/creditExhaustedSkip.d.ts +39 -0
- package/dist/creditExhaustedSkip.js +163 -0
- package/dist/creditExhaustedSkip.js.map +10 -0
- package/dist/entitlement-profile/assertions/denialShape.d.ts +13 -0
- package/dist/entitlement-profile/assertions/dispose.d.ts +16 -0
- package/dist/entitlement-profile/assertions/hierarchyHonoring.d.ts +11 -0
- package/dist/entitlement-profile/assertions/optionalNeverDenied.d.ts +11 -0
- package/dist/entitlement-profile/assertions/requestEntitlementShape.d.ts +12 -0
- package/dist/entitlement-profile/assertions/resourceScoping.d.ts +16 -0
- package/dist/entitlement-profile/assertions/subscribeGrant.d.ts +11 -0
- package/dist/entitlement-profile/assertions/subscribeReload.d.ts +11 -0
- package/dist/entitlement-profile/assertions/subscribeRevocation.d.ts +12 -0
- package/dist/entitlement-profile/assertions/surfaceCoverage.d.ts +7 -0
- package/dist/entitlement-profile/assertions/unsubscribeIdempotent.d.ts +7 -0
- package/dist/entitlement-profile/fixtures.d.ts +21 -0
- package/dist/entitlement-profile/runEntitlementProfileConformance.d.ts +7 -0
- package/dist/entitlement-profile/types.d.ts +45 -0
- package/dist/entitlement-profile.d.ts +23 -0
- package/dist/entitlement-profile.js +436 -0
- package/dist/entitlement-profile.js.map +22 -0
- package/dist/human-connector/MockHumanConnector.d.ts +29 -0
- package/dist/human-connector/assertions/abort.d.ts +7 -0
- package/dist/human-connector/assertions/capabilityHonesty.d.ts +7 -0
- package/dist/human-connector/assertions/concurrentIsolation.d.ts +7 -0
- package/dist/human-connector/assertions/multiTurnFollowUp.d.ts +7 -0
- package/dist/human-connector/assertions/notifyDisplayFastResolve.d.ts +7 -0
- package/dist/human-connector/assertions/roundtrip.d.ts +7 -0
- package/dist/human-connector/fixtures.d.ts +8 -0
- package/dist/human-connector/runHumanConnectorConformance.d.ts +9 -0
- package/dist/human-connector/types.d.ts +91 -0
- package/dist/human-connector.d.ts +19 -0
- package/dist/human-connector.js +843 -0
- package/dist/human-connector.js.map +20 -0
- package/dist/itExpectFail.d.ts +15 -0
- package/dist/itExpectFail.js +180 -0
- package/dist/itExpectFail.js.map +11 -0
- package/dist/storage-migrations/assertions/appliesAndRecords.d.ts +7 -0
- package/dist/storage-migrations/assertions/concurrentRunsSerialize.d.ts +18 -0
- package/dist/storage-migrations/assertions/ensureBookkeepingIdempotent.d.ts +7 -0
- package/dist/storage-migrations/assertions/failedMigrationLeavesNoPartialSchema.d.ts +14 -0
- package/dist/storage-migrations/assertions/failedMigrationNotRecorded.d.ts +7 -0
- package/dist/storage-migrations/assertions/idempotentRun.d.ts +7 -0
- package/dist/storage-migrations/assertions/incrementalApplication.d.ts +7 -0
- package/dist/storage-migrations/runMigrationRunnerContract.d.ts +9 -0
- package/dist/storage-migrations/types.d.ts +78 -0
- package/dist/storage-migrations.d.ts +18 -0
- package/dist/storage-migrations.js +473 -0
- package/dist/storage-migrations.js.map +20 -0
- package/dist/tabular-migrations/assertions/addAndDropIndex.d.ts +7 -0
- package/dist/tabular-migrations/assertions/addColumn.d.ts +7 -0
- package/dist/tabular-migrations/assertions/backfill.d.ts +7 -0
- package/dist/tabular-migrations/assertions/dropColumn.d.ts +7 -0
- package/dist/tabular-migrations/assertions/failedMigrationNotRecorded.d.ts +7 -0
- package/dist/tabular-migrations/assertions/freshDbFastPath.d.ts +7 -0
- package/dist/tabular-migrations/assertions/incrementalApplication.d.ts +7 -0
- package/dist/tabular-migrations/assertions/renameColumn.d.ts +7 -0
- package/dist/tabular-migrations/runTabularMigrationContract.d.ts +8 -0
- package/dist/tabular-migrations/types.d.ts +28 -0
- package/dist/tabular-migrations.d.ts +19 -0
- package/dist/tabular-migrations.js +257 -0
- package/dist/tabular-migrations.js.map +18 -0
- package/dist/tabular-storage/assertions/countMatchesQuery.d.ts +7 -0
- package/dist/tabular-storage/assertions/guardParity.d.ts +25 -0
- package/dist/tabular-storage/assertions/inListCriterion.d.ts +14 -0
- package/dist/tabular-storage/assertions/joinBoundedLeftRead.d.ts +29 -0
- package/dist/tabular-storage/assertions/joinPushdown.d.ts +13 -0
- package/dist/tabular-storage/assertions/notInListCriterion.d.ts +15 -0
- package/dist/tabular-storage/assertions/strategyParity.d.ts +31 -0
- package/dist/tabular-storage/assertions/subscribeToChanges.d.ts +21 -0
- package/dist/tabular-storage/assertions/vectorColumnFormat.d.ts +7 -0
- package/dist/tabular-storage/assertions/withConnectionTransaction.d.ts +7 -0
- package/dist/tabular-storage/assertions/withTransactionRollback.d.ts +7 -0
- package/dist/tabular-storage/fixtures.d.ts +33 -0
- package/dist/tabular-storage/joinFixtures.d.ts +66 -0
- package/dist/tabular-storage/runTabularJoinContract.d.ts +24 -0
- package/dist/tabular-storage/runTabularStorageContract.d.ts +9 -0
- package/dist/tabular-storage/types.d.ts +102 -0
- package/dist/tabular-storage.d.ts +25 -0
- package/dist/tabular-storage.js +1477 -0
- package/dist/tabular-storage.js.map +27 -0
- package/dist/vector-storage/assertions/dimensionValidation.d.ts +17 -0
- package/dist/vector-storage/assertions/legacyEncoding.d.ts +16 -0
- package/dist/vector-storage/assertions/metadataFilter.d.ts +16 -0
- package/dist/vector-storage/assertions/searchOptions.d.ts +16 -0
- package/dist/vector-storage/assertions/shared.d.ts +23 -0
- package/dist/vector-storage/assertions/similarityRanking.d.ts +18 -0
- package/dist/vector-storage/assertions/similaritySearchEvent.d.ts +16 -0
- package/dist/vector-storage/assertions/vectorRoundTrip.d.ts +17 -0
- package/dist/vector-storage/runVectorStorageContract.d.ts +20 -0
- package/dist/vector-storage/types.d.ts +87 -0
- package/dist/vector-storage.d.ts +19 -0
- package/dist/vector-storage.js +412 -0
- package/dist/vector-storage.js.map +21 -0
- package/dist/web-search/assertions/capabilityAgreement.d.ts +23 -0
- package/dist/web-search/assertions/credentialNaming.d.ts +21 -0
- package/dist/web-search/assertions/dateFiltering.d.ts +22 -0
- package/dist/web-search/assertions/domainNormalization.d.ts +24 -0
- package/dist/web-search/assertions/maxResultsCeiling.d.ts +22 -0
- package/dist/web-search/assertions/reportedQuery.d.ts +18 -0
- package/dist/web-search/assertions/signalThreading.d.ts +20 -0
- package/dist/web-search/fixtures.d.ts +40 -0
- package/dist/web-search/runWebSearchProviderConformance.d.ts +24 -0
- package/dist/web-search/types.d.ts +79 -0
- package/dist/web-search.d.ts +19 -0
- package/dist/web-search.js +560 -0
- package/dist/web-search.js.map +18 -0
- package/dist/worker-proxy/assertions/backlogOrdering.d.ts +7 -0
- package/dist/worker-proxy/assertions/disposeTerminatesWorker.d.ts +8 -0
- package/dist/worker-proxy/assertions/errorPropagation.d.ts +7 -0
- package/dist/worker-proxy/assertions/providerCallHelpers.d.ts +23 -0
- package/dist/worker-proxy/browserOnlyStub.d.ts +7 -0
- package/dist/worker-proxy/runWorkerProxyBoundary.d.ts +7 -0
- package/dist/worker-proxy/types.d.ts +47 -0
- package/dist/worker-proxy.d.ts +16 -0
- package/dist/worker-proxy.js +389 -0
- package/dist/worker-proxy.js.map +17 -0
- 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, 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,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";
|