@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
@@ -0,0 +1,876 @@
1
+ // src/ai-provider/fixtures.ts
2
+ var DEFAULT_CONFORMANCE_FIXTURE = {
3
+ textPrompt: "Say hello in one short sentence.",
4
+ weatherTool: {
5
+ name: "get_weather",
6
+ description: "Get the current weather for a given city.",
7
+ inputSchema: {
8
+ type: "object",
9
+ properties: {
10
+ location: { type: "string", description: "City name, e.g. San Francisco" }
11
+ },
12
+ required: ["location"]
13
+ }
14
+ },
15
+ weatherToolPrompt: "What is the weather in San Francisco?",
16
+ multiTurnTranscript: [
17
+ { role: "user", text: "What is the weather in Tokyo?" },
18
+ { role: "assistant", text: "Let me check." },
19
+ { role: "tool", text: '{"temperature":22,"conditions":"sunny"}' }
20
+ ],
21
+ structuredSchema: {
22
+ type: "object",
23
+ properties: {
24
+ name: { type: "string" },
25
+ age: { type: "number" }
26
+ },
27
+ required: ["name", "age"],
28
+ additionalProperties: false
29
+ },
30
+ structuredPrompt: "Generate a JSON object with a person's name and age. Use name 'Alice' and age 30.",
31
+ maxTokens: 100,
32
+ abortGraceMs: 50
33
+ };
34
+ function resolveFixture(override) {
35
+ if (!override)
36
+ return DEFAULT_CONFORMANCE_FIXTURE;
37
+ return { ...DEFAULT_CONFORMANCE_FIXTURE, ...override };
38
+ }
39
+ // src/ai-provider/runAiProviderConformance.ts
40
+ import { afterAll, beforeAll as beforeAll2, describe as describe10 } from "vitest";
41
+
42
+ // src/ai-provider/assertions/capabilityHonesty.ts
43
+ import { getAiProviderRegistry, getGlobalModelRepository, textEmbedding } from "@workglow/ai";
44
+ import { describe, expect } from "vitest";
45
+
46
+ // src/creditExhaustedSkip.ts
47
+ import { it as vitestIt } from "vitest";
48
+ var BILLING_CODES = new Set([
49
+ "insufficient_quota",
50
+ "billing_hard_limit_reached",
51
+ "insufficient_credits",
52
+ "insufficient_balance"
53
+ ]);
54
+ var CREDIT_MESSAGE = /insufficient credits|not enough credits|no credits remaining|out of credits|insufficient[\s_-]balance|balance is (?:too low|insufficient)|exceeded[\s\S]{0,80}credits|monthly included credits|payment required|exceeded your current quota|check your plan and billing|billing_hard_limit/i;
55
+ var HTTP_402_MESSAGE = /\bHTTP\/?\d?(?:\.\d)?\s*402\b|\bstatus(?:\s*code)?\s*[:=]\s*402\b|\berror code:?\s*402\b|(?:^|:\s+)402\s+[a-z]/im;
56
+ function isCreditExhaustedError(err) {
57
+ const seen = new Set;
58
+ let current = err;
59
+ while (current !== undefined && current !== null && !seen.has(current)) {
60
+ seen.add(current);
61
+ if (matchesCreditExhaustion(current))
62
+ return true;
63
+ current = causeOf(current);
64
+ }
65
+ return false;
66
+ }
67
+ function matchesCreditExhaustion(err) {
68
+ if (typeof err === "string")
69
+ return messageLooksLikeCreditExhaustion(err);
70
+ if (!err || typeof err !== "object")
71
+ return false;
72
+ const rec = err;
73
+ const status = numericStatus(rec);
74
+ if (status === 402)
75
+ return true;
76
+ const code = stringish(rec.code) ?? stringish(nested(rec.error, "code"));
77
+ const type = stringish(rec.type) ?? stringish(nested(rec.error, "type"));
78
+ if (code && BILLING_CODES.has(code))
79
+ return true;
80
+ if (type && BILLING_CODES.has(type))
81
+ return true;
82
+ const message = [
83
+ stringish(rec.message),
84
+ stringish(nested(rec.error, "message")),
85
+ stringifyUnknown(err)
86
+ ].filter((part) => typeof part === "string").join(`
87
+ `);
88
+ return messageLooksLikeCreditExhaustion(message);
89
+ }
90
+ function messageLooksLikeCreditExhaustion(message) {
91
+ return CREDIT_MESSAGE.test(message) || HTTP_402_MESSAGE.test(message);
92
+ }
93
+ function numericStatus(rec) {
94
+ for (const key of ["status", "statusCode"]) {
95
+ const value = rec[key];
96
+ if (typeof value === "number" && Number.isFinite(value))
97
+ return value;
98
+ }
99
+ const nestedStatus = nested(rec.error, "status");
100
+ if (typeof nestedStatus === "number" && Number.isFinite(nestedStatus))
101
+ return nestedStatus;
102
+ return;
103
+ }
104
+ function nested(value, key) {
105
+ if (!value || typeof value !== "object")
106
+ return;
107
+ return value[key];
108
+ }
109
+ function stringish(value) {
110
+ return typeof value === "string" && value.length > 0 ? value : undefined;
111
+ }
112
+ function stringifyUnknown(err) {
113
+ if (err instanceof Error)
114
+ return err.message;
115
+ return String(err);
116
+ }
117
+ function causeOf(err) {
118
+ if (!err || typeof err !== "object")
119
+ return;
120
+ return err.cause;
121
+ }
122
+ function creditSkipNote(err) {
123
+ const message = err instanceof Error ? err.message : String(err);
124
+ return `skipped: provider out of credits (${message})`;
125
+ }
126
+ var SKIP_OVERRIDE_ENV = "WORKGLOW_CREDIT_EXHAUSTED_SKIP";
127
+ function isTruthyFlag(value) {
128
+ return !/^(?:0|false|off|no)$/i.test(value.trim());
129
+ }
130
+ function creditExhaustionSkipsTest(env = process.env) {
131
+ const override = env[SKIP_OVERRIDE_ENV];
132
+ if (override !== undefined && override.trim() !== "")
133
+ return isTruthyFlag(override);
134
+ for (const key of ["CI", "GITHUB_ACTIONS"]) {
135
+ const value = env[key];
136
+ if (value !== undefined && value.trim() !== "" && isTruthyFlag(value))
137
+ return true;
138
+ }
139
+ return false;
140
+ }
141
+ function reportLocalCreditFailure(err) {
142
+ const message = err instanceof Error ? err.message : String(err);
143
+ console.error(`provider out of credits (${message})
144
+ ` + ` Failing because this is not CI. Top the account up, or set ${SKIP_OVERRIDE_ENV}=1 to skip instead.`);
145
+ }
146
+ async function runWithCreditSkip(ctx, fn) {
147
+ try {
148
+ return await fn();
149
+ } catch (err) {
150
+ if (isCreditExhaustedError(err)) {
151
+ if (!creditExhaustionSkipsTest()) {
152
+ reportLocalCreditFailure(err);
153
+ throw err;
154
+ }
155
+ ctx.skip(creditSkipNote(err));
156
+ return;
157
+ }
158
+ throw err;
159
+ }
160
+ }
161
+ function wrapTestBodyForCreditSkip(fn) {
162
+ return async function wrapped(...args) {
163
+ try {
164
+ return await fn.apply(this, args);
165
+ } catch (err) {
166
+ if (!isCreditExhaustedError(err))
167
+ throw err;
168
+ if (!creditExhaustionSkipsTest()) {
169
+ reportLocalCreditFailure(err);
170
+ throw err;
171
+ }
172
+ const ctx = args[0];
173
+ if (ctx && typeof ctx.skip === "function") {
174
+ ctx.skip(creditSkipNote(err));
175
+ return;
176
+ }
177
+ console.warn(creditSkipNote(err));
178
+ return;
179
+ }
180
+ };
181
+ }
182
+ function wrapCallable(fn) {
183
+ return new Proxy(fn, {
184
+ apply(target, thisArg, argArray) {
185
+ const wrapped = argArray.map((arg) => typeof arg === "function" ? wrapTestBodyForCreditSkip(arg) : arg);
186
+ const result = Reflect.apply(target, thisArg, wrapped);
187
+ return typeof result === "function" ? wrapCallable(result) : result;
188
+ },
189
+ get(target, prop, receiver) {
190
+ const value = Reflect.get(target, prop, receiver);
191
+ if (typeof value === "function") {
192
+ return wrapCallable(value.bind(target));
193
+ }
194
+ return value;
195
+ }
196
+ });
197
+ }
198
+ var it = wrapCallable(vitestIt);
199
+
200
+ // src/ai-provider/assertions/capabilityHonesty.ts
201
+ var TEXT_GENERATION = ["text.generation"];
202
+ var TOOL_USE = ["text.generation", "tool-use"];
203
+ var STRUCTURED = ["text.generation", "json-mode"];
204
+ var TEXT_EMBEDDING = ["text.embedding"];
205
+ function capabilityHonestyBlock(opts) {
206
+ describe("Capability honesty", () => {
207
+ it.skipIf(opts.capabilities.streaming || !opts.models.textGeneration)("declares streaming=false → registry has no stream function", async () => {
208
+ const registry = getAiProviderRegistry();
209
+ const model = await getGlobalModelRepository().findByName(opts.models.textGeneration);
210
+ expect(model).toBeDefined();
211
+ const fn = registry.getRunFnFor(model.provider, TEXT_GENERATION);
212
+ expect(fn).toBeUndefined();
213
+ }, opts.timeout);
214
+ it.skipIf(opts.capabilities.tools || !opts.models.textGeneration)("declares tools=false → registry rejects ToolCallingTask lookup", async () => {
215
+ const registry = getAiProviderRegistry();
216
+ const model = await getGlobalModelRepository().findByName(opts.models.textGeneration);
217
+ expect(model).toBeDefined();
218
+ const fn = registry.getRunFnFor(model.provider, TOOL_USE);
219
+ expect(fn).toBeUndefined();
220
+ }, opts.timeout);
221
+ it.skipIf(opts.capabilities.embeddings || !opts.models.textGeneration)("declares embeddings=false → registry rejects TextEmbeddingTask lookup", async () => {
222
+ const registry = getAiProviderRegistry();
223
+ const model = await getGlobalModelRepository().findByName(opts.models.textGeneration);
224
+ expect(model).toBeDefined();
225
+ const fn = registry.getRunFnFor(model.provider, TEXT_EMBEDDING);
226
+ expect(fn).toBeUndefined();
227
+ }, opts.timeout);
228
+ it.skipIf(opts.capabilities.structured || !opts.models.textGeneration)("declares structured=false → registry rejects StructuredGenerationTask lookup", async () => {
229
+ const registry = getAiProviderRegistry();
230
+ const model = await getGlobalModelRepository().findByName(opts.models.textGeneration);
231
+ expect(model).toBeDefined();
232
+ const fn = registry.getRunFnFor(model.provider, STRUCTURED);
233
+ expect(fn).toBeUndefined();
234
+ }, opts.timeout);
235
+ it.skipIf(!opts.capabilities.streaming || !opts.models.textGeneration)("declares streaming=true → registry has a stream function", async () => {
236
+ const registry = getAiProviderRegistry();
237
+ const model = await getGlobalModelRepository().findByName(opts.models.textGeneration);
238
+ expect(model).toBeDefined();
239
+ const fn = registry.getRunFnFor(model.provider, TEXT_GENERATION);
240
+ expect(fn).toBeDefined();
241
+ }, opts.timeout);
242
+ it.skipIf(!opts.capabilities.tools || !opts.models.toolCalling)("declares tools=true → registry has a ToolCallingTask run function", async () => {
243
+ const registry = getAiProviderRegistry();
244
+ const model = await getGlobalModelRepository().findByName(opts.models.toolCalling);
245
+ expect(model).toBeDefined();
246
+ const fn = registry.getRunFnFor(model.provider, TOOL_USE);
247
+ expect(fn).toBeDefined();
248
+ }, opts.timeout);
249
+ it.skipIf(!opts.capabilities.structured || !opts.models.structured)("declares structured=true → registry has a StructuredGenerationTask run function", async () => {
250
+ const registry = getAiProviderRegistry();
251
+ const model = await getGlobalModelRepository().findByName(opts.models.structured);
252
+ expect(model).toBeDefined();
253
+ const fn = registry.getRunFnFor(model.provider, STRUCTURED);
254
+ expect(fn).toBeDefined();
255
+ }, opts.timeout);
256
+ it.skipIf(!opts.capabilities.embeddings || !opts.models.embeddings)("declares embeddings=true → registry has a run function and produces a finite vector", async () => {
257
+ const registry = getAiProviderRegistry();
258
+ const model = await getGlobalModelRepository().findByName(opts.models.embeddings);
259
+ expect(model).toBeDefined();
260
+ const fn = registry.getRunFnFor(model.provider, TEXT_EMBEDDING);
261
+ expect(fn).toBeDefined();
262
+ const result = await textEmbedding({
263
+ model: opts.models.embeddings,
264
+ text: "hello"
265
+ });
266
+ const vector = result.vector;
267
+ const isTypedArray = ArrayBuffer.isView(vector) && !(vector instanceof DataView);
268
+ expect(isTypedArray, "vector must be a numeric typed array").toBe(true);
269
+ const values = Array.from(vector);
270
+ expect(values.length).toBeGreaterThan(0);
271
+ for (const v of values) {
272
+ expect(Number.isFinite(v)).toBe(true);
273
+ }
274
+ }, opts.timeout);
275
+ });
276
+ }
277
+
278
+ // src/ai-provider/assertions/dispose.ts
279
+ import { getLogger } from "@workglow/util";
280
+ import { describe as describe2, expect as expect2 } from "vitest";
281
+ function disposeBlock(opts, getHandle) {
282
+ describe2("Dispose", () => {
283
+ it("release resources observable via inspect()", async () => {
284
+ const handle = getHandle();
285
+ const before = handle.inspect();
286
+ if (!before.sessionMap && (!before.disposables || before.disposables.length === 0)) {
287
+ getLogger().warn(`[conformance] ${opts.name} exposes no inspect() handles; dispose assertion skipped`);
288
+ return;
289
+ }
290
+ await handle.dispose();
291
+ const after = handle.inspect();
292
+ if (after.sessionMap) {
293
+ expect2(after.sessionMap.size).toBe(0);
294
+ }
295
+ if (after.disposables) {
296
+ for (const d of after.disposables) {
297
+ expect2(d.alive).toBe(false);
298
+ }
299
+ }
300
+ }, opts.timeout);
301
+ });
302
+ }
303
+
304
+ // src/ai-provider/assertions/registryCoverage.ts
305
+ import { getAiProviderRegistry as getAiProviderRegistry2, getGlobalModelRepository as getGlobalModelRepository2 } from "@workglow/ai";
306
+ import { describe as describe3, expect as expect3 } from "vitest";
307
+ function registryCoverageBlock(opts) {
308
+ describe3("Registry coverage", () => {
309
+ it("resolves a run function for every capability advertised by the provider's models", async () => {
310
+ const candidateModelIds = [
311
+ opts.models.textGeneration,
312
+ opts.models.toolCalling,
313
+ opts.models.structured,
314
+ opts.models.embeddings
315
+ ].filter((id) => typeof id === "string");
316
+ expect3(candidateModelIds.length, "registryCoverageBlock requires at least one model id in opts.models").toBeGreaterThan(0);
317
+ const registry = getAiProviderRegistry2();
318
+ const repo = getGlobalModelRepository2();
319
+ const seenProviders = new Set;
320
+ const seenCapabilities = new Set;
321
+ for (const modelId of candidateModelIds) {
322
+ const model = await repo.findByName(modelId);
323
+ expect3(model, `model "${modelId}" not found in repository`).toBeDefined();
324
+ const providerKey = model.provider;
325
+ const provider = registry.getProvider(providerKey);
326
+ expect3(provider, `provider "${providerKey}" not registered`).toBeDefined();
327
+ const capabilities = model.capabilities ?? [];
328
+ expect3(capabilities.length, `model "${modelId}" advertises zero capabilities`).toBeGreaterThan(0);
329
+ seenProviders.add(providerKey);
330
+ for (const capability of capabilities) {
331
+ seenCapabilities.add(`${providerKey}::${capability}`);
332
+ const runFn = registry.getRunFnFor(providerKey, [capability]);
333
+ expect3(runFn, `provider "${providerKey}" advertises capability "${capability}" via model "${modelId}" but the registry has no matching run function`).toBeDefined();
334
+ }
335
+ }
336
+ expect3(seenProviders.size, "no providers reached during coverage walk").toBeGreaterThan(0);
337
+ expect3(seenCapabilities.size, "no capabilities reached during coverage walk").toBeGreaterThan(0);
338
+ }, opts.timeout);
339
+ });
340
+ }
341
+
342
+ // src/ai-provider/assertions/sessionReuse.ts
343
+ import { accumulatingEmit, getAiProviderRegistry as getAiProviderRegistry3, getGlobalModelRepository as getGlobalModelRepository3 } from "@workglow/ai";
344
+ import { getLogger as getLogger2 } from "@workglow/util";
345
+ import { beforeAll, describe as describe4, expect as expect4 } from "vitest";
346
+
347
+ // src/itExpectFail.ts
348
+ var itExpectFail = (name, fn, timeout) => {
349
+ const native = it.fails;
350
+ if (typeof native === "function") {
351
+ native(name, fn, timeout);
352
+ return;
353
+ }
354
+ it(`${name} [expected-fail]`, async () => {
355
+ let passed = false;
356
+ try {
357
+ await fn();
358
+ passed = true;
359
+ } catch (err) {
360
+ if (isCreditExhaustedError(err))
361
+ throw err;
362
+ }
363
+ if (passed) {
364
+ throw new Error(`Test "${name}" was marked as expected-fail but passed. Remove its name from opts.expectedFailures.`);
365
+ }
366
+ }, timeout);
367
+ };
368
+
369
+ // src/ai-provider/assertions/sessionReuse.ts
370
+ var TEXT_GENERATION2 = ["text.generation"];
371
+ function sessionReuseBlock(opts, fixture, getHandle) {
372
+ const enabled = opts.capabilities.sessions && !!opts.models.textGeneration;
373
+ const expectFails = new Set(opts.expectedFailures ?? []);
374
+ const itImpl = expectFails.has("session.reuse") ? itExpectFail : it;
375
+ describe4.skipIf(!enabled)("Session reuse", () => {
376
+ beforeAll(async () => {
377
+ await getHandle().releaseTransients?.();
378
+ });
379
+ itImpl("two invocations with the same sessionId yield exactly one session-map entry", async () => {
380
+ const handle = getHandle();
381
+ const map = handle.inspect().sessionMap;
382
+ if (!map) {
383
+ getLogger2().warn(`[conformance] ${opts.name} declares sessions=true but inspect().sessionMap is undefined; skipping`);
384
+ return;
385
+ }
386
+ const sizeBefore = map.size;
387
+ const registry = getAiProviderRegistry3();
388
+ const model = await getGlobalModelRepository3().findByName(opts.models.textGeneration);
389
+ expect4(model).toBeDefined();
390
+ const runFn = registry.getRunFnFor(model.provider, TEXT_GENERATION2);
391
+ expect4(runFn, `provider "${model.provider}" has no run-fn for ["text.generation"]`).toBeDefined();
392
+ const sessionId = `conformance-${Date.now()}`;
393
+ {
394
+ const { emit } = accumulatingEmit();
395
+ await runFn({ prompt: fixture.textPrompt, maxTokens: fixture.maxTokens }, model, new AbortController().signal, emit, undefined, { sessionId });
396
+ }
397
+ {
398
+ const { emit } = accumulatingEmit();
399
+ await runFn({ prompt: fixture.textPrompt, maxTokens: fixture.maxTokens }, model, new AbortController().signal, emit, undefined, { sessionId });
400
+ }
401
+ const newEntries = map.size - sizeBefore;
402
+ expect4(newEntries).toBe(1);
403
+ }, opts.timeout);
404
+ });
405
+ }
406
+
407
+ // src/ai-provider/assertions/signalHonoring.ts
408
+ import { accumulatingEmit as accumulatingEmit2, getAiProviderRegistry as getAiProviderRegistry4, getGlobalModelRepository as getGlobalModelRepository4 } from "@workglow/ai";
409
+ import { describe as describe5, expect as expect5 } from "vitest";
410
+ var TEXT_GENERATION3 = ["text.generation"];
411
+ function isAbortError(err) {
412
+ if (!err || typeof err !== "object")
413
+ return false;
414
+ const e = err;
415
+ return e.name === "AbortError" || /abort/i.test(e.message ?? "");
416
+ }
417
+ function signalHonoringBlock(opts, fixture) {
418
+ const expectFails = new Set(opts.expectedFailures ?? []);
419
+ const itNonStreaming = expectFails.has("signal.nonStreaming") ? itExpectFail : it;
420
+ const skipMid = !opts.capabilities.abortMidStream;
421
+ const itMidStream = expectFails.has("signal.midStream") ? itExpectFail : it;
422
+ describe5.skipIf(!opts.models.textGeneration)("Signal honoring", () => {
423
+ itNonStreaming("run-fn rejects with AbortError when aborted before invocation", async () => {
424
+ const registry = getAiProviderRegistry4();
425
+ const repo = getGlobalModelRepository4();
426
+ const model = await repo.findByName(opts.models.textGeneration);
427
+ expect5(model).toBeDefined();
428
+ const runFn = registry.getRunFnFor(model.provider, TEXT_GENERATION3);
429
+ expect5(runFn, `provider "${model.provider}" has no run-fn for ["text.generation"]`).toBeDefined();
430
+ const ac = new AbortController;
431
+ ac.abort();
432
+ let caught;
433
+ try {
434
+ const { emit } = accumulatingEmit2();
435
+ await runFn({ prompt: fixture.textPrompt, maxTokens: fixture.maxTokens }, model, ac.signal, emit, undefined, undefined);
436
+ } catch (err) {
437
+ caught = err;
438
+ }
439
+ expect5(caught).toBeDefined();
440
+ expect5(isAbortError(caught)).toBe(true);
441
+ }, opts.timeout);
442
+ if (!skipMid) {
443
+ itMidStream("streaming run-fn terminates within abortGraceMs * 4 when aborted mid-stream", async () => {
444
+ const registry = getAiProviderRegistry4();
445
+ const repo = getGlobalModelRepository4();
446
+ const model = await repo.findByName(opts.models.textGeneration);
447
+ expect5(model).toBeDefined();
448
+ const streamFn = registry.getRunFnFor(model.provider, TEXT_GENERATION3);
449
+ if (!streamFn)
450
+ return;
451
+ const ac = new AbortController;
452
+ const start = Date.now();
453
+ setTimeout(() => ac.abort(), fixture.abortGraceMs);
454
+ try {
455
+ const emit = (_e) => {};
456
+ await streamFn({ prompt: fixture.textPrompt, maxTokens: fixture.maxTokens }, model, ac.signal, emit, undefined, undefined);
457
+ } catch (err) {
458
+ if (!isAbortError(err))
459
+ throw err;
460
+ }
461
+ const elapsed = Date.now() - start;
462
+ expect5(elapsed).toBeLessThan(fixture.abortGraceMs * 4 + 2000);
463
+ }, opts.timeout);
464
+ }
465
+ });
466
+ }
467
+
468
+ // src/ai-provider/assertions/structuredGeneration.ts
469
+ import { structuredGeneration } from "@workglow/ai";
470
+ import { describe as describe6, expect as expect6 } from "vitest";
471
+ function structuredGenerationBlock(opts, fixture) {
472
+ const enabled = opts.capabilities.structured && !!opts.models.structured;
473
+ describe6.skipIf(!enabled)("Structured generation", () => {
474
+ it("produces an object that contains the schema's required fields", async () => {
475
+ const result = await structuredGeneration({
476
+ model: opts.models.structured,
477
+ prompt: fixture.structuredPrompt,
478
+ outputSchema: fixture.structuredSchema,
479
+ maxTokens: fixture.maxTokens
480
+ });
481
+ expect6(result).toBeDefined();
482
+ expect6(result.object).toBeDefined();
483
+ expect6(typeof result.object).toBe("object");
484
+ expect6(result.object).toHaveProperty("name");
485
+ expect6(result.object).toHaveProperty("age");
486
+ }, opts.timeout);
487
+ });
488
+ }
489
+
490
+ // src/ai-provider/assertions/textGenerationSmoke.ts
491
+ import { getAiProviderRegistry as getAiProviderRegistry5, getGlobalModelRepository as getGlobalModelRepository5, textGeneration } from "@workglow/ai";
492
+ import { describe as describe7, expect as expect7 } from "vitest";
493
+ var TEXT_GENERATION4 = ["text.generation"];
494
+ function textGenerationSmokeBlock(opts, fixture) {
495
+ describe7.skipIf(!opts.models.textGeneration)("TextGeneration smoke", () => {
496
+ it("task-level textGeneration() returns non-empty text", async () => {
497
+ const result = await textGeneration({
498
+ model: opts.models.textGeneration,
499
+ prompt: fixture.textPrompt,
500
+ maxTokens: fixture.maxTokens
501
+ });
502
+ expect7(result).toBeDefined();
503
+ expect7(typeof result.text).toBe("string");
504
+ expect7(result.text.length).toBeGreaterThan(0);
505
+ }, opts.timeout);
506
+ it.skipIf(!opts.capabilities.streaming)("streaming yields ≥1 text-delta and exactly one finish, with no event after finish", async () => {
507
+ const registry = getAiProviderRegistry5();
508
+ const model = await getGlobalModelRepository5().findByName(opts.models.textGeneration);
509
+ expect7(model).toBeDefined();
510
+ const streamFn = registry.getRunFnFor(model.provider, TEXT_GENERATION4);
511
+ expect7(streamFn).toBeDefined();
512
+ let textDeltaCount = 0;
513
+ let finishCount = 0;
514
+ let sawEventAfterFinish = false;
515
+ const emit = (ev) => {
516
+ if (finishCount > 0) {
517
+ sawEventAfterFinish = true;
518
+ }
519
+ const e = ev;
520
+ if (e.type === "text-delta")
521
+ textDeltaCount += 1;
522
+ if (e.type === "finish")
523
+ finishCount += 1;
524
+ };
525
+ await streamFn({ prompt: fixture.textPrompt, maxTokens: fixture.maxTokens }, model, new AbortController().signal, emit, undefined, undefined);
526
+ expect7(textDeltaCount).toBeGreaterThanOrEqual(1);
527
+ expect7(finishCount).toBe(1);
528
+ expect7(sawEventAfterFinish).toBe(false);
529
+ }, opts.timeout);
530
+ });
531
+ }
532
+
533
+ // src/ai-provider/assertions/toolCallAccumulator.ts
534
+ import { toolCalling } from "@workglow/ai";
535
+ import { parsePartialJson } from "@workglow/util/schema";
536
+ import { describe as describe8, expect as expect8 } from "vitest";
537
+ function toolCallAccumulatorBlock(opts, fixture) {
538
+ const enabled = opts.capabilities.tools && !!opts.models.toolCalling;
539
+ describe8.skipIf(!enabled)("Tool-call accumulator", () => {
540
+ it("produces ≥1 call with non-empty id, expected name, and parsable final args", async () => {
541
+ const tool = {
542
+ name: fixture.weatherTool.name,
543
+ description: fixture.weatherTool.description,
544
+ inputSchema: fixture.weatherTool.inputSchema
545
+ };
546
+ const result = await toolCalling({
547
+ model: opts.models.toolCalling,
548
+ prompt: fixture.weatherToolPrompt,
549
+ tools: [tool],
550
+ toolChoice: "required",
551
+ maxTokens: fixture.maxTokens,
552
+ messages: undefined
553
+ });
554
+ const calls = result.toolCalls;
555
+ expect8(calls.length).toBeGreaterThan(0);
556
+ const call = calls[0];
557
+ expect8(call.name).toBe(fixture.weatherTool.name);
558
+ expect8(call.id).toBeTruthy();
559
+ expect8(call.input).toBeDefined();
560
+ const serialized = typeof call.input === "string" ? call.input : JSON.stringify(call.input);
561
+ const parsed = parsePartialJson(serialized);
562
+ expect8(parsed).toBeDefined();
563
+ }, opts.timeout);
564
+ it("produces no tool calls with toolChoice none", async () => {
565
+ const tool = {
566
+ name: fixture.weatherTool.name,
567
+ description: fixture.weatherTool.description,
568
+ inputSchema: fixture.weatherTool.inputSchema
569
+ };
570
+ const result = await toolCalling({
571
+ model: opts.models.toolCalling,
572
+ prompt: fixture.weatherToolPrompt,
573
+ tools: [tool],
574
+ toolChoice: "none",
575
+ maxTokens: fixture.maxTokens,
576
+ messages: undefined
577
+ });
578
+ expect8(result).toBeDefined();
579
+ expect8(typeof result.text).toBe("string");
580
+ expect8(result.text.length).toBeGreaterThan(0);
581
+ expect8(result.toolCalls).toHaveLength(0);
582
+ }, opts.timeout);
583
+ });
584
+ }
585
+
586
+ // src/ai-provider/assertions/toolCallMultiTurn.ts
587
+ import { Workflow } from "@workglow/task-graph";
588
+ import { describe as describe9, expect as expect9 } from "vitest";
589
+ function toolCallMultiTurnBlock(opts, fixture) {
590
+ const enabled = opts.capabilities.tools && !!opts.models.toolCalling;
591
+ describe9.skipIf(!enabled)("Tool-call multi-turn", () => {
592
+ it("second turn returns non-empty text after a tool result is provided", async () => {
593
+ const tool = {
594
+ name: fixture.weatherTool.name,
595
+ description: fixture.weatherTool.description,
596
+ inputSchema: fixture.weatherTool.inputSchema
597
+ };
598
+ const wf1 = new Workflow;
599
+ wf1.toolCalling({
600
+ model: opts.models.toolCalling,
601
+ prompt: fixture.multiTurnTranscript[0].text,
602
+ tools: [tool],
603
+ toolChoice: "auto",
604
+ maxTokens: fixture.maxTokens
605
+ });
606
+ const r1 = await wf1.run();
607
+ if (!r1.toolCalls || r1.toolCalls.length === 0)
608
+ return;
609
+ const call = r1.toolCalls[0];
610
+ const wf2 = new Workflow;
611
+ wf2.toolCalling({
612
+ model: opts.models.toolCalling,
613
+ prompt: fixture.multiTurnTranscript[0].text,
614
+ tools: [tool],
615
+ toolChoice: "auto",
616
+ maxTokens: fixture.maxTokens,
617
+ messages: [
618
+ {
619
+ role: "user",
620
+ content: [{ type: "text", text: fixture.multiTurnTranscript[0].text }]
621
+ },
622
+ {
623
+ role: "assistant",
624
+ content: [
625
+ { type: "text", text: r1.text || fixture.multiTurnTranscript[1].text },
626
+ {
627
+ type: "tool_use",
628
+ id: call.id,
629
+ name: call.name,
630
+ input: call.input,
631
+ providerSignature: call.providerSignature
632
+ }
633
+ ]
634
+ },
635
+ {
636
+ role: "tool",
637
+ content: [
638
+ {
639
+ type: "tool_result",
640
+ tool_use_id: call.id,
641
+ content: [{ type: "text", text: fixture.multiTurnTranscript[2].text }],
642
+ is_error: undefined
643
+ }
644
+ ]
645
+ }
646
+ ]
647
+ });
648
+ const r2 = await wf2.run();
649
+ expect9(typeof r2.text).toBe("string");
650
+ expect9(r2.text.length).toBeGreaterThan(0);
651
+ }, opts.timeout);
652
+ });
653
+ }
654
+
655
+ // src/ai-provider/runAiProviderConformance.ts
656
+ function runAiProviderConformance(opts) {
657
+ const requiredModels = [
658
+ [opts.capabilities.streaming, "streaming", "textGeneration"],
659
+ [opts.capabilities.tools, "tools", "toolCalling"],
660
+ [opts.capabilities.structured, "structured", "structured"],
661
+ [opts.capabilities.embeddings, "embeddings", "embeddings"]
662
+ ];
663
+ for (const [claimed, capabilityKey, modelKey] of requiredModels) {
664
+ if (claimed && !opts.models[modelKey]) {
665
+ throw new Error(`AiProvider conformance "${opts.name}": capabilities.${capabilityKey} is true but models.${modelKey} is not set. ` + `Either provide a model id or set capabilities.${capabilityKey} to false.`);
666
+ }
667
+ }
668
+ describe10.skipIf(opts.skip)(`AiProvider conformance: ${opts.name}`, () => {
669
+ let handle;
670
+ const getHandle = () => {
671
+ if (!handle)
672
+ throw new Error("conformance handle not initialized");
673
+ return handle;
674
+ };
675
+ beforeAll2(async () => {
676
+ handle = await opts.factory();
677
+ await handle.register();
678
+ }, opts.timeout);
679
+ afterAll(async () => {
680
+ if (handle)
681
+ await handle.dispose();
682
+ });
683
+ const fixture = resolveFixture(opts.fixture);
684
+ registryCoverageBlock(opts);
685
+ textGenerationSmokeBlock(opts, fixture);
686
+ signalHonoringBlock(opts, fixture);
687
+ toolCallAccumulatorBlock(opts, fixture);
688
+ toolCallMultiTurnBlock(opts, fixture);
689
+ structuredGenerationBlock(opts, fixture);
690
+ sessionReuseBlock(opts, fixture, getHandle);
691
+ capabilityHonestyBlock(opts);
692
+ disposeBlock(opts, getHandle);
693
+ });
694
+ }
695
+ // src/ai-provider/assertions/inferAdvertisesRegistered.ts
696
+ import { expect as expect10 } from "vitest";
697
+ function assertInferAdvertisesRegistered(name, registered, inferred) {
698
+ const registeredCaps = new Set(registered.flat());
699
+ const inferredCaps = new Set(inferred.flat());
700
+ const missing = [...registeredCaps].filter((cap) => !inferredCaps.has(cap)).sort();
701
+ expect10(missing, `${name} registers capabilities that inferCapabilities never returns: ${missing.join(", ")}`).toEqual([]);
702
+ }
703
+ // src/ai-provider/assertions/inferServesInferred.ts
704
+ import { expect as expect11 } from "vitest";
705
+ var MARKER_CAPABILITIES = new Set(["vision-input"]);
706
+ var SESSION_CAPABILITIES = new Set(["cache.checkpoint", "session.dispose"]);
707
+ function assertInferServesInferred(name, registered, inferred) {
708
+ const unservable = [];
709
+ for (const { id, capabilities } of inferred) {
710
+ const inferredSet = new Set(capabilities);
711
+ for (const capability of capabilities) {
712
+ if (MARKER_CAPABILITIES.has(capability))
713
+ continue;
714
+ const servable = registered.some((serves) => serves.includes(capability) && serves.every((cap) => inferredSet.has(cap)));
715
+ if (!servable) {
716
+ unservable.push(`${id}: ${capability} — no run-fn serves it within this model's set`);
717
+ } else if (SESSION_CAPABILITIES.has(capability) && !inferredSet.has("text.generation")) {
718
+ unservable.push(`${id}: ${capability} — no session without text.generation`);
719
+ }
720
+ }
721
+ }
722
+ expect11(unservable, `${name} infers capabilities it cannot serve for that model:
723
+ ${unservable.join(`
724
+ `)}`).toEqual([]);
725
+ }
726
+ // src/ai-provider/assertions/pricingMatchesModality.ts
727
+ import { expect as expect12 } from "vitest";
728
+ var NOT_TOKEN_BILLED = new Set([
729
+ "image.generation",
730
+ "audio.speech",
731
+ "audio.transcription"
732
+ ]);
733
+ function quotesTokenRate(pricing) {
734
+ if (pricing === undefined)
735
+ return false;
736
+ return typeof pricing.input === "number" || typeof pricing.output === "number" || typeof pricing.cached === "number";
737
+ }
738
+ function assertPricingMatchesModality(name, models, namedByTable = []) {
739
+ const named = new Set(namedByTable);
740
+ const fabricated = [];
741
+ for (const { id, capabilities, pricing } of models) {
742
+ if (!capabilities.some((capability) => NOT_TOKEN_BILLED.has(capability)))
743
+ continue;
744
+ if (!quotesTokenRate(pricing))
745
+ continue;
746
+ if (named.has(id))
747
+ continue;
748
+ const free = (pricing?.input ?? 0) === 0 && (pricing?.output ?? 0) === 0;
749
+ if (free)
750
+ continue;
751
+ const billed = capabilities.filter((capability) => NOT_TOKEN_BILLED.has(capability));
752
+ fabricated.push(`${id}: ${billed.join(", ")} but priced ` + `input=${String(pricing?.input)} output=${String(pricing?.output)} per 1M tokens`);
753
+ }
754
+ expect12(fabricated, `${name} borrows a per-token rate for a model not billed per token`).toEqual([]);
755
+ const stale = namedByTable.filter((id) => !models.some(({ id: modelId, capabilities, pricing }) => modelId === id && capabilities.some((capability) => NOT_TOKEN_BILLED.has(capability)) && quotesTokenRate(pricing)));
756
+ expect12(stale, `${name}: recorded as deliberately priced but no longer is: ${stale.join(", ")}`).toEqual([]);
757
+ }
758
+ function assertPricedGapDoesNotGrow(name, models, knownUnpriced) {
759
+ const unpriced = models.filter(({ capabilities, pricing }) => capabilities.length > 0 && !capabilities.some((capability) => NOT_TOKEN_BILLED.has(capability)) && pricing === undefined).map(({ id }) => id).sort();
760
+ const known = new Set(knownUnpriced);
761
+ expect12(unpriced.filter((id) => !known.has(id)), `${name} has a token-billed model with no rate card that was not recorded`).toEqual([]);
762
+ const closed = knownUnpriced.filter((id) => !unpriced.includes(id));
763
+ expect12(closed, `${name}: now priced — drop from the recorded set: ${closed.join(", ")}`).toEqual([]);
764
+ }
765
+ // src/ai-provider/assertions/usageNormalization.ts
766
+ import { describe as describe11, expect as expect13 } from "vitest";
767
+ var USAGE_COUNTER_FIELDS = [
768
+ "input",
769
+ "output",
770
+ "cached",
771
+ "cacheWrite",
772
+ "reasoning",
773
+ "total"
774
+ ];
775
+ function assertUsageShape(usage) {
776
+ for (const field of USAGE_COUNTER_FIELDS) {
777
+ const value = usage[field];
778
+ if (value === undefined)
779
+ continue;
780
+ expect13(typeof value, `${field} must be a number when reported`).toBe("number");
781
+ expect13(Number.isFinite(value), `${field} must be finite`).toBe(true);
782
+ }
783
+ if (usage.extra !== undefined) {
784
+ for (const [key, value] of Object.entries(usage.extra)) {
785
+ expect13(["number", "string"], `extra.${key} must be a number or string`).toContain(typeof value);
786
+ }
787
+ }
788
+ }
789
+ function assertUsageDisjointness(usage) {
790
+ if (usage.reasoning !== undefined && usage.output !== undefined) {
791
+ expect13(usage.reasoning <= usage.output, `reasoning (${usage.reasoning}) is a subset of output (${usage.output})`).toBe(true);
792
+ }
793
+ if (usage.total === undefined)
794
+ return;
795
+ const parts = [usage.input, usage.cached, usage.cacheWrite, usage.output];
796
+ if (parts.every((part) => part === undefined))
797
+ return;
798
+ const sum = parts.reduce((acc, part) => acc + (part ?? 0), 0);
799
+ expect13(sum, `input + cached + cacheWrite + output must equal the provider's stated total`).toBe(usage.total);
800
+ }
801
+ function usageNormalizationBlock(opts) {
802
+ describe11(`${opts.provider} usage normalization contract`, () => {
803
+ it("reports nothing when the provider sent no usage payload", () => {
804
+ for (const empty of [undefined, null, {}, "usage", 42]) {
805
+ expect13(opts.mapUsage(empty), `mapped ${JSON.stringify(empty) ?? "undefined"}`).toBe(undefined);
806
+ }
807
+ });
808
+ it("leaves counters the provider did not report as undefined, never 0", () => {
809
+ const usage = opts.mapUsage(opts.sparseFrame);
810
+ expect13(usage, "a frame with real counters must map to a Usage").toBeDefined();
811
+ const reported = new Set(opts.sparseReports);
812
+ for (const field of USAGE_COUNTER_FIELDS) {
813
+ if (reported.has(field)) {
814
+ expect13(usage[field], `${field} was reported and must survive`).toBeTypeOf("number");
815
+ } else {
816
+ expect13(usage[field], `${field} was not reported and must be undefined`).toBe(undefined);
817
+ }
818
+ }
819
+ });
820
+ it("preserves a genuinely reported zero rather than dropping it", () => {
821
+ const usage = opts.mapUsage(opts.zeroFrame);
822
+ expect13(usage, "a frame reporting 0 still reported something").toBeDefined();
823
+ for (const field of opts.sparseReports) {
824
+ expect13(usage[field], `${field} reported 0 and must stay 0`).toBe(0);
825
+ }
826
+ });
827
+ it("maps only numbers and undefined into the normalized shape", () => {
828
+ for (const testCase of opts.cases) {
829
+ const usage = opts.mapUsage(testCase.frame);
830
+ if (usage)
831
+ assertUsageShape(usage);
832
+ }
833
+ const sparse = opts.mapUsage(opts.sparseFrame);
834
+ if (sparse)
835
+ assertUsageShape(sparse);
836
+ });
837
+ it("keeps input/cached/cacheWrite disjoint and reasoning inside output", () => {
838
+ for (const testCase of opts.cases) {
839
+ const usage = opts.mapUsage(testCase.frame);
840
+ if (usage)
841
+ assertUsageDisjointness(usage);
842
+ }
843
+ const sparse = opts.mapUsage(opts.sparseFrame);
844
+ if (sparse)
845
+ assertUsageDisjointness(sparse);
846
+ });
847
+ for (const testCase of opts.cases) {
848
+ it(`maps ${testCase.name}`, () => {
849
+ expect13(opts.mapUsage(testCase.frame)).toEqual(testCase.expected);
850
+ });
851
+ }
852
+ });
853
+ }
854
+ export {
855
+ DEFAULT_CONFORMANCE_FIXTURE,
856
+ assertInferAdvertisesRegistered,
857
+ assertInferServesInferred,
858
+ assertPricedGapDoesNotGrow,
859
+ assertPricingMatchesModality,
860
+ assertUsageDisjointness,
861
+ assertUsageShape,
862
+ capabilityHonestyBlock,
863
+ disposeBlock,
864
+ registryCoverageBlock,
865
+ resolveFixture,
866
+ runAiProviderConformance,
867
+ sessionReuseBlock,
868
+ signalHonoringBlock,
869
+ structuredGenerationBlock,
870
+ textGenerationSmokeBlock,
871
+ toolCallAccumulatorBlock,
872
+ toolCallMultiTurnBlock,
873
+ usageNormalizationBlock
874
+ };
875
+
876
+ //# debugId=A13FF2AAA5D2709B64756E2164756E21