Ctrl K
ComunidadGuías…

test/home-mate-models.test.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
var assert = require("node:assert/strict");
var fs = require("node:fs");
var path = require("node:path");
var test = require("node:test");
var attachHomeChat = require("../lib/server-home-chat").attachHomeChat;

function settle() {
  return new Promise(function (resolve) { setImmediate(resolve); });
}

function fixture(options) {
  var opts = options || {};
  var mateModel = Object.prototype.hasOwnProperty.call(opts, "mateModel") ? opts.mateModel : "fable";
  var mateVendor = opts.mateVendor || "claude";
  var mate = { id: "mate-a", name: "A", vendor: mateVendor, model: mateModel };
  if (opts.builtinClay) mate.builtinKey = "clay";
  var sessions = new Map();
  if (opts.existingSession) sessions.set(1, opts.existingSession);
  var created = [];
  var saved = [];
  var dispatched = [];
  var manager = {
    sessions: sessions,
    createSession: function (sessionOptions) {
      created.push(sessionOptions);
      var session = Object.assign({ localId: created.length + 10, history: [], lastActivity: Date.now() }, sessionOptions);
      sessions.set(session.localId, session);
      return session;
    },
    subscribeSession: function () { return function () {}; },
    sendAndRecord: function (session, event) { session.history.push(event); },
    saveSessionFile: opts.noPersistence ? null : function (session) {
      if (opts.persistenceError) throw new Error("save failed");
      saved.push(session.localId);
    },
  };
  var messages = [];
  var ws = {
    readyState: 1,
    _clayUser: opts.unauthenticated ? null : { id: "u1" },
    send: function (value) { messages.push(JSON.parse(value)); },
  };
  var updates = [];
  var project = {
    getSessionManager: function () { return manager; },
    getVendorModelCatalog: function (catalogWs, vendor) {
      var catalog = opts.catalogs && opts.catalogs[vendor] ? opts.catalogs[vendor] : opts.catalog;
      return Promise.resolve(catalog || {
        status: "ready",
        error: "",
        models: [
          { value: "fable", resolvedModel: "claude-fable-5", displayName: "Claude Fable" },
          { value: "sonnet", displayName: "Claude Sonnet" },
        ],
      });
    },
    getVendorModelAvailability: function (availabilityWs) {
      if (opts.onAvailability) opts.onAvailability(availabilityWs);
      return opts.vendors || [{ id: "claude", displayName: "Claude", installed: true }];
    },
    sdk: opts.sdk === null ? null : (opts.sdk || {
      startQuery: function (session) { dispatched.push(session.model); },
      pushMessage: function (session) { dispatched.push(session.model); },
    }),
    getMemoryState: function () { return { entries: [], summary: "" }; },
    listKnowledgeFiles: function () { return []; },
    forEachClient: function (fn) { fn(ws); },
  };
  var handler = attachHomeChat({
    users: { isMultiUser: function () { return true; }, findUserById: function (id) { return id === "u1" ? { id: id } : null; } },
    mates: {
      buildMateCtx: function (userId) { return { userId: userId }; },
      getAllMates: function () { return [mate]; },
      getMate: function () { return mate; },
      getMateDir: function () { return "/tmp/mate-a"; },
      updateMate: function (ctx, mateId, patch) {
        updates.push({ ctx: ctx, mateId: mateId, patch: patch });
        mate = Object.assign({}, mate, patch);
        return mate;
      },
    },
    projects: new Map([["mate-mate-a", project]]),
    addProject: function () {},
    resolveDefaultAi: opts.resolveDefaultAi,
  });
  return { handler: handler, ws: ws, messages: messages, updates: updates, created: created, sessions: sessions, manager: manager, saved: saved, dispatched: dispatched, getMate: function () { return mate; } };
}

test("cold Home model discovery passes the authenticated socket to availability", async function () {
  var received = null;
  var f = fixture({ onAvailability: function (availabilityWs) { received = availabilityWs; } });
  f.handler.handleMessage(f.ws, { type: "home_mate_models_get", mateId: "mate-a", requestId: "cold-home" });
  await settle();
  assert.equal(received, f.ws);
  assert.equal(received._clayUser.id, "u1");
});

