var test = require("node:test"); var assert = require("node:assert/strict"); var fs = require("node:fs"); var os = require("node:os"); var path = require("node:path"); var childProcess = require("node:child_process"); var attachPreferences = require("../lib/users-default-ai-preferences").attachDefaultAiPreferences; var runtime = require("../lib/default-ai-runtime"); function preferenceHarness(options) { options = options || {}; var data = options.data || { users: [{ id: "u1" }, { id: "u2" }] }; var config = options.config || {}; var saves = 0; var prefs = attachPreferences({ loadUsers: function () { return data; }, saveUsers: function (next) { data = next; saves++; }, isMultiUser: function () { return options.multiUser === true; }, configModule: { loadConfig: function () { return config; }, saveConfig: function (next) { config = next; saves++; }, }, }); return { prefs: prefs, data: function () { return data; }, config: function () { return config; }, saves: function () { return saves; } }; } test("Default AI preferences are isolated and migrate legacy values on read only", function () { var h = preferenceHarness({ multiUser: true, data: { users: [{ id: "u1", scheduledTaskInterviewEngine: { vendor: "codex", model: "old", effort: "low" } }, { id: "u2" }] } }); var first = h.prefs.getDefaultAiPreference("u1"); assert.deepStrictEqual(first.preference, { vendor: "codex", model: "old", effort: "low" }); assert.equal(first.source, "legacy"); assert.equal(h.saves(), 0); assert.equal(h.prefs.getDefaultAiPreference("u2").present, false); }); test("Default AI set rejects malformed text and unknown users", function () { var h = preferenceHarness({ multiUser: true }); assert.match(h.prefs.setDefaultAiPreference("u1", { vendor: "codex", model: "bad\nmodel", effort: "medium" }).error, /model/i); assert.match(h.prefs.setDefaultAiPreference("missing", { vendor: "codex", model: "x", effort: "medium" }).error, /User not found/); assert.equal(h.saves(), 0); var failing = attachPreferences({ loadUsers: function () { return { users: [{ id: "u1", keep: true }] }; }, saveUsers: function () { throw new Error("disk full"); }, isMultiUser: function () { return true; } }); assert.match(failing.setDefaultAiPreference("u1", { vendor: "codex", model: "gpt", effort: "medium" }).error, /disk full/); var original = { id: "u1", keep: true }; var safe = attachPreferences({ loadUsers: function () { return { users: [original] }; }, saveUsers: function () { throw new Error("read-only"); }, isMultiUser: function () { return true; } }); safe.setDefaultAiPreference("u1", { vendor: "codex", model: "gpt", effort: "medium" }); assert.equal(Object.prototype.hasOwnProperty.call(original, "defaultAiPreference"), false); var readFailure = attachPreferences({ loadUsers: function () { throw new Error("users unreadable"); }, isMultiUser: function () { return true; } }); assert.match(readFailure.getDefaultAiPreference("u1").error, /users unreadable/); }); test("Single-user persistence preserves daemon config and uses explicit presence", function () { var h = preferenceHarness({ config: { port: 1, scheduledTaskInterviewEngine: { vendor: "claude", model: "legacy" } } }); var legacy = h.prefs.getDefaultAiPreference("default"); assert.equal(legacy.source, "legacy"); assert.equal(h.prefs.setDefaultAiPreference("default", { vendor: "codex", model: "", effort: "" }).ok, true); assert.deepStrictEqual(h.config(), { port: 1, scheduledTaskInterviewEngine: { vendor: "claude", model: "legacy" }, defaultAiPreference: { vendor: "codex", model: "", effort: "" } }); assert.equal(h.prefs.getDefaultAiPreference("default").source, "explicit"); }); function catalog(vendor, models, defaultModel) { return { vendor: vendor, status: "ready", models: models, defaultModel: defaultModel || "" }; } test("Runtime retains an explicit low-tier model and rejects unavailable choices", async function () { var getPreference = function () { return { present: true, source: "explicit", preference: { vendor: "claude", model: "haiku", effort: "low" } }; }; var result = await runtime.resolveDefaultAiRuntime({ userId: "u1", multiUser: true, getPreference: getPreference, installedVendors: ["claude"], getCatalog: function () { return catalog("claude", ["opus", "haiku"], "opus"); } }); assert.deepStrictEqual({ vendor: result.vendor, model: result.model, effort: result.effort }, { vendor: "claude", model: "haiku", effort: "low" }); result = await runtime.resolveDefaultAiRuntime({ userId: "u1", multiUser: true, getPreference: function () { return { present: true, preference: { vendor: "claude", model: "gone", effort: "medium" } }; }, installedVendors: ["claude"], getCatalog: function () { return catalog("claude", ["opus"], "opus"); } }); assert.equal(result.ready, false); assert.match(result.error, /unavailable/); result = await runtime.resolveDefaultAiRuntime({ getPreference: function () { return { present: true, preference: { vendor: "codex", model: "gpt-6-astra", effort: "banana" } }; }, installedVendors: ["codex"], getCatalog: function () { return catalog("codex", [{ value: "gpt-6-astra", supportedEffortLevels: ["low", { value: "medium" }] }], "gpt-6-astra"); } }); assert.equal(result.ready, false); assert.match(result.error, /effort/i); result = await runtime.resolveDefaultAiRuntime({ getPreference: function () { return { present: true, preference: { vendor: "codex", model: "gpt-6-astra", effort: "low" } }; }, installedVendors: ["codex"], getCatalog: function () { return catalog("codex", [{ value: "gpt-6-astra", supportedEffortLevels: [] }], "gpt-6-astra"); } }); assert.equal(result.ready, false); }); test("Runtime requires authenticated identity and does not mutate while resolving", async function () { var called = false; var result = await runtime.resolveDefaultAiRuntime({ multiUser: true, getPreference: function () { called = true; return { present: false }; }, installedVendors: ["claude"] }); assert.equal(result.ready, false); assert.equal(called, false); }); test("Runtime ranks only offered known families and uses unknown vendor catalog default", async function () { assert.equal(runtime.selectTopModel("claude", catalog("claude", ["sonnet", "opus", "haiku"], "sonnet")), "opus"); assert.equal(runtime.selectTopModel("codex", catalog("codex", ["gpt-5", "gpt-6-sol", "gpt-6-astra"], "gpt-5")), "gpt-6-astra"); assert.equal(runtime.selectTopModel("codex", catalog("codex", ["gpt-5", "gpt-6-sol"], "gpt-5")), "gpt-6-sol"); assert.equal(runtime.selectTopModel("unknown", catalog("unknown", [{ value: "a", resolvedModel: "unknown-a" }, { value: "b" }], "unknown-a")), "a"); var result = await runtime.resolveDefaultAiRuntime({ getPreference: function () { return { present: false }; }, installedVendors: ["claude"], getCatalog: function () { return catalog("claude", [{ value: "haiku", resolvedModel: "claude-haiku" }, { value: "opus" }], "claude-haiku"); } }); assert.equal(result.model, "opus"); }); test("Runtime accepts catalog aliases and preserves explicit effort only when supplied", async function () { var result = await runtime.resolveDefaultAiRuntime({ getPreference: function () { return { present: true, preference: { vendor: "claude", model: "claude-opus-4", effort: "" } }; }, installedVendors: ["claude"], getCatalog: function () { return catalog("claude", [{ value: "opus", resolvedModel: "claude-opus-4" }], "opus"); } }); assert.equal(result.model, "opus"); assert.equal(result.effort, ""); result = await runtime.resolveDefaultAiRuntime({ getPreference: function () { return { present: true, preference: { vendor: "codex", model: "gpt-6-astra", effort: "" } }; }, installedVendors: ["codex"], getCatalog: function () { return { status: "ready", models: [{ value: "gpt-6-astra", defaultReasoningEffort: "high", supportedEffortLevels: ["medium", "high"] }] }; } }); assert.equal(result.effort, "", "an explicit Provider default selection remains empty"); result = await runtime.resolveDefaultAiRuntime({ getPreference: function () { return { present: false }; }, installedVendors: ["codex"], getCatalog: function () { return { status: "ready", models: [{ value: "gpt-6-astra", defaultReasoningEffort: "medium", supportedEffortLevels: ["low", "medium"] }], defaultEffort: "banana" }; } }); assert.equal(result.effort, "medium"); result = await runtime.resolveDefaultAiRuntime({ getPreference: function () { return { present: false }; }, installedVendors: ["codex"], getCatalog: function () { return { status: "ready", models: [{ value: "gpt-6-astra", defaultReasoningEffort: "banana", supportedEffortLevels: ["low", "medium"] }], defaultEffort: "medium" }; } }); assert.equal(result.effort, "medium"); }); test("Default AI disk persistence works under an isolated CLAY_HOME", function () { var dir = fs.mkdtempSync(path.join(os.tmpdir(), "clay-default-ai-")); var file = path.join(dir, "users.json"); var data = { users: [{ id: "u1" }] }; var h = preferenceHarness({ multiUser: true, data: data }); assert.equal(h.prefs.setDefaultAiPreference("u1", { vendor: "codex", model: "gpt", effort: "medium" }).ok, true); fs.writeFileSync(file, JSON.stringify(h.data())); assert.deepStrictEqual(JSON.parse(fs.readFileSync(file, "utf8")).users[0].defaultAiPreference, { vendor: "codex", model: "gpt", effort: "medium" }); fs.rmSync(dir, { recursive: true, force: true }); }); test("Default AI uses the real config module with temporary CLAY_HOME and empty CLAY_DEV", function () { var dir = fs.mkdtempSync(path.join(os.tmpdir(), "clay-default-ai-config-")); var script = [ "var config = require(" + JSON.stringify(path.join(process.cwd(), "lib/config")) + ");", "var prefs = require(" + JSON.stringify(path.join(process.cwd(), "lib/users-default-ai-preferences")) + ").attachDefaultAiPreferences({ loadUsers: function () { return { users: [] }; }, saveUsers: function () {}, configModule: config });", "prefs.setDefaultAiPreference('default', { vendor: 'codex', model: 'gpt', effort: 'medium' });", "if (!config.loadConfig()) process.exit(2);", ].join("\n"); try { var result = childProcess.spawnSync(process.execPath, ["-e", script], { env: Object.assign({}, process.env, { CLAY_HOME: dir, CLAY_CONFIG: path.join(dir, "daemon.json"), CLAY_DEV: "" }), encoding: "utf8" }); assert.equal(result.status, 0, result.stderr); var saved = JSON.parse(fs.readFileSync(path.join(dir, "daemon.json"), "utf8")); assert.equal(saved.port, undefined); assert.deepStrictEqual(saved.defaultAiPreference, { vendor: "codex", model: "gpt", effort: "medium" }); } finally { fs.rmSync(dir, { recursive: true, force: true }); } });