{"version":3,"file":"mock-model.mjs","names":[],"sources":["../../../../../../../ai/src/mock/mock-model.ts"],"sourcesContent":["import type { Message } from \"../contracts/conversation-message.type\";\nimport type {\n  ModelCallOptions,\n  ModelCapabilities,\n  ModelContract,\n  ModelResponse,\n  ModelStreamChunk,\n} from \"../contracts/model.contract\";\nimport type { MockModelResponse } from \"./mock-config.type\";\n\ntype RecordedCall = {\n  messages: Message[];\n  options?: ModelCallOptions;\n};\n\n/**\n * Deterministic in-memory `ModelContract` implementation for tests.\n *\n * **Role.** Stands in for a real provider model so agent/workflow/supervisor\n * tests can assert behavior without hitting the network, spending tokens, or\n * depending on non-deterministic LLM output.\n *\n * **Responsibility.**\n * - Owns: a scripted queue of `MockModelResponse` entries, a call-history\n *   log for assertions, and the index pointer that advances through the\n *   queue on each `complete()` / `stream()` call.\n * - Does NOT own: any real inference, tokenization, or network I/O — when\n *   the queue is exhausted, the final entry is reused so tests never crash\n *   on accidental over-consumption.\n *\n * Every AI-related test in this repo uses `MockSDK` / `MockModel` — real\n * provider APIs are never hit from the test suite (see §6 of code-style.md).\n *\n * @example\n * const model = new MockModel(\"mock-gpt\", [\n *   { content: \"Hello!\", finishReason: \"stop\" },\n *   { content: \"Second turn.\", finishReason: \"stop\" },\n * ]);\n *\n * const first = await model.complete([{ role: \"user\", content: \"hi\" }]);\n * expect(first.content).toBe(\"Hello!\");\n * expect(model.callCount).toBe(1);\n */\nexport class MockModel implements ModelContract {\n  public readonly provider = \"mock\";\n  public readonly capabilities?: ModelCapabilities;\n\n  private responseIndex = 0;\n  private calls: RecordedCall[] = [];\n\n  public constructor(\n    public readonly name: string,\n    private readonly responses: MockModelResponse[],\n    capabilities?: ModelCapabilities,\n  ) {\n    this.capabilities = capabilities;\n  }\n\n  /**\n   * Full history of calls made to this model. Each entry is the exact\n   * `{ messages, options }` pair that was passed — useful for asserting\n   * that an agent built the right prompt or forwarded the right tool list.\n   */\n  public get callHistory(): RecordedCall[] {\n    return this.calls;\n  }\n\n  /**\n   * Number of times `complete()` or `stream()` has been invoked. Convenient\n   * shorthand for `callHistory.length` in assertions.\n   */\n  public get callCount(): number {\n    return this.calls.length;\n  }\n\n  /**\n   * Advance the scripted response queue by one and return the entry at the\n   * current pointer. If the queue is exhausted, the final scripted entry is\n   * returned repeatedly so over-consumption in tests produces predictable\n   * output instead of `undefined`.\n   */\n  private nextResponse(): MockModelResponse {\n    const response = this.responses[Math.min(this.responseIndex, this.responses.length - 1)];\n\n    this.responseIndex++;\n\n    return response ?? { content: \"Mock response\", finishReason: \"stop\" };\n  }\n\n  /**\n   * Convert a scripted `MockModelResponse` into a full `ModelResponse` with\n   * synthesized usage numbers when the script didn't supply them. Input\n   * usage is a fixed estimate; output usage is derived from content length.\n   */\n  private buildResponse(mock: MockModelResponse): ModelResponse {\n    const estimatedInput = 10;\n    const estimatedOutput = Math.ceil(mock.content.length / 4);\n\n    return {\n      content: mock.content,\n      finishReason: mock.finishReason ?? \"stop\",\n      usage: {\n        input: mock.usage?.input ?? estimatedInput,\n        output: mock.usage?.output ?? estimatedOutput,\n        total: (mock.usage?.input ?? estimatedInput) + (mock.usage?.output ?? estimatedOutput),\n        ...(mock.usage?.cachedTokens !== undefined ? { cachedTokens: mock.usage.cachedTokens } : {}),\n      },\n      toolCalls: mock.toolCalls,\n    };\n  }\n\n  /**\n   * Record the call, optionally delay (to simulate latency), and either\n   * throw the scripted error or return the scripted response. Mirrors the\n   * real provider's `complete()` contract so agents cannot tell the\n   * difference at runtime.\n   */\n  public async complete(messages: Message[], options?: ModelCallOptions): Promise<ModelResponse> {\n    this.calls.push({ messages, options });\n\n    const mock = this.nextResponse();\n\n    if (mock.delay) {\n      await new Promise((resolve) => setTimeout(resolve, mock.delay));\n    }\n\n    if (mock.error) {\n      throw mock.error;\n    }\n\n    return this.buildResponse(mock);\n  }\n\n  /**\n   * Record the call, optionally delay, then emit the scripted response as a\n   * sequence of stream chunks: the scripted `deltas` when the entry\n   * supplies them, otherwise content split word-by-word, as `delta`\n   * chunks, each scripted tool call as a `tool-call` chunk, and finally a\n   * `done` chunk with finish reason + usage. Throws eagerly if the scripted\n   * entry carries an `error`.\n   */\n  public async *stream(\n    messages: Message[],\n    options?: ModelCallOptions,\n  ): AsyncIterable<ModelStreamChunk> {\n    this.calls.push({ messages, options });\n\n    const mock = this.nextResponse();\n\n    if (mock.delay) {\n      await new Promise((resolve) => setTimeout(resolve, mock.delay));\n    }\n\n    if (mock.error) {\n      throw mock.error;\n    }\n\n    const chunks = mock.deltas ?? mock.content.split(\" \").map((word) => word + \" \");\n\n    for (const chunk of chunks) {\n      yield { type: \"delta\", content: chunk };\n    }\n\n    if (mock.toolCalls) {\n      for (const toolCall of mock.toolCalls) {\n        yield {\n          type: \"tool-call\",\n          id: toolCall.id,\n          name: toolCall.name,\n          input: toolCall.input,\n        };\n      }\n    }\n\n    const response = this.buildResponse(mock);\n\n    yield {\n      type: \"done\",\n      finishReason: response.finishReason,\n      usage: response.usage,\n    };\n  }\n\n  /**\n   * Reset call history and response pointer back to their initial state.\n   * Intended for test-suite `beforeEach` hooks so a single `MockModel`\n   * instance can be reused across cases without cross-test leakage.\n   */\n  public reset(): void {\n    this.calls = [];\n    this.responseIndex = 0;\n  }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,IAAa,YAAb,MAAgD;CAO9C,AAAO,YACL,AAAgB,MAChB,AAAiB,WACjB,cACA;EAHgB;EACC;kBARQ;uBAGH;eACQ,CAAC;EAO/B,KAAK,eAAe;CACtB;;;;;;CAOA,IAAW,cAA8B;EACvC,OAAO,KAAK;CACd;;;;;CAMA,IAAW,YAAoB;EAC7B,OAAO,KAAK,MAAM;CACpB;;;;;;;CAQA,AAAQ,eAAkC;EACxC,MAAM,WAAW,KAAK,UAAU,KAAK,IAAI,KAAK,eAAe,KAAK,UAAU,SAAS,CAAC;EAEtF,KAAK;EAEL,OAAO,YAAY;GAAE,SAAS;GAAiB,cAAc;EAAO;CACtE;;;;;;CAOA,AAAQ,cAAc,MAAwC;EAC5D,MAAM,iBAAiB;EACvB,MAAM,kBAAkB,KAAK,KAAK,KAAK,QAAQ,SAAS,CAAC;EAEzD,OAAO;GACL,SAAS,KAAK;GACd,cAAc,KAAK,gBAAgB;GACnC,OAAO;IACL,OAAO,KAAK,OAAO,SAAS;IAC5B,QAAQ,KAAK,OAAO,UAAU;IAC9B,QAAQ,KAAK,OAAO,SAAS,mBAAmB,KAAK,OAAO,UAAU;IACtE,GAAI,KAAK,OAAO,iBAAiB,SAAY,EAAE,cAAc,KAAK,MAAM,aAAa,IAAI,CAAC;GAC5F;GACA,WAAW,KAAK;EAClB;CACF;;;;;;;CAQA,MAAa,SAAS,UAAqB,SAAoD;EAC7F,KAAK,MAAM,KAAK;GAAE;GAAU;EAAQ,CAAC;EAErC,MAAM,OAAO,KAAK,aAAa;EAE/B,IAAI,KAAK,OACP,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,KAAK,KAAK,CAAC;EAGhE,IAAI,KAAK,OACP,MAAM,KAAK;EAGb,OAAO,KAAK,cAAc,IAAI;CAChC;;;;;;;;;CAUA,OAAc,OACZ,UACA,SACiC;EACjC,KAAK,MAAM,KAAK;GAAE;GAAU;EAAQ,CAAC;EAErC,MAAM,OAAO,KAAK,aAAa;EAE/B,IAAI,KAAK,OACP,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,KAAK,KAAK,CAAC;EAGhE,IAAI,KAAK,OACP,MAAM,KAAK;EAGb,MAAM,SAAS,KAAK,UAAU,KAAK,QAAQ,MAAM,GAAG,CAAC,CAAC,KAAK,SAAS,OAAO,GAAG;EAE9E,KAAK,MAAM,SAAS,QAClB,MAAM;GAAE,MAAM;GAAS,SAAS;EAAM;EAGxC,IAAI,KAAK,WACP,KAAK,MAAM,YAAY,KAAK,WAC1B,MAAM;GACJ,MAAM;GACN,IAAI,SAAS;GACb,MAAM,SAAS;GACf,OAAO,SAAS;EAClB;EAIJ,MAAM,WAAW,KAAK,cAAc,IAAI;EAExC,MAAM;GACJ,MAAM;GACN,cAAc,SAAS;GACvB,OAAO,SAAS;EAClB;CACF;;;;;;CAOA,AAAO,QAAc;EACnB,KAAK,QAAQ,CAAC;EACd,KAAK,gBAAgB;CACvB;AACF"}