test("Mate model catalog responses preserve request correlation and vendor state", async function () {
  var f = fixture();
  assert.equal(f.handler.handleMessage(f.ws, { type: "home_mate_models_get", mateId: "mate-a", requestId: "catalog-1" }), true);
  await settle();
  assert.deepEqual(f.messages[0], {
    type: "home_mate_models_state",
    mateId: "mate-a",
    requestId: "catalog-1",
    vendor: "claude",
    mateVendor: "claude",
    mateModel: "fable",
    model: "fable",
    models: [
      { value: "fable", resolvedModel: "claude-fable-5", displayName: "Claude Fable" },
      { value: "sonnet", displayName: "Claude Sonnet" },
    ],
    vendors: [{ id: "claude", displayName: "Claude", installed: true }],
    status: "ready",
    error: "",
  });
  assert.equal(f.messages.some(function (message) { return message.type === "model_info"; }), false);
});

test("Mate model access errors remain correlated instead of leaving the UI loading", function () {
  var f = fixture({ unauthenticated: true });
  f.handler.handleMessage(f.ws, { type: "home_mate_models_get", mateId: "mate-a", requestId: "catalog-auth" });
  assert.deepEqual(f.messages[0], { type: "home_mate_models_state", mateId: "mate-a", requestId: "catalog-auth", vendor: "", mateVendor: "", mateModel: "", model: "", models: [], vendors: [], status: "error", error: "Not authenticated." });
});

test("Mate model selection validates the Mate vendor catalog before persistence", async function () {
  var f = fixture();
  f.handler.handleMessage(f.ws, { type: "home_mate_model_set", mateId: "mate-a", vendor: "claude", model: "unknown", requestId: "set-invalid" });
  await settle();
  assert.equal(f.updates.length, 0);
  assert.equal(f.messages[0].type, "home_mate_model_result");
  assert.equal(f.messages[0].requestId, "set-invalid");
  assert.equal(f.messages[0].ok, false);
  assert.match(f.messages[0].error, /not available/);
});

test("valid Mate model selection persists and broadcasts the server-confirmed record", async function () {
  var f = fixture();
  f.handler.handleMessage(f.ws, { type: "home_mate_model_set", mateId: "mate-a", vendor: "claude", model: "claude-fable-5", requestId: "set-valid" });
  await settle();
  assert.deepEqual(f.updates[0], { ctx: { userId: "u1" }, mateId: "mate-a", patch: { vendor: "claude", model: "fable" } });
  assert.equal(f.getMate().model, "fable");
  assert.equal(f.messages[0].type, "mate_updated");
  assert.equal(f.messages[0].mate.model, "fable");
  assert.deepEqual(f.messages[1], { type: "home_mate_model_result", mateId: "mate-a", requestId: "set-valid", ok: true, vendor: "claude", model: "fable" });
});

test("Mate model chooser loads another configured vendor without partially persisting it", async function () {
  var f = fixture({
    vendors: [
      { id: "claude", displayName: "Claude", installed: true },
      { id: "codex", displayName: "Codex", installed: true },
    ],
    catalogs: {
      claude: { status: "error", models: [], error: "Claude authentication expired." },
      codex: { status: "ready", models: [{ value: "gpt-5.6", displayName: "GPT-5.6" }], error: "" },
    },
  });
  f.handler.handleMessage(f.ws, { type: "home_mate_models_get", mateId: "mate-a", vendor: "codex", requestId: "catalog-codex" });
  await settle();
  assert.equal(f.messages[0].vendor, "codex");
  assert.equal(f.messages[0].mateVendor, "claude");
  assert.equal(f.messages[0].model, "");
  assert.deepEqual(f.messages[0].models, [{ value: "gpt-5.6", displayName: "GPT-5.6" }]);
  assert.equal(f.updates.length, 0);
  assert.equal(f.getMate().vendor, "claude");
});

