@memberjunction/unit-testing 6.1.0-edge.2 → 6.1.0-edge.4

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.
@@ -0,0 +1,131 @@
1
+ function abortErrorLike() {
2
+ const error = new Error('Request was aborted.');
3
+ error.name = 'AbortError';
4
+ return error;
5
+ }
6
+ function usagePayload(usage) {
7
+ return {
8
+ prompt_tokens: usage.PromptTokens,
9
+ completion_tokens: usage.CompletionTokens,
10
+ total_tokens: usage.PromptTokens + usage.CompletionTokens
11
+ };
12
+ }
13
+ function contentChunk(model, content) {
14
+ return {
15
+ id: 'chatcmpl-conformance',
16
+ object: 'chat.completion.chunk',
17
+ model,
18
+ choices: [{ delta: { content }, finish_reason: null, index: 0 }]
19
+ };
20
+ }
21
+ function finalChunk(model, usage) {
22
+ return {
23
+ id: 'chatcmpl-conformance',
24
+ object: 'chat.completion.chunk',
25
+ model,
26
+ choices: [{ delta: {}, finish_reason: 'stop', index: 0 }],
27
+ usage: usagePayload(usage),
28
+ x_groq: { usage: usagePayload(usage) }
29
+ };
30
+ }
31
+ /**
32
+ * Build a fresh scriptable OpenAI-compatible seam. One seam instance can be shared across the
33
+ * whole conformance run — each `Script*` call replaces the pending behavior, and the conformance
34
+ * suite scripts before every request.
35
+ */
36
+ export function CreateOpenAICompatibleSeamMock() {
37
+ let behavior = null;
38
+ async function* streamOfChunks(model, chunks, usage) {
39
+ for (const chunk of chunks) {
40
+ yield contentChunk(model, chunk);
41
+ }
42
+ yield finalChunk(model, usage);
43
+ }
44
+ async function* streamFailingMidway(model, error, chunkBeforeError) {
45
+ if (chunkBeforeError) {
46
+ yield contentChunk(model, chunkBeforeError);
47
+ }
48
+ throw error;
49
+ }
50
+ async function* streamAbortedMidway(model, chunks, controller) {
51
+ for (const chunk of chunks) {
52
+ yield contentChunk(model, chunk);
53
+ }
54
+ // The caller (or a timeout layer) aborts while the stream is still open; the SDK then
55
+ // surfaces the abort as an error from the stream iterator.
56
+ controller.abort();
57
+ throw abortErrorLike();
58
+ }
59
+ const create = async (body, options) => {
60
+ // SDK-faithful: an already-aborted signal never dials out.
61
+ if (options?.signal?.aborted) {
62
+ throw abortErrorLike();
63
+ }
64
+ if (!behavior) {
65
+ throw new Error('OpenAI-compatible seam: no behavior scripted — call a Script* function first');
66
+ }
67
+ const model = body.model ?? 'conformance-test-model';
68
+ if (body.stream === true) {
69
+ switch (behavior.Kind) {
70
+ case 'streamingSuccess':
71
+ return streamOfChunks(model, behavior.Chunks, behavior.Usage);
72
+ case 'streamingCancellation':
73
+ return streamAbortedMidway(model, behavior.Chunks, behavior.Controller);
74
+ case 'failure':
75
+ if (behavior.At === 'streamStart') {
76
+ throw behavior.Error;
77
+ }
78
+ if (behavior.At === 'midStream') {
79
+ return streamFailingMidway(model, behavior.Error, behavior.ChunkBeforeError);
80
+ }
81
+ throw new Error(`OpenAI-compatible seam: failure scripted at '${behavior.At}' but a streaming call arrived`);
82
+ default:
83
+ throw new Error(`OpenAI-compatible seam: '${behavior.Kind}' scripted but a streaming call arrived`);
84
+ }
85
+ }
86
+ switch (behavior.Kind) {
87
+ case 'nonStreamingSuccess':
88
+ return {
89
+ id: 'chatcmpl-conformance',
90
+ object: 'chat.completion',
91
+ created: 0,
92
+ model,
93
+ choices: [
94
+ {
95
+ message: { role: 'assistant', content: behavior.Content },
96
+ finish_reason: 'stop',
97
+ index: 0
98
+ }
99
+ ],
100
+ usage: usagePayload(behavior.Usage)
101
+ };
102
+ case 'failure':
103
+ if (behavior.At === 'nonStreaming') {
104
+ throw behavior.Error;
105
+ }
106
+ throw new Error(`OpenAI-compatible seam: failure scripted at '${behavior.At}' but a non-streaming call arrived`);
107
+ default:
108
+ throw new Error(`OpenAI-compatible seam: '${behavior.Kind}' scripted but a non-streaming call arrived`);
109
+ }
110
+ };
111
+ return {
112
+ Client: {
113
+ chat: {
114
+ completions: { create }
115
+ }
116
+ },
117
+ ScriptNonStreamingSuccess: (content, usage) => {
118
+ behavior = { Kind: 'nonStreamingSuccess', Content: content, Usage: usage };
119
+ },
120
+ ScriptStreamingSuccess: (chunks, usage) => {
121
+ behavior = { Kind: 'streamingSuccess', Chunks: chunks, Usage: usage };
122
+ },
123
+ ScriptFailure: (error, at, options) => {
124
+ behavior = { Kind: 'failure', Error: error, At: at, ChunkBeforeError: options?.ChunkBeforeError };
125
+ },
126
+ ScriptStreamingCancellation: (chunksBeforeAbort, controller) => {
127
+ behavior = { Kind: 'streamingCancellation', Chunks: chunksBeforeAbort, Controller: controller };
128
+ }
129
+ };
130
+ }
131
+ //# sourceMappingURL=openai-compatible-seam.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai-compatible-seam.js","sourceRoot":"","sources":["../../src/ai/openai-compatible-seam.ts"],"names":[],"mappings":"AA+GA,SAAS,cAAc;IACnB,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;IAChD,KAAK,CAAC,IAAI,GAAG,YAAY,CAAC;IAC1B,OAAO,KAAK,CAAC;AACjB,CAAC;AAED,SAAS,YAAY,CAAC,KAA0B;IAC5C,OAAO;QACH,aAAa,EAAE,KAAK,CAAC,YAAY;QACjC,iBAAiB,EAAE,KAAK,CAAC,gBAAgB;QACzC,YAAY,EAAE,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC,gBAAgB;KAC5D,CAAC;AACN,CAAC;AAED,SAAS,YAAY,CAAC,KAAa,EAAE,OAAe;IAChD,OAAO;QACH,EAAE,EAAE,sBAAsB;QAC1B,MAAM,EAAE,uBAAuB;QAC/B,KAAK;QACL,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;KACnE,CAAC;AACN,CAAC;AAED,SAAS,UAAU,CAAC,KAAa,EAAE,KAA0B;IACzD,OAAO;QACH,EAAE,EAAE,sBAAsB;QAC1B,MAAM,EAAE,uBAAuB;QAC/B,KAAK;QACL,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;QACzD,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC;QAC1B,MAAM,EAAE,EAAE,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,EAAE;KACzC,CAAC;AACN,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,8BAA8B;IAC1C,IAAI,QAAQ,GAA4B,IAAI,CAAC;IAE7C,KAAK,SAAS,CAAC,CAAC,cAAc,CAAC,KAAa,EAAE,MAAgB,EAAE,KAA0B;QACtF,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YACzB,MAAM,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACrC,CAAC;QACD,MAAM,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACnC,CAAC;IAED,KAAK,SAAS,CAAC,CAAC,mBAAmB,CAAC,KAAa,EAAE,KAAY,EAAE,gBAAyB;QACtF,IAAI,gBAAgB,EAAE,CAAC;YACnB,MAAM,YAAY,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC;QAChD,CAAC;QACD,MAAM,KAAK,CAAC;IAChB,CAAC;IAED,KAAK,SAAS,CAAC,CAAC,mBAAmB,CAAC,KAAa,EAAE,MAAgB,EAAE,UAA2B;QAC5F,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YACzB,MAAM,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACrC,CAAC;QACD,sFAAsF;QACtF,2DAA2D;QAC3D,UAAU,CAAC,KAAK,EAAE,CAAC;QACnB,MAAM,cAAc,EAAE,CAAC;IAC3B,CAAC;IAED,MAAM,MAAM,GAAG,KAAK,EAChB,IAAiC,EACjC,OAAwC,EAC+C,EAAE;QACzF,2DAA2D;QAC3D,IAAI,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;YAC3B,MAAM,cAAc,EAAE,CAAC;QAC3B,CAAC;QACD,IAAI,CAAC,QAAQ,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,8EAA8E,CAAC,CAAC;QACpG,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,wBAAwB,CAAC;QAErD,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;YACvB,QAAQ,QAAQ,CAAC,IAAI,EAAE,CAAC;gBACpB,KAAK,kBAAkB;oBACnB,OAAO,cAAc,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;gBAClE,KAAK,uBAAuB;oBACxB,OAAO,mBAAmB,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;gBAC5E,KAAK,SAAS;oBACV,IAAI,QAAQ,CAAC,EAAE,KAAK,aAAa,EAAE,CAAC;wBAChC,MAAM,QAAQ,CAAC,KAAK,CAAC;oBACzB,CAAC;oBACD,IAAI,QAAQ,CAAC,EAAE,KAAK,WAAW,EAAE,CAAC;wBAC9B,OAAO,mBAAmB,CAAC,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAC;oBACjF,CAAC;oBACD,MAAM,IAAI,KAAK,CAAC,gDAAgD,QAAQ,CAAC,EAAE,gCAAgC,CAAC,CAAC;gBACjH;oBACI,MAAM,IAAI,KAAK,CAAC,4BAA4B,QAAQ,CAAC,IAAI,yCAAyC,CAAC,CAAC;YAC5G,CAAC;QACL,CAAC;QAED,QAAQ,QAAQ,CAAC,IAAI,EAAE,CAAC;YACpB,KAAK,qBAAqB;gBACtB,OAAO;oBACH,EAAE,EAAE,sBAAsB;oBAC1B,MAAM,EAAE,iBAAiB;oBACzB,OAAO,EAAE,CAAC;oBACV,KAAK;oBACL,OAAO,EAAE;wBACL;4BACI,OAAO,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE;4BACzD,aAAa,EAAE,MAAM;4BACrB,KAAK,EAAE,CAAC;yBACX;qBACJ;oBACD,KAAK,EAAE,YAAY,CAAC,QAAQ,CAAC,KAAK,CAAC;iBACtC,CAAC;YACN,KAAK,SAAS;gBACV,IAAI,QAAQ,CAAC,EAAE,KAAK,cAAc,EAAE,CAAC;oBACjC,MAAM,QAAQ,CAAC,KAAK,CAAC;gBACzB,CAAC;gBACD,MAAM,IAAI,KAAK,CAAC,gDAAgD,QAAQ,CAAC,EAAE,oCAAoC,CAAC,CAAC;YACrH;gBACI,MAAM,IAAI,KAAK,CAAC,4BAA4B,QAAQ,CAAC,IAAI,6CAA6C,CAAC,CAAC;QAChH,CAAC;IACL,CAAC,CAAC;IAEF,OAAO;QACH,MAAM,EAAE;YACJ,IAAI,EAAE;gBACF,WAAW,EAAE,EAAE,MAAM,EAAE;aAC1B;SACJ;QACD,yBAAyB,EAAE,CAAC,OAAe,EAAE,KAA0B,EAAQ,EAAE;YAC7E,QAAQ,GAAG,EAAE,IAAI,EAAE,qBAAqB,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QAC/E,CAAC;QACD,sBAAsB,EAAE,CAAC,MAAgB,EAAE,KAA0B,EAAQ,EAAE;YAC3E,QAAQ,GAAG,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QAC1E,CAAC;QACD,aAAa,EAAE,CAAC,KAAY,EAAE,EAAe,EAAE,OAAuC,EAAQ,EAAE;YAC5F,QAAQ,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,gBAAgB,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC;QACtG,CAAC;QACD,2BAA2B,EAAE,CAAC,iBAA2B,EAAE,UAA2B,EAAQ,EAAE;YAC5F,QAAQ,GAAG,EAAE,IAAI,EAAE,uBAAuB,EAAE,MAAM,EAAE,iBAAiB,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC;QACpG,CAAC;KACJ,CAAC;AACN,CAAC"}
@@ -0,0 +1,151 @@
1
+ /**
2
+ * A controllable LLM driver for unit tests that extends the REAL `BaseLLM` from
3
+ * `@memberjunction/ai`.
4
+ *
5
+ * Why this exists: tests across the AI stack used to hand-fake `BaseLLM` (and
6
+ * the `ChatResult` shapes it returns) per test file. Those structural fakes
7
+ * drift from the real contracts as they evolve — the exact bug class behind the
8
+ * historical failover bug, where drivers RETURN `ChatResult{success:false}`
9
+ * instead of throwing. `TestLLM` inherits the real `ChatCompletion` routing,
10
+ * the real streaming template method, and the real capability getters, so a
11
+ * contract change in `@memberjunction/ai` breaks the harness at compile time
12
+ * instead of silently diverging.
13
+ *
14
+ * Usage:
15
+ * ```ts
16
+ * const llm = new TestLLM();
17
+ * llm.Script(
18
+ * { kind: 'fail', error: new Error('Rate limit exceeded') }, // real ErrorAnalyzer errorInfo
19
+ * { kind: 'succeed', content: 'recovered' },
20
+ * );
21
+ * registerTestLLM(llm, ['AnthropicLLM', 'OpenAILLM']); // real ClassFactory resolution
22
+ * // ... drive the code under test ...
23
+ * expect(llm.CalledModels).toEqual(['api-claude', 'api-gpt']);
24
+ * ```
25
+ */
26
+ import { BaseLLM, ChatParams, ChatResult, ModelUsage, type ClassifyParams, type ClassifyResult, type FileCapabilities, type SummarizeParams, type SummarizeResult } from '@memberjunction/ai';
27
+ /**
28
+ * One scripted per-call outcome for {@link TestLLM.Script}.
29
+ *
30
+ * - `succeed` — resolves with a successful real `ChatResult` carrying `content`
31
+ * (and optionally a specific usage / reported model / thinking text).
32
+ * - `fail` — DRIVER-style failure: resolves with `ChatResult{success:false}`
33
+ * whose `errorInfo` is built by the REAL `ErrorAnalyzer` from `error`, exactly
34
+ * how production drivers report provider errors (the historical failover bug
35
+ * class — failover must trigger off the returned result, not just exceptions).
36
+ * - `failResult` — resolves with a test-supplied failed `ChatResult` verbatim,
37
+ * for cases where the test controls `errorInfo` (including leaving it absent).
38
+ * - `throw` — SDK-style failure: the returned promise rejects with `error`.
39
+ * - `hang` — never settles; models a hung provider socket (timeout tests).
40
+ * - `stream` — streams `chunks` through the real BaseLLM streaming template
41
+ * method when the caller requested streaming; in a non-streaming call the
42
+ * chunks resolve joined as one successful result.
43
+ */
44
+ export type TestLLMOutcome = {
45
+ kind: 'succeed';
46
+ content: string;
47
+ usage?: ModelUsage;
48
+ model?: string;
49
+ thinking?: string;
50
+ delayMS?: number;
51
+ } | {
52
+ kind: 'fail';
53
+ error: Error;
54
+ delayMS?: number;
55
+ } | {
56
+ kind: 'failResult';
57
+ result: ChatResult;
58
+ } | {
59
+ kind: 'throw';
60
+ error: Error;
61
+ delayMS?: number;
62
+ } | {
63
+ kind: 'hang';
64
+ } | {
65
+ kind: 'stream';
66
+ chunks: string[];
67
+ usage?: ModelUsage;
68
+ model?: string;
69
+ };
70
+ /**
71
+ * Scriptable, call-recording LLM driver extending the real `BaseLLM`.
72
+ *
73
+ * Scripted outcomes are consumed one per `ChatCompletion` call, in order. When
74
+ * the script is exhausted the {@link SetDefaultOutcome default outcome} is used
75
+ * (or, with {@link RepeatLastOutcome}, the final scripted outcome repeats).
76
+ * Every call's real `ChatParams` is recorded on {@link Calls}.
77
+ */
78
+ export declare class TestLLM extends BaseLLM {
79
+ private script;
80
+ private defaultOutcome;
81
+ private recordedCalls;
82
+ private supportsStreaming;
83
+ private supportsPrefill;
84
+ private fileCapabilities;
85
+ private streamFinalUsage;
86
+ private streamFinalModel;
87
+ /**
88
+ * When true and the script is down to its final outcome, that outcome keeps
89
+ * repeating for every subsequent call instead of falling back to the default
90
+ * outcome ("advance through the script, then repeat the last entry").
91
+ */
92
+ RepeatLastOutcome: boolean;
93
+ constructor(apiKey?: string);
94
+ /** Replaces the outcome queue with `outcomes` (consumed one per call, in order). */
95
+ Script(...outcomes: TestLLMOutcome[]): this;
96
+ /** Appends `outcomes` to the current queue. */
97
+ Enqueue(...outcomes: TestLLMOutcome[]): this;
98
+ /** Sets the outcome used whenever the script is exhausted (default: succeed with `'test response'`). */
99
+ SetDefaultOutcome(outcome: TestLLMOutcome): this;
100
+ /** Clears the script, recorded calls, and all configuration back to initial state. */
101
+ Reset(): this;
102
+ /** Number of scripted outcomes not yet consumed. */
103
+ get PendingOutcomeCount(): number;
104
+ /** The real `ChatParams` of every `ChatCompletion` call, in order. */
105
+ get Calls(): readonly ChatParams[];
106
+ /** Number of `ChatCompletion` calls received. */
107
+ get CallCount(): number;
108
+ /** `ChatParams.model` (the API name handed to the driver) of every call, in order. */
109
+ get CalledModels(): string[];
110
+ get SupportsStreaming(): boolean;
111
+ /** Enables/disables the real BaseLLM streaming path for this driver. */
112
+ SetSupportsStreaming(value: boolean): this;
113
+ get SupportsPrefill(): boolean;
114
+ /** Sets the code-level prefill default this driver reports. */
115
+ SetSupportsPrefill(value: boolean): this;
116
+ GetFileCapabilities(): FileCapabilities | null;
117
+ /** Sets the native file-input capabilities this driver reports (`null` = none). */
118
+ SetFileCapabilities(capabilities: FileCapabilities | null): this;
119
+ ChatCompletion(params: ChatParams): Promise<ChatResult>;
120
+ protected nonStreamingChatCompletion(_params: ChatParams): Promise<ChatResult>;
121
+ ClassifyText(_params: ClassifyParams): Promise<ClassifyResult>;
122
+ SummarizeText(_params: SummarizeParams): Promise<SummarizeResult>;
123
+ protected createStreamingRequest(_params: ChatParams): Promise<AsyncGenerator<string>>;
124
+ protected processStreamingChunk(chunk: string): {
125
+ content: string;
126
+ finishReason?: string | undefined;
127
+ usage?: ModelUsage | null;
128
+ };
129
+ protected finalizeStreamingResponse(accumulatedContent: string | null | undefined, _lastChunk: string | null | undefined, _usage: ModelUsage | null | undefined): ChatResult;
130
+ private nextOutcome;
131
+ private executeOutcome;
132
+ private wait;
133
+ private chunkGenerator;
134
+ private neverGenerator;
135
+ }
136
+ /**
137
+ * Registers `llm` on the real `MJGlobal.Instance.ClassFactory` under one or more
138
+ * DriverClass names — exactly how production drivers resolve. Every
139
+ * `ClassFactory.CreateInstance<BaseLLM>(BaseLLM, driverClass, apiKey)` for a
140
+ * registered name yields the SAME scripted instance, so a test scripts and
141
+ * asserts through one object no matter how many drivers the code under test
142
+ * instantiates.
143
+ *
144
+ * Registrations persist on the MJGlobal singleton and the ClassFactory has no
145
+ * unregister API — pair with `resetMJSingletons()` from this package in
146
+ * `beforeEach`/`afterEach` so each test gets a fresh factory (re-registering the
147
+ * same DriverClass name without a reset logs a duplicate-registration warning,
148
+ * though the newest registration still wins).
149
+ */
150
+ export declare function registerTestLLM(llm: TestLLM, driverClass: string | string[], priority?: number): void;
151
+ //# sourceMappingURL=test-llm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-llm.d.ts","sourceRoot":"","sources":["../../src/ai/test-llm.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,261 @@
1
+ /**
2
+ * A controllable LLM driver for unit tests that extends the REAL `BaseLLM` from
3
+ * `@memberjunction/ai`.
4
+ *
5
+ * Why this exists: tests across the AI stack used to hand-fake `BaseLLM` (and
6
+ * the `ChatResult` shapes it returns) per test file. Those structural fakes
7
+ * drift from the real contracts as they evolve — the exact bug class behind the
8
+ * historical failover bug, where drivers RETURN `ChatResult{success:false}`
9
+ * instead of throwing. `TestLLM` inherits the real `ChatCompletion` routing,
10
+ * the real streaming template method, and the real capability getters, so a
11
+ * contract change in `@memberjunction/ai` breaks the harness at compile time
12
+ * instead of silently diverging.
13
+ *
14
+ * Usage:
15
+ * ```ts
16
+ * const llm = new TestLLM();
17
+ * llm.Script(
18
+ * { kind: 'fail', error: new Error('Rate limit exceeded') }, // real ErrorAnalyzer errorInfo
19
+ * { kind: 'succeed', content: 'recovered' },
20
+ * );
21
+ * registerTestLLM(llm, ['AnthropicLLM', 'OpenAILLM']); // real ClassFactory resolution
22
+ * // ... drive the code under test ...
23
+ * expect(llm.CalledModels).toEqual(['api-claude', 'api-gpt']);
24
+ * ```
25
+ */
26
+ import { BaseLLM, } from '@memberjunction/ai';
27
+ import { MJGlobal } from '@memberjunction/global';
28
+ import { makeDriverFailureChatResult, makeModelUsage, makeSuccessChatResult } from './chat-result-factories.js';
29
+ const DEFAULT_OUTCOME = { kind: 'succeed', content: 'test response' };
30
+ /**
31
+ * Scriptable, call-recording LLM driver extending the real `BaseLLM`.
32
+ *
33
+ * Scripted outcomes are consumed one per `ChatCompletion` call, in order. When
34
+ * the script is exhausted the {@link SetDefaultOutcome default outcome} is used
35
+ * (or, with {@link RepeatLastOutcome}, the final scripted outcome repeats).
36
+ * Every call's real `ChatParams` is recorded on {@link Calls}.
37
+ */
38
+ export class TestLLM extends BaseLLM {
39
+ constructor(apiKey = 'test-api-key') {
40
+ super(apiKey);
41
+ this.script = [];
42
+ this.defaultOutcome = DEFAULT_OUTCOME;
43
+ this.recordedCalls = [];
44
+ this.supportsStreaming = false;
45
+ this.supportsPrefill = false;
46
+ this.fileCapabilities = null;
47
+ /**
48
+ * When true and the script is down to its final outcome, that outcome keeps
49
+ * repeating for every subsequent call instead of falling back to the default
50
+ * outcome ("advance through the script, then repeat the last entry").
51
+ */
52
+ this.RepeatLastOutcome = false;
53
+ }
54
+ // ------------------------------------------------------------------
55
+ // Scripting
56
+ // ------------------------------------------------------------------
57
+ /** Replaces the outcome queue with `outcomes` (consumed one per call, in order). */
58
+ Script(...outcomes) {
59
+ this.script = [...outcomes];
60
+ return this;
61
+ }
62
+ /** Appends `outcomes` to the current queue. */
63
+ Enqueue(...outcomes) {
64
+ this.script.push(...outcomes);
65
+ return this;
66
+ }
67
+ /** Sets the outcome used whenever the script is exhausted (default: succeed with `'test response'`). */
68
+ SetDefaultOutcome(outcome) {
69
+ this.defaultOutcome = outcome;
70
+ return this;
71
+ }
72
+ /** Clears the script, recorded calls, and all configuration back to initial state. */
73
+ Reset() {
74
+ this.script = [];
75
+ this.recordedCalls = [];
76
+ this.defaultOutcome = DEFAULT_OUTCOME;
77
+ this.RepeatLastOutcome = false;
78
+ this.supportsStreaming = false;
79
+ this.supportsPrefill = false;
80
+ this.fileCapabilities = null;
81
+ this.streamFinalUsage = undefined;
82
+ this.streamFinalModel = undefined;
83
+ return this;
84
+ }
85
+ /** Number of scripted outcomes not yet consumed. */
86
+ get PendingOutcomeCount() {
87
+ return this.script.length;
88
+ }
89
+ // ------------------------------------------------------------------
90
+ // Call recording
91
+ // ------------------------------------------------------------------
92
+ /** The real `ChatParams` of every `ChatCompletion` call, in order. */
93
+ get Calls() {
94
+ return this.recordedCalls;
95
+ }
96
+ /** Number of `ChatCompletion` calls received. */
97
+ get CallCount() {
98
+ return this.recordedCalls.length;
99
+ }
100
+ /** `ChatParams.model` (the API name handed to the driver) of every call, in order. */
101
+ get CalledModels() {
102
+ return this.recordedCalls.map((c) => c.model ?? '<none>');
103
+ }
104
+ // ------------------------------------------------------------------
105
+ // Capability knobs (all default to the plainest possible driver)
106
+ // ------------------------------------------------------------------
107
+ get SupportsStreaming() {
108
+ return this.supportsStreaming;
109
+ }
110
+ /** Enables/disables the real BaseLLM streaming path for this driver. */
111
+ SetSupportsStreaming(value) {
112
+ this.supportsStreaming = value;
113
+ return this;
114
+ }
115
+ get SupportsPrefill() {
116
+ return this.supportsPrefill;
117
+ }
118
+ /** Sets the code-level prefill default this driver reports. */
119
+ SetSupportsPrefill(value) {
120
+ this.supportsPrefill = value;
121
+ return this;
122
+ }
123
+ GetFileCapabilities() {
124
+ return this.fileCapabilities;
125
+ }
126
+ /** Sets the native file-input capabilities this driver reports (`null` = none). */
127
+ SetFileCapabilities(capabilities) {
128
+ this.fileCapabilities = capabilities;
129
+ return this;
130
+ }
131
+ // ------------------------------------------------------------------
132
+ // BaseLLM implementation
133
+ // ------------------------------------------------------------------
134
+ async ChatCompletion(params) {
135
+ this.recordedCalls.push(params);
136
+ return super.ChatCompletion(params);
137
+ }
138
+ async nonStreamingChatCompletion(_params) {
139
+ return this.executeOutcome(this.nextOutcome());
140
+ }
141
+ async ClassifyText(_params) {
142
+ throw new Error('TestLLM.ClassifyText is not supported — this harness scripts ChatCompletion outcomes only');
143
+ }
144
+ async SummarizeText(_params) {
145
+ throw new Error('TestLLM.SummarizeText is not supported — this harness scripts ChatCompletion outcomes only');
146
+ }
147
+ // ------------------------------------------------------------------
148
+ // Streaming implementation (drives the REAL BaseLLM streaming template method)
149
+ // ------------------------------------------------------------------
150
+ async createStreamingRequest(_params) {
151
+ const outcome = this.nextOutcome();
152
+ switch (outcome.kind) {
153
+ case 'stream':
154
+ this.streamFinalUsage = outcome.usage;
155
+ this.streamFinalModel = outcome.model;
156
+ return this.chunkGenerator(outcome.chunks);
157
+ case 'succeed':
158
+ this.streamFinalUsage = outcome.usage;
159
+ this.streamFinalModel = outcome.model;
160
+ await this.wait(outcome.delayMS);
161
+ return this.chunkGenerator([outcome.content]);
162
+ case 'fail':
163
+ case 'throw':
164
+ await this.wait(outcome.delayMS);
165
+ throw outcome.error;
166
+ case 'failResult':
167
+ throw new Error(outcome.result.errorMessage ?? 'TestLLM: scripted failResult during streaming');
168
+ case 'hang':
169
+ return this.neverGenerator();
170
+ }
171
+ }
172
+ processStreamingChunk(chunk) {
173
+ return { content: chunk, usage: null };
174
+ }
175
+ finalizeStreamingResponse(accumulatedContent, _lastChunk, _usage) {
176
+ return makeSuccessChatResult(accumulatedContent ?? '', {
177
+ usage: this.streamFinalUsage ?? makeModelUsage(),
178
+ model: this.streamFinalModel,
179
+ });
180
+ }
181
+ // ------------------------------------------------------------------
182
+ // Internals
183
+ // ------------------------------------------------------------------
184
+ nextOutcome() {
185
+ if (this.script.length === 1 && this.RepeatLastOutcome) {
186
+ return this.script[0];
187
+ }
188
+ return this.script.shift() ?? this.defaultOutcome;
189
+ }
190
+ async executeOutcome(outcome) {
191
+ switch (outcome.kind) {
192
+ case 'succeed':
193
+ await this.wait(outcome.delayMS);
194
+ return makeSuccessChatResult(outcome.content, {
195
+ usage: outcome.usage,
196
+ model: outcome.model,
197
+ thinking: outcome.thinking,
198
+ });
199
+ case 'fail':
200
+ await this.wait(outcome.delayMS);
201
+ return makeDriverFailureChatResult(outcome.error, this.constructor.name);
202
+ case 'failResult':
203
+ return outcome.result;
204
+ case 'throw':
205
+ await this.wait(outcome.delayMS);
206
+ throw outcome.error;
207
+ case 'hang':
208
+ return new Promise(() => {
209
+ /* never settles — models a hung provider socket */
210
+ });
211
+ case 'stream':
212
+ // Non-streaming call against a stream outcome: resolve the joined chunks.
213
+ return makeSuccessChatResult(outcome.chunks.join(''), { usage: outcome.usage, model: outcome.model });
214
+ }
215
+ }
216
+ async wait(delayMS) {
217
+ if (delayMS !== undefined && delayMS > 0) {
218
+ await new Promise((resolve) => setTimeout(resolve, delayMS));
219
+ }
220
+ }
221
+ async *chunkGenerator(chunks) {
222
+ for (const chunk of chunks) {
223
+ yield chunk;
224
+ }
225
+ }
226
+ async *neverGenerator() {
227
+ await new Promise(() => {
228
+ /* never settles — models a hung provider socket */
229
+ });
230
+ }
231
+ }
232
+ /**
233
+ * Registers `llm` on the real `MJGlobal.Instance.ClassFactory` under one or more
234
+ * DriverClass names — exactly how production drivers resolve. Every
235
+ * `ClassFactory.CreateInstance<BaseLLM>(BaseLLM, driverClass, apiKey)` for a
236
+ * registered name yields the SAME scripted instance, so a test scripts and
237
+ * asserts through one object no matter how many drivers the code under test
238
+ * instantiates.
239
+ *
240
+ * Registrations persist on the MJGlobal singleton and the ClassFactory has no
241
+ * unregister API — pair with `resetMJSingletons()` from this package in
242
+ * `beforeEach`/`afterEach` so each test gets a fresh factory (re-registering the
243
+ * same DriverClass name without a reset logs a duplicate-registration warning,
244
+ * though the newest registration still wins).
245
+ */
246
+ export function registerTestLLM(llm, driverClass, priority = 100) {
247
+ const driverClasses = Array.isArray(driverClass) ? driverClass : [driverClass];
248
+ for (const name of driverClasses) {
249
+ // The constructor's object-return makes the ClassFactory hand back the
250
+ // shared scripted instance while still going through a real registration
251
+ // and a real `new` — no CreateInstance spying required.
252
+ class TestLLMRegistrationHandle extends TestLLM {
253
+ constructor(apiKey) {
254
+ super(apiKey);
255
+ return llm;
256
+ }
257
+ }
258
+ MJGlobal.Instance.ClassFactory.Register(BaseLLM, TestLLMRegistrationHandle, name, priority);
259
+ }
260
+ }
261
+ //# sourceMappingURL=test-llm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-llm.js","sourceRoot":"","sources":["../../src/ai/test-llm.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -3,4 +3,9 @@ export { createMockEntity, type MockEntityOptions } from './mock-entity.js';
3
3
  export { mockRunView, mockRunViews, resetRunViewMocks } from './mock-run-view.js';
