Q173: agents work in the company's or task's language, not the display language
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
538d834647
commit
3f14e3f706
36 files changed
+268
-105
No files matched your search
@@ -173,11 +173,21 @@ describe("agent instruction wiring", () => {
|
||||
expect(capturedInstructions).toContain("Connected external services you carry in this run: GitHub");
|
||||
expect(capturedInstructions).toContain("list_connectors");
|
||||
|
||||
// Switching the app language redirects the whole company to Chinese.
|
||||
const rerun = async () => {
|
||||
connection.db.prepare("update task_runs set state = 'failed' where org_id = ?").run(orgId);
|
||||
await execution.runTask({ orgId, taskId });
|
||||
return capturedInstructions;
|
||||
};
|
||||
// The display language changes only what the operator sees.
|
||||
organization.updateUiPreferences({ language: "zh" });
|
||||
connection.db.prepare("update task_runs set state = 'failed' where org_id = ?").run(orgId);
|
||||
await execution.runTask({ orgId, taskId });
|
||||
expect(capturedInstructions).toContain("公司的工作语言是简体中文");
|
||||
expect(await rerun()).toContain("The working language is English");
|
||||
expect(capturedInstructions).not.toContain("公司的工作语言是简体中文");
|
||||
// The company's working language redirects its agents, and a task can keep its own.
|
||||
connection.db.prepare("update organizations set work_language = 'zh' where id = ?").run(orgId);
|
||||
expect(await rerun()).toContain("公司的工作语言是简体中文");
|
||||
connection.db.prepare("update tasks set work_language = 'en' where id = ?").run(taskId);
|
||||
expect(await rerun()).toContain("The working language is English");
|
||||
expect(capturedInstructions).not.toContain("公司的工作语言是简体中文");
|
||||
});
|
||||
|
||||
test("a new department head's task prompt tells them to staff their department, not to work as an IC", async () => {
|
||||
|
||||
@@ -171,7 +171,7 @@ describe("built-in skills", () => {
|
||||
h.skills.bind({ orgId: h.orgId, agentId, skillId: skill.id, revisionId: skill.revision_id });
|
||||
});
|
||||
const prompt = (runId: string) => {
|
||||
const id = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId, runId, maxTokens: SKILL_TOKENS, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true }));
|
||||
const id = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId, runId, maxTokens: SKILL_TOKENS, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true, language: "en" }));
|
||||
return (h.connection.db.prepare("select compiled_prompt from skill_runtime_snapshots where id = ?").get(id) as { compiled_prompt: string }).compiled_prompt;
|
||||
};
|
||||
expect(prompt("run_before")).toContain("# Skill: Backend Architect");
|
||||
@@ -265,7 +265,7 @@ describe("built-in skills", () => {
|
||||
expect(hired).toContain("Unknown skills skipped: no-such-skill");
|
||||
|
||||
const danaId = (h.connection.db.prepare("select id from agents where org_id = ? and name = 'Dana Lee'").get(h.orgId) as { id: string }).id;
|
||||
const danaSnapshot = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId: danaId, runId: "run_dana", maxTokens: SKILL_TOKENS, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true }));
|
||||
const danaSnapshot = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId: danaId, runId: "run_dana", maxTokens: SKILL_TOKENS, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true, language: "en" }));
|
||||
const danaPrompt = (h.connection.db.prepare("select compiled_prompt from skill_runtime_snapshots where id = ?").get(danaSnapshot) as { compiled_prompt: string }).compiled_prompt;
|
||||
expect(danaPrompt.indexOf("# Skill: humanizer")).toBe(0);
|
||||
expect(danaPrompt).toContain("# Skill: Backend Architect");
|
||||
@@ -274,15 +274,15 @@ describe("built-in skills", () => {
|
||||
expect(danaPrompt.length).toBeLessThan(SKILL_TOKENS * 3 + 5_000);
|
||||
|
||||
// The CEO carries humanizer too - forced by resolution, not by binding.
|
||||
expect(h.skills.hasRuntimeSkills(h.orgId, ceoId, true)).toBe(true);
|
||||
const ceoSnapshot = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId: ceoId, runId: "run_ceo", maxTokens: SKILL_TOKENS, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true }));
|
||||
expect(h.skills.hasRuntimeSkills(h.orgId, ceoId, true, "en")).toBe(true);
|
||||
const ceoSnapshot = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId: ceoId, runId: "run_ceo", maxTokens: SKILL_TOKENS, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true, language: "en" }));
|
||||
const ceoPrompt = (h.connection.db.prepare("select compiled_prompt from skill_runtime_snapshots where id = ?").get(ceoSnapshot) as { compiled_prompt: string }).compiled_prompt;
|
||||
expect(ceoPrompt).toContain("# Skill: humanizer");
|
||||
expect(ceoPrompt).not.toContain("# Skill: Backend Architect");
|
||||
|
||||
// Technical and structured work drops house style; a CEO without grants then carries nothing.
|
||||
expect(h.skills.hasRuntimeSkills(h.orgId, ceoId, false)).toBe(false);
|
||||
const technicalSnapshot = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId: danaId, runId: "run_dana_spec", maxTokens: SKILL_TOKENS, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: false }));
|
||||
expect(h.skills.hasRuntimeSkills(h.orgId, ceoId, false, "en")).toBe(false);
|
||||
const technicalSnapshot = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId: danaId, runId: "run_dana_spec", maxTokens: SKILL_TOKENS, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: false, language: "en" }));
|
||||
const technicalPrompt = (h.connection.db.prepare("select compiled_prompt from skill_runtime_snapshots where id = ?").get(technicalSnapshot) as { compiled_prompt: string }).compiled_prompt;
|
||||
expect(technicalPrompt).not.toContain("# Skill: humanizer");
|
||||
expect(technicalPrompt.indexOf("# Skill: Backend Architect")).toBe(0);
|
||||
@@ -295,7 +295,7 @@ describe("built-in skills", () => {
|
||||
h.connection.close();
|
||||
});
|
||||
|
||||
test("the forced humanizer follows the app language, with cross-language fallback", () => {
|
||||
test("the forced humanizer follows the working language, with cross-language fallback", () => {
|
||||
const h = harness();
|
||||
const staged = stageBuiltinSkillsLibrary(h.sourceRoot, h.libraryRoot);
|
||||
h.connection.transaction(() => importStagedSkills(h.skills, h.orgId, staged));
|
||||
@@ -306,28 +306,27 @@ describe("built-in skills", () => {
|
||||
modelProviderId: null, modelId: null, budgetCents: 1000, managerAgentId: null,
|
||||
});
|
||||
});
|
||||
const promptFor = (runId: string) => {
|
||||
const snapshotId = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId, runId, maxTokens: SKILL_TOKENS, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true }));
|
||||
const promptFor = (runId: string, language: "en" | "zh") => {
|
||||
const snapshotId = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId, runId, maxTokens: SKILL_TOKENS, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true, language }));
|
||||
return (h.connection.db.prepare("select compiled_prompt from skill_runtime_snapshots where id = ?").get(snapshotId) as { compiled_prompt: string }).compiled_prompt;
|
||||
};
|
||||
|
||||
// English UI (default): the English humanizer, and only that one.
|
||||
const englishPrompt = promptFor("run_lang_en");
|
||||
// English work: the English humanizer, and only that one.
|
||||
const englishPrompt = promptFor("run_lang_en", "en");
|
||||
expect(englishPrompt).toContain("# Skill: humanizer");
|
||||
expect(englishPrompt).not.toContain("humanizer-zh");
|
||||
|
||||
// Chinese UI: the Chinese variant replaces it.
|
||||
h.connection.transaction(() => h.organization.updateUiPreferences({ language: "zh" }));
|
||||
const chinesePrompt = promptFor("run_lang_zh");
|
||||
// Chinese work: the Chinese variant replaces it.
|
||||
const chinesePrompt = promptFor("run_lang_zh", "zh");
|
||||
expect(chinesePrompt).toContain("# Skill: humanizer-zh");
|
||||
expect(chinesePrompt).not.toContain("# Skill: humanizer\n");
|
||||
|
||||
// Chinese UI against a catalogue without the zh variant falls back to
|
||||
// Chinese work against a catalogue without the zh variant falls back to
|
||||
// English house style instead of silently dropping it.
|
||||
h.connection.transaction(() => {
|
||||
h.connection.db.prepare("update skill_definitions set status = 'retired' where org_id = ? and slug = 'humanizer-zh'").run(h.orgId);
|
||||
});
|
||||
const fallbackPrompt = promptFor("run_lang_fallback");
|
||||
const fallbackPrompt = promptFor("run_lang_fallback", "zh");
|
||||
expect(fallbackPrompt).toContain("# Skill: humanizer");
|
||||
h.connection.close();
|
||||
});
|
||||
@@ -353,7 +352,7 @@ describe("built-in skills", () => {
|
||||
modelProviderId: null, modelId: null, budgetCents: 1000, managerAgentId: null,
|
||||
});
|
||||
});
|
||||
const snapshotId = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId, runId: "run_tiny", maxTokens: 10, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true }));
|
||||
const snapshotId = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId, runId: "run_tiny", maxTokens: 10, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true, language: "en" }));
|
||||
const prompt = (h.connection.db.prepare("select compiled_prompt from skill_runtime_snapshots where id = ?").get(snapshotId) as { compiled_prompt: string }).compiled_prompt;
|
||||
expect(prompt).not.toContain("# Skill:");
|
||||
expect(prompt).toContain("# Skills omitted for size");
|
||||
@@ -380,7 +379,7 @@ describe("built-in skills", () => {
|
||||
bindSkillSlugs({ db: h.connection.db, organization: h.organization, chat: new ChatService(h.connection.db) }, h.orgId, agentId, ["marketing-growth-hacker", "engineering-backend-architect"]);
|
||||
});
|
||||
const compile = (runId: string, maxTokens: number, taskText: string) => {
|
||||
const snapshotId = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId, runId, maxTokens, taskText, runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true }));
|
||||
const snapshotId = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId, runId, maxTokens, taskText, runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true, language: "en" }));
|
||||
return (h.connection.db.prepare("select compiled_prompt from skill_runtime_snapshots where id = ?").get(snapshotId) as { compiled_prompt: string }).compiled_prompt;
|
||||
};
|
||||
const parts = compile("run_full", 1_000_000, "").split("\n\n---\n\n");
|
||||
|
||||
@@ -35,7 +35,7 @@ describe("domain services", () => {
|
||||
const connection = createConnection();
|
||||
|
||||
const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>;
|
||||
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103]);
|
||||
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104]);
|
||||
connection.close();
|
||||
});
|
||||
|
||||
@@ -193,7 +193,7 @@ describe("domain services", () => {
|
||||
skills.bind({ orgId, agentId, skillId: skill.id, revisionId });
|
||||
rmSync(skillDir, { recursive: true, force: true });
|
||||
skills.runTests({ orgId, revisionId });
|
||||
skills.resolveRuntimeSnapshot({ orgId, agentId, runId: "run-review-1", maxTokens: 44_800, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true });
|
||||
skills.resolveRuntimeSnapshot({ orgId, agentId, runId: "run-review-1", maxTokens: 44_800, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true, language: "en" });
|
||||
});
|
||||
|
||||
// Catalogue metadata ships in the snapshot; skill internals stay in the
|
||||
@@ -254,10 +254,10 @@ describe("domain services", () => {
|
||||
.get(orgA, "review-routes") as { id: string }).id;
|
||||
expect(() => skills.bind({ orgId: orgA, agentId: agentB, skillId, revisionId })).toThrow("Agent was not found.");
|
||||
skills.bind({ orgId: orgA, agentId: agentA, skillId, revisionId });
|
||||
const firstResolve = skills.resolveRuntimeSnapshot({ orgId: orgA, agentId: agentA, runId: "run-review-1", maxTokens: 44_800, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true });
|
||||
const firstResolve = skills.resolveRuntimeSnapshot({ orgId: orgA, agentId: agentA, runId: "run-review-1", maxTokens: 44_800, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true, language: "en" });
|
||||
// Idempotent per run: a recovered run reuses its frozen snapshot
|
||||
// instead of failing on the uniqueness guard.
|
||||
expect(skills.resolveRuntimeSnapshot({ orgId: orgA, agentId: agentA, runId: "run-review-1", maxTokens: 44_800, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true })).toBe(firstResolve);
|
||||
expect(skills.resolveRuntimeSnapshot({ orgId: orgA, agentId: agentA, runId: "run-review-1", maxTokens: 44_800, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true, language: "en" })).toBe(firstResolve);
|
||||
expect(connection.db.prepare("select count(*) as n from skill_runtime_snapshots where run_id = 'run-review-1'").get())
|
||||
.toEqual({ n: 1 });
|
||||
});
|
||||
|
||||
@@ -315,7 +315,7 @@ describe("execution modes", () => {
|
||||
}));
|
||||
const task = () => db.prepare("select * from tasks where id = ?").get(taskId) as unknown as Parameters<typeof executionApprovalProblem>[2];
|
||||
const taskApprovals = () => db.prepare("select id, state, details from approvals where source_entity_id = ? order by rowid").all(taskId) as Array<{ id: string; state: string; details: string }>;
|
||||
const edit = (description: string) => h.connection.transaction(() => h.deps.organization.updateTask({
|
||||
const edit = (description: string) => h.connection.transaction(() => h.deps.organization.updateTask({ workLanguage: null,
|
||||
orgId: h.orgId, taskId, title: "Wipe staging", description, assigneeAgentId: h.ceoId, risk: "high", priority: TASK_PRIORITY.NORMAL, parentTaskId: null,
|
||||
...currentLinks(db, taskId),
|
||||
}));
|
||||
|
||||
@@ -15,7 +15,7 @@ describe("company settings after onboarding", () => {
|
||||
});
|
||||
const update = (change: Partial<{ name: string; executionMode: "ai_ceo" | "ai_cofounder" }>) => connection.transaction(() => organization.updateOrganization({
|
||||
orgId, name: "Acme Labs", description: "Analytics for CSMs.", mission: "Keep customers.", goal: "Ship the MVP.",
|
||||
executionMode: "ai_cofounder", autoApproveHires: false, reviewDeliverables: true, ...change,
|
||||
executionMode: "ai_cofounder", autoApproveHires: false, reviewDeliverables: true, workLanguage: "en", ...change,
|
||||
}));
|
||||
update({});
|
||||
expect(connection.db.prepare("select name, description, mission, goal, execution_mode, auto_approve_hires, review_deliverables from organizations where id = ?").get(orgId))
|
||||
@@ -23,7 +23,7 @@ describe("company settings after onboarding", () => {
|
||||
const audit = connection.db.prepare("select payload_json from audit_events where entity_type = 'organization' and action = 'updated' order by rowid desc limit 1").get() as { payload_json: string };
|
||||
expect(JSON.parse(audit.payload_json).changed).toEqual(expect.arrayContaining(["name", "executionMode", "reviewDeliverables"]));
|
||||
expect(() => update({ name: " " })).toThrow("The company needs a name.");
|
||||
expect(() => organization.updateOrganization({ orgId: "org_missing", name: "X", description: "", mission: "", goal: "", executionMode: "ai_ceo", autoApproveHires: true, reviewDeliverables: false }))
|
||||
expect(() => organization.updateOrganization({ orgId: "org_missing", name: "X", description: "", mission: "", goal: "", executionMode: "ai_ceo", autoApproveHires: true, reviewDeliverables: false, workLanguage: "en" }))
|
||||
.toThrow("Organization was not found.");
|
||||
connection.close();
|
||||
});
|
||||
|
||||
@@ -42,7 +42,7 @@ describe("skill compatibility", () => {
|
||||
skills.bind({ orgId, agentId, skillId: skill.id, revisionId });
|
||||
});
|
||||
const prompt = (runId: string, providerKey: string, model: string) => {
|
||||
const id = connection.transaction(() => skills.resolveRuntimeSnapshot({ orgId, agentId, runId, maxTokens: 40_000, taskText: "", runModel: { providerKey, model }, houseStyle: true }));
|
||||
const id = connection.transaction(() => skills.resolveRuntimeSnapshot({ orgId, agentId, runId, maxTokens: 40_000, taskText: "", runModel: { providerKey, model }, houseStyle: true, language: "en" }));
|
||||
return connection.db.prepare("select compiled_prompt, source_revision_ids from skill_runtime_snapshots where id = ?").get(id) as { compiled_prompt: string; source_revision_ids: string };
|
||||
};
|
||||
|
||||
|
||||
@@ -98,10 +98,10 @@ describe("skill grants", () => {
|
||||
expect(listSkills?.description).not.toContain("every skill automatically");
|
||||
expect(listSkills?.description).toContain("the CEO included");
|
||||
const skills = new SkillService(h.connection.db, "");
|
||||
expect(skills.hasRuntimeSkills(h.orgId, h.ceoId, true)).toBe(false);
|
||||
expect(skills.hasRuntimeSkills(h.orgId, h.ceoId, true, "en")).toBe(false);
|
||||
expect(dispatchAgentTool(h.deps, h.ceoCtx, call("grant_skill", { agent_name: "CEO", skill: "release-gate" })))
|
||||
.toBe("CEO now has the Release Gate skill; it joins their next run.");
|
||||
expect(skills.hasRuntimeSkills(h.orgId, h.ceoId, true)).toBe(true);
|
||||
expect(skills.hasRuntimeSkills(h.orgId, h.ceoId, true, "en")).toBe(true);
|
||||
h.connection.close();
|
||||
});
|
||||
|
||||
@@ -116,11 +116,11 @@ describe("skill grants", () => {
|
||||
expect(listing).toContain("Release Gate (release-gate): granted to Dana");
|
||||
|
||||
const skills = new SkillService(h.connection.db, "");
|
||||
expect(skills.hasRuntimeSkills(h.orgId, h.danaId, true)).toBe(true);
|
||||
expect(skills.hasRuntimeSkills(h.orgId, h.danaId, true, "en")).toBe(true);
|
||||
|
||||
const revoked = dispatchAgentTool(h.deps, h.ceoCtx, call("revoke_skill", { agent_name: "Dana", skill: "release-gate" }));
|
||||
expect(revoked).toContain("no longer has the Release Gate skill");
|
||||
expect(skills.hasRuntimeSkills(h.orgId, h.danaId, true)).toBe(false);
|
||||
expect(skills.hasRuntimeSkills(h.orgId, h.danaId, true, "en")).toBe(false);
|
||||
});
|
||||
|
||||
test("staff cannot hand out skills", () => {
|
||||
@@ -128,7 +128,7 @@ describe("skill grants", () => {
|
||||
importSkill(h, "Release Gate", "release-gate");
|
||||
const denied = dispatchAgentTool(h.deps, h.danaCtx, call("grant_skill", { agent_name: "Dana", skill: "release-gate" }));
|
||||
expect(denied).toContain("ERROR");
|
||||
expect(new SkillService(h.connection.db, "").hasRuntimeSkills(h.orgId, h.danaId, true)).toBe(false);
|
||||
expect(new SkillService(h.connection.db, "").hasRuntimeSkills(h.orgId, h.danaId, true, "en")).toBe(false);
|
||||
});
|
||||
|
||||
test("the humanizer house-style skill reaches every agent without bindings and cannot be revoked", () => {
|
||||
@@ -136,18 +136,18 @@ describe("skill grants", () => {
|
||||
const skills = new SkillService(h.connection.db, "");
|
||||
// A non-forced skill grants nothing on its own - not even to the CEO.
|
||||
importSkill(h, "Release Gate", "release-gate");
|
||||
expect(skills.hasRuntimeSkills(h.orgId, h.ceoId, true)).toBe(false);
|
||||
expect(skills.hasRuntimeSkills(h.orgId, h.danaId, true)).toBe(false);
|
||||
expect(skills.hasRuntimeSkills(h.orgId, h.ceoId, true, "en")).toBe(false);
|
||||
expect(skills.hasRuntimeSkills(h.orgId, h.danaId, true, "en")).toBe(false);
|
||||
// Once the forced skill exists in the catalogue, everyone carries it.
|
||||
importSkill(h, "humanizer", "humanizer");
|
||||
expect(skills.hasRuntimeSkills(h.orgId, h.ceoId, true)).toBe(true);
|
||||
expect(skills.hasRuntimeSkills(h.orgId, h.danaId, true)).toBe(true);
|
||||
expect(skills.hasRuntimeSkills(h.orgId, h.ceoId, true, "en")).toBe(true);
|
||||
expect(skills.hasRuntimeSkills(h.orgId, h.danaId, true, "en")).toBe(true);
|
||||
// House style is not revocable, and granting it is a no-op with feedback.
|
||||
const revoked = dispatchAgentTool(h.deps, h.ceoCtx, call("revoke_skill", { agent_name: "Dana", skill: "humanizer" }));
|
||||
expect(revoked).toContain("ERROR");
|
||||
expect(revoked).toContain("house style");
|
||||
const granted = dispatchAgentTool(h.deps, h.ceoCtx, call("grant_skill", { agent_name: "Dana", skill: "humanizer" }));
|
||||
expect(granted).toContain("automatically");
|
||||
expect(skills.hasRuntimeSkills(h.orgId, h.danaId, true)).toBe(true);
|
||||
expect(skills.hasRuntimeSkills(h.orgId, h.danaId, true, "en")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -28,7 +28,7 @@ describe("editing a task", () => {
|
||||
projectId: ids.projectA, goalId: ids.goalA, workspaceId: ids.wsA, priority: TASK_PRIORITY.NORMAL, requiresFilesystem: false,
|
||||
});
|
||||
});
|
||||
const edit = (change: { projectId: string | null; goalId: string | null; workspaceId: string | null }) => connection.transaction(() => organization.updateTask({
|
||||
const edit = (change: { projectId: string | null; goalId: string | null; workspaceId: string | null }) => connection.transaction(() => organization.updateTask({ workLanguage: null,
|
||||
orgId: ids.org, taskId: ids.task, title: "Ship", description: "d", assigneeAgentId: null, risk: RISK_LEVEL.LOW, priority: TASK_PRIORITY.NORMAL,
|
||||
parentTaskId: null, requiresFilesystem: true, writingMode: WRITING_MODE.TECHNICAL, ...change,
|
||||
}));
|
||||
|
||||
@@ -734,7 +734,7 @@ describe("task execution service", () => {
|
||||
expect(connection.db.prepare("select count(*) as n from audit_events where entity_id = ? and action = 'stalled'").get(taskId)).toEqual({ n: 1 });
|
||||
expect(notified).toBe(1);
|
||||
|
||||
organization.updateTask({ orgId, taskId, title: "Ship it", description: "Narrower scope.", assigneeAgentId: agentId, risk: RISK_LEVEL.LOW, priority: TASK_PRIORITY.NORMAL, parentTaskId: null, ...currentLinks(connection.db, taskId) });
|
||||
organization.updateTask({ workLanguage: null, orgId, taskId, title: "Ship it", description: "Narrower scope.", assigneeAgentId: agentId, risk: RISK_LEVEL.LOW, priority: TASK_PRIORITY.NORMAL, parentTaskId: null, ...currentLinks(connection.db, taskId) });
|
||||
expect(connection.db.prepare("select stalled_reason from tasks where id = ?").get(taskId)).toEqual({ stalled_reason: null });
|
||||
expect(connection.transaction(() => execution.enqueueHeartbeatRuns())).toHaveLength(1);
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ describe("task hierarchy", () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
const reparent = (title: string, parent: string | null) => organization.updateTask({
|
||||
const reparent = (title: string, parent: string | null) => organization.updateTask({ workLanguage: null,
|
||||
orgId, taskId: ids[title], title, description: "d", assigneeAgentId: null, risk: RISK_LEVEL.LOW, priority: TASK_PRIORITY.NORMAL, parentTaskId: parent,
|
||||
...currentLinks(connection.db, ids[title]),
|
||||
});
|
||||
|
||||
@@ -58,7 +58,7 @@ describe("task status authority", () => {
|
||||
expect(h.as("dev", { status: "backlog", evidence: 123 })).toBe('ERROR: "evidence" is required.');
|
||||
h.as("dev", { status: "done", evidence: "file_id=fil_1" });
|
||||
const reviewId = (h.connection.db.prepare("select id from tasks where review_of_task_id = ?").get(h.ids.task) as { id: string }).id;
|
||||
h.organization.updateTask({ orgId: h.orgId, taskId: h.ids.task, title: "Login page", description: "Build it.", assigneeAgentId: h.ids.cto, risk: RISK_LEVEL.LOW, priority: TASK_PRIORITY.NORMAL, parentTaskId: null, ...currentLinks(h.connection.db, h.ids.task) });
|
||||
h.organization.updateTask({ workLanguage: null, orgId: h.orgId, taskId: h.ids.task, title: "Login page", description: "Build it.", assigneeAgentId: h.ids.cto, risk: RISK_LEVEL.LOW, priority: TASK_PRIORITY.NORMAL, parentTaskId: null, ...currentLinks(h.connection.db, h.ids.task) });
|
||||
expect(h.status()).toBe("in_progress");
|
||||
expect(h.connection.db.prepare("select status from tasks where id = ?").get(reviewId)).toEqual({ status: "done" });
|
||||
expect(h.as("cto", { status: "done", evidence: "Rebuilt the form." })).toBe(`Task ${h.ids.task} moved to review: done needs sign-off from your manager. Cleo will review it.`);
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { DatabaseConnection } from "../../src/main/database/connection";
|
||||
import { OrganizationService } from "../../src/main/services/organization-service";
|
||||
import { ChatService } from "../../src/main/services/chat-service";
|
||||
import { dispatchAgentTool } from "../../src/main/services/agent-tools";
|
||||
import { taskWorkLanguage } from "../../src/main/services/work-language";
|
||||
import { workLanguageSchema } from "../../src/main/database/migrations/104-work-language";
|
||||
import { RISK_LEVEL, TASK_PRIORITY, TASK_STATUS, WRITING_MODE } from "../../src/shared/domain";
|
||||
|
||||
describe("the working language", () => {
|
||||
test("is the company's unless a task sets its own, and delegated work keeps its parent's", () => {
|
||||
const connection = new DatabaseConnection(join(tmpdir(), `ac-work-language-${randomUUID()}.db`));
|
||||
const db = connection.db;
|
||||
const organization = new OrganizationService(db);
|
||||
let orgId = "";
|
||||
let ceoId = "";
|
||||
let taskId = "";
|
||||
connection.transaction(() => {
|
||||
orgId = organization.createOrganization({ organizationName: "Acme", employeeName: "Yang", mission: "m", goal: "g" });
|
||||
ceoId = organization.createAgent({ orgId, name: "CEO", role: "CEO", department: "Executive", modelProviderId: null, modelId: null, budgetCents: 1000, managerAgentId: null, roleLevel: "c_suite" });
|
||||
taskId = organization.createTask({
|
||||
orgId, title: "Plan", description: "d", status: TASK_STATUS.IN_PROGRESS, assigneeAgentId: ceoId, risk: RISK_LEVEL.LOW,
|
||||
projectId: null, goalId: null, workspaceId: null, priority: TASK_PRIORITY.NORMAL, requiresFilesystem: false,
|
||||
});
|
||||
});
|
||||
expect(taskWorkLanguage(db, orgId, taskId)).toBe("en");
|
||||
|
||||
// The display language plays no part.
|
||||
organization.updateUiPreferences({ language: "zh" });
|
||||
expect(taskWorkLanguage(db, orgId, taskId)).toBe("en");
|
||||
|
||||
organization.updateOrganization({
|
||||
orgId, name: "Acme", description: "", mission: "m", goal: "g", executionMode: "ai_ceo", autoApproveHires: true, reviewDeliverables: false, workLanguage: "zh",
|
||||
});
|
||||
expect(taskWorkLanguage(db, orgId, taskId)).toBe("zh");
|
||||
|
||||
const edit = (workLanguage: "en" | "zh" | null) => connection.transaction(() => organization.updateTask({
|
||||
orgId, taskId, title: "Plan", description: "d", assigneeAgentId: ceoId, risk: RISK_LEVEL.LOW, priority: TASK_PRIORITY.NORMAL,
|
||||
parentTaskId: null, projectId: null, goalId: null, workspaceId: null, requiresFilesystem: false, writingMode: WRITING_MODE.CONVERSATIONAL, workLanguage,
|
||||
}));
|
||||
edit("en");
|
||||
expect(taskWorkLanguage(db, orgId, taskId)).toBe("en");
|
||||
|
||||
const delegated = dispatchAgentTool({ db, organization, chat: new ChatService(db) }, {
|
||||
orgId, agent: { id: ceoId, modelProviderId: null, modelId: null }, taskProjectId: null, taskGoalId: null, taskId, sourceThreadId: null, workspace: null,
|
||||
}, { id: "call_1", name: "create_task", arguments: { title: "Draft", description: "d" } });
|
||||
const childId = /task_id=([^)\s]+)\)/.exec(delegated)![1];
|
||||
expect(db.prepare("select work_language from tasks where id = ?").get(childId)).toEqual({ work_language: "en" });
|
||||
|
||||
edit(null);
|
||||
expect(taskWorkLanguage(db, orgId, taskId)).toBe("zh");
|
||||
});
|
||||
|
||||
test("an existing company keeps the language its agents had, taken from the app setting", () => {
|
||||
const db = new DatabaseSync(":memory:");
|
||||
db.exec(`
|
||||
create table schema_migrations(version integer, name text, applied_at text);
|
||||
create table app_state(id integer primary key, language text);
|
||||
create table organizations(id text primary key);
|
||||
create table tasks(id text primary key, org_id text);
|
||||
insert into app_state(id, language) values (1, 'zh');
|
||||
insert into organizations(id) values ('org_a');
|
||||
`);
|
||||
db.exec(workLanguageSchema);
|
||||
expect(db.prepare("select work_language from organizations").get()).toEqual({ work_language: "zh" });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user