@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
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
// src/storage-migrations/runMigrationRunnerContract.ts
|
|
2
|
+
import { afterAll, beforeAll, describe as describe8 } from "vitest";
|
|
3
|
+
|
|
4
|
+
// src/storage-migrations/assertions/appliesAndRecords.ts
|
|
5
|
+
import { describe, expect, it as it2 } from "vitest";
|
|
6
|
+
|
|
7
|
+
// src/creditExhaustedSkip.ts
|
|
8
|
+
import { it as vitestIt } from "vitest";
|
|
9
|
+
var BILLING_CODES = new Set([
|
|
10
|
+
"insufficient_quota",
|
|
11
|
+
"billing_hard_limit_reached",
|
|
12
|
+
"insufficient_credits",
|
|
13
|
+
"insufficient_balance"
|
|
14
|
+
]);
|
|
15
|
+
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;
|
|
16
|
+
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;
|
|
17
|
+
function isCreditExhaustedError(err) {
|
|
18
|
+
const seen = new Set;
|
|
19
|
+
let current = err;
|
|
20
|
+
while (current !== undefined && current !== null && !seen.has(current)) {
|
|
21
|
+
seen.add(current);
|
|
22
|
+
if (matchesCreditExhaustion(current))
|
|
23
|
+
return true;
|
|
24
|
+
current = causeOf(current);
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
function matchesCreditExhaustion(err) {
|
|
29
|
+
if (typeof err === "string")
|
|
30
|
+
return messageLooksLikeCreditExhaustion(err);
|
|
31
|
+
if (!err || typeof err !== "object")
|
|
32
|
+
return false;
|
|
33
|
+
const rec = err;
|
|
34
|
+
const status = numericStatus(rec);
|
|
35
|
+
if (status === 402)
|
|
36
|
+
return true;
|
|
37
|
+
const code = stringish(rec.code) ?? stringish(nested(rec.error, "code"));
|
|
38
|
+
const type = stringish(rec.type) ?? stringish(nested(rec.error, "type"));
|
|
39
|
+
if (code && BILLING_CODES.has(code))
|
|
40
|
+
return true;
|
|
41
|
+
if (type && BILLING_CODES.has(type))
|
|
42
|
+
return true;
|
|
43
|
+
const message = [
|
|
44
|
+
stringish(rec.message),
|
|
45
|
+
stringish(nested(rec.error, "message")),
|
|
46
|
+
stringifyUnknown(err)
|
|
47
|
+
].filter((part) => typeof part === "string").join(`
|
|
48
|
+
`);
|
|
49
|
+
return messageLooksLikeCreditExhaustion(message);
|
|
50
|
+
}
|
|
51
|
+
function messageLooksLikeCreditExhaustion(message) {
|
|
52
|
+
return CREDIT_MESSAGE.test(message) || HTTP_402_MESSAGE.test(message);
|
|
53
|
+
}
|
|
54
|
+
function numericStatus(rec) {
|
|
55
|
+
for (const key of ["status", "statusCode"]) {
|
|
56
|
+
const value = rec[key];
|
|
57
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
const nestedStatus = nested(rec.error, "status");
|
|
61
|
+
if (typeof nestedStatus === "number" && Number.isFinite(nestedStatus))
|
|
62
|
+
return nestedStatus;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
function nested(value, key) {
|
|
66
|
+
if (!value || typeof value !== "object")
|
|
67
|
+
return;
|
|
68
|
+
return value[key];
|
|
69
|
+
}
|
|
70
|
+
function stringish(value) {
|
|
71
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
72
|
+
}
|
|
73
|
+
function stringifyUnknown(err) {
|
|
74
|
+
if (err instanceof Error)
|
|
75
|
+
return err.message;
|
|
76
|
+
return String(err);
|
|
77
|
+
}
|
|
78
|
+
function causeOf(err) {
|
|
79
|
+
if (!err || typeof err !== "object")
|
|
80
|
+
return;
|
|
81
|
+
return err.cause;
|
|
82
|
+
}
|
|
83
|
+
function creditSkipNote(err) {
|
|
84
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
85
|
+
return `skipped: provider out of credits (${message})`;
|
|
86
|
+
}
|
|
87
|
+
var SKIP_OVERRIDE_ENV = "WORKGLOW_CREDIT_EXHAUSTED_SKIP";
|
|
88
|
+
function isTruthyFlag(value) {
|
|
89
|
+
return !/^(?:0|false|off|no)$/i.test(value.trim());
|
|
90
|
+
}
|
|
91
|
+
function creditExhaustionSkipsTest(env = process.env) {
|
|
92
|
+
const override = env[SKIP_OVERRIDE_ENV];
|
|
93
|
+
if (override !== undefined && override.trim() !== "")
|
|
94
|
+
return isTruthyFlag(override);
|
|
95
|
+
for (const key of ["CI", "GITHUB_ACTIONS"]) {
|
|
96
|
+
const value = env[key];
|
|
97
|
+
if (value !== undefined && value.trim() !== "" && isTruthyFlag(value))
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
function reportLocalCreditFailure(err) {
|
|
103
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
104
|
+
console.error(`provider out of credits (${message})
|
|
105
|
+
` + ` Failing because this is not CI. Top the account up, or set ${SKIP_OVERRIDE_ENV}=1 to skip instead.`);
|
|
106
|
+
}
|
|
107
|
+
async function runWithCreditSkip(ctx, fn) {
|
|
108
|
+
try {
|
|
109
|
+
return await fn();
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (isCreditExhaustedError(err)) {
|
|
112
|
+
if (!creditExhaustionSkipsTest()) {
|
|
113
|
+
reportLocalCreditFailure(err);
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
ctx.skip(creditSkipNote(err));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function wrapTestBodyForCreditSkip(fn) {
|
|
123
|
+
return async function wrapped(...args) {
|
|
124
|
+
try {
|
|
125
|
+
return await fn.apply(this, args);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (!isCreditExhaustedError(err))
|
|
128
|
+
throw err;
|
|
129
|
+
if (!creditExhaustionSkipsTest()) {
|
|
130
|
+
reportLocalCreditFailure(err);
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
const ctx = args[0];
|
|
134
|
+
if (ctx && typeof ctx.skip === "function") {
|
|
135
|
+
ctx.skip(creditSkipNote(err));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
console.warn(creditSkipNote(err));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function wrapCallable(fn) {
|
|
144
|
+
return new Proxy(fn, {
|
|
145
|
+
apply(target, thisArg, argArray) {
|
|
146
|
+
const wrapped = argArray.map((arg) => typeof arg === "function" ? wrapTestBodyForCreditSkip(arg) : arg);
|
|
147
|
+
const result = Reflect.apply(target, thisArg, wrapped);
|
|
148
|
+
return typeof result === "function" ? wrapCallable(result) : result;
|
|
149
|
+
},
|
|
150
|
+
get(target, prop, receiver) {
|
|
151
|
+
const value = Reflect.get(target, prop, receiver);
|
|
152
|
+
if (typeof value === "function") {
|
|
153
|
+
return wrapCallable(value.bind(target));
|
|
154
|
+
}
|
|
155
|
+
return value;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
var it = wrapCallable(vitestIt);
|
|
160
|
+
|
|
161
|
+
// src/itExpectFail.ts
|
|
162
|
+
var itExpectFail = (name, fn, timeout) => {
|
|
163
|
+
const native = it.fails;
|
|
164
|
+
if (typeof native === "function") {
|
|
165
|
+
native(name, fn, timeout);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
it(`${name} [expected-fail]`, async () => {
|
|
169
|
+
let passed = false;
|
|
170
|
+
try {
|
|
171
|
+
await fn();
|
|
172
|
+
passed = true;
|
|
173
|
+
} catch (err) {
|
|
174
|
+
if (isCreditExhaustedError(err))
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
if (passed) {
|
|
178
|
+
throw new Error(`Test "${name}" was marked as expected-fail but passed. Remove its name from opts.expectedFailures.`);
|
|
179
|
+
}
|
|
180
|
+
}, timeout);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// src/storage-migrations/types.ts
|
|
184
|
+
function createMigrationCallRecorder() {
|
|
185
|
+
const calls = [];
|
|
186
|
+
return {
|
|
187
|
+
get calls() {
|
|
188
|
+
return calls;
|
|
189
|
+
},
|
|
190
|
+
record: (component, version) => {
|
|
191
|
+
calls.push({ component, version });
|
|
192
|
+
},
|
|
193
|
+
clear: () => {
|
|
194
|
+
calls.length = 0;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/storage-migrations/assertions/appliesAndRecords.ts
|
|
200
|
+
function appliesAndRecordsBlock(opts, getHandle) {
|
|
201
|
+
const expectFails = new Set(opts.expectedFailures ?? []);
|
|
202
|
+
const itImpl = expectFails.has("appliesAndRecords") ? itExpectFail : it2;
|
|
203
|
+
describe("Applies and records", () => {
|
|
204
|
+
itImpl("first run applies all migrations in (component, version) order and records each version", async () => {
|
|
205
|
+
const handle = getHandle();
|
|
206
|
+
const runner = await handle.createRunner();
|
|
207
|
+
const recorder = createMigrationCallRecorder();
|
|
208
|
+
const component = `contract-applies-${Date.now()}`;
|
|
209
|
+
const migrations = [
|
|
210
|
+
handle.buildMigration(component, 2, recorder),
|
|
211
|
+
handle.buildMigration(component, 1, recorder),
|
|
212
|
+
handle.buildMigration(component, 3, recorder)
|
|
213
|
+
];
|
|
214
|
+
const applied = await runner.run(migrations);
|
|
215
|
+
expect(applied.length, "all three migrations are reported as applied").toBe(3);
|
|
216
|
+
expect(applied.map((m) => m.version), "applied[] is returned in (component, version) order").toEqual([1, 2, 3]);
|
|
217
|
+
expect(recorder.calls.map((c) => c.version), "up() is invoked in (component, version) order").toEqual([1, 2, 3]);
|
|
218
|
+
const recordedVersions = await runner.appliedVersions(component);
|
|
219
|
+
expect([...recordedVersions].sort((a, b) => a - b), "every applied version is recorded in the bookkeeping table").toEqual([1, 2, 3]);
|
|
220
|
+
}, opts.timeout);
|
|
221
|
+
itImpl("interleaved multi-component input is applied in (component, version) order", async () => {
|
|
222
|
+
const handle = getHandle();
|
|
223
|
+
const runner = await handle.createRunner();
|
|
224
|
+
const recorder = createMigrationCallRecorder();
|
|
225
|
+
const suffix = Date.now();
|
|
226
|
+
const a = `contract-applies-a-${suffix}`;
|
|
227
|
+
const b = `contract-applies-b-${suffix}`;
|
|
228
|
+
const applied = await runner.run([
|
|
229
|
+
handle.buildMigration(b, 2, recorder),
|
|
230
|
+
handle.buildMigration(a, 1, recorder),
|
|
231
|
+
handle.buildMigration(b, 1, recorder),
|
|
232
|
+
handle.buildMigration(a, 2, recorder)
|
|
233
|
+
]);
|
|
234
|
+
const expected = [
|
|
235
|
+
[a, 1],
|
|
236
|
+
[a, 2],
|
|
237
|
+
[b, 1],
|
|
238
|
+
[b, 2]
|
|
239
|
+
];
|
|
240
|
+
expect(applied.map((m) => [m.component, m.version]), "interleaved multi-component input is sorted (component asc, version asc) before applying").toEqual(expected);
|
|
241
|
+
expect(recorder.calls.map((c) => [c.component, c.version]), "up() invocation order matches sorted (component, version)").toEqual(expected);
|
|
242
|
+
const aVersions = await runner.appliedVersions(a);
|
|
243
|
+
const bVersions = await runner.appliedVersions(b);
|
|
244
|
+
expect([...aVersions].sort((x, y) => x - y), "appliedVersions(a) reports both versions").toEqual([1, 2]);
|
|
245
|
+
expect([...bVersions].sort((x, y) => x - y), "appliedVersions(b) reports both versions").toEqual([1, 2]);
|
|
246
|
+
}, opts.timeout);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/storage-migrations/assertions/concurrentRunsSerialize.ts
|
|
251
|
+
import { describe as describe2, expect as expect2, it as it3 } from "vitest";
|
|
252
|
+
function concurrentRunsSerializeBlock(opts, getHandle) {
|
|
253
|
+
const expectFails = new Set(opts.expectedFailures ?? []);
|
|
254
|
+
const itImpl = expectFails.has("concurrentRunsSerialize") ? itExpectFail : it3;
|
|
255
|
+
describe2("Concurrent runs serialize", () => {
|
|
256
|
+
itImpl("three racing runners against the same database execute up() exactly once", async () => {
|
|
257
|
+
const handle = getHandle();
|
|
258
|
+
const recorder = createMigrationCallRecorder();
|
|
259
|
+
const component = `contract-serialize-${Date.now()}`;
|
|
260
|
+
const migration = handle.buildMigration(component, 1, recorder);
|
|
261
|
+
const r1 = await handle.createRunner();
|
|
262
|
+
const r2 = await handle.createRunner();
|
|
263
|
+
const r3 = await handle.createRunner();
|
|
264
|
+
await Promise.allSettled([r1.run([migration]), r2.run([migration]), r3.run([migration])]);
|
|
265
|
+
expect2(recorder.calls.filter((c) => c.component === component), "exactly one of the three racing runners executed up()").toHaveLength(1);
|
|
266
|
+
const versions = await (await handle.createRunner()).appliedVersions(component);
|
|
267
|
+
expect2([...versions].sort((a, b) => a - b), "appliedVersions converges on a single (component, version) row").toEqual([1]);
|
|
268
|
+
}, opts.timeout);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/storage-migrations/assertions/ensureBookkeepingIdempotent.ts
|
|
273
|
+
import { describe as describe3, expect as expect3, it as it4 } from "vitest";
|
|
274
|
+
function ensureBookkeepingIdempotentBlock(opts, getHandle) {
|
|
275
|
+
const expectFails = new Set(opts.expectedFailures ?? []);
|
|
276
|
+
const itImpl = expectFails.has("ensureBookkeepingIdempotent") ? itExpectFail : it4;
|
|
277
|
+
describe3("Ensure bookkeeping idempotent", () => {
|
|
278
|
+
itImpl("calling ensureBookkeepingTable twice does not throw", async () => {
|
|
279
|
+
const runner = await getHandle().createRunner();
|
|
280
|
+
await runner.ensureBookkeepingTable();
|
|
281
|
+
await expect3(runner.ensureBookkeepingTable()).resolves.toBeUndefined();
|
|
282
|
+
}, opts.timeout);
|
|
283
|
+
itImpl("appliedVersions on a never-touched component returns an empty set", async () => {
|
|
284
|
+
const runner = await getHandle().createRunner();
|
|
285
|
+
await runner.ensureBookkeepingTable();
|
|
286
|
+
const seen = await runner.appliedVersions(`never-touched-${Date.now()}`);
|
|
287
|
+
expect3(seen.size, "untouched component has no applied versions").toBe(0);
|
|
288
|
+
}, opts.timeout);
|
|
289
|
+
itImpl("ensureBookkeepingTable does not contaminate sibling components", async () => {
|
|
290
|
+
const handle = getHandle();
|
|
291
|
+
const runner = await handle.createRunner();
|
|
292
|
+
const recorder = createMigrationCallRecorder();
|
|
293
|
+
const suffix = Date.now();
|
|
294
|
+
const owner = `contract-bookkeeping-owner-${suffix}`;
|
|
295
|
+
const sibling = `contract-bookkeeping-sibling-${suffix}`;
|
|
296
|
+
await runner.run([handle.buildMigration(owner, 1, recorder)]);
|
|
297
|
+
await runner.ensureBookkeepingTable();
|
|
298
|
+
await runner.ensureBookkeepingTable();
|
|
299
|
+
const ownerVersions = await runner.appliedVersions(owner);
|
|
300
|
+
expect3([...ownerVersions].sort((a, b) => a - b), "ensureBookkeepingTable preserves prior applied versions for the seeded component").toEqual([1]);
|
|
301
|
+
const siblingVersions = await runner.appliedVersions(sibling);
|
|
302
|
+
expect3(siblingVersions.size, "ensureBookkeepingTable does not leak rows into a sibling component").toBe(0);
|
|
303
|
+
}, opts.timeout);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/storage-migrations/assertions/failedMigrationLeavesNoPartialSchema.ts
|
|
308
|
+
import { describe as describe4, expect as expect4, it as it5 } from "vitest";
|
|
309
|
+
function failedMigrationLeavesNoPartialSchemaBlock(opts, getHandle) {
|
|
310
|
+
const expectFails = new Set(opts.expectedFailures ?? []);
|
|
311
|
+
const itImpl = expectFails.has("failedMigrationLeavesNoPartialSchema") ? itExpectFail : it5;
|
|
312
|
+
describe4("Failed migration leaves no partial schema", () => {
|
|
313
|
+
itImpl("probe schema is rolled back after up() throws", async () => {
|
|
314
|
+
const handle = getHandle();
|
|
315
|
+
const runner = await handle.createRunner();
|
|
316
|
+
const recorder = createMigrationCallRecorder();
|
|
317
|
+
const component = `contract-partial-schema-${Date.now()}`;
|
|
318
|
+
const probeName = `_probe_${Math.random().toString(36).slice(2, 10)}`;
|
|
319
|
+
const migration = handle.buildMigration(component, 1, recorder, { probeName });
|
|
320
|
+
await expect4(runner.run([migration])).rejects.toBeDefined();
|
|
321
|
+
const exists = await handle.probeExists(probeName);
|
|
322
|
+
expect4(exists, "probe schema element does not persist after up() throws").toBe(false);
|
|
323
|
+
}, opts.timeout);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/storage-migrations/assertions/failedMigrationNotRecorded.ts
|
|
328
|
+
import { describe as describe5, expect as expect5, it as it6 } from "vitest";
|
|
329
|
+
function failedMigrationNotRecordedBlock(opts, getHandle) {
|
|
330
|
+
const expectFails = new Set(opts.expectedFailures ?? []);
|
|
331
|
+
const itImpl = expectFails.has("failedMigrationNotRecorded") ? itExpectFail : it6;
|
|
332
|
+
describe5("Failed migration not recorded", () => {
|
|
333
|
+
itImpl("when up() throws, the failed migration's version is not recorded and a re-run with a healthy version applies it", async () => {
|
|
334
|
+
const handle = getHandle();
|
|
335
|
+
const runner = await handle.createRunner();
|
|
336
|
+
const recorder = createMigrationCallRecorder();
|
|
337
|
+
const component = `contract-failure-${Date.now()}`;
|
|
338
|
+
const v1 = handle.buildMigration(component, 1, recorder);
|
|
339
|
+
const v2Failing = handle.buildMigration(component, 2, recorder, { fail: true });
|
|
340
|
+
await expect5(runner.run([v1, v2Failing])).rejects.toThrow();
|
|
341
|
+
const afterFailure = await runner.appliedVersions(component);
|
|
342
|
+
expect5(afterFailure.has(2), "failed migration is not recorded as applied").toBe(false);
|
|
343
|
+
recorder.clear();
|
|
344
|
+
const v2Recovered = handle.buildMigration(component, 2, recorder);
|
|
345
|
+
const applied = await runner.run([v1, v2Recovered]);
|
|
346
|
+
expect5(applied.length, "recovery run applies at least one migration").toBeGreaterThanOrEqual(1);
|
|
347
|
+
expect5(applied.some((m) => m.version === 2), "recovery run applies v2").toBe(true);
|
|
348
|
+
expect5(recorder.calls.length, "recovery run invokes up() at least once").toBeGreaterThanOrEqual(1);
|
|
349
|
+
expect5(recorder.calls.some((c) => c.component === component && c.version === 2), "recovery run invokes up() for v2").toBe(true);
|
|
350
|
+
expect5(recorder.calls.length, "recovery up() invocations match `applied` exactly (no double-invocation)").toBe(applied.length);
|
|
351
|
+
const finalVersions = await runner.appliedVersions(component);
|
|
352
|
+
expect5([...finalVersions].sort((a, b) => a - b), "after recovery the bookkeeping table reports the full set").toEqual([1, 2]);
|
|
353
|
+
}, opts.timeout);
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/storage-migrations/assertions/idempotentRun.ts
|
|
358
|
+
import { describe as describe6, expect as expect6, it as it7 } from "vitest";
|
|
359
|
+
function idempotentRunBlock(opts, getHandle) {
|
|
360
|
+
const expectFails = new Set(opts.expectedFailures ?? []);
|
|
361
|
+
const itImpl = expectFails.has("idempotentRun") ? itExpectFail : it7;
|
|
362
|
+
describe6("Idempotent run", () => {
|
|
363
|
+
itImpl("second run with the same migration set applies nothing and re-invokes no up()", async () => {
|
|
364
|
+
const handle = getHandle();
|
|
365
|
+
const runner = await handle.createRunner();
|
|
366
|
+
const recorder = createMigrationCallRecorder();
|
|
367
|
+
const component = `contract-idempotent-${Date.now()}`;
|
|
368
|
+
const firstBatch = [
|
|
369
|
+
handle.buildMigration(component, 1, recorder),
|
|
370
|
+
handle.buildMigration(component, 2, recorder)
|
|
371
|
+
];
|
|
372
|
+
const firstApplied = await runner.run(firstBatch);
|
|
373
|
+
expect6(firstApplied.length, "first run applies all pending migrations").toBe(2);
|
|
374
|
+
expect6(recorder.calls.length, "first run invokes up() for each pending migration").toBe(2);
|
|
375
|
+
recorder.clear();
|
|
376
|
+
const secondBatch = [
|
|
377
|
+
handle.buildMigration(component, 1, recorder),
|
|
378
|
+
handle.buildMigration(component, 2, recorder)
|
|
379
|
+
];
|
|
380
|
+
const secondApplied = await runner.run(secondBatch);
|
|
381
|
+
expect6(secondApplied.length, "second run skips already-applied migrations").toBe(0);
|
|
382
|
+
expect6(recorder.calls.length, "second run invokes no up() callbacks").toBe(0);
|
|
383
|
+
const recordedVersions = await runner.appliedVersions(component);
|
|
384
|
+
expect6([...recordedVersions].sort((a, b) => a - b), "appliedVersions persists across the no-op rerun").toEqual([1, 2]);
|
|
385
|
+
}, opts.timeout);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// src/storage-migrations/assertions/incrementalApplication.ts
|
|
390
|
+
import { describe as describe7, expect as expect7, it as it8 } from "vitest";
|
|
391
|
+
function incrementalApplicationBlock(opts, getHandle) {
|
|
392
|
+
const expectFails = new Set(opts.expectedFailures ?? []);
|
|
393
|
+
const itImpl = expectFails.has("incrementalApplication") ? itExpectFail : it8;
|
|
394
|
+
describe7("Incremental application", () => {
|
|
395
|
+
itImpl("adding a new version to an already-migrated component runs only the new version", async () => {
|
|
396
|
+
const handle = getHandle();
|
|
397
|
+
const runner = await handle.createRunner();
|
|
398
|
+
const recorder = createMigrationCallRecorder();
|
|
399
|
+
const component = `contract-incremental-${Date.now()}`;
|
|
400
|
+
await runner.run([
|
|
401
|
+
handle.buildMigration(component, 1, recorder),
|
|
402
|
+
handle.buildMigration(component, 2, recorder)
|
|
403
|
+
]);
|
|
404
|
+
expect7(recorder.calls.length, "seed run applies v1 and v2").toBe(2);
|
|
405
|
+
recorder.clear();
|
|
406
|
+
const applied = await runner.run([
|
|
407
|
+
handle.buildMigration(component, 1, recorder),
|
|
408
|
+
handle.buildMigration(component, 2, recorder),
|
|
409
|
+
handle.buildMigration(component, 3, recorder)
|
|
410
|
+
]);
|
|
411
|
+
expect7(applied.length, "only the newly-added version is applied").toBe(1);
|
|
412
|
+
expect7(applied[0].version, "the applied version is v3").toBe(3);
|
|
413
|
+
expect7(recorder.calls.length, "up() runs once for the incremental version").toBe(1);
|
|
414
|
+
expect7(recorder.calls[0].version, "the up() that ran was v3").toBe(3);
|
|
415
|
+
const recordedVersions = await runner.appliedVersions(component);
|
|
416
|
+
expect7([...recordedVersions].sort((a, b) => a - b), "all three versions are recorded after the incremental run").toEqual([1, 2, 3]);
|
|
417
|
+
}, opts.timeout);
|
|
418
|
+
itImpl("appliedVersions returns numerically-sorted versions even past version 9", async () => {
|
|
419
|
+
const handle = getHandle();
|
|
420
|
+
const runner = await handle.createRunner();
|
|
421
|
+
const recorder = createMigrationCallRecorder();
|
|
422
|
+
const component = `contract-numeric-sort-${Date.now()}`;
|
|
423
|
+
const applied = await runner.run([
|
|
424
|
+
handle.buildMigration(component, 10, recorder),
|
|
425
|
+
handle.buildMigration(component, 1, recorder),
|
|
426
|
+
handle.buildMigration(component, 2, recorder)
|
|
427
|
+
]);
|
|
428
|
+
expect7(applied.map((m) => m.version), "applied[] is sorted numerically across the 1↔10 boundary").toEqual([1, 2, 10]);
|
|
429
|
+
expect7(recorder.calls.map((c) => c.version), "up() invocation order is sorted numerically").toEqual([1, 2, 10]);
|
|
430
|
+
const recordedVersions = await runner.appliedVersions(component);
|
|
431
|
+
expect7([...recordedVersions].sort((a, b) => a - b), "appliedVersions enumerates all three versions").toEqual([1, 2, 10]);
|
|
432
|
+
}, opts.timeout);
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/storage-migrations/runMigrationRunnerContract.ts
|
|
437
|
+
function runMigrationRunnerContract(opts) {
|
|
438
|
+
describe8.skipIf(opts.skip)(`Migration runner contract: ${opts.name}`, () => {
|
|
439
|
+
let handle;
|
|
440
|
+
const getHandle = () => {
|
|
441
|
+
if (!handle)
|
|
442
|
+
throw new Error("migration contract handle not initialized");
|
|
443
|
+
return handle;
|
|
444
|
+
};
|
|
445
|
+
beforeAll(async () => {
|
|
446
|
+
handle = await opts.factory();
|
|
447
|
+
}, opts.timeout);
|
|
448
|
+
afterAll(async () => {
|
|
449
|
+
if (handle)
|
|
450
|
+
await handle.dispose();
|
|
451
|
+
});
|
|
452
|
+
ensureBookkeepingIdempotentBlock(opts, getHandle);
|
|
453
|
+
appliesAndRecordsBlock(opts, getHandle);
|
|
454
|
+
idempotentRunBlock(opts, getHandle);
|
|
455
|
+
incrementalApplicationBlock(opts, getHandle);
|
|
456
|
+
failedMigrationNotRecordedBlock(opts, getHandle);
|
|
457
|
+
concurrentRunsSerializeBlock(opts, getHandle);
|
|
458
|
+
failedMigrationLeavesNoPartialSchemaBlock(opts, getHandle);
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
export {
|
|
462
|
+
appliesAndRecordsBlock,
|
|
463
|
+
concurrentRunsSerializeBlock,
|
|
464
|
+
createMigrationCallRecorder,
|
|
465
|
+
ensureBookkeepingIdempotentBlock,
|
|
466
|
+
failedMigrationLeavesNoPartialSchemaBlock,
|
|
467
|
+
failedMigrationNotRecordedBlock,
|
|
468
|
+
idempotentRunBlock,
|
|
469
|
+
incrementalApplicationBlock,
|
|
470
|
+
runMigrationRunnerContract
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
//# debugId=0F07C135E2F03E9264756E2164756E21
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/storage-migrations/runMigrationRunnerContract.ts", "../src/storage-migrations/assertions/appliesAndRecords.ts", "../src/creditExhaustedSkip.ts", "../src/itExpectFail.ts", "../src/storage-migrations/types.ts", "../src/storage-migrations/assertions/concurrentRunsSerialize.ts", "../src/storage-migrations/assertions/ensureBookkeepingIdempotent.ts", "../src/storage-migrations/assertions/failedMigrationLeavesNoPartialSchema.ts", "../src/storage-migrations/assertions/failedMigrationNotRecorded.ts", "../src/storage-migrations/assertions/idempotentRun.ts", "../src/storage-migrations/assertions/incrementalApplication.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { afterAll, beforeAll, describe } from \"vitest\";\n\nimport { appliesAndRecordsBlock } from \"./assertions/appliesAndRecords\";\nimport { concurrentRunsSerializeBlock } from \"./assertions/concurrentRunsSerialize\";\nimport { ensureBookkeepingIdempotentBlock } from \"./assertions/ensureBookkeepingIdempotent\";\nimport { failedMigrationLeavesNoPartialSchemaBlock } from \"./assertions/failedMigrationLeavesNoPartialSchema\";\nimport { failedMigrationNotRecordedBlock } from \"./assertions/failedMigrationNotRecorded\";\nimport { idempotentRunBlock } from \"./assertions/idempotentRun\";\nimport { incrementalApplicationBlock } from \"./assertions/incrementalApplication\";\nimport type { MigrationContractHandle, MigrationRunnerContractOpts } from \"./types\";\n\nexport function runMigrationRunnerContract<DB>(opts: MigrationRunnerContractOpts<DB>): void {\n describe.skipIf(opts.skip)(`Migration runner contract: ${opts.name}`, () => {\n let handle: MigrationContractHandle<DB> | undefined;\n const getHandle = (): MigrationContractHandle<DB> => {\n if (!handle) throw new Error(\"migration contract handle not initialized\");\n return handle;\n };\n\n beforeAll(async () => {\n handle = await opts.factory();\n }, opts.timeout);\n\n afterAll(async () => {\n if (handle) await handle.dispose();\n });\n\n ensureBookkeepingIdempotentBlock(opts, getHandle);\n appliesAndRecordsBlock(opts, getHandle);\n idempotentRunBlock(opts, getHandle);\n incrementalApplicationBlock(opts, getHandle);\n failedMigrationNotRecordedBlock(opts, getHandle);\n concurrentRunsSerializeBlock(opts, getHandle);\n failedMigrationLeavesNoPartialSchemaBlock(opts, getHandle);\n });\n}\n\nexport { createMigrationCallRecorder } from \"./types\";\nexport type {\n BuildMigrationFn,\n BuildMigrationOptions,\n MigrationCallRecorder,\n MigrationContractAssertion,\n MigrationContractHandle,\n MigrationRunnerContractOpts,\n} from \"./types\";\n",
|
|
6
|
+
"/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { describe, expect, it } from \"vitest\";\n\nimport { itExpectFail } from \"../../itExpectFail\";\nimport {\n createMigrationCallRecorder,\n type MigrationContractHandle,\n type MigrationRunnerContractOpts,\n} from \"../types\";\n\nexport function appliesAndRecordsBlock<DB>(\n opts: MigrationRunnerContractOpts<DB>,\n getHandle: () => MigrationContractHandle<DB>\n): void {\n const expectFails = new Set(opts.expectedFailures ?? []);\n const itImpl = expectFails.has(\"appliesAndRecords\") ? itExpectFail : it;\n\n describe(\"Applies and records\", () => {\n itImpl(\n \"first run applies all migrations in (component, version) order and records each version\",\n async () => {\n const handle = getHandle();\n const runner = await handle.createRunner();\n const recorder = createMigrationCallRecorder();\n const component = `contract-applies-${Date.now()}`;\n const migrations = [\n handle.buildMigration(component, 2, recorder),\n handle.buildMigration(component, 1, recorder),\n handle.buildMigration(component, 3, recorder),\n ];\n\n const applied = await runner.run(migrations);\n\n expect(applied.length, \"all three migrations are reported as applied\").toBe(3);\n expect(\n applied.map((m) => m.version),\n \"applied[] is returned in (component, version) order\"\n ).toEqual([1, 2, 3]);\n expect(\n recorder.calls.map((c) => c.version),\n \"up() is invoked in (component, version) order\"\n ).toEqual([1, 2, 3]);\n const recordedVersions = await runner.appliedVersions(component);\n expect(\n [...recordedVersions].sort((a, b) => a - b),\n \"every applied version is recorded in the bookkeeping table\"\n ).toEqual([1, 2, 3]);\n },\n opts.timeout\n );\n\n itImpl(\n \"interleaved multi-component input is applied in (component, version) order\",\n async () => {\n const handle = getHandle();\n const runner = await handle.createRunner();\n const recorder = createMigrationCallRecorder();\n const suffix = Date.now();\n const a = `contract-applies-a-${suffix}`;\n const b = `contract-applies-b-${suffix}`;\n\n // Interleaved input across two components in not-already-sorted\n // order. `sortMigrations` is documented to stably sort by\n // (component asc, version asc), and every concrete runner calls\n // it before iterating; this assertion locks that invariant in.\n const applied = await runner.run([\n handle.buildMigration(b, 2, recorder),\n handle.buildMigration(a, 1, recorder),\n handle.buildMigration(b, 1, recorder),\n handle.buildMigration(a, 2, recorder),\n ]);\n\n const expected = [\n [a, 1],\n [a, 2],\n [b, 1],\n [b, 2],\n ];\n expect(\n applied.map((m) => [m.component, m.version]),\n \"interleaved multi-component input is sorted (component asc, version asc) before applying\"\n ).toEqual(expected);\n // Recorder agrees with the runner loop order, not just `applied` —\n // catches a runner that returns the right `applied` array but\n // invokes `up()` in input order.\n expect(\n recorder.calls.map((c) => [c.component, c.version]),\n \"up() invocation order matches sorted (component, version)\"\n ).toEqual(expected);\n\n const aVersions = await runner.appliedVersions(a);\n const bVersions = await runner.appliedVersions(b);\n expect(\n [...aVersions].sort((x, y) => x - y),\n \"appliedVersions(a) reports both versions\"\n ).toEqual([1, 2]);\n expect(\n [...bVersions].sort((x, y) => x - y),\n \"appliedVersions(b) reports both versions\"\n ).toEqual([1, 2]);\n },\n opts.timeout\n );\n });\n}\n",
|
|
7
|
+
"/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { it as vitestIt } from \"vitest\";\n\n/**\n * Live provider tests treat \"the account is out of credits\" as a billing\n * condition rather than a product bug. A key being present is not the same as\n * the account being able to pay; HuggingFace / OpenRouter 402s, Anthropic's\n * credit-balance error, DeepSeek's `Insufficient Balance`, and OpenAI's\n * `insufficient_quota` are all billing.\n *\n * Rate limits (429 + `rate_limit_exceeded`) are NOT this: those are transient\n * and the existing retry policy handles them.\n *\n * Where it skips and where it fails is a deliberate split\n * ({@link creditExhaustionSkipsTest}): on CI it skips for every provider,\n * because a red build tells a reviewer nothing they can act on; locally it\n * fails, because the developer running the suite is the one who can top the\n * account up. `WORKGLOW_CREDIT_EXHAUSTED_SKIP=1` forces the skip locally\n * (and `=0` forces the failure on CI).\n */\n\nconst BILLING_CODES = new Set([\n \"insufficient_quota\",\n \"billing_hard_limit_reached\",\n \"insufficient_credits\",\n \"insufficient_balance\",\n]);\n\nconst CREDIT_MESSAGE =\n /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;\n\n/**\n * A 402 that survives only as text. `classifyProviderError` rebuilds the error\n * as a `PermanentJobError` carrying the message and no `cause`, so by the time\n * a live test sees a DeepSeek billing failure the numeric `status` is gone and\n * the OpenAI-SDK message shape (`402 Insufficient Balance`) is all that is\n * left. Anchored to an HTTP-shaped position — start of a line, or after a\n * summary line's colon — so prose that merely contains the number\n * (\"Item 402(c)\") is not scavenged as a status.\n */\nconst HTTP_402_MESSAGE =\n /\\bHTTP\\/?\\d?(?:\\.\\d)?\\s*402\\b|\\bstatus(?:\\s*code)?\\s*[:=]\\s*402\\b|\\berror code:?\\s*402\\b|(?:^|:\\s+)402\\s+[a-z]/im;\n\nexport function isCreditExhaustedError(err: unknown): boolean {\n const seen = new Set<unknown>();\n let current: unknown = err;\n while (current !== undefined && current !== null && !seen.has(current)) {\n seen.add(current);\n if (matchesCreditExhaustion(current)) return true;\n current = causeOf(current);\n }\n return false;\n}\n\nfunction matchesCreditExhaustion(err: unknown): boolean {\n if (typeof err === \"string\") return messageLooksLikeCreditExhaustion(err);\n\n if (!err || typeof err !== \"object\") return false;\n\n const rec = err as Record<string, unknown>;\n const status = numericStatus(rec);\n if (status === 402) return true;\n\n const code = stringish(rec.code) ?? stringish(nested(rec.error, \"code\"));\n const type = stringish(rec.type) ?? stringish(nested(rec.error, \"type\"));\n if (code && BILLING_CODES.has(code)) return true;\n if (type && BILLING_CODES.has(type)) return true;\n\n const message = [\n stringish(rec.message),\n stringish(nested(rec.error, \"message\")),\n stringifyUnknown(err),\n ]\n .filter((part): part is string => typeof part === \"string\")\n .join(\"\\n\");\n return messageLooksLikeCreditExhaustion(message);\n}\n\nfunction messageLooksLikeCreditExhaustion(message: string): boolean {\n return CREDIT_MESSAGE.test(message) || HTTP_402_MESSAGE.test(message);\n}\n\nfunction numericStatus(rec: Record<string, unknown>): number | undefined {\n for (const key of [\"status\", \"statusCode\"] as const) {\n const value = rec[key];\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n }\n const nestedStatus = nested(rec.error, \"status\");\n if (typeof nestedStatus === \"number\" && Number.isFinite(nestedStatus)) return nestedStatus;\n return undefined;\n}\n\nfunction nested(value: unknown, key: string): unknown {\n if (!value || typeof value !== \"object\") return undefined;\n return (value as Record<string, unknown>)[key];\n}\n\nfunction stringish(value: unknown): string | undefined {\n return typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n\nfunction stringifyUnknown(err: unknown): string {\n if (err instanceof Error) return err.message;\n return String(err);\n}\n\nfunction causeOf(err: unknown): unknown {\n if (!err || typeof err !== \"object\") return undefined;\n return (err as { cause?: unknown }).cause;\n}\n\nexport function creditSkipNote(err: unknown): string {\n const message = err instanceof Error ? err.message : String(err);\n return `skipped: provider out of credits (${message})`;\n}\n\nconst SKIP_OVERRIDE_ENV = \"WORKGLOW_CREDIT_EXHAUSTED_SKIP\";\n\nfunction isTruthyFlag(value: string): boolean {\n return !/^(?:0|false|off|no)$/i.test(value.trim());\n}\n\n/**\n * Whether a billing failure skips the test or fails it.\n *\n * CI skips: nobody watching a build can top an account up, and one exhausted\n * key would otherwise turn every provider suite red for a change that touched\n * none of them — so the leniency is provider-agnostic by construction, keyed on\n * the environment rather than on which vendor ran out.\n *\n * Locally it fails, loudly, because the developer running the suite IS the\n * person who can act on it — and a silently green local run is how an\n * exhausted account goes unnoticed until CI is the only thing exercising the\n * provider at all.\n *\n * `WORKGLOW_CREDIT_EXHAUSTED_SKIP` overrides in both directions.\n */\nexport function creditExhaustionSkipsTest(\n env: Record<string, string | undefined> = process.env\n): boolean {\n const override = env[SKIP_OVERRIDE_ENV];\n if (override !== undefined && override.trim() !== \"\") return isTruthyFlag(override);\n for (const key of [\"CI\", \"GITHUB_ACTIONS\"] as const) {\n const value = env[key];\n if (value !== undefined && value.trim() !== \"\" && isTruthyFlag(value)) return true;\n }\n return false;\n}\n\n/**\n * The local-failure path. Rethrows the provider's own error unchanged — its\n * identity and stack are what makes the failure diagnosable — after one line\n * saying why it was not skipped and how to skip it.\n */\nfunction reportLocalCreditFailure(err: unknown): void {\n const message = err instanceof Error ? err.message : String(err);\n console.error(\n `provider out of credits (${message})\\n` +\n ` Failing because this is not CI. Top the account up, or set ${SKIP_OVERRIDE_ENV}=1 to skip instead.`\n );\n}\n\nexport async function runWithCreditSkip<T>(\n ctx: { skip: (note?: string) => void },\n fn: () => Promise<T> | T\n): Promise<T> {\n try {\n return await fn();\n } catch (err) {\n if (isCreditExhaustedError(err)) {\n if (!creditExhaustionSkipsTest()) {\n reportLocalCreditFailure(err);\n throw err;\n }\n ctx.skip(creditSkipNote(err));\n // vitest's skip() is `never` (throws). Mocks used in unit tests return.\n return undefined as T;\n }\n throw err;\n }\n}\n\n/**\n * Wraps one test body. This is the seam every conformance assertion actually\n * runs through (the exported {@link it} proxies each callback through it), so\n * the CI-skips / locally-fails policy is applied here rather than at each\n * call site.\n */\nexport function wrapTestBodyForCreditSkip(\n fn: (...args: never[]) => unknown\n): (...args: never[]) => unknown {\n return async function wrapped(this: unknown, ...args: never[]): Promise<unknown> {\n try {\n return await fn.apply(this, args);\n } catch (err) {\n if (!isCreditExhaustedError(err)) throw err;\n if (!creditExhaustionSkipsTest()) {\n reportLocalCreditFailure(err);\n throw err;\n }\n const ctx = args[0] as { skip?: (note?: string) => void } | undefined;\n if (ctx && typeof ctx.skip === \"function\") {\n ctx.skip(creditSkipNote(err));\n return undefined;\n }\n // bun:test has no TestContext.skip; passing is \"don't fail the test\".\n console.warn(creditSkipNote(err));\n return undefined;\n }\n };\n}\n\nfunction wrapCallable(fn: CallableFunction): CallableFunction {\n return new Proxy(fn, {\n apply(target, thisArg, argArray: unknown[]) {\n const wrapped = argArray.map((arg) =>\n typeof arg === \"function\" ? wrapTestBodyForCreditSkip(arg as never) : arg\n );\n const result = Reflect.apply(target, thisArg, wrapped);\n return typeof result === \"function\" ? wrapCallable(result) : result;\n },\n get(target, prop, receiver) {\n const value = Reflect.get(target, prop, receiver);\n if (typeof value === \"function\") {\n return wrapCallable((value as CallableFunction).bind(target));\n }\n return value;\n },\n });\n}\n\n/**\n * Drop-in `it` for live provider tests: a credit/billing error skips the test on\n * CI and fails it locally (see {@link creditExhaustionSkipsTest}).\n */\nexport const it = wrapCallable(vitestIt) as typeof vitestIt;\n",
|
|
8
|
+
"/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { isCreditExhaustedError, it } from \"./creditExhaustedSkip\";\n\ntype ItFn = (name: string, fn: () => Promise<void> | void, timeout?: number) => void;\n\n/**\n * Cross-runner polyfill for `it.fails`. Vitest exposes it natively; bun's\n * native `bun test` runner does not. Under bun we wrap the body in a\n * try/catch and assert that it threw — so a passing-when-expected-fail\n * test still surfaces as a CI failure (signalling \"remove from\n * expectedFailures\").\n */\nexport const itExpectFail: ItFn = (name, fn, timeout) => {\n const native = (it as unknown as { fails?: ItFn }).fails;\n if (typeof native === \"function\") {\n native(name, fn, timeout);\n return;\n }\n it(\n `${name} [expected-fail]`,\n async () => {\n let passed = false;\n try {\n await fn();\n passed = true;\n } catch (err) {\n // Billing exhaustion is not the adapter bug this marker records.\n if (isCreditExhaustedError(err)) throw err;\n // expected; the test was supposed to fail\n }\n if (passed) {\n throw new Error(\n `Test \"${name}\" was marked as expected-fail but passed. Remove its name from opts.expectedFailures.`\n );\n }\n },\n timeout\n );\n};\n",
|
|
9
|
+
"/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { IMigration, IMigrationRunner } from \"@workglow/storage\";\n\n/**\n * Records each invocation of a migration's `up()` callback. The contract\n * suite uses this to verify which migrations actually ran (orthogonal to\n * the runner's own bookkeeping table).\n *\n * Caveat: the recorder reflects whether `up()` *executed*, not whether\n * its effects *persisted*. For backends that wrap `up()` in a transaction\n * (Postgres BEGIN/ROLLBACK; IndexedDB upgrade tx.abort), a migration whose\n * `up()` records and then throws has `recorder.calls` containing that\n * version even though the bookkeeping insert and any DDL were rolled back.\n * Use `runner.appliedVersions(component)` to check what *persisted*; use\n * the recorder to check what *ran*.\n */\nexport interface MigrationCallRecorder {\n readonly calls: ReadonlyArray<{ readonly component: string; readonly version: number }>;\n readonly record: (component: string, version: number) => void;\n readonly clear: () => void;\n}\n\nexport function createMigrationCallRecorder(): MigrationCallRecorder {\n const calls: { component: string; version: number }[] = [];\n return {\n get calls() {\n return calls;\n },\n record: (component: string, version: number) => {\n calls.push({ component, version });\n },\n clear: () => {\n calls.length = 0;\n },\n };\n}\n\nexport interface BuildMigrationOptions {\n /** When set, the migration's `up()` throws synchronously. */\n readonly fail?: boolean;\n /**\n * When set, `up()` first creates a probe schema element with this name\n * (a table for SQL backends, an object store for IDB) and then throws.\n * Used by the partial-schema assertion to check whether the runner rolls\n * DDL back on failure.\n */\n readonly probeName?: string;\n}\n\n/**\n * Adapter-supplied factory that constructs a migration of the right\n * shape for the underlying DB (Postgres pool, SQLite database, or\n * IndexedDb upgrade context). The returned migration's `up()` calls the\n * recorder unless `opts.fail` is true.\n */\nexport type BuildMigrationFn<DB> = (\n component: string,\n version: number,\n recorder: MigrationCallRecorder,\n opts?: BuildMigrationOptions\n) => IMigration<DB>;\n\nexport interface MigrationContractHandle<DB> {\n readonly createRunner: () => Promise<IMigrationRunner<DB>>;\n readonly buildMigration: BuildMigrationFn<DB>;\n /**\n * Returns true iff a probe schema element with the given name currently\n * exists in the backing database. Required by the\n * `failedMigrationLeavesNoPartialSchema` assertion. Implementations\n * inspect the relevant catalog (`information_schema.tables`,\n * `sqlite_master`, `db.objectStoreNames`).\n */\n readonly probeExists: (name: string) => Promise<boolean>;\n readonly dispose: () => Promise<void>;\n}\n\n/**\n * Closed string-literal union of contract assertions adapters can mark as\n * known failing. Adding a new assertion to the suite must extend this\n * union — that way a typo in `expectedFailures` becomes a TS error rather\n * than a silently-ignored entry.\n */\nexport type MigrationContractAssertion =\n | \"appliesAndRecords\"\n | \"idempotentRun\"\n | \"incrementalApplication\"\n | \"failedMigrationNotRecorded\"\n | \"ensureBookkeepingIdempotent\"\n | \"concurrentRunsSerialize\"\n | \"failedMigrationLeavesNoPartialSchema\";\n\nexport interface MigrationRunnerContractOpts<DB> {\n readonly name: string;\n readonly skip?: boolean;\n readonly timeout: number;\n readonly factory: () => Promise<MigrationContractHandle<DB>>;\n /**\n * Names of contract assertions currently broken in this adapter; each\n * named test wraps with `itExpectFail`. The string-literal union catches\n * typos at compile time.\n */\n readonly expectedFailures?: ReadonlyArray<MigrationContractAssertion>;\n}\n",
|
|
10
|
+
"/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { describe, expect, it } from \"vitest\";\n\nimport { itExpectFail } from \"../../itExpectFail\";\nimport {\n createMigrationCallRecorder,\n type MigrationContractHandle,\n type MigrationRunnerContractOpts,\n} from \"../types\";\n\n/**\n * Concurrent `run()` calls against the same backing database — possibly\n * across different runner instances — must execute each migration's `up()`\n * exactly once and converge appliedVersions on a single row per\n * `(component, version)`.\n *\n * Adapters that rely solely on the bookkeeping PK to resolve races (race\n * losers see 23505 and treat as success) will run `up()` multiple times\n * before that resolution, and should mark this assertion as\n * `expectedFailures: [\"concurrentRunsSerialize\"]`.\n */\nexport function concurrentRunsSerializeBlock<DB>(\n opts: MigrationRunnerContractOpts<DB>,\n getHandle: () => MigrationContractHandle<DB>\n): void {\n const expectFails = new Set(opts.expectedFailures ?? []);\n const itImpl = expectFails.has(\"concurrentRunsSerialize\") ? itExpectFail : it;\n\n describe(\"Concurrent runs serialize\", () => {\n itImpl(\n \"three racing runners against the same database execute up() exactly once\",\n async () => {\n const handle = getHandle();\n const recorder = createMigrationCallRecorder();\n const component = `contract-serialize-${Date.now()}`;\n const migration = handle.buildMigration(component, 1, recorder);\n\n const r1 = await handle.createRunner();\n const r2 = await handle.createRunner();\n const r3 = await handle.createRunner();\n // allSettled — when serialization is broken, racing callers can\n // raise (e.g. PK violation on bookkeeping insert). The contract\n // violation we care about is observable via the recorder and\n // appliedVersions, not via which call rejected.\n await Promise.allSettled([r1.run([migration]), r2.run([migration]), r3.run([migration])]);\n\n expect(\n recorder.calls.filter((c) => c.component === component),\n \"exactly one of the three racing runners executed up()\"\n ).toHaveLength(1);\n const versions = await (await handle.createRunner()).appliedVersions(component);\n expect(\n [...versions].sort((a, b) => a - b),\n \"appliedVersions converges on a single (component, version) row\"\n ).toEqual([1]);\n },\n opts.timeout\n );\n });\n}\n",
|
|
11
|
+
"/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { describe, expect, it } from \"vitest\";\n\nimport { itExpectFail } from \"../../itExpectFail\";\nimport {\n createMigrationCallRecorder,\n type MigrationContractHandle,\n type MigrationRunnerContractOpts,\n} from \"../types\";\n\nexport function ensureBookkeepingIdempotentBlock<DB>(\n opts: MigrationRunnerContractOpts<DB>,\n getHandle: () => MigrationContractHandle<DB>\n): void {\n const expectFails = new Set(opts.expectedFailures ?? []);\n const itImpl = expectFails.has(\"ensureBookkeepingIdempotent\") ? itExpectFail : it;\n\n describe(\"Ensure bookkeeping idempotent\", () => {\n itImpl(\n \"calling ensureBookkeepingTable twice does not throw\",\n async () => {\n const runner = await getHandle().createRunner();\n await runner.ensureBookkeepingTable();\n await expect(runner.ensureBookkeepingTable()).resolves.toBeUndefined();\n },\n opts.timeout\n );\n\n itImpl(\n \"appliedVersions on a never-touched component returns an empty set\",\n async () => {\n const runner = await getHandle().createRunner();\n await runner.ensureBookkeepingTable();\n const seen = await runner.appliedVersions(`never-touched-${Date.now()}`);\n expect(seen.size, \"untouched component has no applied versions\").toBe(0);\n },\n opts.timeout\n );\n\n itImpl(\n \"ensureBookkeepingTable does not contaminate sibling components\",\n async () => {\n const handle = getHandle();\n const runner = await handle.createRunner();\n const recorder = createMigrationCallRecorder();\n const suffix = Date.now();\n const owner = `contract-bookkeeping-owner-${suffix}`;\n const sibling = `contract-bookkeeping-sibling-${suffix}`;\n\n // Seed `owner` with one applied migration.\n await runner.run([handle.buildMigration(owner, 1, recorder)]);\n\n // Re-running ensureBookkeepingTable on an already-initialised store\n // must not drop, duplicate, or leak rows across components.\n await runner.ensureBookkeepingTable();\n await runner.ensureBookkeepingTable();\n\n const ownerVersions = await runner.appliedVersions(owner);\n expect(\n [...ownerVersions].sort((a, b) => a - b),\n \"ensureBookkeepingTable preserves prior applied versions for the seeded component\"\n ).toEqual([1]);\n\n const siblingVersions = await runner.appliedVersions(sibling);\n expect(\n siblingVersions.size,\n \"ensureBookkeepingTable does not leak rows into a sibling component\"\n ).toBe(0);\n },\n opts.timeout\n );\n });\n}\n",
|
|
12
|
+
"/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { describe, expect, it } from \"vitest\";\n\nimport { itExpectFail } from \"../../itExpectFail\";\nimport {\n createMigrationCallRecorder,\n type MigrationContractHandle,\n type MigrationRunnerContractOpts,\n} from \"../types\";\n\n/**\n * A migration whose `up()` creates a probe schema element and then throws\n * must leave no trace of the probe after the runner rejects. Backends\n * without atomic DDL (e.g. SQLite without an explicit transaction wrapper)\n * retain the partial schema and should mark this assertion as\n * `expectedFailures: [\"failedMigrationLeavesNoPartialSchema\"]`.\n */\nexport function failedMigrationLeavesNoPartialSchemaBlock<DB>(\n opts: MigrationRunnerContractOpts<DB>,\n getHandle: () => MigrationContractHandle<DB>\n): void {\n const expectFails = new Set(opts.expectedFailures ?? []);\n const itImpl = expectFails.has(\"failedMigrationLeavesNoPartialSchema\") ? itExpectFail : it;\n\n describe(\"Failed migration leaves no partial schema\", () => {\n itImpl(\n \"probe schema is rolled back after up() throws\",\n async () => {\n const handle = getHandle();\n const runner = await handle.createRunner();\n const recorder = createMigrationCallRecorder();\n const component = `contract-partial-schema-${Date.now()}`;\n const probeName = `_probe_${Math.random().toString(36).slice(2, 10)}`;\n const migration = handle.buildMigration(component, 1, recorder, { probeName });\n\n await expect(runner.run([migration])).rejects.toBeDefined();\n\n const exists = await handle.probeExists(probeName);\n expect(exists, \"probe schema element does not persist after up() throws\").toBe(false);\n },\n opts.timeout\n );\n });\n}\n",
|
|
13
|
+
"/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { describe, expect, it } from \"vitest\";\n\nimport { itExpectFail } from \"../../itExpectFail\";\nimport {\n createMigrationCallRecorder,\n type MigrationContractHandle,\n type MigrationRunnerContractOpts,\n} from \"../types\";\n\nexport function failedMigrationNotRecordedBlock<DB>(\n opts: MigrationRunnerContractOpts<DB>,\n getHandle: () => MigrationContractHandle<DB>\n): void {\n const expectFails = new Set(opts.expectedFailures ?? []);\n const itImpl = expectFails.has(\"failedMigrationNotRecorded\") ? itExpectFail : it;\n\n describe(\"Failed migration not recorded\", () => {\n itImpl(\n \"when up() throws, the failed migration's version is not recorded and a re-run with a healthy version applies it\",\n async () => {\n const handle = getHandle();\n const runner = await handle.createRunner();\n const recorder = createMigrationCallRecorder();\n const component = `contract-failure-${Date.now()}`;\n\n // First migration succeeds; second migration is configured to throw.\n const v1 = handle.buildMigration(component, 1, recorder);\n const v2Failing = handle.buildMigration(component, 2, recorder, { fail: true });\n\n await expect(runner.run([v1, v2Failing])).rejects.toThrow();\n\n // The failed version MUST NOT be recorded (otherwise retry is\n // impossible). Whether v1 is recorded depends on per-migration\n // versus per-batch atomicity — Postgres commits each migration\n // independently, while IndexedDB rolls back the entire upgrade\n // transaction. Both behaviors are spec-compliant; the contract\n // only requires that the failed version stays absent.\n const afterFailure = await runner.appliedVersions(component);\n expect(afterFailure.has(2), \"failed migration is not recorded as applied\").toBe(false);\n\n // Re-running with a non-failing v2 must converge to the full\n // applied set [1, 2] regardless of which atomicity style the\n // adapter uses.\n recorder.clear();\n const v2Recovered = handle.buildMigration(component, 2, recorder);\n const applied = await runner.run([v1, v2Recovered]);\n expect(\n applied.length,\n \"recovery run applies at least one migration\"\n ).toBeGreaterThanOrEqual(1);\n expect(\n applied.some((m) => m.version === 2),\n \"recovery run applies v2\"\n ).toBe(true);\n\n // Recorder converges with `applied` regardless of atomicity style:\n // - Per-migration-atomic (Postgres / SQLite) — v1 was already\n // committed, only v2 re-runs: applied = [v2], recorder = [v2].\n // - Per-batch-atomic (IndexedDB rolls back the whole upgrade)\n // — both re-run: applied = [v1, v2], recorder = [v1, v2].\n // Either way, every entry the runner reports as applied this call\n // corresponds to exactly one `up()` invocation. A future regression\n // that double-invokes `up()` would break the strict-equality check.\n expect(\n recorder.calls.length,\n \"recovery run invokes up() at least once\"\n ).toBeGreaterThanOrEqual(1);\n expect(\n recorder.calls.some((c) => c.component === component && c.version === 2),\n \"recovery run invokes up() for v2\"\n ).toBe(true);\n expect(\n recorder.calls.length,\n \"recovery up() invocations match `applied` exactly (no double-invocation)\"\n ).toBe(applied.length);\n\n const finalVersions = await runner.appliedVersions(component);\n expect(\n [...finalVersions].sort((a, b) => a - b),\n \"after recovery the bookkeeping table reports the full set\"\n ).toEqual([1, 2]);\n },\n opts.timeout\n );\n });\n}\n",
|
|
14
|
+
"/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { describe, expect, it } from \"vitest\";\n\nimport { itExpectFail } from \"../../itExpectFail\";\nimport {\n createMigrationCallRecorder,\n type MigrationContractHandle,\n type MigrationRunnerContractOpts,\n} from \"../types\";\n\nexport function idempotentRunBlock<DB>(\n opts: MigrationRunnerContractOpts<DB>,\n getHandle: () => MigrationContractHandle<DB>\n): void {\n const expectFails = new Set(opts.expectedFailures ?? []);\n const itImpl = expectFails.has(\"idempotentRun\") ? itExpectFail : it;\n\n describe(\"Idempotent run\", () => {\n itImpl(\n \"second run with the same migration set applies nothing and re-invokes no up()\",\n async () => {\n const handle = getHandle();\n const runner = await handle.createRunner();\n const recorder = createMigrationCallRecorder();\n const component = `contract-idempotent-${Date.now()}`;\n\n // Build a fresh batch for each run so the assertion exercises the\n // bookkeeping lookup, not migration-object identity. A runner that\n // (incorrectly) cached \"already applied\" by object identity would\n // pass with reused references but fail here.\n const firstBatch = [\n handle.buildMigration(component, 1, recorder),\n handle.buildMigration(component, 2, recorder),\n ];\n const firstApplied = await runner.run(firstBatch);\n expect(firstApplied.length, \"first run applies all pending migrations\").toBe(2);\n expect(recorder.calls.length, \"first run invokes up() for each pending migration\").toBe(2);\n\n recorder.clear();\n const secondBatch = [\n handle.buildMigration(component, 1, recorder),\n handle.buildMigration(component, 2, recorder),\n ];\n const secondApplied = await runner.run(secondBatch);\n expect(secondApplied.length, \"second run skips already-applied migrations\").toBe(0);\n expect(recorder.calls.length, \"second run invokes no up() callbacks\").toBe(0);\n\n // Bookkeeping table still reports both versions.\n const recordedVersions = await runner.appliedVersions(component);\n expect(\n [...recordedVersions].sort((a, b) => a - b),\n \"appliedVersions persists across the no-op rerun\"\n ).toEqual([1, 2]);\n },\n opts.timeout\n );\n });\n}\n",
|
|
15
|
+
"/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { describe, expect, it } from \"vitest\";\n\nimport { itExpectFail } from \"../../itExpectFail\";\nimport {\n createMigrationCallRecorder,\n type MigrationContractHandle,\n type MigrationRunnerContractOpts,\n} from \"../types\";\n\nexport function incrementalApplicationBlock<DB>(\n opts: MigrationRunnerContractOpts<DB>,\n getHandle: () => MigrationContractHandle<DB>\n): void {\n const expectFails = new Set(opts.expectedFailures ?? []);\n const itImpl = expectFails.has(\"incrementalApplication\") ? itExpectFail : it;\n\n describe(\"Incremental application\", () => {\n itImpl(\n \"adding a new version to an already-migrated component runs only the new version\",\n async () => {\n const handle = getHandle();\n const runner = await handle.createRunner();\n const recorder = createMigrationCallRecorder();\n const component = `contract-incremental-${Date.now()}`;\n\n await runner.run([\n handle.buildMigration(component, 1, recorder),\n handle.buildMigration(component, 2, recorder),\n ]);\n expect(recorder.calls.length, \"seed run applies v1 and v2\").toBe(2);\n\n recorder.clear();\n const applied = await runner.run([\n handle.buildMigration(component, 1, recorder),\n handle.buildMigration(component, 2, recorder),\n handle.buildMigration(component, 3, recorder),\n ]);\n\n expect(applied.length, \"only the newly-added version is applied\").toBe(1);\n expect(applied[0].version, \"the applied version is v3\").toBe(3);\n expect(recorder.calls.length, \"up() runs once for the incremental version\").toBe(1);\n expect(recorder.calls[0].version, \"the up() that ran was v3\").toBe(3);\n\n const recordedVersions = await runner.appliedVersions(component);\n expect(\n [...recordedVersions].sort((a, b) => a - b),\n \"all three versions are recorded after the incremental run\"\n ).toEqual([1, 2, 3]);\n },\n opts.timeout\n );\n\n itImpl(\n \"appliedVersions returns numerically-sorted versions even past version 9\",\n async () => {\n const handle = getHandle();\n const runner = await handle.createRunner();\n const recorder = createMigrationCallRecorder();\n const component = `contract-numeric-sort-${Date.now()}`;\n\n // Interleaved input — exercises both `sortMigrations` (which must\n // order numerically across the 1↔10 boundary) and the contract's\n // own assertion sort (which previously used the default\n // lexicographic comparator and silently allowed [1, 10, 2]).\n const applied = await runner.run([\n handle.buildMigration(component, 10, recorder),\n handle.buildMigration(component, 1, recorder),\n handle.buildMigration(component, 2, recorder),\n ]);\n\n expect(\n applied.map((m) => m.version),\n \"applied[] is sorted numerically across the 1↔10 boundary\"\n ).toEqual([1, 2, 10]);\n expect(\n recorder.calls.map((c) => c.version),\n \"up() invocation order is sorted numerically\"\n ).toEqual([1, 2, 10]);\n const recordedVersions = await runner.appliedVersions(component);\n expect(\n [...recordedVersions].sort((a, b) => a - b),\n \"appliedVersions enumerates all three versions\"\n ).toEqual([1, 2, 10]);\n },\n opts.timeout\n );\n });\n}\n"
|
|
16
|
+
],
|
|
17
|
+
"mappings": "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",
|
|
18
|
+
"debugId": "0F07C135E2F03E9264756E2164756E21",
|
|
19
|
+
"names": []
|
|
20
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Steven Roussey <sroussey@gmail.com>
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import type { TabularMigrationContractHandle, TabularMigrationContractOpts } from "../types";
|
|
7
|
+
export declare function addAndDropIndexBlock(_opts: TabularMigrationContractOpts, getHandle: () => TabularMigrationContractHandle): void;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Steven Roussey <sroussey@gmail.com>
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import type { TabularMigrationContractHandle, TabularMigrationContractOpts } from "../types";
|
|
7
|
+
export declare function addColumnBlock(_opts: TabularMigrationContractOpts, getHandle: () => TabularMigrationContractHandle): void;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Steven Roussey <sroussey@gmail.com>
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import type { TabularMigrationContractHandle, TabularMigrationContractOpts } from "../types";
|
|
7
|
+
export declare function backfillBlock(_opts: TabularMigrationContractOpts, getHandle: () => TabularMigrationContractHandle): void;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Steven Roussey <sroussey@gmail.com>
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import type { TabularMigrationContractHandle, TabularMigrationContractOpts } from "../types";
|
|
7
|
+
export declare function dropColumnBlock(_opts: TabularMigrationContractOpts, getHandle: () => TabularMigrationContractHandle): void;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Steven Roussey <sroussey@gmail.com>
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import type { TabularMigrationContractHandle, TabularMigrationContractOpts } from "../types";
|
|
7
|
+
export declare function failedMigrationNotRecordedBlock(_opts: TabularMigrationContractOpts, getHandle: () => TabularMigrationContractHandle): void;
|