test("vendor and concrete model persist atomically and seed new Home sessions", async function () {
  var f = fixture({
    vendors: [
      { id: "claude", displayName: "Claude", installed: true },
      { id: "codex", displayName: "Codex", installed: true },
    ],
    catalogs: {
      codex: { status: "ready", models: [{ value: "gpt-5.6", resolvedModel: "gpt-5.6-codex", displayName: "GPT-5.6" }], error: "" },
    },
  });
  f.handler.handleMessage(f.ws, { type: "home_mate_model_set", mateId: "mate-a", vendor: "codex", model: "gpt-5.6-codex", requestId: "set-codex" });
  await settle();
  assert.deepEqual(f.updates[0].patch, { vendor: "codex", model: "gpt-5.6" });
  assert.equal(f.messages[0].type, "mate_updated");
  assert.equal(f.messages[0].mate.vendor, "codex");
  assert.deepEqual(f.messages[1], { type: "home_mate_model_result", mateId: "mate-a", requestId: "set-codex", ok: true, vendor: "codex", model: "gpt-5.6" });
  f.handler.handleMessage(f.ws, { type: "home_mate_new_session", mateId: "mate-a", requestId: "new-codex" });
  await settle();
  assert.equal(f.created[0].vendor, "codex");
  assert.equal(f.created[0].model, "gpt-5.6");
});

test("fresh builtin Clay sessions use shared Default AI while custom Mates retain their configured runtime", async function () {
  var resolver = function () { return Promise.resolve({ ready: true, vendor: "codex", model: "gpt-6-astra", effort: "high" }); };
  var clay = fixture({ builtinClay: true, resolveDefaultAi: resolver });
  clay.handler.handleMessage(clay.ws, { type: "home_mate_new_session", mateId: "mate-a", requestId: "clay-default" });
  await settle();
  assert.deepEqual([clay.created[0].vendor, clay.created[0].model, clay.created[0].effort], ["codex", "gpt-6-astra", "high"]);
  assert.equal(clay.updates.length, 0, "the shared default does not overwrite the builtin Mate record");

  var custom = fixture({ resolveDefaultAi: resolver });
  custom.handler.handleMessage(custom.ws, { type: "home_mate_new_session", mateId: "mate-a", requestId: "custom-config" });
  await settle();
  assert.deepEqual([custom.created[0].vendor, custom.created[0].model], ["claude", "fable"]);
});

test("composer model selection updates the same pristine Home draft and its next send", async function () {
  var f = fixture({
    vendors: [
      { id: "claude", displayName: "Claude", installed: true },
      { id: "codex", displayName: "Codex", installed: true },
    ],
    catalogs: { codex: { status: "ready", models: [{ value: "gpt-5.6", resolvedModel: "gpt-5.6-codex" }] } },
  });
  f.handler.handleMessage(f.ws, { type: "home_mate_new_session", mateId: "mate-a", requestId: "new-draft" });
  await settle();
  var history = f.messages.filter(function (message) { return message.type === "home_mate_history"; })[0];
  assert.equal(history.sessionId, "local:11");
  f.handler.handleMessage(f.ws, { type: "home_mate_model_set", mateId: "mate-a", vendor: "codex", model: "gpt-5.6-codex", sessionId: history.sessionId, requestId: "draft-model" });
  await settle();
  var result = f.messages.filter(function (message) { return message.type === "home_mate_model_result"; })[0];
  assert.equal(result.ok, true);
  assert.equal(result.requestedSessionId, "local:11");
  assert.equal(result.sessionId, "local:11");
  assert.equal(result.sessionApplied, true);
  assert.equal(result.sessionVendor, "codex");
  assert.equal(result.sessionModel, "gpt-5.6");
  assert.equal(f.sessions.get(11).vendor, "codex");
  assert.equal(f.sessions.get(11).model, "gpt-5.6");
  assert.deepEqual(f.saved, [11]);
  assert.equal(f.created.length, 1);
  assert.deepEqual(f.getMate(), { id: "mate-a", name: "A", vendor: "codex", model: "gpt-5.6" });
  f.handler.handleMessage(f.ws, { type: "home_mate_send", mateId: "mate-a", sessionId: "local:11", requestId: "new-draft", text: "Use the draft model" });
  await settle();
  assert.deepEqual(f.dispatched, ["gpt-5.6"]);
  assert.equal(f.created.length, 1);
});

