@rulvar/testing 1.13.0 → 1.15.0
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/index.d.ts +88 -1
- package/dist/index.js +132 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -27,6 +27,93 @@ declare function replayRun<A, R>(wf: Workflow<A, R>, args: A, options: ReplayRun
|
|
|
27
27
|
preview: ResumePreview;
|
|
28
28
|
}>;
|
|
29
29
|
//#endregion
|
|
30
|
+
//#region src/live.d.ts
|
|
31
|
+
/**
|
|
32
|
+
* True only when `RULVAR_LIVE_TESTS` is exactly `'1'` AND every named
|
|
33
|
+
* environment key is set to a non-empty value. Gate live tests as
|
|
34
|
+
* `it.skipIf(!liveTestEnabled('ANTHROPIC_API_KEY'))(...)` so an
|
|
35
|
+
* unrelated key in the shell never triggers a paid provider call from
|
|
36
|
+
* an ordinary test run.
|
|
37
|
+
*/
|
|
38
|
+
declare function liveTestEnabled(...requiredEnvKeys: string[]): boolean;
|
|
39
|
+
/** Default total `runLiveSmoke` attempts including the first. */
|
|
40
|
+
declare const DEFAULT_LIVE_SMOKE_ATTEMPTS = 3;
|
|
41
|
+
/**
|
|
42
|
+
* Hard ceiling on `runLiveSmoke` attempts. The helper's whole contract
|
|
43
|
+
* is a bounded spend, so it refuses configurations that are not.
|
|
44
|
+
*/
|
|
45
|
+
declare const MAX_LIVE_SMOKE_ATTEMPTS = 10;
|
|
46
|
+
interface RunLiveSmokeOptions {
|
|
47
|
+
/**
|
|
48
|
+
* Total attempts including the first: an integer from 1 to
|
|
49
|
+
* {@link MAX_LIVE_SMOKE_ATTEMPTS} (default 3). Anything else, NaN and
|
|
50
|
+
* Infinity included, rejects with ConfigError before any stream opens.
|
|
51
|
+
*/
|
|
52
|
+
attempts?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Backoff before retry n (1-based) is `baseDelayMs * n`: a
|
|
55
|
+
* non-negative integer (default 2000). Pass 0 to retry without
|
|
56
|
+
* sleeping (unit tests). Anything else rejects with ConfigError
|
|
57
|
+
* before any stream opens.
|
|
58
|
+
*/
|
|
59
|
+
baseDelayMs?: number;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The classified result of a bounded live smoke. `attempts` is how many
|
|
63
|
+
* streams were actually opened; only `'exhausted'` reaches the
|
|
64
|
+
* configured bound.
|
|
65
|
+
*/
|
|
66
|
+
type LiveSmokeOutcome = {
|
|
67
|
+
status: "ok";
|
|
68
|
+
attempts: number;
|
|
69
|
+
events: ChatEvent[];
|
|
70
|
+
} | {
|
|
71
|
+
status: "failed";
|
|
72
|
+
attempts: number;
|
|
73
|
+
error: WireError;
|
|
74
|
+
events: ChatEvent[];
|
|
75
|
+
} | {
|
|
76
|
+
status: "exhausted";
|
|
77
|
+
attempts: number;
|
|
78
|
+
errors: WireError[];
|
|
79
|
+
} | {
|
|
80
|
+
status: "no-terminal";
|
|
81
|
+
attempts: number;
|
|
82
|
+
events: ChatEvent[];
|
|
83
|
+
} | {
|
|
84
|
+
status: "contract-violation";
|
|
85
|
+
attempts: number;
|
|
86
|
+
reason: "multiple-terminals" | "terminal-not-final";
|
|
87
|
+
events: ChatEvent[];
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Drains `adapter.stream(req)` with a bounded retry policy and classifies
|
|
91
|
+
* the outcome instead of throwing:
|
|
92
|
+
*
|
|
93
|
+
* - `'ok'`: the stream ended on a single terminal `finish` (the events of
|
|
94
|
+
* the successful attempt are included for further assertions).
|
|
95
|
+
* - `'failed'`: a terminal error with `retryable: false`; never retried,
|
|
96
|
+
* diagnostics preserved.
|
|
97
|
+
* - `'exhausted'`: every attempt ended in a `retryable: true` error; the
|
|
98
|
+
* per-attempt errors are preserved in order.
|
|
99
|
+
* - `'no-terminal'`: the stream ended with neither `finish` nor `error`,
|
|
100
|
+
* which violates the provider SPI; never retried (spending again on a
|
|
101
|
+
* misbehaving adapter is wrong).
|
|
102
|
+
* - `'contract-violation'`: the stream carried more than one terminal
|
|
103
|
+
* event (`'multiple-terminals'`, e.g. an error followed by a finish) or
|
|
104
|
+
* its single terminal was not the final event
|
|
105
|
+
* (`'terminal-not-final'`). Equally an SPI violation, equally never
|
|
106
|
+
* retried, and never reported as a pass.
|
|
107
|
+
*
|
|
108
|
+
* Retries only ever follow a well-formed stream whose single final
|
|
109
|
+
* terminal is a typed retryable error, so a live smoke never converts a
|
|
110
|
+
* real adapter failure or a malformed stream into a pass and never
|
|
111
|
+
* spends more than `attempts` calls. Options are validated first:
|
|
112
|
+
* invalid `attempts` or `baseDelayMs` reject with ConfigError before any
|
|
113
|
+
* adapter call.
|
|
114
|
+
*/
|
|
115
|
+
declare function runLiveSmoke(adapter: Pick<ProviderAdapter, "stream">, req: ChatRequest, options?: RunLiveSmokeOptions): Promise<LiveSmokeOutcome>;
|
|
116
|
+
//#endregion
|
|
30
117
|
//#region src/cassettes/build-fixtures.d.ts
|
|
31
118
|
/** One cassette fixture file: id, provenance note, and the journal. */
|
|
32
119
|
/** @internal */
|
|
@@ -152,4 +239,4 @@ declare function replay(options: {
|
|
|
152
239
|
adapters?: ProviderAdapter[];
|
|
153
240
|
}): ProviderAdapter[];
|
|
154
241
|
//#endregion
|
|
155
|
-
export { type CassetteFixture, type CreateTestEngineOptions, FAKE_MODEL, FAKE_MODEL_REF, FakeAdapter, type FakeAdapterOptions, type FakeCall, type FakeResponder, type FakeToolCallsValue, type FakeWireErrorValue, M6_ORCH_GOAL, M6_ORCH_PROFILES, M6_ORCH_RUN_ID, RedactFn, type ReplayRunOptions, type TestEngine, type TestRunHandle, VcrCassette, VcrMissError, VcrRow, buildFrozenV1JournalRaw, buildM2CassetteFixtures, buildV2GoldenIdentity, createTestEngine, defaultRedact, fakeToolCalls, fakeWireError, handlesInRequest, normalizeM6Entries, readCassette, record, recordLiveCassettes, recordOrchestratorCrash, replay, replayRun, requestHash };
|
|
242
|
+
export { type CassetteFixture, type CreateTestEngineOptions, DEFAULT_LIVE_SMOKE_ATTEMPTS, FAKE_MODEL, FAKE_MODEL_REF, FakeAdapter, type FakeAdapterOptions, type FakeCall, type FakeResponder, type FakeToolCallsValue, type FakeWireErrorValue, type LiveSmokeOutcome, M6_ORCH_GOAL, M6_ORCH_PROFILES, M6_ORCH_RUN_ID, MAX_LIVE_SMOKE_ATTEMPTS, RedactFn, type ReplayRunOptions, type RunLiveSmokeOptions, type TestEngine, type TestRunHandle, VcrCassette, VcrMissError, VcrRow, buildFrozenV1JournalRaw, buildM2CassetteFixtures, buildV2GoldenIdentity, createTestEngine, defaultRedact, fakeToolCalls, fakeWireError, handlesInRequest, liveTestEnabled, normalizeM6Entries, readCassette, record, recordLiveCassettes, recordOrchestratorCrash, replay, replayRun, requestHash, runLiveSmoke };
|
package/dist/index.js
CHANGED
|
@@ -353,6 +353,137 @@ async function replayRun(wf, args, options) {
|
|
|
353
353
|
};
|
|
354
354
|
}
|
|
355
355
|
//#endregion
|
|
356
|
+
//#region src/live.ts
|
|
357
|
+
/**
|
|
358
|
+
* Live-test opt-in gate and the bounded live smoke.
|
|
359
|
+
*
|
|
360
|
+
* A provider key in the environment is not an opt-in: key-gated live
|
|
361
|
+
* tests spend provider budget, so they additionally require the explicit
|
|
362
|
+
* RULVAR_LIVE_TESTS=1 switch (the repository's `pnpm test:live` sets it
|
|
363
|
+
* for its child run only). runLiveSmoke drains one adapter stream per
|
|
364
|
+
* attempt and classifies the terminal event: a typed retryable error
|
|
365
|
+
* (429 rate limit, 529 overload, transport) is retried a bounded number
|
|
366
|
+
* of times with linear backoff; a non-retryable error (authentication,
|
|
367
|
+
* invalid model, invalid request) fails immediately with the typed
|
|
368
|
+
* WireError intact. The provider SPI requires exactly one terminal event
|
|
369
|
+
* per stream, as its final event: a stream with no terminal is
|
|
370
|
+
* `'no-terminal'`, one with multiple terminals or a terminal followed by
|
|
371
|
+
* more events is `'contract-violation'`, and neither is ever retried
|
|
372
|
+
* (spending again cannot repair a broken adapter contract). A stream
|
|
373
|
+
* that THROWS propagates unchanged: adapters surface failures as typed
|
|
374
|
+
* error events, so a raw throw is itself a contract violation the caller
|
|
375
|
+
* must see. Options are validated before any stream is opened; invalid
|
|
376
|
+
* values reject with ConfigError instead of being clamped or defaulted.
|
|
377
|
+
*/
|
|
378
|
+
/**
|
|
379
|
+
* True only when `RULVAR_LIVE_TESTS` is exactly `'1'` AND every named
|
|
380
|
+
* environment key is set to a non-empty value. Gate live tests as
|
|
381
|
+
* `it.skipIf(!liveTestEnabled('ANTHROPIC_API_KEY'))(...)` so an
|
|
382
|
+
* unrelated key in the shell never triggers a paid provider call from
|
|
383
|
+
* an ordinary test run.
|
|
384
|
+
*/
|
|
385
|
+
function liveTestEnabled(...requiredEnvKeys) {
|
|
386
|
+
if (process.env.RULVAR_LIVE_TESTS !== "1") return false;
|
|
387
|
+
return requiredEnvKeys.every((key) => {
|
|
388
|
+
const value = process.env[key];
|
|
389
|
+
return value !== void 0 && value !== "";
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
/** Default total `runLiveSmoke` attempts including the first. */
|
|
393
|
+
const DEFAULT_LIVE_SMOKE_ATTEMPTS = 3;
|
|
394
|
+
/**
|
|
395
|
+
* Hard ceiling on `runLiveSmoke` attempts. The helper's whole contract
|
|
396
|
+
* is a bounded spend, so it refuses configurations that are not.
|
|
397
|
+
*/
|
|
398
|
+
const MAX_LIVE_SMOKE_ATTEMPTS = 10;
|
|
399
|
+
/**
|
|
400
|
+
* Drains `adapter.stream(req)` with a bounded retry policy and classifies
|
|
401
|
+
* the outcome instead of throwing:
|
|
402
|
+
*
|
|
403
|
+
* - `'ok'`: the stream ended on a single terminal `finish` (the events of
|
|
404
|
+
* the successful attempt are included for further assertions).
|
|
405
|
+
* - `'failed'`: a terminal error with `retryable: false`; never retried,
|
|
406
|
+
* diagnostics preserved.
|
|
407
|
+
* - `'exhausted'`: every attempt ended in a `retryable: true` error; the
|
|
408
|
+
* per-attempt errors are preserved in order.
|
|
409
|
+
* - `'no-terminal'`: the stream ended with neither `finish` nor `error`,
|
|
410
|
+
* which violates the provider SPI; never retried (spending again on a
|
|
411
|
+
* misbehaving adapter is wrong).
|
|
412
|
+
* - `'contract-violation'`: the stream carried more than one terminal
|
|
413
|
+
* event (`'multiple-terminals'`, e.g. an error followed by a finish) or
|
|
414
|
+
* its single terminal was not the final event
|
|
415
|
+
* (`'terminal-not-final'`). Equally an SPI violation, equally never
|
|
416
|
+
* retried, and never reported as a pass.
|
|
417
|
+
*
|
|
418
|
+
* Retries only ever follow a well-formed stream whose single final
|
|
419
|
+
* terminal is a typed retryable error, so a live smoke never converts a
|
|
420
|
+
* real adapter failure or a malformed stream into a pass and never
|
|
421
|
+
* spends more than `attempts` calls. Options are validated first:
|
|
422
|
+
* invalid `attempts` or `baseDelayMs` reject with ConfigError before any
|
|
423
|
+
* adapter call.
|
|
424
|
+
*/
|
|
425
|
+
async function runLiveSmoke(adapter, req, options) {
|
|
426
|
+
const attempts = validatedAttempts(options?.attempts);
|
|
427
|
+
const baseDelayMs = validatedBaseDelayMs(options?.baseDelayMs);
|
|
428
|
+
const retryableErrors = [];
|
|
429
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
430
|
+
const events = [];
|
|
431
|
+
for await (const event of adapter.stream(req)) events.push(event);
|
|
432
|
+
const terminals = events.filter((event) => event.type === "finish" || event.type === "error");
|
|
433
|
+
const terminal = terminals[0];
|
|
434
|
+
if (terminal === void 0) return {
|
|
435
|
+
status: "no-terminal",
|
|
436
|
+
attempts: attempt,
|
|
437
|
+
events
|
|
438
|
+
};
|
|
439
|
+
if (terminals.length > 1) return {
|
|
440
|
+
status: "contract-violation",
|
|
441
|
+
attempts: attempt,
|
|
442
|
+
reason: "multiple-terminals",
|
|
443
|
+
events
|
|
444
|
+
};
|
|
445
|
+
if (terminal !== events.at(-1)) return {
|
|
446
|
+
status: "contract-violation",
|
|
447
|
+
attempts: attempt,
|
|
448
|
+
reason: "terminal-not-final",
|
|
449
|
+
events
|
|
450
|
+
};
|
|
451
|
+
if (terminal.type === "finish") return {
|
|
452
|
+
status: "ok",
|
|
453
|
+
attempts: attempt,
|
|
454
|
+
events
|
|
455
|
+
};
|
|
456
|
+
if (!terminal.error.retryable) return {
|
|
457
|
+
status: "failed",
|
|
458
|
+
attempts: attempt,
|
|
459
|
+
error: terminal.error,
|
|
460
|
+
events
|
|
461
|
+
};
|
|
462
|
+
retryableErrors.push(terminal.error);
|
|
463
|
+
if (attempt < attempts && baseDelayMs > 0) await delay(baseDelayMs * attempt);
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
status: "exhausted",
|
|
467
|
+
attempts,
|
|
468
|
+
errors: retryableErrors
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function validatedAttempts(value) {
|
|
472
|
+
if (value === void 0) return 3;
|
|
473
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 10) throw new ConfigError(`runLiveSmoke attempts must be an integer from 1 to 10, got ${String(value)}`);
|
|
474
|
+
return value;
|
|
475
|
+
}
|
|
476
|
+
function validatedBaseDelayMs(value) {
|
|
477
|
+
if (value === void 0) return 2e3;
|
|
478
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new ConfigError(`runLiveSmoke baseDelayMs must be a non-negative integer, got ${String(value)}`);
|
|
479
|
+
return value;
|
|
480
|
+
}
|
|
481
|
+
function delay(ms) {
|
|
482
|
+
return new Promise((resolve) => {
|
|
483
|
+
setTimeout(resolve, ms);
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
//#endregion
|
|
356
487
|
//#region src/cassettes/build-fixtures.ts
|
|
357
488
|
/**
|
|
358
489
|
* M2 cassette and frozen-fixture builders (M2-T12). Fixtures are
|
|
@@ -2017,4 +2148,4 @@ function replay(options) {
|
|
|
2017
2148
|
});
|
|
2018
2149
|
}
|
|
2019
2150
|
//#endregion
|
|
2020
|
-
export { FAKE_MODEL, FAKE_MODEL_REF, FakeAdapter, M6_ORCH_GOAL, M6_ORCH_PROFILES, M6_ORCH_RUN_ID, VcrMissError, buildFrozenV1JournalRaw, buildM2CassetteFixtures, buildV2GoldenIdentity, createTestEngine, defaultRedact, fakeToolCalls, fakeWireError, handlesInRequest, normalizeM6Entries, readCassette, record, recordLiveCassettes, recordOrchestratorCrash, replay, replayRun, requestHash };
|
|
2151
|
+
export { DEFAULT_LIVE_SMOKE_ATTEMPTS, FAKE_MODEL, FAKE_MODEL_REF, FakeAdapter, M6_ORCH_GOAL, M6_ORCH_PROFILES, M6_ORCH_RUN_ID, MAX_LIVE_SMOKE_ATTEMPTS, VcrMissError, buildFrozenV1JournalRaw, buildM2CassetteFixtures, buildV2GoldenIdentity, createTestEngine, defaultRedact, fakeToolCalls, fakeWireError, handlesInRequest, liveTestEnabled, normalizeM6Entries, readCassette, record, recordLiveCassettes, recordOrchestratorCrash, replay, replayRun, requestHash, runLiveSmoke };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rulvar/testing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"description": "Rulvar test harness: createTestEngine, FakeAdapter, VCR cassettes, replay-strict runs, matchers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"access": "public"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@rulvar/core": "1.
|
|
29
|
+
"@rulvar/core": "1.15.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^22.20.0",
|