4
4
  export { installCustomMatchers } from './custom-matchers.js';
5
5
  export type {} from './vitest.d';
6
+ export { TestLLM, registerTestLLM, type TestLLMOutcome } from './ai/test-llm.js';
7
+ export { makeModelUsage, makeSuccessChatResult, makeFailedChatResult, makeDriverFailureChatResult, makeErrorInfo, makeChatParams, type ModelUsageOverrides, type SuccessChatResultOptions, type FailedChatResultOptions, } from './ai/chat-result-factories.js';
8
+ export { VENDOR_TYPE, VENDOR, MODEL_TYPE, CONFIG, MODEL, makeModelVendor, makeModel, makePromptModel, buildRealisticCatalog, DEFAULT_CONFIGURED_DRIVERS, type FxVendorType, type FxVendor, type FxModelType, type FxConfiguration, type FxModelVendor, type FxModel, type FxPromptModel, type AICatalog, } from './ai/catalog-fixtures.js';
9
+ export { RunLLMConformanceSuite, type ExpectedUsageCounts, type FailureSite, type LLMConformanceDeviationKind, type LLMConformanceDeviation, type LLMConformanceSuiteConfig, } from './ai/llm-conformance.js';
10
+ export { CreateOpenAICompatibleSeamMock, type OpenAICompatibleSeam, type OpenAICompatibleChatClient, type OpenAICompatibleUsagePayload, type OpenAICompatibleResponsePayload, type OpenAICompatibleChunkPayload, type OpenAICompatibleRequestBody, type OpenAICompatibleRequestOptions, } from './ai/openai-compatible-seam.js';
6
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC3F,OAAO,EAAE,gBAAgB,EAAE,KAAK,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,YAAY,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC3F,OAAO,EAAE,gBAAgB,EAAE,KAAK,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,YAAY,EAAE,MAAM,YAAY,CAAC;AAGjC,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAC9E,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,oBAAoB,EACpB,2BAA2B,EAC3B,aAAa,EACb,cAAc,EACd,KAAK,mBAAmB,EACxB,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,GAC7B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,WAAW,EACX,MAAM,EACN,UAAU,EACV,MAAM,EACN,KAAK,EACL,eAAe,EACf,SAAS,EACT,eAAe,EACf,qBAAqB,EACrB,0BAA0B,EAC1B,KAAK,YAAY,EACjB,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,OAAO,EACZ,KAAK,aAAa,EAClB,KAAK,SAAS,GACf,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EACL,sBAAsB,EACtB,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAChB,KAAK,2BAA2B,EAChC,KAAK,uBAAuB,EAC5B,KAAK,yBAAyB,GAC/B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,8BAA8B,EAC9B,KAAK,oBAAoB,EACzB,KAAK,0BAA0B,EAC/B,KAAK,4BAA4B,EACjC,KAAK,+BAA+B,EACpC,KAAK,4BAA4B,EACjC,KAAK,2BAA2B,EAChC,KAAK,8BAA8B,GACpC,MAAM,6BAA6B,CAAC"}
package/dist/index.js CHANGED
@@ -2,4 +2,11 @@ export { resetMJSingletons, resetClassFactory, resetObjectCache } from './single
2
2
  export { createMockEntity } from './mock-entity.js';