test("activity, processing, stale, and other-user sessions remain immutable while the Mate default updates", async function () {
  var activityTypes = ["user_message", "delta", "tool_start", "tool_result"];
  for (var i = 0; i < activityTypes.length; i++) {
    var session = { localId: 1, cliSessionId: "activity-" + i, ownerId: "u1", vendor: "claude", model: "fable", history: [{ type: activityTypes[i] }], lastActivity: 20 };
    var active = fixture({ existingSession: session });
    active.handler.handleMessage(active.ws, { type: "home_mate_model_set", mateId: "mate-a", vendor: "claude", model: "sonnet", sessionId: session.cliSessionId, requestId: "activity-set-" + i });
    await settle();
    var activeResult = active.messages.filter(function (message) { return message.type === "home_mate_model_result"; })[0];
    assert.equal(activeResult.sessionApplied, false);
    assert.match(activeResult.sessionReason, /already has activity/);
    assert.equal(session.model, "fable");
    assert.deepEqual(active.saved, []);
    assert.equal(active.getMate().model, "sonnet");
  }

  var processingSession = { localId: 1, cliSessionId: "processing", ownerId: "u1", vendor: "claude", model: "fable", history: [], isProcessing: true };
  var processing = fixture({ existingSession: processingSession });
  processing.handler.handleMessage(processing.ws, { type: "home_mate_model_set", mateId: "mate-a", vendor: "claude", model: "sonnet", sessionId: "processing", requestId: "processing-set" });
  await settle();
  assert.equal(processingSession.model, "fable");
  assert.equal(processing.messages.filter(function (message) { return message.type === "home_mate_model_result"; })[0].sessionApplied, false);

  var owned = { localId: 1, cliSessionId: "owned", ownerId: "u1", vendor: "claude", model: "fable", history: [] };
  var inaccessible = fixture({ existingSession: owned });
  inaccessible.sessions.set(2, { localId: 2, cliSessionId: "other-user", ownerId: "u2", vendor: "claude", model: "fable", history: [] });
  inaccessible.handler.handleMessage(inaccessible.ws, { type: "home_mate_model_set", mateId: "mate-a", vendor: "claude", model: "sonnet", sessionId: "other-user", requestId: "other-set" });
  await settle();
  var inaccessibleResult = inaccessible.messages.filter(function (message) { return message.type === "home_mate_model_result"; })[0];
  assert.equal(inaccessibleResult.sessionApplied, false);
  assert.equal(inaccessible.sessions.get(2).model, "fable");
  assert.deepEqual(inaccessible.saved, []);
});

test("a draft persistence failure rolls back in-memory session metadata", async function () {
  var session = { localId: 1, cliSessionId: "draft-save-error", ownerId: "u1", vendor: "claude", model: "fable", history: [] };
  var f = fixture({ existingSession: session, persistenceError: true });
  f.handler.handleMessage(f.ws, { type: "home_mate_model_set", mateId: "mate-a", vendor: "claude", model: "sonnet", sessionId: "draft-save-error", requestId: "draft-save" });
  await settle();
  var result = f.messages.filter(function (message) { return message.type === "home_mate_model_result"; })[0];
  assert.equal(result.ok, true);
  assert.equal(result.sessionApplied, false);
  assert.match(result.sessionReason, /could not be persisted/);
  assert.equal(session.vendor, "claude");
  assert.equal(session.model, "fable");
});

