test/home-mate-creation.test.js
Rama main · réplica de lectura del repositorio original.
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 attachHomeMateCreation = require("../lib/server-home-mate-creation").attachHomeMateCreation;
var prompt = require("../lib/server-home-mate-creation").INITIATION_PROMPT;
var attachProposal = require("../lib/project-mate-creation-proposal").attachMateCreationProposal;
var readyCreation = require("../lib/mate-ready-creation");
var homeEvents = require("../lib/server-home-chat-events");
var firstQuestion = require("../lib/server-home-mate-creation").FIRST_QUESTION;
var createSDKBridge = require("../lib/sdk-bridge").createSDKBridge;
var createSessionManager = require("../lib/sessions").createSessionManager;
function settle() { return new Promise(function (resolve) { setImmediate(resolve); }); }
test("Mate creation mode and phase survive a session manager restart", function (t) {
var root = fs.mkdtempSync(path.join(os.tmpdir(), "clay-mate-creation-restore-"));
t.after(function () { fs.rmSync(root, { recursive: true, force: true }); });
var options = { cwd: path.join(root, "project"), sessionsBase: path.join(root, "sessions"), cliSessionsDir: path.join(root, "cli"), send: function () {} };
var first = createSessionManager(options);
var session = first.createSession({ vendor: "claude" });
session.title = "New Mate";
session.mateCreationMode = true;
session.homeMateCreationPhase = "interview";
assert.equal(first.saveSessionFile(session), true);
var second = createSessionManager(options);
var restored = Array.from(second.sessions.values()).find(function (item) { return item.title === "New Mate"; });
assert.ok(restored);
assert.equal(restored.mateCreationMode, true);
assert.equal(restored.homeMateCreationPhase, "interview");
});
test("Clay-led Mate creation presents its fixed opening question before starting a model", async function () {
var sessions = new Map();
var starts = [];
var pushes = [];
var savedModes = [];
var ordering = [];
var nextId = 1;
var manager = {
sessions: sessions,
createSessionRaw: function (options) { var session = { localId: nextId++, ownerId: options.ownerId, vendor: options.vendor, model: options.model, effort: options.effort || null, history: [], pendingAskUser: {}, isProcessing: false }; sessions.set(session.localId, session); ordering.push({ type: "created", session: session }); return session; },
sendAndRecord: function (session, event) { session.history.push(event); ordering.push({ type: "recorded", event: event, session: session }); },
switchSession: function (id, target) { var current = sessions.get(id); ordering.push({ type: "switched", id: id, target: target, title: current.title, mateCreationMode: current.mateCreationMode }); },
saveSessionFile: function (session) { savedModes.push({ mode: session.mateCreationMode, phase: session.homeMateCreationPhase, historyLength: session.history.length }); return true; },
};
var found = { mate: { id: "clay", builtinKey: "clay" }, ctx: { getSessionManager: function () { return manager; }, sdk: { pushMessage: function (session, text) { pushes.push({ session: session, text: text }); return true; }, startQuery: function (session, text) { starts.push({ session: session, text: text }); } } } };
var histories = [];
var persisted = [];
var creation = attachHomeMateCreation({
findMateProject: function () { return found; }, resolveHomeSession: function (value, userId, ref) { return ref === "local:1" ? sessions.get(1) : null; }, sessionReference: function (session) { return "local:" + session.localId; },
setupTap: function (ws, value, localId, requestId) { ws._homeChatTap = { mateId: value.mate.id, sessionId: localId, requestId: requestId }; }, sendHistory: function (ws, value, session) { histories.push(session); }, sendSessionList: function () {}, sendError: function () {}, sendModelError: function () {},
homeModels: { resolveMateModel: function () { return Promise.resolve({ vendor: "codex", model: "gpt-6-astra", effort: "high" }); } },
persistActiveSession: function (target, value, userId, active) { persisted.push({ target: target, found: value, userId: userId, session: active }); },
});
var ws = { _homeMateCreationRequests: {}, readyState: 1 };
creation.start(ws, "u1", { requestId: "create-1" });
await settle();
creation.start(ws, "u1", { requestId: "create-1" });
await settle();
assert.equal(sessions.size, 1);
var session = Array.from(sessions.values())[0];
assert.equal(session.title, "New Mate");
assert.equal(session.mateCreationMode, true);
assert.equal(session.homeMateCreationPhase, "interview");
var switchStep = ordering.find(function (step) { return step.type === "switched"; });
assert.equal(switchStep.title, "New Mate");
assert.equal(switchStep.mateCreationMode, true);
assert.equal(switchStep.target, ws);
assert.deepEqual(persisted, [{ target: ws, found: found, userId: "u1", session: session }]);
assert.equal(ordering.filter(function (step) { return step.type === "recorded"; }).every(function (step) { return ordering.indexOf(step) < ordering.indexOf(switchStep); }), true);
assert.deepEqual(savedModes[0], { mode: true, phase: "interview", historyLength: 0 });
assert.deepEqual({ vendor: session.vendor, model: session.model, effort: session.effort }, { vendor: "codex", model: "gpt-6-astra", effort: "high" });
assert.equal(starts.length, 0);
assert.equal(session.history.some(function (event) { return event.type === "user_message"; }), false);
assert.equal(session.history.filter(function (event) { return event.type === "home_mate_creation_started"; }).length, 1);
var opening = session.history.filter(function (event) { return event.type === "tool_executing"; })[0];
assert.equal(opening.input.questions[0].question, firstQuestion.question);
assert.deepEqual(opening.input.questions[0].options, []);
assert.equal(histories.length >= 1, true);
creation.respondToQuestion(ws, "u1", { mateId: "clay", sessionId: "local:1", requestId: "create-1", toolId: opening.id, answers: { 0: "A research partner who challenges my assumptions" } });
await settle();
assert.equal(starts.length, 1);
assert.match(starts[0].text, /A research partner who challenges my assumptions/);
assert.equal(session.history.filter(function (event) { return event.type === "ask_user_answered"; }).length, 1);
assert.match(prompt, /Clay facilitating the creation/);
assert.doesNotMatch(prompt, /\/clay-mate-interview/);
assert.match(prompt, /complete Mate interview contract/);
assert.match(prompt, /Do not invoke Skill, read SKILL\.md/);
assert.match(prompt, /Before the user answers again, your first and only action must be the next most useful AskUserQuestion/);
assert.match(prompt, /When that tool returns the user's answer, continue the interview normally/);
assert.match(prompt, /do not use Bash, Read, Glob, Grep/);
assert.match(prompt, /do not bulk-read knowledge files/);
assert.match(prompt, /exactly one focused question at a time/);
assert.match(prompt, /already answered the fixed opening question/);
assert.match(prompt, /propose_mate/);
assert.match(prompt, /Do not write files/);
session.isProcessing = false;
session.onTurnDone(session);
await settle();
assert.equal(starts.length, 1);
assert.equal(pushes.length, 1);
assert.match(pushes[0].text, /Continue the Mate creation interview/);
});
test("an answered AI interview question resumes in the exact session when its provider turn ends", function () {
var starts = [];
var pushes = [];
var session = {
localId: 9,
ownerId: "u1",
mateCreationMode: true,
homeMateCreationPhase: "interview",
vendor: "codex",
model: "gpt",
isProcessing: false,
history: [
{ type: "home_mate_creation_query_started", internal: true },
{ type: "tool_executing", name: "AskUserQuestion", id: "provider-question-1", input: { questions: [{ header: "Role", question: "Who should it help?", options: [] }] } },
{ type: "ask_user_answered", toolId: "provider-question-1", answers: { 0: "My team" } },
{ type: "result" },
{ type: "done" },
],
};
var manager = { sessions: new Map([[9, session]]), sendAndRecord: function (target, event) { target.history.push(event); } };
var found = { mate: { id: "clay", builtinKey: "clay" }, ctx: { getSessionManager: function () { return manager; }, sdk: { pushMessage: function (target, text) { pushes.push({ target: target, text: text }); return false; }, startQuery: function (target, text) { starts.push({ target: target, text: text }); } } } };
var creation = attachHomeMateCreation({
findMateProject: function () { return found; },
resolveHomeSession: function () { return session; },
sessionReference: function () { return "durable-session"; },
setupTap: function (ws) { ws._homeChatTap = { mateId: "clay", sessionId: 9, requestId: "resume-1" }; },
sendHistory: function () {}, sendSessionList: function () {}, sendError: function () {}, sendModelError: function () {},
homeModels: { resolveMateModel: function () { throw new Error("model lookup should not run"); } },
});
creation.start({ readyState: 1 }, "u1", { requestId: "resume-1", sessionId: "durable-session" });
assert.equal(starts.length, 1);
assert.equal(pushes.length, 1);
assert.equal(starts[0].target, session);
assert.match(starts[0].text, /latest structured answer/);
assert.equal(session.isProcessing, true);
});
test("the exact Mate creation proposal tool opens its approval card without a permission prompt", async function () {
var bridge = createSDKBridge({
cwd: process.cwd(),
sessionManager: { currentPermissionMode: "default" },
adapter: { vendor: "codex" },
send: function () {},
onProcessingChanged: function () {},
});
var input = { name: "Atlas" };
var session = {
mateCreationMode: true,
history: [
{ type: "tool_executing", name: "AskUserQuestion", id: "provider-question-1" },
{ type: "ask_user_answered", toolId: "provider-question-1", answers: { 0: "A planning partner" } },
],
};
var decision = await bridge.handleCanUseTool(session, "propose_mate", input, {});
assert.equal(decision.behavior, "allow");
assert.equal(decision.updatedInput, input);
assert.equal(bridge.checkToolWhitelist("propose_mate", input), null);
});
test("Mate proposal is exact-session bound and creates only after approval", async function () {
var records = [];
var created = [];
var proposal = attachProposal({
isMate: true, isHostAgent: true, isMultiUser: function () { return true; }, getProjectOwnerId: function () { return "u1"; },
recordSessionEvent: function (session, event) { session.history.push(event); records.push(event); },
getVendorModelCatalog: function (ws, vendor) {
if (vendor === "codex") return Promise.resolve({ status: "ready", defaultModel: "gpt-5.6-terra", models: [{ value: "gpt-5.6-terra", displayName: "GPT-5.6 Terra" }, { value: "gpt-5.6-sol", displayName: "GPT-5.6 Sol" }] });
return Promise.resolve({ status: "ready", defaultModel: "sonnet", models: [{ value: "sonnet", displayName: "Sonnet" }, { value: "opus", displayName: "Opus" }] });
},
createReadyMate: function (ws, userId, definition) { created.push({ ws: ws, userId: userId, definition: definition }); return { id: "mate-new", name: definition.name }; },
});
var session = { localId: 1, ownerId: "u1", mateCreationMode: true, vendor: "claude", history: [{ type: "ask_user_answered", toolId: "q1" }] };
var other = { localId: 2, ownerId: "u1", mateCreationMode: true, history: [] };
var args = { name: "Atlas", bio: "Plans difficult work", relationship: "Partner", activities: "[\"Planning\"]", communicationStyle: "[\"Direct\"]", autonomy: "Ask before changes", identityMarkdown: "# Identity\n\n" + "I collaborate carefully and explain decisions. ".repeat(8) };
var resultPromise = proposal.callBridgeTool(session, "propose_mate", args);
assert.equal(records.length, 1);
assert.equal(records[0].type, "mate_creation_proposal");
assert.equal(created.length, 0);
assert.equal(proposal.handleHomeMessage({ _clayUser: { id: "u1" } }, { proposalId: records[0].proposal.proposalId, action: "create" }, other), false);
assert.equal(created.length, 0);
assert.equal(proposal.handleHomeMessage({ _clayUser: { id: "u1" } }, { proposalId: records[0].proposal.proposalId, action: "create" }, session), true);
var result = await resultPromise;
assert.equal(created.length, 1);
assert.equal(created[0].userId, "u1");
assert.equal(created[0].definition.model, "opus");
assert.equal(session.homeMateCreationPhase, "created");
assert.match(result.content[0].text, /Mate created/);
var codexSession = { localId: 3, ownerId: "u1", mateCreationMode: true, vendor: "codex", history: [{ type: "ask_user_answered", toolId: "q2" }] };
var codexResultPromise = proposal.callBridgeTool(codexSession, "propose_mate", Object.assign({}, args, { name: "Nova" }));
var codexProposal = records[records.length - 1].proposal;
assert.equal(proposal.handleHomeMessage({ _clayUser: { id: "u1" } }, { proposalId: codexProposal.proposalId, action: "create" }, codexSession), true);
await codexResultPromise;
assert.equal(created[1].definition.vendor, "codex");
assert.equal(created[1].definition.model, "gpt-5.6-sol");
});
test("ready Mate finalization writes a completed identity and rolls back failures", function () {
var root = fs.mkdtempSync(path.join(os.tmpdir(), "clay-ready-mate-"));
var registry = null;
var deleted = [];
var mates = {
createMate: function () { registry = { id: "mate-1", createdBy: "u1", createdAt: 10, profile: { avatarSeed: "seed" }, seedData: {} }; fs.mkdirSync(path.join(root, registry.id), { recursive: true }); return registry; },
getMateDir: function (ctx, id) { return path.join(root, id); },
enforceAllSections: function (file) { fs.appendFileSync(file, "\n<!-- system -->\n"); },
updateMate: function (ctx, id, updates) { registry = Object.assign({}, registry, updates); return registry; },
backupIdentity: function () {}, logIdentityChange: function () {}, deleteMate: function (ctx, id) { deleted.push(id); fs.rmSync(path.join(root, id), { recursive: true, force: true }); },
};
var identity = "# Identity\n\n" + "I am a deliberate planning partner. ".repeat(10);
var mate = readyCreation.createReadyMate(mates, {}, { name: "Atlas", bio: "Planning partner", relationship: "Partner", activities: ["Planning"], communicationStyle: ["Direct"], autonomy: "Ask before changes", identityMarkdown: identity, vendor: "claude", model: "opus" });
assert.equal(mate.status, "ready");
assert.equal(mate.profile.displayName, "Atlas");
assert.equal(mate.model, "opus");
assert.match(fs.readFileSync(path.join(root, "mate-1", "AGENTS.md"), "utf8"), /I am a deliberate planning partner/);
assert.match(fs.readFileSync(path.join(root, "mate-1", "mate.yaml"), "utf8"), /status: ready/);
assert.match(fs.readFileSync(path.join(root, "mate-1", "mate.yaml"), "utf8"), /model: "opus"/);
assert.deepEqual(deleted, []);
fs.rmSync(root, { recursive: true, force: true });
});
test("Mate interview history suppresses narration and restores questions and proposals", function () {
var messages = homeEvents.historyToHomeChat([
{ type: "delta", text: "I am using a skill." },
{ type: "tool_executing", name: "AskUserQuestion", id: "q1", input: { questions: [{ header: "Role", question: "What kind of Mate?", options: [] }] } },
{ type: "ask_user_answered", toolId: "q1", answers: { 0: "A planning partner" } },
{ type: "mate_creation_proposal", proposal: { proposalId: "p1", name: "Atlas" } },
], false, true);
assert.equal(messages.some(function (message) { return message.role === "assistant"; }), false);
assert.equal(messages[0].role, "question");
assert.equal(messages[0].flow, "mate_creation");
assert.equal(messages[0].status, "answered");
assert.equal(messages[1].role, "mate_proposal");
var resolved = homeEvents.transformEvent({ type: "mate_creation_proposal_resolved", proposalId: "p1", action: "create", mateId: "created-mate", mateName: "Atlas" }, "clay-id", { mateCreationMode: true, model: "opus", vendor: "claude" }, "request-1", "session-1");
assert.equal(resolved.mateId, "clay-id");
assert.equal(resolved.createdMateId, "created-mate");
});