deepline 0.3.201 → 0.3.203
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/dist/bundling-sources/sdk/src/client.ts +62 -1
- package/dist/bundling-sources/sdk/src/http.ts +55 -6
- package/dist/bundling-sources/sdk/src/index.ts +1 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/types.ts +8 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +4 -2
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +460 -96
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +11 -1
- package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-invocation-fence.ts +6 -2
- package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +401 -2
- package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +458 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-no-progress.ts +40 -0
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +25 -3
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +119 -171
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-work-counters.ts +37 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +238 -204
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +16 -7
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +136 -25
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +21 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +64 -15
- package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +5 -5
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/index.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/result-receipt-policy.ts +46 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +14 -5
- package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +5 -1
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +3 -9
- package/dist/bundling-sources/shared_libs/product-notifications/catalog.ts +1 -1
- package/dist/cli/index.js +362 -55
- package/dist/cli/index.mjs +365 -57
- package/dist/index.d.mts +129 -1
- package/dist/index.d.ts +129 -1
- package/dist/index.js +64 -7
- package/dist/index.mjs +64 -7
- package/dist/install-integrity.json +3 -0
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -1
- package/dist/release.mjs +1 -1
- package/package.json +1 -1
|
@@ -782,6 +782,8 @@ export interface ContextOptions {
|
|
|
782
782
|
runtimeTestFaultHeader?: string | null;
|
|
783
783
|
/** Runner-owned graceful deadline: stop new rows without aborting commits. */
|
|
784
784
|
rowAdmissionSignal?: AbortSignal;
|
|
785
|
+
/** Local runner cancellation; never serialized into a Play artifact. */
|
|
786
|
+
executionSignal?: AbortSignal;
|
|
785
787
|
/**
|
|
786
788
|
* Runtime-sheet transport selected by the runner. Daytona sandboxes use the
|
|
787
789
|
* execution gateway and must never fall back to minting direct DB sessions.
|
|
@@ -823,7 +825,8 @@ export interface ContextOptions {
|
|
|
823
825
|
* Run-level cache policy.
|
|
824
826
|
* forceStepRefresh bypasses completed ctx.step receipts while preserving
|
|
825
827
|
* completed provider-call receipts.
|
|
826
|
-
* forceToolRefresh
|
|
828
|
+
* forceToolRefresh refreshes completed ctx.tools.execute receipts from prior
|
|
829
|
+
* runs; a resumed attempt of the current run reuses its completed receipts.
|
|
827
830
|
* forceFailedToolRefresh reclaims only failed receipts, preserving completed
|
|
828
831
|
* provider-call idempotency while allowing forced repair runs to make progress.
|
|
829
832
|
*/
|
|
@@ -1168,6 +1171,13 @@ export interface PlayCheckpoint {
|
|
|
1168
1171
|
* staleAfterSeconds receipt buckets use this value across durable resumes.
|
|
1169
1172
|
*/
|
|
1170
1173
|
durableCallCacheEpochMs?: number;
|
|
1174
|
+
/** Retry identity for a provider-owned 429 while its map row is suspended. */
|
|
1175
|
+
providerRetryCursors?: Record<
|
|
1176
|
+
string,
|
|
1177
|
+
{ invocationAttempt: number; retryFromInvocationAttempt: number }
|
|
1178
|
+
>;
|
|
1179
|
+
/** First provider hold per credential scope, retained across durable resumes. */
|
|
1180
|
+
providerNoProgressStartedAtMsByScope?: Record<string, number>;
|
|
1171
1181
|
/** Waterfall batches that have completed: key = `${toolName}:${provider}`, value = results array. */
|
|
1172
1182
|
completedBatches: Record<string, BatchResult[]>;
|
|
1173
1183
|
/** Tool call batches that have completed: key = toolId, value = sparse row-cache-key -> result map. */
|
package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-invocation-fence.ts
CHANGED
|
@@ -333,7 +333,9 @@ export async function executeWithDurableInvocationFence(input: {
|
|
|
333
333
|
});
|
|
334
334
|
} catch (error) {
|
|
335
335
|
diagnose('completion_failed', error);
|
|
336
|
-
|
|
336
|
+
// A lost database reply does not mean the response commit failed. Read
|
|
337
|
+
// the exact winner even for billed tools before failing closed; this
|
|
338
|
+
// never repeats the provider side effect.
|
|
337
339
|
try {
|
|
338
340
|
const winner = await input.store.get({
|
|
339
341
|
invocationKey: input.invocationKey,
|
|
@@ -352,8 +354,10 @@ export async function executeWithDurableInvocationFence(input: {
|
|
|
352
354
|
}
|
|
353
355
|
}
|
|
354
356
|
} catch {
|
|
355
|
-
//
|
|
357
|
+
// Recovery is best effort. The original commit error remains the
|
|
358
|
+
// authority when the winner cannot be proved.
|
|
356
359
|
}
|
|
360
|
+
if (!input.replaySafeAfterDispatch) throw error;
|
|
357
361
|
return await retryReplaySafeInvocation(begun);
|
|
358
362
|
}
|
|
359
363
|
if (!completed) {
|
|
@@ -4,6 +4,7 @@ export const FIXTURE_BEHAVIOR_REPLAY_VERSION = 3 as const;
|
|
|
4
4
|
export const MAX_FIXTURE_RESPONSE_DELAY_SAMPLES = 256;
|
|
5
5
|
export const MAX_FIXTURE_RESPONSE_DELAY_MS = 8 * 60_000;
|
|
6
6
|
export const MAX_FIXTURE_RESPONSE_ERROR_MESSAGE_LENGTH = 500;
|
|
7
|
+
export const MAX_FIXTURE_RETRY_AFTER_MS = 60 * 60_000;
|
|
7
8
|
|
|
8
9
|
export type FixtureBehaviorV1 = {
|
|
9
10
|
version: typeof FIXTURE_BEHAVIOR_VERSION;
|
|
@@ -19,12 +20,42 @@ export type FixtureResponseSample = {
|
|
|
19
20
|
httpError?: {
|
|
20
21
|
status: number;
|
|
21
22
|
message: string;
|
|
23
|
+
/** Omitted preserves the historical one-second 429 response. */
|
|
24
|
+
retryAfterMs?: number;
|
|
25
|
+
retryAfterFormat?: 'delta_seconds' | 'http_date';
|
|
26
|
+
/** Omitted preserves a permanent configured HTTP error. */
|
|
27
|
+
failuresBeforeSuccess?: number;
|
|
22
28
|
};
|
|
23
29
|
};
|
|
24
30
|
|
|
25
31
|
export type FixtureBehaviorV2 = {
|
|
26
32
|
version: typeof FIXTURE_BEHAVIOR_RESPONSE_VERSION;
|
|
27
33
|
responseSamples: FixtureResponseSample[];
|
|
34
|
+
transient429?: FixtureTransient429;
|
|
35
|
+
sharedRateLimit?: FixtureSharedRateLimit;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Synthetic provider quotas shared by all calls in the fixture workspace. */
|
|
39
|
+
export type FixtureSharedRateLimit = {
|
|
40
|
+
requestsPerSecond?: number;
|
|
41
|
+
requestsPerMinute?: number;
|
|
42
|
+
requestsPerHour?: number;
|
|
43
|
+
/** Test-only target for the next hourly boundary, anchored by the first DB quota attempt. */
|
|
44
|
+
requestsPerHourBoundaryAfterMs?: number;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const FIXTURE_RATE_WINDOWS = [
|
|
48
|
+
{ field: 'requestsPerSecond', name: 'rps', windowMs: 1_000 },
|
|
49
|
+
{ field: 'requestsPerMinute', name: 'rpm', windowMs: 60_000 },
|
|
50
|
+
{ field: 'requestsPerHour', name: 'rph', windowMs: 3_600_000 },
|
|
51
|
+
] as const;
|
|
52
|
+
|
|
53
|
+
export type FixtureTransient429 = {
|
|
54
|
+
/** Select one of every N stable tool-call identities. */
|
|
55
|
+
everyNthCall: number;
|
|
56
|
+
retryAfterMs: number;
|
|
57
|
+
failuresBeforeSuccess: number;
|
|
58
|
+
when?: { provider?: string; operation?: string };
|
|
28
59
|
};
|
|
29
60
|
|
|
30
61
|
export type FixtureReplayBundle = {
|
|
@@ -37,6 +68,8 @@ export type FixtureBehaviorV3 = {
|
|
|
37
68
|
version: typeof FIXTURE_BEHAVIOR_REPLAY_VERSION;
|
|
38
69
|
responseSamples: FixtureResponseSample[];
|
|
39
70
|
replayBundle: FixtureReplayBundle;
|
|
71
|
+
transient429?: FixtureTransient429;
|
|
72
|
+
sharedRateLimit?: FixtureSharedRateLimit;
|
|
40
73
|
};
|
|
41
74
|
|
|
42
75
|
export type FixtureBehavior =
|
|
@@ -79,6 +112,8 @@ export function validateFixtureBehavior(
|
|
|
79
112
|
? new Set([
|
|
80
113
|
'version',
|
|
81
114
|
'responseSamples',
|
|
115
|
+
'transient429',
|
|
116
|
+
'sharedRateLimit',
|
|
82
117
|
...(record.version === FIXTURE_BEHAVIOR_REPLAY_VERSION
|
|
83
118
|
? ['replayBundle']
|
|
84
119
|
: []),
|
|
@@ -221,7 +256,12 @@ export function validateFixtureBehavior(
|
|
|
221
256
|
}
|
|
222
257
|
const error = sample.httpError as Record<string, unknown>;
|
|
223
258
|
const errorUnknownKeys = Object.keys(error).filter(
|
|
224
|
-
(key) =>
|
|
259
|
+
(key) =>
|
|
260
|
+
key !== 'status' &&
|
|
261
|
+
key !== 'message' &&
|
|
262
|
+
key !== 'retryAfterMs' &&
|
|
263
|
+
key !== 'retryAfterFormat' &&
|
|
264
|
+
key !== 'failuresBeforeSuccess',
|
|
225
265
|
);
|
|
226
266
|
if (errorUnknownKeys.length > 0) {
|
|
227
267
|
return {
|
|
@@ -256,7 +296,66 @@ export function validateFixtureBehavior(
|
|
|
256
296
|
`must contain 1-${MAX_FIXTURE_RESPONSE_ERROR_MESSAGE_LENGTH} characters.`,
|
|
257
297
|
};
|
|
258
298
|
}
|
|
259
|
-
|
|
299
|
+
if (
|
|
300
|
+
error.retryAfterMs !== undefined &&
|
|
301
|
+
(error.status !== 429 ||
|
|
302
|
+
typeof error.retryAfterMs !== 'number' ||
|
|
303
|
+
!Number.isSafeInteger(error.retryAfterMs) ||
|
|
304
|
+
error.retryAfterMs < 1 ||
|
|
305
|
+
error.retryAfterMs > MAX_FIXTURE_RETRY_AFTER_MS)
|
|
306
|
+
) {
|
|
307
|
+
return {
|
|
308
|
+
ok: false,
|
|
309
|
+
error:
|
|
310
|
+
`fixtureBehavior.responseSamples[${index}].httpError.retryAfterMs ` +
|
|
311
|
+
`requires status 429 and an integer from 1 through ${MAX_FIXTURE_RETRY_AFTER_MS}.`,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
if (
|
|
315
|
+
error.retryAfterFormat !== undefined &&
|
|
316
|
+
(error.status !== 429 ||
|
|
317
|
+
(error.retryAfterFormat !== 'delta_seconds' &&
|
|
318
|
+
error.retryAfterFormat !== 'http_date'))
|
|
319
|
+
) {
|
|
320
|
+
return {
|
|
321
|
+
ok: false,
|
|
322
|
+
error:
|
|
323
|
+
`fixtureBehavior.responseSamples[${index}].httpError.retryAfterFormat ` +
|
|
324
|
+
'requires status 429 and delta_seconds or http_date.',
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
if (
|
|
328
|
+
error.failuresBeforeSuccess !== undefined &&
|
|
329
|
+
(error.status !== 429 ||
|
|
330
|
+
typeof error.failuresBeforeSuccess !== 'number' ||
|
|
331
|
+
!Number.isSafeInteger(error.failuresBeforeSuccess) ||
|
|
332
|
+
error.failuresBeforeSuccess < 1 ||
|
|
333
|
+
error.failuresBeforeSuccess > 20)
|
|
334
|
+
) {
|
|
335
|
+
return {
|
|
336
|
+
ok: false,
|
|
337
|
+
error:
|
|
338
|
+
`fixtureBehavior.responseSamples[${index}].httpError.failuresBeforeSuccess ` +
|
|
339
|
+
'requires status 429 and an integer from 1 through 20.',
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
httpError = {
|
|
343
|
+
status: error.status,
|
|
344
|
+
message: error.message.trim(),
|
|
345
|
+
...(error.retryAfterMs !== undefined
|
|
346
|
+
? { retryAfterMs: error.retryAfterMs as number }
|
|
347
|
+
: {}),
|
|
348
|
+
...(error.retryAfterFormat !== undefined
|
|
349
|
+
? {
|
|
350
|
+
retryAfterFormat: error.retryAfterFormat as
|
|
351
|
+
| 'delta_seconds'
|
|
352
|
+
| 'http_date',
|
|
353
|
+
}
|
|
354
|
+
: {}),
|
|
355
|
+
...(error.failuresBeforeSuccess !== undefined
|
|
356
|
+
? { failuresBeforeSuccess: error.failuresBeforeSuccess as number }
|
|
357
|
+
: {}),
|
|
358
|
+
};
|
|
260
359
|
}
|
|
261
360
|
samples.push({
|
|
262
361
|
delayMs: sample.delayMs,
|
|
@@ -264,6 +363,144 @@ export function validateFixtureBehavior(
|
|
|
264
363
|
...(httpError ? { httpError } : {}),
|
|
265
364
|
});
|
|
266
365
|
}
|
|
366
|
+
let sharedRateLimit: FixtureSharedRateLimit | undefined;
|
|
367
|
+
if (record.sharedRateLimit !== undefined) {
|
|
368
|
+
const plan = record.sharedRateLimit;
|
|
369
|
+
if (!plan || typeof plan !== 'object' || Array.isArray(plan)) {
|
|
370
|
+
return {
|
|
371
|
+
ok: false,
|
|
372
|
+
error: 'fixtureBehavior.sharedRateLimit must be an object.',
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
const planRecord = plan as Record<string, unknown>;
|
|
376
|
+
const entries = Object.entries(planRecord);
|
|
377
|
+
const boundaryAfterMs = planRecord.requestsPerHourBoundaryAfterMs;
|
|
378
|
+
const quotaEntries = entries.filter(
|
|
379
|
+
([name]) => name !== 'requestsPerHourBoundaryAfterMs',
|
|
380
|
+
);
|
|
381
|
+
if (
|
|
382
|
+
quotaEntries.length === 0 ||
|
|
383
|
+
quotaEntries.some(
|
|
384
|
+
([name, limit]) =>
|
|
385
|
+
!FIXTURE_RATE_WINDOWS.some((window) => window.field === name) ||
|
|
386
|
+
typeof limit !== 'number' ||
|
|
387
|
+
!Number.isSafeInteger(limit) ||
|
|
388
|
+
limit < 1,
|
|
389
|
+
)
|
|
390
|
+
) {
|
|
391
|
+
return {
|
|
392
|
+
ok: false,
|
|
393
|
+
error:
|
|
394
|
+
'fixtureBehavior.sharedRateLimit requires positive integer requestsPerSecond, requestsPerMinute, or requestsPerHour quotas.',
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
if (
|
|
398
|
+
boundaryAfterMs !== undefined &&
|
|
399
|
+
(planRecord.requestsPerHour === undefined ||
|
|
400
|
+
typeof boundaryAfterMs !== 'number' ||
|
|
401
|
+
!Number.isSafeInteger(boundaryAfterMs) ||
|
|
402
|
+
boundaryAfterMs < 1_000 ||
|
|
403
|
+
boundaryAfterMs > 300_000)
|
|
404
|
+
) {
|
|
405
|
+
return {
|
|
406
|
+
ok: false,
|
|
407
|
+
error:
|
|
408
|
+
'fixtureBehavior.sharedRateLimit.requestsPerHourBoundaryAfterMs requires requestsPerHour and an integer from 1000 through 300000.',
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
sharedRateLimit = Object.fromEntries(entries) as FixtureSharedRateLimit;
|
|
412
|
+
}
|
|
413
|
+
let transient429: FixtureTransient429 | undefined;
|
|
414
|
+
if (record.transient429 !== undefined) {
|
|
415
|
+
const selected = record.transient429;
|
|
416
|
+
if (
|
|
417
|
+
!selected ||
|
|
418
|
+
typeof selected !== 'object' ||
|
|
419
|
+
Array.isArray(selected)
|
|
420
|
+
) {
|
|
421
|
+
return {
|
|
422
|
+
ok: false,
|
|
423
|
+
error: 'fixtureBehavior.transient429 must be an object.',
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
const fault = selected as Record<string, unknown>;
|
|
427
|
+
const unknownFaultKeys = Object.keys(fault).filter(
|
|
428
|
+
(key) =>
|
|
429
|
+
key !== 'everyNthCall' &&
|
|
430
|
+
key !== 'retryAfterMs' &&
|
|
431
|
+
key !== 'failuresBeforeSuccess' &&
|
|
432
|
+
key !== 'when',
|
|
433
|
+
);
|
|
434
|
+
if (unknownFaultKeys.length > 0) {
|
|
435
|
+
return {
|
|
436
|
+
ok: false,
|
|
437
|
+
error: `Unsupported fixtureBehavior.transient429 field "${unknownFaultKeys[0]}".`,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
for (const [name, value, max] of [
|
|
441
|
+
['everyNthCall', fault.everyNthCall, 10_000],
|
|
442
|
+
['retryAfterMs', fault.retryAfterMs, MAX_FIXTURE_RETRY_AFTER_MS],
|
|
443
|
+
['failuresBeforeSuccess', fault.failuresBeforeSuccess, 20],
|
|
444
|
+
] as const) {
|
|
445
|
+
if (
|
|
446
|
+
typeof value !== 'number' ||
|
|
447
|
+
!Number.isSafeInteger(value) ||
|
|
448
|
+
value < 1 ||
|
|
449
|
+
value > max
|
|
450
|
+
) {
|
|
451
|
+
return {
|
|
452
|
+
ok: false,
|
|
453
|
+
error: `fixtureBehavior.transient429.${name} must be an integer from 1 through ${max}.`,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
let when: FixtureTransient429['when'];
|
|
458
|
+
if (fault.when !== undefined) {
|
|
459
|
+
if (
|
|
460
|
+
!fault.when ||
|
|
461
|
+
typeof fault.when !== 'object' ||
|
|
462
|
+
Array.isArray(fault.when)
|
|
463
|
+
) {
|
|
464
|
+
return {
|
|
465
|
+
ok: false,
|
|
466
|
+
error: 'fixtureBehavior.transient429.when must be an object.',
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
const condition = fault.when as Record<string, unknown>;
|
|
470
|
+
const unknownConditionKeys = Object.keys(condition).filter(
|
|
471
|
+
(key) => key !== 'provider' && key !== 'operation',
|
|
472
|
+
);
|
|
473
|
+
if (unknownConditionKeys.length > 0) {
|
|
474
|
+
return {
|
|
475
|
+
ok: false,
|
|
476
|
+
error: `Unsupported fixtureBehavior.transient429.when field "${unknownConditionKeys[0]}".`,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
const provider = normalizeFixtureScopeValue(condition.provider, 100);
|
|
480
|
+
const operation = normalizeFixtureScopeValue(condition.operation, 200);
|
|
481
|
+
if (
|
|
482
|
+
provider.error ||
|
|
483
|
+
operation.error ||
|
|
484
|
+
(!provider.value && !operation.value)
|
|
485
|
+
) {
|
|
486
|
+
return {
|
|
487
|
+
ok: false,
|
|
488
|
+
error:
|
|
489
|
+
'fixtureBehavior.transient429.when must select a valid provider or operation.',
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
when = {
|
|
493
|
+
...(provider.value ? { provider: provider.value } : {}),
|
|
494
|
+
...(operation.value ? { operation: operation.value } : {}),
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
transient429 = {
|
|
498
|
+
everyNthCall: fault.everyNthCall as number,
|
|
499
|
+
retryAfterMs: fault.retryAfterMs as number,
|
|
500
|
+
failuresBeforeSuccess: fault.failuresBeforeSuccess as number,
|
|
501
|
+
...(when ? { when } : {}),
|
|
502
|
+
};
|
|
503
|
+
}
|
|
267
504
|
let replayBundle: FixtureReplayBundle | undefined;
|
|
268
505
|
if (record.version === FIXTURE_BEHAVIOR_REPLAY_VERSION) {
|
|
269
506
|
if (
|
|
@@ -355,6 +592,8 @@ export function validateFixtureBehavior(
|
|
|
355
592
|
behavior: {
|
|
356
593
|
version: FIXTURE_BEHAVIOR_REPLAY_VERSION,
|
|
357
594
|
responseSamples: samples,
|
|
595
|
+
...(transient429 ? { transient429 } : {}),
|
|
596
|
+
...(sharedRateLimit ? { sharedRateLimit } : {}),
|
|
358
597
|
replayBundle: replayBundle!,
|
|
359
598
|
},
|
|
360
599
|
};
|
|
@@ -364,6 +603,8 @@ export function validateFixtureBehavior(
|
|
|
364
603
|
behavior: {
|
|
365
604
|
version: FIXTURE_BEHAVIOR_RESPONSE_VERSION,
|
|
366
605
|
responseSamples: samples,
|
|
606
|
+
...(transient429 ? { transient429 } : {}),
|
|
607
|
+
...(sharedRateLimit ? { sharedRateLimit } : {}),
|
|
367
608
|
},
|
|
368
609
|
};
|
|
369
610
|
}
|
|
@@ -472,6 +713,164 @@ function stableFixtureRequestHash(value: string): number {
|
|
|
472
713
|
return hash >>> 0;
|
|
473
714
|
}
|
|
474
715
|
|
|
716
|
+
/** Durable test-only count of injected provider 429s for one Play run. */
|
|
717
|
+
export function fixtureTransient429RunCounterKey(input: {
|
|
718
|
+
orgId: string;
|
|
719
|
+
runId: string;
|
|
720
|
+
}): string {
|
|
721
|
+
const orgId = input.orgId.trim();
|
|
722
|
+
const runId = input.runId.trim();
|
|
723
|
+
if (!orgId || !runId) {
|
|
724
|
+
throw new Error('Fixture 429 run counter requires an org and run id.');
|
|
725
|
+
}
|
|
726
|
+
return `fixture-http-error-run:${orgId}:${runId}`;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export const FIXTURE_RUN_CALL_COUNTER_SHARDS = 32;
|
|
730
|
+
|
|
731
|
+
export function fixtureShared429RunCounterKey(input: {
|
|
732
|
+
orgId: string;
|
|
733
|
+
runId: string;
|
|
734
|
+
window?: 'rps' | 'rpm' | 'rph';
|
|
735
|
+
}): string {
|
|
736
|
+
if (!input.orgId.trim() || !input.runId.trim()) {
|
|
737
|
+
throw new Error('Fixture shared-rate counter requires an org and run id.');
|
|
738
|
+
}
|
|
739
|
+
return `fixture-shared-rate-429-run:${input.orgId.trim()}:${input.runId.trim()}:${input.window ?? 'all'}`;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** The same fixed-window contract is used for seconds, minutes, and hours.
|
|
743
|
+
* Run identity is intentionally absent: concurrent runs share the provider's
|
|
744
|
+
* quota. A different configured limit is a different synthetic provider policy. */
|
|
745
|
+
export function fixtureSharedRateWindows(input: {
|
|
746
|
+
orgId: string;
|
|
747
|
+
provider: string;
|
|
748
|
+
plan: FixtureSharedRateLimit;
|
|
749
|
+
}) {
|
|
750
|
+
if (!input.orgId.trim() || !input.provider.trim()) {
|
|
751
|
+
throw new Error('Fixture shared rate windows require an org and provider.');
|
|
752
|
+
}
|
|
753
|
+
return FIXTURE_RATE_WINDOWS.flatMap(({ field, name, windowMs }) => {
|
|
754
|
+
const limit = input.plan[field];
|
|
755
|
+
return limit === undefined
|
|
756
|
+
? []
|
|
757
|
+
: [
|
|
758
|
+
{
|
|
759
|
+
name,
|
|
760
|
+
windowMs,
|
|
761
|
+
limit,
|
|
762
|
+
boundaryAfterMs:
|
|
763
|
+
name === 'rph'
|
|
764
|
+
? input.plan.requestsPerHourBoundaryAfterMs
|
|
765
|
+
: undefined,
|
|
766
|
+
key: `fixture-rate:${input.orgId.trim()}:${input.provider.trim()}:${name}:${limit}`,
|
|
767
|
+
},
|
|
768
|
+
];
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export type FixtureExecuteBoundaryCounts = {
|
|
773
|
+
started: number;
|
|
774
|
+
finished: number;
|
|
775
|
+
expectedFailures: number;
|
|
776
|
+
unexpectedFailures: number;
|
|
777
|
+
unprovenResponses: number;
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
export const FIXTURE_EXECUTE_BOUNDARY_COUNTER_KINDS = {
|
|
781
|
+
started: 'boundary_started',
|
|
782
|
+
finished: 'boundary_finished',
|
|
783
|
+
expectedFailures: 'boundary_expected_failures',
|
|
784
|
+
unexpectedFailures: 'boundary_unexpected_failures',
|
|
785
|
+
unprovenResponses: 'boundary_unproven_responses',
|
|
786
|
+
} as const;
|
|
787
|
+
|
|
788
|
+
/** Fixed-size, run-scoped test-only counters survive every scheduler resume. */
|
|
789
|
+
export function fixtureRunCallCounterKey(input: {
|
|
790
|
+
orgId: string;
|
|
791
|
+
runId: string;
|
|
792
|
+
kind:
|
|
793
|
+
| 'physical'
|
|
794
|
+
| 'logical'
|
|
795
|
+
| 'success_members'
|
|
796
|
+
| 'distinct_success_members'
|
|
797
|
+
| 'duplicate_success_members'
|
|
798
|
+
| 'unproven_successes'
|
|
799
|
+
| 'quota_attempts'
|
|
800
|
+
| 'boundary_started'
|
|
801
|
+
| 'boundary_finished'
|
|
802
|
+
| 'boundary_expected_failures'
|
|
803
|
+
| 'boundary_unexpected_failures'
|
|
804
|
+
| 'boundary_unproven_responses';
|
|
805
|
+
shard: number;
|
|
806
|
+
}): string {
|
|
807
|
+
const orgId = input.orgId.trim();
|
|
808
|
+
const runId = input.runId.trim();
|
|
809
|
+
if (
|
|
810
|
+
!orgId ||
|
|
811
|
+
!runId ||
|
|
812
|
+
!Number.isInteger(input.shard) ||
|
|
813
|
+
input.shard < 0 ||
|
|
814
|
+
input.shard >= FIXTURE_RUN_CALL_COUNTER_SHARDS
|
|
815
|
+
) {
|
|
816
|
+
throw new Error(
|
|
817
|
+
'Fixture run call counter requires an org, run, and valid shard.',
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
return `fixture-run-call-${input.kind}:${orgId}:${runId}:${input.shard}`;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/** Detail is addressable separately from the fixed-size run counter set. */
|
|
824
|
+
export function fixtureQuotaAttemptKey(input: {
|
|
825
|
+
orgId: string;
|
|
826
|
+
runId: string;
|
|
827
|
+
shard: number;
|
|
828
|
+
ordinal: number;
|
|
829
|
+
}): string {
|
|
830
|
+
if (!Number.isSafeInteger(input.ordinal) || input.ordinal < 1) {
|
|
831
|
+
throw new Error('Fixture quota attempt requires a positive safe ordinal.');
|
|
832
|
+
}
|
|
833
|
+
return `${fixtureRunCallCounterKey({ ...input, kind: 'quota_attempts' })}:attempt:${input.ordinal}`;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
export function fixtureRunCallCounterShard(
|
|
837
|
+
durableCallReceiptKey: string,
|
|
838
|
+
): number {
|
|
839
|
+
const key = durableCallReceiptKey.trim();
|
|
840
|
+
if (!key)
|
|
841
|
+
throw new Error('Fixture call counter requires a durable receipt key.');
|
|
842
|
+
return stableFixtureRequestHash(key) % FIXTURE_RUN_CALL_COUNTER_SHARDS;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
export function selectFixtureTransient429(input: {
|
|
846
|
+
behavior: FixtureBehavior;
|
|
847
|
+
stableRequestKey: string;
|
|
848
|
+
}): FixtureTransient429 | null {
|
|
849
|
+
const plan =
|
|
850
|
+
input.behavior.version === FIXTURE_BEHAVIOR_VERSION
|
|
851
|
+
? undefined
|
|
852
|
+
: input.behavior.transient429;
|
|
853
|
+
if (!plan) return null;
|
|
854
|
+
const stableRequestKey = input.stableRequestKey.trim().toLowerCase();
|
|
855
|
+
if (!stableRequestKey) {
|
|
856
|
+
throw new Error('Fixture 429 selection requires a stable request key.');
|
|
857
|
+
}
|
|
858
|
+
const [provider = '', operation = ''] = stableRequestKey.split(':', 3);
|
|
859
|
+
if (
|
|
860
|
+
(plan.when?.provider && plan.when.provider !== provider) ||
|
|
861
|
+
(plan.when?.operation && plan.when.operation !== operation)
|
|
862
|
+
) {
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
865
|
+
return stableFixtureRequestHash(
|
|
866
|
+
`fixture-transient-429-v1:${stableRequestKey}`,
|
|
867
|
+
) %
|
|
868
|
+
plan.everyNthCall ===
|
|
869
|
+
0
|
|
870
|
+
? plan
|
|
871
|
+
: null;
|
|
872
|
+
}
|
|
873
|
+
|
|
475
874
|
export function selectFixtureReplaySampleIndex(input: {
|
|
476
875
|
stableRequestKey: string;
|
|
477
876
|
sampleCount: number;
|