test("vendor-only, unavailable-vendor, and cross-catalog selections never persist", async function () {
  var options = {
    vendors: [
      { id: "claude", displayName: "Claude", installed: true },
      { id: "codex", displayName: "Codex", installed: true },
    ],
    catalogs: { codex: { status: "ready", models: ["gpt-5.6"], error: "" } },
  };
  var missing = fixture(options);
  missing.handler.handleMessage(missing.ws, { type: "home_mate_model_set", mateId: "mate-a", vendor: "codex", model: "", requestId: "missing-model" });
  await settle();
  assert.equal(missing.updates.length, 0);
  assert.equal(missing.messages[0].ok, false);

  var unavailable = fixture(options);
  unavailable.handler.handleMessage(unavailable.ws, { type: "home_mate_model_set", mateId: "mate-a", vendor: "gemini", model: "gemini-2", requestId: "bad-vendor" });
  await settle();
  assert.equal(unavailable.updates.length, 0);
  assert.match(unavailable.messages[0].error, /not configured/);

  var mismatched = fixture(options);
  mismatched.handler.handleMessage(mismatched.ws, { type: "home_mate_model_set", mateId: "mate-a", vendor: "codex", model: "sonnet", requestId: "cross-catalog" });
  await settle();
  assert.equal(mismatched.updates.length, 0);
  assert.match(mismatched.messages[0].error, /not available/);
});

test("Mate model resolver repairs invalid records with the saved catalog default then first entry", async function () {
  var preferred = fixture({
    mateModel: "removed-model",
    catalog: { status: "ready", models: ["fable", "sonnet"], defaultModel: "sonnet", error: "" },
  });
  preferred.handler.handleMessage(preferred.ws, { type: "home_mate_new_session", mateId: "mate-a", requestId: "new-preferred" });
  await settle();
  assert.equal(preferred.getMate().model, "sonnet");
  assert.equal(preferred.created[0].model, "sonnet");
  assert.equal(preferred.messages[0].type, "mate_updated");
  assert.equal(preferred.messages[0].mate.model, "sonnet");

  var first = fixture({
    mateModel: "removed-model",
    catalog: { status: "ready", models: [{ displayName: "Unavailable heading" }, "fable", "sonnet"], defaultModel: "removed-default", error: "" },
  });
  first.handler.handleMessage(first.ws, { type: "home_mate_new_session", mateId: "mate-a", requestId: "new-first" });
  await settle();
  assert.equal(first.getMate().model, "fable");
  assert.equal(first.created[0].model, "fable");
});

test("catalog failures create no Home session and return an actionable correlated error", async function () {
  var f = fixture({ mateModel: null, catalog: { status: "error", models: [], error: "Vendor authentication expired. Reconnect the vendor and try again." } });
  f.handler.handleMessage(f.ws, { type: "home_mate_new_session", mateId: "mate-a", requestId: "new-error" });
  await settle();
  assert.equal(f.created.length, 0);
  assert.equal(f.messages[0].type, "home_mate_error");
  assert.equal(f.messages[0].requestId, "new-error");
  assert.equal(f.messages[0].code, "model_unavailable");
  assert.match(f.messages[0].text, /authentication expired/);
});

test("automatic Home open resolves a concrete model and enables the correlated send path", async function () {
  var f = fixture({ mateModel: null, catalog: { status: "ready", models: ["sonnet"], defaultModel: "sonnet", error: "" } });
  f.handler.handleMessage(f.ws, { type: "home_mate_open", mateId: "mate-a", requestId: "open-resolve" });
  await settle();
  var history = f.messages.filter(function (message) { return message.type === "home_mate_history"; })[0];
  assert.equal(history.requestId, "open-resolve");
  assert.equal(history.vendor, "claude");
  assert.equal(history.model, "sonnet");
  assert.equal(f.getMate().model, "sonnet");
  f.handler.handleMessage(f.ws, { type: "home_mate_send", mateId: "mate-a", sessionId: history.sessionId, requestId: "open-resolve", text: "Hello" });
  await settle();
  assert.deepEqual(f.dispatched, ["sonnet"]);
});