3
3
  export { mockRunView, mockRunViews, resetRunViewMocks } from './mock-run-view.js';
4
4
  export { installCustomMatchers } from './custom-matchers.js';
5
+ // ---- Shared AI test harness (real @memberjunction/ai contracts, no structural fakes) ----
6
+ export { TestLLM, registerTestLLM } from './ai/test-llm.js';
7
+ export { makeModelUsage, makeSuccessChatResult, makeFailedChatResult, makeDriverFailureChatResult, makeErrorInfo, makeChatParams, } from './ai/chat-result-factories.js';
8
+ export { VENDOR_TYPE, VENDOR, MODEL_TYPE, CONFIG, MODEL, makeModelVendor, makeModel, makePromptModel, buildRealisticCatalog, DEFAULT_CONFIGURED_DRIVERS, } from './ai/catalog-fixtures.js';
9
+ // ---- Shared BaseLLM streaming/ChatResult conformance suite (applied by provider packages) ----
10
+ export { RunLLMConformanceSuite, } from './ai/llm-conformance.js';
11
+ export { CreateOpenAICompatibleSeamMock, } from './ai/openai-compatible-seam.js';
5
12
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC3F,OAAO,EAAE,gBAAgB,EAA0B,MAAM,eAAe,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC3F,OAAO,EAAE,gBAAgB,EAA0B,MAAM,eAAe,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAG1D,4FAA4F;AAC5F,OAAO,EAAE,OAAO,EAAE,eAAe,EAAuB,MAAM,eAAe,CAAC;AAC9E,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,oBAAoB,EACpB,2BAA2B,EAC3B,aAAa,EACb,cAAc,GAIf,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,WAAW,EACX,MAAM,EACN,UAAU,EACV,MAAM,EACN,KAAK,EACL,eAAe,EACf,SAAS,EACT,eAAe,EACf,qBAAqB,EACrB,0BAA0B,GAS3B,MAAM,uBAAuB,CAAC;AAE/B,iGAAiG;AACjG,OAAO,EACL,sBAAsB,GAMvB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,8BAA8B,GAQ/B,MAAM,6BAA6B,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@memberjunction/unit-testing",
3
3
  "type": "module",
4
- "version": "6.1.0-edge.2",
4
+ "version": "6.1.0-edge.4",
5
5
  "description": "MemberJunction: Unit testing utilities and mock infrastructure for Vitest",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -9,14 +9,15 @@
9
9
  "/dist"
10
10
  ],
11
11
  "author": "MemberJunction.com",
12
- "license": "ISC",
12
+ "license": "BUSL-1.1",
13
13
  "devDependencies": {
14
14
  "ts-node-dev": "^2.0.0",
15
15
  "typescript": "^5.9.3",
16
16
  "vitest": "^4.0.18"
17
17
  },
18
18
  "dependencies": {
19
- "@memberjunction/global": "6.1.0-edge.2"
19
+ "@memberjunction/ai": "6.1.0-edge.4",
20
+ "@memberjunction/global": "6.1.0-edge.4"
20
21
  },
21
22
  "repository": {
22
23
  "type": "git",