import assert from "node:assert/strict"; import test from "node:test"; import { reconcileConsultSessionModel } from "../src/lib/consult/session-model-fallback.ts"; import { loadConsultTurnHead } from "../src/lib/consult/turn-prep.ts"; import type { ResolvedLanguageModel } from "../src/mastra/model.ts"; function model(id: string, version: number): ResolvedLanguageModel { return { id, label: id, description: "", creditCost: 1, isDefault: id === "default-model", mode: "openai", model: { providerId: "openai", modelId: id, apiKey: "sk-test" }, configVersion: version, }; } test("a session pinned to a deleted version continues on the default model", async () => { const writes: Array = []; const decision = await reconcileConsultSessionModel({ session: { id: "session-1", model_id: "retired-model", model_config_version: 4 }, requestedModelId: "retired-model", resolveBound: async (modelId) => modelId === "retired-model" ? null : model(modelId, 1), loadCatalog: async () => ({ models: [model("default-model", 2)], publicModels: [], defaultModelId: "default-model", issues: [], }), persistModelId: async (_sessionId, modelId) => { writes.push(modelId); return true; }, }); assert.equal(decision.kind, "ready"); if (decision.kind !== "ready") return; assert.equal(decision.model.id, "default-model"); assert.equal(decision.session.model_id, "default-model"); assert.equal(decision.session.model_config_version, 2); assert.deepEqual(writes, ["default-model"]); }); test("the next turn still uses the default when the client sends the deleted model id", async () => { const decision = await reconcileConsultSessionModel({ session: { id: "session-1", model_id: "default-model", model_config_version: 2 }, requestedModelId: "retired-model", resolveBound: async (modelId, version) => { if (modelId === "retired-model") return null; if (modelId === "default-model" && version === 2) return model("default-model", 2); if (modelId === "default-model" && version == null) return model("default-model", 2); return null; }, loadCatalog: async () => ({ models: [], publicModels: [], defaultModelId: null, issues: [] }), persistModelId: async () => { throw new Error("persist should not run"); }, }); assert.equal(decision.kind, "ready"); if (decision.kind !== "ready") return; assert.equal(decision.model.id, "default-model"); }); test("a live different model is still refused as a model change", async () => { const decision = await reconcileConsultSessionModel({ session: { id: "session-1", model_id: "default-model", model_config_version: 2 }, requestedModelId: "other-model", resolveBound: async (modelId) => model(modelId, modelId === "other-model" ? 1 : 2), loadCatalog: async () => ({ models: [], publicModels: [], defaultModelId: null, issues: [] }), persistModelId: async () => false, }); assert.equal(decision.kind, "changed"); }); test("a deleted version with no default model still reports the model as unavailable", async () => { const head = await loadConsultTurnHead({ parallel: false, getUser: async () => ({ data: { user: { id: "user-1" } }, error: null }), readPendingDeletion: async () => null, readBody: async () => ({ success: true as const, data: { sessionId: "session-1", modelId: "retired-model" } }), readSession: async () => ({ data: { id: "session-1", model_id: "retired-model", model_config_version: 4, session_type: "consultation" }, error: null, }), resolveModel: async (): Promise => null, reconcileModel: async (chatSession, requestedModelId) => { const decision = await reconcileConsultSessionModel({ session: chatSession, requestedModelId, resolveBound: async () => null, loadCatalog: async () => ({ models: [], publicModels: [], defaultModelId: null, issues: ["default_model_unavailable"] }), persistModelId: async () => false, }); if (decision.kind !== "ready") return decision; return { kind: "ready" as const, chatSession: decision.session, sessionModel: decision.model }; }, }); assert.equal(head.ok, false); if (head.ok) return; assert.equal(head.response.status, 503); const body = await head.response.json() as { error?: string }; assert.equal(body.error, "模型暂不可用"); });