test("catalog outages do not block exact or default resume of concrete sessions", async function () {
  var exactSession = { localId: 1, cliSessionId: "exact-ready", ownerId: "u1", vendor: "codex", model: "gpt-5.6", history: [], lastActivity: 20 };
  var exact = fixture({ mateModel: null, existingSession: exactSession, catalog: { status: "error", models: [], error: "Vendor authentication expired." } });
  exact.handler.handleMessage(exact.ws, { type: "home_mate_session_open", mateId: "mate-a", sessionId: "exact-ready", requestId: "exact-offline" });
  await settle();
  assert.equal(exact.messages[0].type, "home_mate_history");
  assert.equal(exact.messages[0].model, "gpt-5.6");
  assert.equal(exact.messages[0].requestId, "exact-offline");
  assert.deepEqual(exact.saved, []);
  assert.deepEqual(exact.updates, []);

  var latestSession = { localId: 1, cliSessionId: "latest-ready", ownerId: "u1", vendor: "claude", model: "sonnet", history: [], lastActivity: 30 };
  var latest = fixture({ mateModel: null, existingSession: latestSession, catalog: { status: "error", models: [], error: "Vendor authentication expired." } });
  latest.handler.handleMessage(latest.ws, { type: "home_mate_open", mateId: "mate-a", requestId: "default-offline" });
  await settle();
  assert.equal(latest.messages[0].type, "home_mate_history");
  assert.equal(latest.messages[0].sessionId, "latest-ready");
  assert.equal(latest.messages[0].model, "sonnet");
  assert.deepEqual(latest.saved, []);
  assert.deepEqual(latest.updates, []);
});

test("legacy model-less sessions commit once while concrete sessions remain immutable", async function () {
  var legacySession = { localId: 1, cliSessionId: "legacy", ownerId: "u1", vendor: "claude", model: null, history: [], lastActivity: 20 };
  var legacy = fixture({ mateModel: "sonnet", existingSession: legacySession });
  legacy.handler.handleMessage(legacy.ws, { type: "home_mate_session_open", mateId: "mate-a", sessionId: "legacy", requestId: "legacy-open" });
  await settle();
  assert.equal(legacySession.model, "sonnet");
  assert.deepEqual(legacy.saved, [1]);
  legacy.handler.handleMessage(legacy.ws, { type: "home_mate_session_open", mateId: "mate-a", sessionId: "legacy", requestId: "legacy-reopen" });
  await settle();
  assert.deepEqual(legacy.saved, [1]);

  var concreteSession = { localId: 1, cliSessionId: "concrete", ownerId: "u1", vendor: "claude", model: "fable", history: [], lastActivity: 20 };
  var concrete = fixture({ mateModel: "sonnet", existingSession: concreteSession });
  concrete.handler.handleMessage(concrete.ws, { type: "home_mate_session_open", mateId: "mate-a", sessionId: "concrete", requestId: "concrete-open" });
  await settle();
  assert.equal(concreteSession.model, "fable");
  assert.deepEqual(concrete.saved, []);
});

test("legacy session mutation waits for metadata persistence capability", async function () {
  var session = { localId: 1, cliSessionId: "legacy-no-save", ownerId: "u1", vendor: "legacy-vendor", model: null, history: [], lastActivity: 20 };
  var f = fixture({ mateModel: "sonnet", existingSession: session, noPersistence: true });
  f.handler.handleMessage(f.ws, { type: "home_mate_session_open", mateId: "mate-a", sessionId: "legacy-no-save", requestId: "legacy-no-save-open" });
  await settle();
  assert.equal(session.vendor, "legacy-vendor");
  assert.equal(session.model, null);
  assert.deepEqual(f.updates, []);
  assert.equal(f.messages[0].type, "home_mate_error");
  assert.equal(f.messages[0].requestId, "legacy-no-save-open");
  assert.equal(f.messages[0].sessionId, "legacy-no-save");
  assert.equal(f.messages[0].code, "model_unavailable");
  assert.match(f.messages[0].text, /persist/);
});

test("Home send resolves a legacy model before dispatch and blocks when no catalog is available", async function () {
  var session = { localId: 1, cliSessionId: "legacy-send", ownerId: "u1", vendor: "claude", model: "", history: [], lastActivity: 20 };
  var ready = fixture({ mateModel: "sonnet", existingSession: session });
  ready.handler.handleMessage(ready.ws, { type: "home_mate_session_open", mateId: "mate-a", sessionId: "legacy-send", requestId: "send-open" });
  await settle();
  ready.handler.handleMessage(ready.ws, { type: "home_mate_send", mateId: "mate-a", sessionId: "legacy-send", requestId: "send-open", text: "Hello" });
  await settle();
  assert.deepEqual(ready.dispatched, ["sonnet"]);

  var blockedSession = { localId: 1, cliSessionId: "blocked", ownerId: "u1", vendor: "claude", model: null, history: [], lastActivity: 20 };
  var blocked = fixture({ mateModel: null, existingSession: blockedSession, catalog: { status: "empty", models: [], error: "No installed models." } });
  blocked.ws._homeChatTap = { mateId: "mate-a", sessionId: 1, sessionReference: "blocked", requestId: "blocked-send" };
  blocked.handler.handleMessage(blocked.ws, { type: "home_mate_send", mateId: "mate-a", sessionId: "blocked", requestId: "blocked-send", text: "Hello" });
  await settle();
  assert.deepEqual(blocked.dispatched, []);
  assert.equal(blocked.messages[0].code, "model_unavailable");
  assert.equal(blocked.messages[0].requestId, "blocked-send");
});

test("new Home sessions inherit the Mate model while existing sessions remain unchanged", async function () {
  var fresh = fixture({ mateModel: "sonnet" });
  fresh.handler.handleMessage(fresh.ws, { type: "home_mate_open", mateId: "mate-a" });
  await settle();
  fresh.handler.handleMessage(fresh.ws, { type: "home_mate_new_session", mateId: "mate-a" });
  await settle();
  assert.equal(fresh.created.length, 2);
  assert.deepEqual(fresh.created.map(function (options) { return [options.vendor, options.model]; }), [["claude", "sonnet"], ["claude", "sonnet"]]);

  var existingSession = { localId: 1, cliSessionId: "existing", ownerId: "u1", vendor: "claude", model: "fable", history: [], lastActivity: 20 };
  var existing = fixture({ mateModel: "sonnet", existingSession: existingSession });
  existing.handler.handleMessage(existing.ws, { type: "home_mate_open", mateId: "mate-a" });
  await settle();
  assert.equal(existing.created.length, 0);
  assert.equal(existingSession.model, "fable");
});

test("Mate model messages route through Home ownership and cannot bypass catalog validation", function () {
  var root = path.join(__dirname, "..");
  var projectSource = fs.readFileSync(path.join(root, "lib/project.js"), "utf8");
  var schemaSource = fs.readFileSync(path.join(root, "lib/ws-schema.js"), "utf8");
  var matesSource = fs.readFileSync(path.join(root, "lib/server-mates.js"), "utf8");
  var homeSource = fs.readFileSync(path.join(root, "lib/server-home-chat.js"), "utf8");
  var homeModelsSource = fs.readFileSync(path.join(root, "lib/server-home-models.js"), "utf8");
  assert.match(projectSource, /home_mate_models_get[\s\S]*home_mate_model_set[\s\S]*opts\.onDmMessage/);
  assert.match(schemaSource, /"home_mate_models_get"[\s\S]*"home_mate_model_set"[\s\S]*"home_mate_models_state"[\s\S]*"home_mate_model_result"/);
  assert.match(homeSource, /findMateProject\(userId, msg\.mateId, true\)/);
  assert.match(homeModelsSource, /catalogModel\(catalog\.models \|\| \[\], msg\.model\)/);
  assert.match(matesSource, /hasOwnProperty\.call\(msg\.updates, "model"\)[\s\S]*Use the Mate model selector/);
});