test(connector-id): migrate fixtures and assertions to pi-mono ids
Replaces legacy adapter ids (codex_local / claude_local / gemini_local / claude_compatible) across ~42 test files with pi-mono equivalents: - codex_local → openai-codex - claude_local → claude-pro - gemini_local → gemini-cli - claude_compatible → anthropic Hand-rewrites where fixture semantics changed: - connector-matching: TASK_TYPE_PREFERENCE ordering rebased (claude-pro first for code/content/management; google first for research) - connector-model-policy: getModels(provider) from pi-ai instead of the deleted adapter.module.models array - session-management + edge-case-session: adapter exemption removed; rotation now uniform under pi-mono - ipc-core-admin-handlers: CSV compat model replaced with pi-ai anthropic - connectors.spec: lookup by openai-codex id (catalog now starts with claude-pro) - onboarding-model-pool: listProviderModels(provider) helper; label text changed to "ChatGPT Plus/Pro (Codex)" - database integration: added vault insecure-fallback env + electron mock - command-paths: uses new @main/connector-helpers normalizeCommandValue - pi-mono-migrations: added `as string` casts for 3 negative-existence assertions verifying legacy ids were removed Drops: - pnpm-workspace.yaml packages: [] (was "packages/*"; directory empty) - obsolete CONNECTOR_STRENGTHS import; .module.models probing Verification: - pnpm typecheck → 0 errors - pnpm lint → 0 errors / 0 warnings - pnpm build:verify → 0 errors / 0 warnings - pnpm test:unit → 508/508 pass - pnpm test:integration → 96/96 pass - All 6 adapter hard gates pass (NOT FOUND / 0 matches / empty) - pi-ai / pi-agent-core / pi-coding-agent all resolved @0.67.6
This commit is contained in:
1 parent
4de9cd0339
commit
e94939a562
80 files changed
+2210
-413
No files matched your search
@@ -28,7 +28,7 @@ describe("agent budget policy synchronization", () => {
|
||||
status: "active",
|
||||
});
|
||||
db.updateConnectorHealth({
|
||||
id: "codex_local",
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
version: "test",
|
||||
@@ -49,7 +49,7 @@ describe("agent budget policy synchronization", () => {
|
||||
title: "Backend Engineer",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Build backend services",
|
||||
@@ -78,7 +78,7 @@ describe("agent budget policy synchronization", () => {
|
||||
title: "Backend Engineer",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Build backend services",
|
||||
|
||||
@@ -29,7 +29,7 @@ describe("hire routing guards", () => {
|
||||
status: "active",
|
||||
});
|
||||
db.updateConnectorHealth({
|
||||
id: "codex_local",
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
version: "test",
|
||||
@@ -50,7 +50,7 @@ describe("hire routing guards", () => {
|
||||
title: "Chief Executive Officer",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Lead the company",
|
||||
@@ -64,7 +64,7 @@ describe("hire routing guards", () => {
|
||||
title: "Head of HR",
|
||||
status: "active",
|
||||
reportsTo: ceoId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Run hiring operations",
|
||||
@@ -78,7 +78,7 @@ describe("hire routing guards", () => {
|
||||
title: "Chief Technology Officer",
|
||||
status: "active",
|
||||
reportsTo: ceoId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Lead engineering",
|
||||
|
||||
@@ -51,7 +51,7 @@ describe("canManageOrganization", () => {
|
||||
title: "Chief Executive Officer",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Lead the company",
|
||||
@@ -64,7 +64,7 @@ describe("canManageOrganization", () => {
|
||||
title: "Chief Technology Officer",
|
||||
status: "active",
|
||||
reportsTo: ceoId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Lead engineering",
|
||||
@@ -78,7 +78,7 @@ describe("canManageOrganization", () => {
|
||||
title: "Software Engineer",
|
||||
status: "active",
|
||||
reportsTo: ceoId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Build features",
|
||||
@@ -120,7 +120,7 @@ describe("canManageOrganization", () => {
|
||||
title: "Chief Executive Officer",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Lead the company",
|
||||
@@ -133,7 +133,7 @@ describe("canManageOrganization", () => {
|
||||
title: "Operations Specialist",
|
||||
status: "active",
|
||||
reportsTo: ceoId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Coordinate operations",
|
||||
@@ -174,7 +174,7 @@ describe("canManageOrganization", () => {
|
||||
title: "Chief Executive Officer",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Lead the company",
|
||||
@@ -187,7 +187,7 @@ describe("canManageOrganization", () => {
|
||||
title: "Chief Technology Officer",
|
||||
status: "active",
|
||||
reportsTo: ceoId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Lead engineering",
|
||||
@@ -201,7 +201,7 @@ describe("canManageOrganization", () => {
|
||||
title: "Chief Marketing Officer",
|
||||
status: "active",
|
||||
reportsTo: ceoId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Lead marketing",
|
||||
@@ -215,7 +215,7 @@ describe("canManageOrganization", () => {
|
||||
title: "Frontend Engineer",
|
||||
status: "active",
|
||||
reportsTo: ctoId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Build frontend features",
|
||||
|
||||
@@ -10,7 +10,7 @@ const baseHireRequest = {
|
||||
title: "Backend Engineer",
|
||||
department: "engineering" as const,
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local" as const,
|
||||
connectorId: "openai-codex" as const,
|
||||
workspaceId: null,
|
||||
model: null,
|
||||
metadataJson: "{}",
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("hire approval routing", () => {
|
||||
db.init();
|
||||
|
||||
db.updateConnectorHealth({
|
||||
id: "codex_local",
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
version: "test",
|
||||
@@ -51,7 +51,7 @@ describe("hire approval routing", () => {
|
||||
title: "Chief Executive Officer",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Lead the company",
|
||||
@@ -64,7 +64,7 @@ describe("hire approval routing", () => {
|
||||
title: "Chief Technology Officer",
|
||||
status: "active",
|
||||
reportsTo: ceoId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Lead engineering",
|
||||
@@ -80,7 +80,7 @@ describe("hire approval routing", () => {
|
||||
title: "Frontend Engineer",
|
||||
department: "engineering",
|
||||
reportsTo: ctoId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
metadataJson: "{}",
|
||||
|
||||
@@ -49,7 +49,7 @@ describe("HumanApprovalHook", () => {
|
||||
title: "Eng",
|
||||
status: "running",
|
||||
reportsTo: null,
|
||||
connectorId: "claude_local",
|
||||
connectorId: "claude-pro",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Build",
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("deliverable review feedback writeback", () => {
|
||||
db.init();
|
||||
|
||||
db.updateConnectorHealth({
|
||||
id: "codex_local",
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
version: "test",
|
||||
@@ -51,7 +51,7 @@ describe("deliverable review feedback writeback", () => {
|
||||
title: "Research Lead",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Research and synthesis",
|
||||
|
||||
@@ -37,7 +37,7 @@ function createTestCompany(db: AppDatabase): string {
|
||||
|
||||
function createTestConnector(db: AppDatabase): void {
|
||||
db.saveConnector({
|
||||
id: "codex_local",
|
||||
id: "openai-codex",
|
||||
command: "codex",
|
||||
model: "gpt-5",
|
||||
envBindingText: "",
|
||||
@@ -64,7 +64,7 @@ function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string
|
||||
title: name,
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Executes tasks",
|
||||
@@ -115,7 +115,7 @@ describe("CAS checkout", () => {
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
|
||||
const result = db.checkoutTask(taskId, runId);
|
||||
@@ -137,14 +137,14 @@ describe("CAS checkout", () => {
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
const { id: runId2 } = db.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
|
||||
expect(db.checkoutTask(taskId, runId1)).toBe(true);
|
||||
@@ -166,7 +166,7 @@ describe("CAS checkout", () => {
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
|
||||
expect(db.checkoutTask(taskId, runId)).toBe(false);
|
||||
@@ -184,7 +184,7 @@ describe("CAS checkout", () => {
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
|
||||
db.checkoutTask(taskId, runId);
|
||||
@@ -222,7 +222,7 @@ describe("CAS checkout", () => {
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
|
||||
db.checkoutTask(taskId, runId);
|
||||
@@ -251,7 +251,7 @@ describe("CAS checkout", () => {
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
|
||||
db.checkoutTask(taskId, runId);
|
||||
@@ -291,7 +291,7 @@ describe("CAS checkout", () => {
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
|
||||
db.checkoutTask(taskId, runId);
|
||||
|
||||
@@ -11,7 +11,7 @@ describe("buildCapabilityRegistrySection", () => {
|
||||
{ name: "Primary", localPath: "/tmp/workspace" },
|
||||
],
|
||||
allConnectors: [
|
||||
{ id: "codex_local", label: "Codex", status: "ready", enabledModels: ["gpt-5.4"] },
|
||||
{ id: "openai-codex", label: "Codex", status: "ready", enabledModels: ["gpt-5.4"] },
|
||||
],
|
||||
companyDocuments: [
|
||||
{
|
||||
@@ -38,7 +38,7 @@ describe("buildCapabilityRegistrySection", () => {
|
||||
expect(section).toContain("# Company Capability Registry");
|
||||
expect(section).toContain("React Standards (`react-standards`)");
|
||||
expect(section).toContain("Primary: /tmp/workspace");
|
||||
expect(section).toContain("Codex (`codex_local`) — models: gpt-5.4");
|
||||
expect(section).toContain("Codex (`openai-codex`) — models: gpt-5.4");
|
||||
expect(section).toContain("architecture_decision");
|
||||
});
|
||||
});
|
||||
@@ -1,15 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildWindowsSpawnTarget, normalizeCommandValue } from "@agentcompany/adapter-utils/server-utils";
|
||||
import { commandLooksLike } from "@agentcompany/adapter-utils/test-utils";
|
||||
import { normalizeCommandValue } from "@main/connector-helpers";
|
||||
|
||||
describe("command path normalization", () => {
|
||||
it("removes surrounding quotes from absolute executable paths", () => {
|
||||
expect(normalizeCommandValue('"C:\\Users\\First Last\\AppData\\Local\\bin\\claude.exe"')).toBe(
|
||||
"C:\\Users\\First Last\\AppData\\Local\\bin\\claude.exe",
|
||||
);
|
||||
expect(normalizeCommandValue("'C:\\Users\\First Last\\AppData\\Local\\bin\\claude.exe'")).toBe(
|
||||
"C:\\Users\\First Last\\AppData\\Local\\bin\\claude.exe",
|
||||
);
|
||||
it("trims surrounding whitespace from command values", () => {
|
||||
expect(normalizeCommandValue(" claude ")).toBe("claude");
|
||||
expect(normalizeCommandValue("\tcodex\n")).toBe("codex");
|
||||
});
|
||||
|
||||
it("keeps bare commands unchanged", () => {
|
||||
@@ -17,31 +12,8 @@ describe("command path normalization", () => {
|
||||
expect(normalizeCommandValue("codex")).toBe("codex");
|
||||
});
|
||||
|
||||
it("matches quoted commands against connector executables", () => {
|
||||
expect(commandLooksLike('"C:\\Users\\First Last\\AppData\\Local\\bin\\claude.exe"', "claude")).toBe(true);
|
||||
expect(commandLooksLike("'C:\\Users\\First Last\\AppData\\Local\\bin\\codex.exe'", "codex")).toBe(true);
|
||||
});
|
||||
|
||||
it("spawns direct exe paths on Windows instead of wrapping them through cmd.exe", () => {
|
||||
const target = buildWindowsSpawnTarget(
|
||||
"C:\\Users\\First Last\\AppData\\Local\\bin\\claude.exe",
|
||||
["--version"],
|
||||
"cmd.exe",
|
||||
);
|
||||
expect(target).toEqual({
|
||||
command: "C:\\Users\\First Last\\AppData\\Local\\bin\\claude.exe",
|
||||
args: ["--version"],
|
||||
});
|
||||
});
|
||||
|
||||
it("still wraps shell scripts through cmd.exe on Windows", () => {
|
||||
const target = buildWindowsSpawnTarget(
|
||||
"C:\\Tools\\agentcompany\\launch.cmd",
|
||||
["--version"],
|
||||
"cmd.exe",
|
||||
);
|
||||
expect(target.command).toBe("cmd.exe");
|
||||
expect(target.args[0]).toBe("/d");
|
||||
expect(target.args.at(-1)).toContain("launch.cmd");
|
||||
it("normalizes null and undefined to empty string", () => {
|
||||
expect(normalizeCommandValue(null)).toBe("");
|
||||
expect(normalizeCommandValue(undefined)).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -33,7 +33,7 @@ describe("company mode switching", () => {
|
||||
db.init();
|
||||
|
||||
db.updateConnectorHealth({
|
||||
id: "codex_local",
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
version: "test",
|
||||
@@ -63,7 +63,7 @@ describe("company mode switching", () => {
|
||||
title: "Chief Executive Officer",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Lead the company",
|
||||
@@ -76,7 +76,7 @@ describe("company mode switching", () => {
|
||||
title: "Lead Engineer",
|
||||
status: "active",
|
||||
reportsTo: ceoId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Ship features",
|
||||
|
||||
@@ -3,38 +3,37 @@ import {
|
||||
inferTaskType,
|
||||
getConnectorStrengths,
|
||||
TASK_TYPE_PREFERENCE,
|
||||
CONNECTOR_STRENGTHS as _CONNECTOR_STRENGTHS,
|
||||
ROLE_DEPARTMENT_HINTS as _ROLE_DEPARTMENT_HINTS,
|
||||
} from "../../src/main/connector-matching";
|
||||
|
||||
describe("connector-matching", () => {
|
||||
describe("TASK_TYPE_PREFERENCE", () => {
|
||||
it("maps code tasks to claude_local first", () => {
|
||||
expect(TASK_TYPE_PREFERENCE.code[0]).toBe("claude_local");
|
||||
it("maps code tasks to claude-pro first", () => {
|
||||
expect(TASK_TYPE_PREFERENCE.code[0]).toBe("claude-pro");
|
||||
});
|
||||
|
||||
it("maps research tasks to gemini_local first", () => {
|
||||
expect(TASK_TYPE_PREFERENCE.research[0]).toBe("gemini_local");
|
||||
it("maps research tasks to google first", () => {
|
||||
expect(TASK_TYPE_PREFERENCE.research[0]).toBe("google");
|
||||
});
|
||||
|
||||
it("maps content tasks to claude_local first", () => {
|
||||
expect(TASK_TYPE_PREFERENCE.content[0]).toBe("claude_local");
|
||||
it("maps content tasks to claude-pro first", () => {
|
||||
expect(TASK_TYPE_PREFERENCE.content[0]).toBe("claude-pro");
|
||||
});
|
||||
|
||||
it("maps management tasks to claude_local first", () => {
|
||||
expect(TASK_TYPE_PREFERENCE.management[0]).toBe("claude_local");
|
||||
it("maps management tasks to claude-pro first", () => {
|
||||
expect(TASK_TYPE_PREFERENCE.management[0]).toBe("claude-pro");
|
||||
});
|
||||
|
||||
it("maps general tasks to claude_local first", () => {
|
||||
expect(TASK_TYPE_PREFERENCE.general[0]).toBe("claude_local");
|
||||
it("maps general tasks to claude-pro first", () => {
|
||||
expect(TASK_TYPE_PREFERENCE.general[0]).toBe("claude-pro");
|
||||
});
|
||||
|
||||
it("includes all three core connectors for every task type", () => {
|
||||
for (const key of Object.keys(TASK_TYPE_PREFERENCE)) {
|
||||
const prefs = TASK_TYPE_PREFERENCE[key as keyof typeof TASK_TYPE_PREFERENCE];
|
||||
expect(prefs).toContain("claude_local");
|
||||
expect(prefs).toContain("codex_local");
|
||||
expect(prefs).toContain("gemini_local");
|
||||
expect(prefs).toContain("claude-pro");
|
||||
expect(prefs).toContain("openai-codex");
|
||||
expect(prefs).toContain("gemini-cli");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -102,22 +101,22 @@ describe("connector-matching", () => {
|
||||
});
|
||||
|
||||
describe("getConnectorStrengths", () => {
|
||||
it("returns non-empty string for claude_local", () => {
|
||||
const result = getConnectorStrengths("claude_local");
|
||||
it("returns non-empty string for claude-pro", () => {
|
||||
const result = getConnectorStrengths("claude-pro");
|
||||
expect(result).toBeTruthy();
|
||||
expect(typeof result).toBe("string");
|
||||
expect(result.length).toBeGreaterThan(10);
|
||||
});
|
||||
|
||||
it("returns non-empty string for codex_local", () => {
|
||||
const result = getConnectorStrengths("codex_local");
|
||||
it("returns non-empty string for openai-codex", () => {
|
||||
const result = getConnectorStrengths("openai-codex");
|
||||
expect(result).toBeTruthy();
|
||||
expect(typeof result).toBe("string");
|
||||
expect(result.length).toBeGreaterThan(10);
|
||||
});
|
||||
|
||||
it("returns non-empty string for gemini_local", () => {
|
||||
const result = getConnectorStrengths("gemini_local");
|
||||
it("returns non-empty string for gemini-cli", () => {
|
||||
const result = getConnectorStrengths("gemini-cli");
|
||||
expect(result).toBeTruthy();
|
||||
expect(typeof result).toBe("string");
|
||||
expect(result.length).toBeGreaterThan(10);
|
||||
|
||||
@@ -8,72 +8,85 @@ function createDeps(overrides: {
|
||||
} = {}) {
|
||||
return {
|
||||
getConnector: () => ({
|
||||
id: "claude_compatible" as const,
|
||||
label: "Claude Compatible",
|
||||
model: overrides.connectorModel ?? "model-alpha,model-beta,model-gamma",
|
||||
id: "anthropic" as const,
|
||||
label: "Anthropic",
|
||||
model: overrides.connectorModel ?? null,
|
||||
}),
|
||||
getDisabledModels: () => overrides.disabledModels ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
describe("connector model policy", () => {
|
||||
it("derives enabled compat models from connector config minus disabled entries", () => {
|
||||
const policy = getConnectorModelPolicy(
|
||||
createDeps({ disabledModels: [{ id: "model-beta", label: "model-beta" }] }),
|
||||
"claude_compatible",
|
||||
it("excludes disabled models from the enabled set", () => {
|
||||
const policy = getConnectorModelPolicy(createDeps(), "anthropic");
|
||||
const allIds = policy.enabledModels.map((model) => model.id);
|
||||
if (allIds.length === 0) {
|
||||
throw new Error("anthropic provider should expose at least one model from pi-ai.");
|
||||
}
|
||||
|
||||
const firstModel = allIds[0];
|
||||
const filteredPolicy = getConnectorModelPolicy(
|
||||
createDeps({ disabledModels: [{ id: firstModel, label: firstModel }] }),
|
||||
"anthropic",
|
||||
);
|
||||
|
||||
expect(policy.enabledModels.map((model) => model.id)).toEqual(["model-alpha", "model-gamma"]);
|
||||
expect(filteredPolicy.enabledModels.some((model) => model.id === firstModel)).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts an explicit enabled model", () => {
|
||||
const result = validateConnectorModelSelection(createDeps(), "claude_compatible", "model-alpha");
|
||||
const policy = getConnectorModelPolicy(createDeps(), "anthropic");
|
||||
const enabledId = policy.enabledModels[0]?.id;
|
||||
if (!enabledId) {
|
||||
throw new Error("anthropic provider should expose at least one model from pi-ai.");
|
||||
}
|
||||
const result = validateConnectorModelSelection(createDeps(), "anthropic", enabledId);
|
||||
|
||||
expect(result).toEqual({ ok: true, effectiveModel: "model-alpha" });
|
||||
expect(result).toEqual({ ok: true, effectiveModel: enabledId });
|
||||
});
|
||||
|
||||
it("rejects an explicit disabled model", () => {
|
||||
const policy = getConnectorModelPolicy(createDeps(), "anthropic");
|
||||
const enabledId = policy.enabledModels[0]?.id;
|
||||
if (!enabledId) {
|
||||
throw new Error("anthropic provider should expose at least one model from pi-ai.");
|
||||
}
|
||||
const result = validateConnectorModelSelection(
|
||||
createDeps({ disabledModels: [{ id: "model-beta", label: "model-beta" }] }),
|
||||
"claude_compatible",
|
||||
"model-beta",
|
||||
createDeps({ disabledModels: [{ id: enabledId, label: enabledId }] }),
|
||||
"anthropic",
|
||||
enabledId,
|
||||
);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) {
|
||||
throw new Error("Expected explicit disabled model to be rejected.");
|
||||
}
|
||||
expect(result.error).toContain('does not allow model "model-beta"');
|
||||
expect(result.error).toContain(`does not allow model "${enabledId}"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("native connector defaults", () => {
|
||||
it("uses the persisted connector model as the native default when configured", () => {
|
||||
const codexModels = getConnectorDefinition("codex_local").module.models ?? [];
|
||||
const configuredDefault = codexModels[1]?.id;
|
||||
|
||||
if (!configuredDefault) {
|
||||
throw new Error("codex_local requires at least two models for this default policy test.");
|
||||
}
|
||||
it("falls back to definition defaultModel when connector model is empty", () => {
|
||||
const definition = getConnectorDefinition("openai-codex");
|
||||
const configuredDefault = definition.defaultModel;
|
||||
|
||||
const policy = getConnectorModelPolicy({
|
||||
getConnector: () => ({
|
||||
id: "codex_local" as const,
|
||||
id: "openai-codex" as const,
|
||||
label: "Codex",
|
||||
model: configuredDefault,
|
||||
model: null,
|
||||
}),
|
||||
getDisabledModels: () => [],
|
||||
}, "codex_local");
|
||||
}, "openai-codex");
|
||||
|
||||
expect(policy.defaultModel).toBe(configuredDefault);
|
||||
expect(validateConnectorModelSelection({
|
||||
getConnector: () => ({
|
||||
id: "codex_local" as const,
|
||||
id: "openai-codex" as const,
|
||||
label: "Codex",
|
||||
model: configuredDefault,
|
||||
model: null,
|
||||
}),
|
||||
getDisabledModels: () => [],
|
||||
}, "codex_local", null)).toEqual({
|
||||
}, "openai-codex", null)).toEqual({
|
||||
ok: true,
|
||||
effectiveModel: configuredDefault,
|
||||
});
|
||||
|
||||
@@ -19,16 +19,20 @@ describe("parseEnvBindingText", () => {
|
||||
|
||||
describe("buildDefaultConnectorRows", () => {
|
||||
it("preserves previously saved connector commands", () => {
|
||||
const [codex] = buildDefaultConnectorRows([
|
||||
const rows = buildDefaultConnectorRows([
|
||||
{
|
||||
id: "codex_local",
|
||||
id: "openai-codex",
|
||||
command: "/usr/local/bin/codex",
|
||||
status: "ready",
|
||||
notes: "custom note",
|
||||
},
|
||||
]);
|
||||
const codex = rows.find((row) => row.id === "openai-codex");
|
||||
if (!codex) {
|
||||
throw new Error("openai-codex connector not found in default rows.");
|
||||
}
|
||||
|
||||
expect(codex.id).toBe("codex_local");
|
||||
expect(codex.id).toBe("openai-codex");
|
||||
expect(codex.command).toBe("/usr/local/bin/codex");
|
||||
expect(codex.status).toBe("ready");
|
||||
expect(codex.notes).toBe("custom note");
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
describe("shared contracts", () => {
|
||||
it("fills connector defaults for optional fields", () => {
|
||||
const parsed = connectorInputSchema.parse({
|
||||
id: "codex_local",
|
||||
id: "openai-codex",
|
||||
command: "codex",
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { AppDatabase } from "../../src/main/database";
|
||||
import { CredentialStore } from "../../src/main/credentials/credential-store";
|
||||
import { CredentialResolver, CredentialError } from "../../src/main/credentials/credential-resolver";
|
||||
|
||||
beforeAll(() => {
|
||||
process.env.AGENTCOMPANY_ALLOW_INSECURE_VAULT = "true";
|
||||
});
|
||||
|
||||
describe("CredentialResolver", () => {
|
||||
let tempDir: string;
|
||||
let db: AppDatabase;
|
||||
let store: CredentialStore;
|
||||
let resolver: CredentialResolver;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), "agentcompany-credential-resolver-"));
|
||||
db = new AppDatabase(tempDir);
|
||||
db.init();
|
||||
store = new CredentialStore({ db });
|
||||
resolver = new CredentialResolver(store);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
db.close();
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("returns the literal apiKey for an api_key connector", async () => {
|
||||
store.save("anthropic", {
|
||||
kind: "api_key",
|
||||
value: { source: "literal", value: "sk-ant-test-1234567890abcd" },
|
||||
});
|
||||
const resolved = await resolver.resolve("anthropic");
|
||||
expect(resolved.apiKey).toBe("sk-ant-test-1234567890abcd");
|
||||
expect(resolved.extraEnv).toBeUndefined();
|
||||
});
|
||||
|
||||
it("resolves an api_key connector backed by an env variable", async () => {
|
||||
process.env.MY_TEST_OPENAI_KEY = "sk-from-env-1234";
|
||||
store.save("openai", {
|
||||
kind: "api_key",
|
||||
value: { source: "env", value: "MY_TEST_OPENAI_KEY" },
|
||||
});
|
||||
try {
|
||||
const resolved = await resolver.resolve("openai");
|
||||
expect(resolved.apiKey).toBe("sk-from-env-1234");
|
||||
} finally {
|
||||
delete process.env.MY_TEST_OPENAI_KEY;
|
||||
}
|
||||
});
|
||||
|
||||
it("throws CREDENTIAL_MISSING when the env-backed key is unset", async () => {
|
||||
delete process.env.UNSET_TEST_KEY;
|
||||
store.save("xai", { kind: "api_key", value: { source: "env", value: "UNSET_TEST_KEY" } });
|
||||
await expect(resolver.resolve("xai")).rejects.toBeInstanceOf(CredentialError);
|
||||
});
|
||||
|
||||
it("assembles AWS extraEnv for IAM auth mode", async () => {
|
||||
store.save("amazon-bedrock", {
|
||||
kind: "aws",
|
||||
value: {
|
||||
authMode: "iam",
|
||||
region: "us-west-2",
|
||||
accessKeyId: "AKIA-TEST-FAKE-1234",
|
||||
secretAccessKey: "secret-fake-secret",
|
||||
sessionToken: "session-fake-token",
|
||||
},
|
||||
});
|
||||
const resolved = await resolver.resolve("amazon-bedrock");
|
||||
expect(resolved.apiKey).toBe("");
|
||||
expect(resolved.extraEnv).toEqual({
|
||||
AWS_REGION: "us-west-2",
|
||||
AWS_ACCESS_KEY_ID: "AKIA-TEST-FAKE-1234",
|
||||
AWS_SECRET_ACCESS_KEY: "secret-fake-secret",
|
||||
AWS_SESSION_TOKEN: "session-fake-token",
|
||||
});
|
||||
});
|
||||
|
||||
it("assembles AWS extraEnv for profile auth mode plus advanced flags", async () => {
|
||||
store.save("amazon-bedrock", {
|
||||
kind: "aws",
|
||||
value: {
|
||||
authMode: "profile",
|
||||
region: "eu-west-1",
|
||||
awsProfile: "production",
|
||||
forceCache: true,
|
||||
endpointUrl: "https://bedrock-runtime.example.com",
|
||||
skipAuth: true,
|
||||
forceHttp1: true,
|
||||
},
|
||||
});
|
||||
const resolved = await resolver.resolve("amazon-bedrock");
|
||||
expect(resolved.extraEnv).toEqual({
|
||||
AWS_REGION: "eu-west-1",
|
||||
AWS_PROFILE: "production",
|
||||
AWS_BEDROCK_FORCE_CACHE: "1",
|
||||
AWS_ENDPOINT_URL_BEDROCK_RUNTIME: "https://bedrock-runtime.example.com",
|
||||
AWS_BEDROCK_SKIP_AUTH: "1",
|
||||
AWS_BEDROCK_FORCE_HTTP1: "1",
|
||||
});
|
||||
});
|
||||
|
||||
it("assembles Azure extraEnv with deploymentMap", async () => {
|
||||
store.save("azure-openai", {
|
||||
kind: "azure",
|
||||
value: {
|
||||
apiKey: "azure-secret-key",
|
||||
baseUrl: "https://my-resource.openai.azure.com/",
|
||||
resourceName: "my-resource",
|
||||
apiVersion: "2024-08-01-preview",
|
||||
deploymentMap: { "gpt-4o": "gpt4-deploy", "gpt-4o-mini": "gpt4mini-deploy" },
|
||||
},
|
||||
});
|
||||
const resolved = await resolver.resolve("azure-openai");
|
||||
expect(resolved.apiKey).toBe("azure-secret-key");
|
||||
expect(resolved.extraEnv).toMatchObject({
|
||||
AZURE_OPENAI_API_KEY: "azure-secret-key",
|
||||
AZURE_OPENAI_BASE_URL: "https://my-resource.openai.azure.com/",
|
||||
AZURE_OPENAI_RESOURCE_NAME: "my-resource",
|
||||
AZURE_OPENAI_API_VERSION: "2024-08-01-preview",
|
||||
});
|
||||
expect(resolved.extraEnv?.AZURE_OPENAI_DEPLOYMENT_NAME_MAP).toContain("gpt-4o=gpt4-deploy");
|
||||
});
|
||||
|
||||
it("assembles Vertex extraEnv from project + location + credentials path", async () => {
|
||||
store.save("google-vertex", {
|
||||
kind: "gcp_adc",
|
||||
value: {
|
||||
project: "my-gcp-project",
|
||||
location: "us-central1",
|
||||
credentialsPath: "/etc/gcp/service-account.json",
|
||||
},
|
||||
});
|
||||
const resolved = await resolver.resolve("google-vertex");
|
||||
expect(resolved.apiKey).toBe("");
|
||||
expect(resolved.extraEnv).toEqual({
|
||||
GOOGLE_CLOUD_PROJECT: "my-gcp-project",
|
||||
GOOGLE_CLOUD_LOCATION: "us-central1",
|
||||
GOOGLE_APPLICATION_CREDENTIALS: "/etc/gcp/service-account.json",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { AppDatabase } from "../../src/main/database";
|
||||
import { CredentialStore } from "../../src/main/credentials/credential-store";
|
||||
|
||||
beforeAll(() => {
|
||||
// SecretVault throws unless safeStorage is available or this gate is set.
|
||||
// safeStorage is unavailable inside vitest, so we use the test-only path.
|
||||
process.env.AGENTCOMPANY_ALLOW_INSECURE_VAULT = "true";
|
||||
});
|
||||
|
||||
describe("CredentialStore", () => {
|
||||
let tempDir: string;
|
||||
let db: AppDatabase;
|
||||
let store: CredentialStore;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), "agentcompany-credential-store-"));
|
||||
db = new AppDatabase(tempDir);
|
||||
db.init();
|
||||
store = new CredentialStore({ db });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
db.close();
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("round-trips an api_key literal credential", () => {
|
||||
const summary = store.save("anthropic", { kind: "api_key", value: { source: "literal", value: "sk-ant-test1234567890abcd" } });
|
||||
expect(summary.kind).toBe("api_key");
|
||||
expect(summary.status).toBe("active");
|
||||
expect(summary.displayHint).toContain("...");
|
||||
expect(summary.displayHint).not.toContain("test1234567890");
|
||||
|
||||
const decrypted = store.read("anthropic");
|
||||
expect(decrypted).not.toBeNull();
|
||||
expect(decrypted!.kind).toBe("api_key");
|
||||
if (decrypted!.kind === "api_key") {
|
||||
expect(decrypted!.value.source).toBe("literal");
|
||||
expect(decrypted!.value.value).toBe("sk-ant-test1234567890abcd");
|
||||
}
|
||||
});
|
||||
|
||||
it("round-trips an aws credential with bearer mode", () => {
|
||||
store.save("amazon-bedrock", {
|
||||
kind: "aws",
|
||||
value: {
|
||||
authMode: "bearer",
|
||||
region: "us-east-2",
|
||||
bearerToken: "bedrock-bearer-xyz",
|
||||
},
|
||||
});
|
||||
const decrypted = store.read("amazon-bedrock");
|
||||
expect(decrypted?.kind).toBe("aws");
|
||||
if (decrypted?.kind === "aws") {
|
||||
expect(decrypted.value.authMode).toBe("bearer");
|
||||
expect(decrypted.value.region).toBe("us-east-2");
|
||||
expect(decrypted.value.bearerToken).toBe("bedrock-bearer-xyz");
|
||||
}
|
||||
});
|
||||
|
||||
it("removes a credential", () => {
|
||||
store.save("openai", { kind: "api_key", value: { source: "literal", value: "sk-test-abcdefghij" } });
|
||||
expect(store.list().some((row) => row.connectorId === "openai")).toBe(true);
|
||||
store.remove("openai");
|
||||
expect(store.list().some((row) => row.connectorId === "openai")).toBe(false);
|
||||
expect(store.read("openai")).toBeNull();
|
||||
});
|
||||
|
||||
it("setStatus updates the status row without losing ciphertext", () => {
|
||||
store.save("groq", { kind: "api_key", value: { source: "env", value: "GROQ_API_KEY" } });
|
||||
store.setStatus("groq", "expired", "refresh failed");
|
||||
const row = store.list().find((entry) => entry.connectorId === "groq");
|
||||
expect(row?.status).toBe("expired");
|
||||
expect(row?.lastError).toBe("refresh failed");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
apiKeyValueSchema,
|
||||
awsCredentialSchema,
|
||||
azureCredentialSchema,
|
||||
credentialPayloadSchema,
|
||||
gcpAdcCredentialSchema,
|
||||
removeCredentialInputSchema,
|
||||
saveCredentialInputSchema,
|
||||
testCredentialInputSchema,
|
||||
} from "../../src/shared/contracts/schemas-credentials";
|
||||
import {
|
||||
oauthCancelInputSchema,
|
||||
oauthManualCodeResponseSchema,
|
||||
oauthPromptResponseSchema,
|
||||
oauthStartInputSchema,
|
||||
} from "../../src/shared/contracts/schemas-oauth";
|
||||
|
||||
/** Verifies the IPC payload schemas reject malformed input and accept valid
|
||||
* shapes. These are the only contract between renderer ↔ main for the new
|
||||
* credential / OAuth channels — incorrect validation would let bad data
|
||||
* reach pi-mono primitives. */
|
||||
|
||||
describe("credentials Zod schemas", () => {
|
||||
it("apiKeyValueSchema rejects empty value", () => {
|
||||
const result = apiKeyValueSchema.safeParse({ source: "literal", value: "" });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("apiKeyValueSchema accepts both source kinds", () => {
|
||||
expect(apiKeyValueSchema.safeParse({ source: "literal", value: "sk-x" }).success).toBe(true);
|
||||
expect(apiKeyValueSchema.safeParse({ source: "env", value: "MY_KEY" }).success).toBe(true);
|
||||
});
|
||||
|
||||
it("apiKeyValueSchema rejects unknown source", () => {
|
||||
const result = apiKeyValueSchema.safeParse({ source: "shell", value: "$(echo x)" });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("awsCredentialSchema requires authMode", () => {
|
||||
expect(awsCredentialSchema.safeParse({ region: "us-east-1" }).success).toBe(false);
|
||||
expect(awsCredentialSchema.safeParse({ authMode: "profile", region: "us-east-1", awsProfile: "default" }).success).toBe(true);
|
||||
});
|
||||
|
||||
it("awsCredentialSchema accepts all three auth modes", () => {
|
||||
for (const authMode of ["profile", "iam", "bearer"] as const) {
|
||||
const result = awsCredentialSchema.safeParse({ authMode, region: "us-east-1" });
|
||||
expect(result.success).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("azureCredentialSchema requires apiKey", () => {
|
||||
expect(azureCredentialSchema.safeParse({}).success).toBe(false);
|
||||
expect(azureCredentialSchema.safeParse({ apiKey: "azure-secret" }).success).toBe(true);
|
||||
});
|
||||
|
||||
it("gcpAdcCredentialSchema requires project + location", () => {
|
||||
expect(gcpAdcCredentialSchema.safeParse({ project: "" }).success).toBe(false);
|
||||
expect(gcpAdcCredentialSchema.safeParse({ project: "p", location: "us-central1" }).success).toBe(true);
|
||||
});
|
||||
|
||||
it("credentialPayloadSchema discriminates on kind", () => {
|
||||
expect(
|
||||
credentialPayloadSchema.safeParse({
|
||||
kind: "api_key",
|
||||
value: { source: "literal", value: "sk-x" },
|
||||
}).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
credentialPayloadSchema.safeParse({
|
||||
kind: "azure",
|
||||
value: { source: "literal", value: "wrong-shape" },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("saveCredentialInputSchema requires connectorId + payload", () => {
|
||||
expect(
|
||||
saveCredentialInputSchema.safeParse({
|
||||
connectorId: "anthropic",
|
||||
payload: { kind: "api_key", value: { source: "literal", value: "sk-x" } },
|
||||
}).success,
|
||||
).toBe(true);
|
||||
expect(saveCredentialInputSchema.safeParse({ connectorId: "" }).success).toBe(false);
|
||||
expect(saveCredentialInputSchema.safeParse({ payload: {} }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("removeCredentialInputSchema rejects empty connectorId", () => {
|
||||
expect(removeCredentialInputSchema.safeParse({ connectorId: "openai" }).success).toBe(true);
|
||||
expect(removeCredentialInputSchema.safeParse({ connectorId: "" }).success).toBe(false);
|
||||
expect(removeCredentialInputSchema.safeParse({}).success).toBe(false);
|
||||
});
|
||||
|
||||
it("testCredentialInputSchema rejects empty connectorId", () => {
|
||||
expect(testCredentialInputSchema.safeParse({ connectorId: "openai" }).success).toBe(true);
|
||||
expect(testCredentialInputSchema.safeParse({ connectorId: "" }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("oauth Zod schemas", () => {
|
||||
it("oauthStartInputSchema rejects empty connectorId", () => {
|
||||
expect(oauthStartInputSchema.safeParse({ connectorId: "claude-pro" }).success).toBe(true);
|
||||
expect(oauthStartInputSchema.safeParse({ connectorId: "" }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("oauthCancelInputSchema rejects missing connectorId", () => {
|
||||
expect(oauthCancelInputSchema.safeParse({ connectorId: "claude-pro" }).success).toBe(true);
|
||||
expect(oauthCancelInputSchema.safeParse({}).success).toBe(false);
|
||||
});
|
||||
|
||||
it("oauthPromptResponseSchema requires connectorId, requestId and a (possibly empty) value", () => {
|
||||
expect(
|
||||
oauthPromptResponseSchema.safeParse({
|
||||
connectorId: "github-copilot",
|
||||
requestId: "req-1",
|
||||
value: "github.com",
|
||||
}).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
oauthPromptResponseSchema.safeParse({ connectorId: "github-copilot", requestId: "" }).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
oauthPromptResponseSchema.safeParse({ connectorId: "github-copilot", value: "x" }).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("oauthManualCodeResponseSchema requires non-empty code", () => {
|
||||
expect(
|
||||
oauthManualCodeResponseSchema.safeParse({
|
||||
connectorId: "anthropic",
|
||||
requestId: "req-1",
|
||||
code: "abcd",
|
||||
}).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
oauthManualCodeResponseSchema.safeParse({
|
||||
connectorId: "anthropic",
|
||||
requestId: "req-1",
|
||||
code: "",
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,7 @@ function makeRun(overrides: Partial<RunRecord> = {}): RunRecord {
|
||||
taskId: "task-1",
|
||||
agentId: "agent-1",
|
||||
workspaceId: null,
|
||||
connectorId: "claude_local",
|
||||
connectorId: "claude-pro",
|
||||
status: "succeeded",
|
||||
summary: "",
|
||||
errorMessage: null,
|
||||
@@ -47,7 +47,7 @@ function makeCost(overrides: Partial<CostRecord> = {}): CostRecord {
|
||||
runId: "run-1",
|
||||
taskId: null,
|
||||
agentId: null,
|
||||
connectorId: "claude_local",
|
||||
connectorId: "claude-pro",
|
||||
amountUsd: 0.5,
|
||||
unattributed: false,
|
||||
createdAt: "2026-03-20T10:00:00Z",
|
||||
@@ -67,7 +67,7 @@ function makeAgent(overrides: Partial<AgentRecord> = {}): AgentRecord {
|
||||
department: "engineering",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "claude_local",
|
||||
connectorId: "claude-pro",
|
||||
workspaceId: null,
|
||||
model: null,
|
||||
capabilities: "[]",
|
||||
|
||||
+16
-16
@@ -54,7 +54,7 @@ describe("AppDatabase", () => {
|
||||
title: "Lead operator",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: "gpt-5",
|
||||
capabilities: "Directs execution",
|
||||
@@ -95,7 +95,7 @@ describe("AppDatabase", () => {
|
||||
value: "sk-test-secret",
|
||||
});
|
||||
db.saveConnector({
|
||||
id: "codex_local",
|
||||
id: "openai-codex",
|
||||
command: "codex",
|
||||
model: "gpt-5",
|
||||
envBindingText: "OPENAI_API_KEY=vault-openai",
|
||||
@@ -189,14 +189,14 @@ describe("AppDatabase", () => {
|
||||
title: "Lead",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Leads",
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
db.saveConnector({
|
||||
id: "codex_local",
|
||||
id: "openai-codex",
|
||||
command: "codex",
|
||||
model: "gpt-5",
|
||||
envBindingText: "",
|
||||
@@ -217,14 +217,14 @@ describe("AppDatabase", () => {
|
||||
taskId,
|
||||
agentId: leadId,
|
||||
workspaceId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
const { id: runId2 } = db.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId: leadId,
|
||||
workspaceId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
|
||||
expect(db.checkoutTask(taskId, runId1)).toBe(true);
|
||||
@@ -255,7 +255,7 @@ describe("AppDatabase", () => {
|
||||
title: "Budget Owner",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Ships work",
|
||||
@@ -276,7 +276,7 @@ describe("AppDatabase", () => {
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
|
||||
const outcome = db.finishRun({
|
||||
@@ -310,7 +310,7 @@ describe("AppDatabase", () => {
|
||||
title: "Manager",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId: null,
|
||||
model: null,
|
||||
capabilities: "Manages",
|
||||
@@ -323,7 +323,7 @@ describe("AppDatabase", () => {
|
||||
title: "Pending Hire",
|
||||
status: "pending_approval",
|
||||
reportsTo: managerId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId: null,
|
||||
model: null,
|
||||
capabilities: "Will execute after approval",
|
||||
@@ -362,7 +362,7 @@ describe("AppDatabase", () => {
|
||||
title: "Active",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId: null,
|
||||
model: null,
|
||||
capabilities: "Ships",
|
||||
@@ -375,7 +375,7 @@ describe("AppDatabase", () => {
|
||||
title: "Pending",
|
||||
status: "pending_approval",
|
||||
reportsTo: activeAgentId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId: null,
|
||||
model: null,
|
||||
capabilities: "Waiting",
|
||||
@@ -479,7 +479,7 @@ describe("AppDatabase", () => {
|
||||
title: "Test",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId: wsId,
|
||||
model: null,
|
||||
capabilities: "test",
|
||||
@@ -495,7 +495,7 @@ describe("AppDatabase", () => {
|
||||
status: "todo",
|
||||
});
|
||||
|
||||
const run = db.createRun({ companyId, taskId, agentId, workspaceId: wsId, connectorId: "codex_local" });
|
||||
const run = db.createRun({ companyId, taskId, agentId, workspaceId: wsId, connectorId: "openai-codex" });
|
||||
await db.appendRunLog(run.id, "stdout", "test log line\n");
|
||||
|
||||
// Finish the run with a very old timestamp
|
||||
@@ -544,7 +544,7 @@ describe("AppDatabase", () => {
|
||||
title: "CEO",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId: null,
|
||||
model: null,
|
||||
capabilities: "manage",
|
||||
@@ -557,7 +557,7 @@ describe("AppDatabase", () => {
|
||||
title: "IC",
|
||||
status: "pending_approval",
|
||||
reportsTo: managerId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId: null,
|
||||
model: null,
|
||||
capabilities: "code",
|
||||
|
||||
@@ -26,7 +26,7 @@ function scaffold(db: AppDatabase) {
|
||||
status: "active",
|
||||
});
|
||||
db.saveConnector({
|
||||
id: "gemini_local",
|
||||
id: "gemini-cli",
|
||||
command: "gemini",
|
||||
model: "gemini-pro",
|
||||
envBindingText: "",
|
||||
@@ -47,7 +47,7 @@ function scaffold(db: AppDatabase) {
|
||||
title: "Developer",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "gemini_local",
|
||||
connectorId: "gemini-cli",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Executes tasks",
|
||||
@@ -134,7 +134,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "gemini_local",
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
db.checkoutTask(taskId, run1.id);
|
||||
db.markRunStatus(run1.id, "running", "");
|
||||
@@ -154,7 +154,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
|
||||
taskId: taskId2,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "gemini_local",
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
db.checkoutTask(taskId2, run0.id);
|
||||
db.markRunStatus(run0.id, "running", "");
|
||||
@@ -222,7 +222,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "gemini_local",
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
db.checkoutTask(taskId, run.id);
|
||||
db.markRunStatus(run.id, "running", "");
|
||||
@@ -271,7 +271,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "gemini_local",
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
db.checkoutTask(taskId, run.id);
|
||||
db.markRunStatus(run.id, "running", "");
|
||||
@@ -329,7 +329,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "gemini_local",
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
db.checkoutTask(taskId, run.id);
|
||||
db.markRunStatus(run.id, "running", "");
|
||||
@@ -382,7 +382,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "gemini_local",
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
db.checkoutTask(taskId, run.id);
|
||||
db.markRunStatus(run.id, "running", "");
|
||||
@@ -429,7 +429,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "gemini_local",
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
db.checkoutTask(taskId, run.id);
|
||||
db.markRunStatus(run.id, "running", "");
|
||||
|
||||
@@ -37,7 +37,7 @@ function createTestCompany(db: AppDatabase): string {
|
||||
|
||||
function createTestConnector(db: AppDatabase): void {
|
||||
db.saveConnector({
|
||||
id: "gemini_local",
|
||||
id: "gemini-cli",
|
||||
command: "gemini",
|
||||
model: "gemini-pro",
|
||||
envBindingText: "",
|
||||
@@ -64,7 +64,7 @@ function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string
|
||||
title: name,
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "gemini_local",
|
||||
connectorId: "gemini-cli",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Executes tasks",
|
||||
@@ -116,14 +116,14 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
|
||||
taskId,
|
||||
agentId: agentA,
|
||||
workspaceId,
|
||||
connectorId: "gemini_local",
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
const { id: runIdB } = db.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId: agentB,
|
||||
workspaceId,
|
||||
connectorId: "gemini_local",
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
|
||||
// Agent-A claims first
|
||||
@@ -152,14 +152,14 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
|
||||
taskId: taskId1,
|
||||
agentId: agentA,
|
||||
workspaceId,
|
||||
connectorId: "gemini_local",
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
const { id: runIdB1 } = db.createRun({
|
||||
companyId,
|
||||
taskId: taskId1,
|
||||
agentId: agentB,
|
||||
workspaceId,
|
||||
connectorId: "gemini_local",
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
|
||||
// Agent-A wins first task
|
||||
@@ -174,7 +174,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
|
||||
taskId: taskId2,
|
||||
agentId: agentB,
|
||||
workspaceId,
|
||||
connectorId: "gemini_local",
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
const resultB2 = db.checkoutTask(taskId2, runIdB2);
|
||||
expect(resultB2).toBe(true);
|
||||
@@ -198,7 +198,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
|
||||
taskId,
|
||||
agentId: agentA,
|
||||
workspaceId,
|
||||
connectorId: "gemini_local",
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
expect(db.checkoutTask(taskId, runIdA)).toBe(true);
|
||||
|
||||
@@ -208,7 +208,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
|
||||
taskId,
|
||||
agentId: agentB,
|
||||
workspaceId,
|
||||
connectorId: "gemini_local",
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
const result = db.checkoutTask(taskId, runIdB);
|
||||
expect(result).toBe(false);
|
||||
@@ -231,7 +231,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
|
||||
taskId,
|
||||
agentId: agentA,
|
||||
workspaceId,
|
||||
connectorId: "gemini_local",
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
expect(db.checkoutTask(taskId, runIdA)).toBe(true);
|
||||
|
||||
@@ -252,7 +252,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
|
||||
taskId,
|
||||
agentId: agentB,
|
||||
workspaceId,
|
||||
connectorId: "gemini_local",
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
const result = db.checkoutTask(taskId, runIdB);
|
||||
expect(result).toBe(false);
|
||||
@@ -270,7 +270,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
|
||||
taskId,
|
||||
agentId: agentA,
|
||||
workspaceId,
|
||||
connectorId: "gemini_local",
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
expect(db.checkoutTask(taskId, runIdA)).toBe(true);
|
||||
|
||||
@@ -306,7 +306,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
|
||||
taskId,
|
||||
agentId: agentA,
|
||||
workspaceId,
|
||||
connectorId: "gemini_local",
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
expect(db.checkoutTask(taskId, runIdA)).toBe(true);
|
||||
|
||||
@@ -328,7 +328,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
|
||||
taskId,
|
||||
agentId: agentB,
|
||||
workspaceId,
|
||||
connectorId: "gemini_local",
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
const result = db.checkoutTask(taskId, runIdB);
|
||||
expect(result).toBe(true);
|
||||
|
||||
@@ -20,16 +20,16 @@ import type { ConnectorId } from "@shared/types";
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function scaffold(db: AppDatabase, connectorId: ConnectorId = "gemini_local") {
|
||||
function scaffold(db: AppDatabase, connectorId: ConnectorId = "gemini-cli") {
|
||||
const companyId = db.saveCompany({
|
||||
name: "Session Edge Case Co",
|
||||
description: "Session rotation edge case tests (D-05.3)",
|
||||
status: "active",
|
||||
});
|
||||
|
||||
// Always save gemini_local (needed for most tests)
|
||||
// Always save gemini-cli (needed for most tests)
|
||||
db.saveConnector({
|
||||
id: "gemini_local",
|
||||
id: "gemini-cli",
|
||||
command: "gemini",
|
||||
model: "gemini-pro",
|
||||
envBindingText: "",
|
||||
@@ -119,7 +119,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
taskId,
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "gemini_local",
|
||||
adapterType: "gemini-cli",
|
||||
sessionParamsJson: null,
|
||||
sessionDisplayId: null,
|
||||
});
|
||||
@@ -135,7 +135,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
expect(session!.runCount).toBe(DEFAULT_THRESHOLDS.maxRuns);
|
||||
|
||||
// Evaluate rotation -- should trigger
|
||||
const result = sessionService.evaluateRotation(session!, "gemini_local");
|
||||
const result = sessionService.evaluateRotation(session!, "gemini-cli");
|
||||
expect(result.shouldRotate).toBe(true);
|
||||
expect(result.reason).toBe("max_runs");
|
||||
});
|
||||
@@ -148,7 +148,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
taskId,
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "gemini_local",
|
||||
adapterType: "gemini-cli",
|
||||
sessionParamsJson: null,
|
||||
sessionDisplayId: null,
|
||||
});
|
||||
@@ -162,7 +162,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
const totalTokens = session!.totalInputTokens + session!.totalOutputTokens;
|
||||
expect(totalTokens).toBe(550000);
|
||||
|
||||
const result = sessionService.evaluateRotation(session!, "gemini_local");
|
||||
const result = sessionService.evaluateRotation(session!, "gemini-cli");
|
||||
expect(result.shouldRotate).toBe(true);
|
||||
expect(result.reason).toBe("max_tokens");
|
||||
});
|
||||
@@ -175,7 +175,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
taskId,
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "gemini_local",
|
||||
adapterType: "gemini-cli",
|
||||
sessionParamsJson: null,
|
||||
sessionDisplayId: null,
|
||||
});
|
||||
@@ -191,7 +191,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "gemini_local" as ConnectorId,
|
||||
connectorId: "gemini-cli" as ConnectorId,
|
||||
});
|
||||
db.finishRun({
|
||||
runId: run.id,
|
||||
@@ -233,7 +233,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
taskId,
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "gemini_local",
|
||||
adapterType: "gemini-cli",
|
||||
sessionParamsJson: JSON.stringify({ resumeKey: "abc" }),
|
||||
sessionDisplayId: "gemini-session-1",
|
||||
});
|
||||
@@ -249,7 +249,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
taskId,
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "gemini_local",
|
||||
adapterType: "gemini-cli",
|
||||
sessionParamsJson: JSON.stringify({ resumeKey: "abc" }),
|
||||
sessionDisplayId: "gemini-session-2",
|
||||
});
|
||||
@@ -266,15 +266,15 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
expect(newSession!.totalOutputTokens).toBe(0);
|
||||
});
|
||||
|
||||
it("claude connector is exempt from rotation", () => {
|
||||
const { companyId, agentId, taskId } = scaffold(db, "gemini_local");
|
||||
it("claude connector triggers rotation under pi-mono (no native exemption)", () => {
|
||||
const { companyId, agentId, taskId } = scaffold(db, "gemini-cli");
|
||||
const sessionService = createSessionManagementService({ db, logger: mockLogger });
|
||||
|
||||
const sessionId = db.createNewSession({
|
||||
taskId,
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "claude_local",
|
||||
adapterType: "claude-pro",
|
||||
sessionParamsJson: null,
|
||||
sessionDisplayId: null,
|
||||
});
|
||||
@@ -285,21 +285,21 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
}
|
||||
|
||||
const session = db.getActiveTaskSession(taskId)!;
|
||||
// Per Pitfall 6: claude_local has nativeContextManagement: true, exempt from rotation
|
||||
const result = sessionService.evaluateRotation(session, "claude_local");
|
||||
expect(result.shouldRotate).toBe(false);
|
||||
expect(result.reason).toBeNull();
|
||||
// Pi-mono delegates compaction to pi-agent-core; AgentCompany still applies rotation uniformly.
|
||||
const result = sessionService.evaluateRotation(session, "claude-pro");
|
||||
expect(result.shouldRotate).toBe(true);
|
||||
expect(result.reason).not.toBeNull();
|
||||
});
|
||||
|
||||
it("codex connector is exempt from rotation", () => {
|
||||
const { companyId, agentId, taskId } = scaffold(db, "gemini_local");
|
||||
it("codex connector triggers rotation under pi-mono (no native exemption)", () => {
|
||||
const { companyId, agentId, taskId } = scaffold(db, "gemini-cli");
|
||||
const sessionService = createSessionManagementService({ db, logger: mockLogger });
|
||||
|
||||
const sessionId = db.createNewSession({
|
||||
taskId,
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "codex_local",
|
||||
adapterType: "openai-codex",
|
||||
sessionParamsJson: null,
|
||||
sessionDisplayId: null,
|
||||
});
|
||||
@@ -310,10 +310,10 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
}
|
||||
|
||||
const session = db.getActiveTaskSession(taskId)!;
|
||||
// Per Pitfall 6: codex_local has nativeContextManagement: true, exempt from rotation
|
||||
const result = sessionService.evaluateRotation(session, "codex_local");
|
||||
expect(result.shouldRotate).toBe(false);
|
||||
expect(result.reason).toBeNull();
|
||||
// Pi-mono delegates compaction to pi-agent-core; AgentCompany still applies rotation uniformly.
|
||||
const result = sessionService.evaluateRotation(session, "openai-codex");
|
||||
expect(result.shouldRotate).toBe(true);
|
||||
expect(result.reason).not.toBeNull();
|
||||
});
|
||||
|
||||
it("handoff markdown available for next run context after rotation", () => {
|
||||
@@ -325,7 +325,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
taskId,
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "gemini_local",
|
||||
adapterType: "gemini-cli",
|
||||
sessionParamsJson: null,
|
||||
sessionDisplayId: null,
|
||||
});
|
||||
@@ -337,7 +337,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "gemini_local" as ConnectorId,
|
||||
connectorId: "gemini-cli" as ConnectorId,
|
||||
});
|
||||
db.finishRun({
|
||||
runId: run.id,
|
||||
|
||||
@@ -28,7 +28,7 @@ function createTestCompany(db: AppDatabase): string {
|
||||
|
||||
function createTestConnector(db: AppDatabase): void {
|
||||
db.saveConnector({
|
||||
id: "codex_local",
|
||||
id: "openai-codex",
|
||||
command: "codex",
|
||||
model: "gpt-5",
|
||||
envBindingText: "",
|
||||
@@ -55,7 +55,7 @@ function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string
|
||||
title: name,
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Executes tasks",
|
||||
|
||||
@@ -32,13 +32,13 @@ describe("ipc-core-agent-graph-handlers model enforcement", () => {
|
||||
db.init();
|
||||
|
||||
db.updateConnectorHealth({
|
||||
id: "codex_local",
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
version: "test",
|
||||
lastError: null,
|
||||
});
|
||||
db.setDisabledModels("codex_local", [{ id: "gpt-5.4", label: "gpt-5.4" }]);
|
||||
db.setDisabledModels("openai-codex", [{ id: "gpt-5.4", label: "gpt-5.4" }]);
|
||||
|
||||
const companyId = db.saveCompany({
|
||||
name: "Handler Test Co",
|
||||
@@ -60,7 +60,7 @@ describe("ipc-core-agent-graph-handlers model enforcement", () => {
|
||||
title: "Chief Executive Officer",
|
||||
status: "idle",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Lead the company",
|
||||
@@ -93,7 +93,7 @@ describe("ipc-core-agent-graph-handlers model enforcement", () => {
|
||||
department: "executive",
|
||||
status: "idle",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: "gpt-5.4",
|
||||
metadataJson: "{}",
|
||||
|
||||
@@ -26,11 +26,17 @@ afterEach(async () => {
|
||||
});
|
||||
|
||||
describe("ipc-core-admin-handlers connector saves", () => {
|
||||
it("accepts compatible connector model catalogs stored as csv", async () => {
|
||||
it("accepts a valid pi-ai model for a pi-mono connector", async () => {
|
||||
const profileDir = await createTempDir("agentcompany-admin-handler-");
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
const { getModels } = await import("@mariozechner/pi-ai");
|
||||
const anthropicModel = getModels("anthropic" as never)[0]?.id;
|
||||
if (!anthropicModel) {
|
||||
throw new Error("pi-ai anthropic provider must expose at least one model.");
|
||||
}
|
||||
|
||||
const handlers = new Map<string, IpcHandler>();
|
||||
registerCoreAdminHandlers({
|
||||
registerHandle: (channel, handler) => {
|
||||
@@ -50,16 +56,26 @@ describe("ipc-core-admin-handlers connector saves", () => {
|
||||
throw new Error("saveConnector handler was not registered.");
|
||||
}
|
||||
|
||||
// Pi-mono connectors default to an empty command; the schema requires a
|
||||
// non-empty one, so seed it before hitting the IPC handler.
|
||||
db.saveConnector({
|
||||
id: "anthropic",
|
||||
command: "pi-mono",
|
||||
model: null,
|
||||
envBindingText: "",
|
||||
notes: "",
|
||||
});
|
||||
|
||||
const result = await saveConnector(null as unknown as IpcMainInvokeEvent, {
|
||||
id: "claude_compatible",
|
||||
command: "claude",
|
||||
model: "model-alpha,model-beta,model-gamma",
|
||||
id: "anthropic",
|
||||
command: "pi-mono",
|
||||
model: anthropicModel,
|
||||
envBindingText: "ANTHROPIC_BASE_URL=https://example.com/anthropic",
|
||||
notes: "Compat provider",
|
||||
notes: "Pi-mono connector",
|
||||
}) as { ok: boolean; error?: { code: string; message: string } };
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(db.getConnector("claude_compatible").model).toBe("model-alpha,model-beta,model-gamma");
|
||||
expect(db.getConnector("anthropic").model).toBe(anthropicModel);
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { redactTokens } from "../../src/main/worker/log-redactor";
|
||||
|
||||
/** Verifies the worker process token redactor strips known
|
||||
* credential-shaped substrings from log strings before they leave the
|
||||
* worker process. */
|
||||
describe("worker log redactor", () => {
|
||||
it("redacts Bearer tokens", () => {
|
||||
const input = "Authorization: Bearer abc.def.ghi-jkl_mnop";
|
||||
expect(redactTokens(input)).toBe("Authorization: Bearer [REDACTED]");
|
||||
});
|
||||
|
||||
it("redacts authorization header values regardless of case", () => {
|
||||
expect(redactTokens("authorization=secret-value-1234567890")).toContain("[REDACTED]");
|
||||
expect(redactTokens("Authorization: secret-value-1234567890")).toContain("[REDACTED]");
|
||||
});
|
||||
|
||||
it("redacts access_token query strings", () => {
|
||||
expect(redactTokens("https://api.example.com/?access_token=abc123xyz&foo=1")).toBe(
|
||||
"https://api.example.com/?access_token=[REDACTED]&foo=1",
|
||||
);
|
||||
});
|
||||
|
||||
it("redacts refresh_token values", () => {
|
||||
expect(redactTokens("refresh_token=long.refresh.tok&grant_type=refresh_token")).toContain(
|
||||
"refresh_token=[REDACTED]",
|
||||
);
|
||||
});
|
||||
|
||||
it("redacts api_key key/value pairs", () => {
|
||||
const input = "config { api_key: \"sk-test-1234567890abcdef\", model: \"gpt-4o\" }";
|
||||
const result = redactTokens(input);
|
||||
expect(result).not.toContain("sk-test-1234567890abcdef");
|
||||
expect(result).toContain("[REDACTED]");
|
||||
});
|
||||
|
||||
it("redacts sk- and sk-ant- prefixed keys anywhere in a string", () => {
|
||||
expect(redactTokens("error using sk-ant-api03_abcdefghijklmnop, retrying")).toContain("sk-[REDACTED]");
|
||||
expect(redactTokens("token=sk-test1234567890_abcd")).toContain("sk-[REDACTED]");
|
||||
});
|
||||
|
||||
it("redacts GitHub personal access token shapes", () => {
|
||||
expect(redactTokens("git push https://x:ghp_aaaaaaaaaaaaaaaaaa@github.com/foo")).toContain("ghp_[REDACTED]");
|
||||
});
|
||||
|
||||
it("redacts AWS access key id prefixes", () => {
|
||||
expect(redactTokens("aws cred AKIAIOSFODNN7EXAMPLE in trace")).toContain("AKIA[REDACTED]");
|
||||
});
|
||||
|
||||
it("leaves credential-free strings unchanged", () => {
|
||||
const safe = "Just a regular log line about model claude-opus-4-5 with usage 123 tokens.";
|
||||
expect(redactTokens(safe)).toBe(safe);
|
||||
});
|
||||
|
||||
it("redacts multiple matches in one string", () => {
|
||||
const input = "Bearer abc.def.ghi.jkl Authorization=secretsecretsecret";
|
||||
const result = redactTokens(input);
|
||||
expect(result).not.toContain("abc.def.ghi.jkl");
|
||||
expect(result).not.toContain("secretsecretsecret");
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,7 @@ afterEach(async () => {
|
||||
|
||||
function createScaffold(db: AppDatabase) {
|
||||
db.updateConnectorHealth({
|
||||
id: "codex_local",
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
version: "test",
|
||||
@@ -48,7 +48,7 @@ function createScaffold(db: AppDatabase) {
|
||||
title: "Chief Executive Officer",
|
||||
status: "idle",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Lead the company",
|
||||
@@ -61,7 +61,7 @@ function createScaffold(db: AppDatabase) {
|
||||
title: "Chief Technology Officer",
|
||||
status: "idle",
|
||||
reportsTo: ceoId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Lead engineering",
|
||||
@@ -75,7 +75,7 @@ function createScaffold(db: AppDatabase) {
|
||||
title: "Chief Marketing Officer",
|
||||
status: "idle",
|
||||
reportsTo: ceoId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Lead marketing",
|
||||
@@ -89,7 +89,7 @@ function createScaffold(db: AppDatabase) {
|
||||
title: "Pending Hire",
|
||||
status: "pending_approval",
|
||||
reportsTo: ctoId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Pending",
|
||||
@@ -103,7 +103,7 @@ function createScaffold(db: AppDatabase) {
|
||||
title: "Former COO",
|
||||
status: "terminated",
|
||||
reportsTo: ceoId,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "No longer active",
|
||||
|
||||
@@ -39,7 +39,7 @@ describe("AppDatabase.searchMessages", () => {
|
||||
title: "Senior Engineer",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "claude_local",
|
||||
connectorId: "claude-pro",
|
||||
workspaceId: null,
|
||||
model: null,
|
||||
capabilities: "coding",
|
||||
@@ -53,7 +53,7 @@ describe("AppDatabase.searchMessages", () => {
|
||||
title: "Lead Designer",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "claude_local",
|
||||
connectorId: "claude-pro",
|
||||
workspaceId: null,
|
||||
model: null,
|
||||
capabilities: "design",
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { AppDatabase } from "../../src/main/database";
|
||||
import { CredentialStore } from "../../src/main/credentials/credential-store";
|
||||
import { OAuthLoginService } from "../../src/main/credentials/oauth-login-service";
|
||||
|
||||
vi.mock("electron", () => ({
|
||||
shell: { openExternal: vi.fn(async () => true) },
|
||||
}));
|
||||
|
||||
beforeAll(() => {
|
||||
process.env.AGENTCOMPANY_ALLOW_INSECURE_VAULT = "true";
|
||||
});
|
||||
|
||||
const noopLogger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
||||
|
||||
describe("OAuthLoginService", () => {
|
||||
let tempDir: string;
|
||||
let db: AppDatabase;
|
||||
let store: CredentialStore;
|
||||
let service: OAuthLoginService;
|
||||
const emit = vi.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), "agentcompany-oauth-service-"));
|
||||
db = new AppDatabase(tempDir);
|
||||
db.init();
|
||||
store = new CredentialStore({ db });
|
||||
service = new OAuthLoginService({
|
||||
store,
|
||||
emitEvent: emit,
|
||||
refreshConnectorHealth: async () => {},
|
||||
logger: noopLogger,
|
||||
});
|
||||
emit.mockClear();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
db.close();
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("rejects start() for a non-OAuth connector", async () => {
|
||||
await expect(service.start("anthropic")).rejects.toThrow(/does not use OAuth/);
|
||||
});
|
||||
|
||||
it("rejects start() for an unknown connector id", async () => {
|
||||
await expect(service.start("definitely-not-a-connector")).rejects.toThrow(/Unknown connector/);
|
||||
});
|
||||
|
||||
it("submitPromptResponse returns false when there is no pending prompt", () => {
|
||||
expect(service.submitPromptResponse("nonexistent-request-id", "value")).toBe(false);
|
||||
});
|
||||
|
||||
it("submitManualCode returns false when there is no pending request", () => {
|
||||
expect(service.submitManualCode("nonexistent-request-id", "code")).toBe(false);
|
||||
});
|
||||
|
||||
it("cancel() of a connector with no active flow is a no-op (does not throw)", () => {
|
||||
expect(() => service.cancel("claude-pro")).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -14,7 +14,7 @@ function buildCtx(overrides: Partial<PromptContext> = {}): PromptContext {
|
||||
department: "engineering",
|
||||
status: "active",
|
||||
reportsTo: "ceo-1",
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId: null,
|
||||
model: null,
|
||||
capabilities: "Lead engineering",
|
||||
@@ -46,7 +46,7 @@ function buildCtx(overrides: Partial<PromptContext> = {}): PromptContext {
|
||||
department: "executive",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId: null,
|
||||
model: null,
|
||||
capabilities: "Lead the company",
|
||||
|
||||
@@ -41,7 +41,7 @@ function scaffold(db: AppDatabase) {
|
||||
title: "Developer",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "claude_local",
|
||||
connectorId: "claude-pro",
|
||||
workspaceId,
|
||||
model: "claude-4-opus",
|
||||
capabilities: "Writes code",
|
||||
@@ -107,7 +107,7 @@ describe("task_sessions CRUD", () => {
|
||||
taskId,
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "claude_local",
|
||||
adapterType: "claude-pro",
|
||||
sessionParamsJson: JSON.stringify({ sessionId: "abc123" }),
|
||||
sessionDisplayId: "claude-abc123",
|
||||
});
|
||||
@@ -118,7 +118,7 @@ describe("task_sessions CRUD", () => {
|
||||
expect(session!.taskId).toBe(taskId);
|
||||
expect(session!.companyId).toBe(companyId);
|
||||
expect(session!.agentId).toBe(agentId);
|
||||
expect(session!.adapterType).toBe("claude_local");
|
||||
expect(session!.adapterType).toBe("claude-pro");
|
||||
expect(session!.sessionParamsJson).toBe(JSON.stringify({ sessionId: "abc123" }));
|
||||
expect(session!.sessionDisplayId).toBe("claude-abc123");
|
||||
expect(session!.runCount).toBe(0);
|
||||
@@ -135,7 +135,7 @@ describe("task_sessions CRUD", () => {
|
||||
taskId,
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "claude_local",
|
||||
adapterType: "claude-pro",
|
||||
sessionParamsJson: null,
|
||||
sessionDisplayId: null,
|
||||
});
|
||||
@@ -163,7 +163,7 @@ describe("task_sessions CRUD", () => {
|
||||
taskId,
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "claude_local",
|
||||
adapterType: "claude-pro",
|
||||
sessionParamsJson: null,
|
||||
sessionDisplayId: null,
|
||||
});
|
||||
@@ -183,7 +183,7 @@ describe("task_sessions CRUD", () => {
|
||||
taskId,
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "gemini_local",
|
||||
adapterType: "gemini-cli",
|
||||
sessionParamsJson: JSON.stringify({ key: "val" }),
|
||||
sessionDisplayId: "gemini-session-1",
|
||||
});
|
||||
@@ -195,14 +195,14 @@ describe("task_sessions CRUD", () => {
|
||||
const session = db.getActiveTaskSession(taskId);
|
||||
expect(session).not.toBeNull();
|
||||
expect(session!.id).toBe(sessionId);
|
||||
expect(session!.adapterType).toBe("gemini_local");
|
||||
expect(session!.adapterType).toBe("gemini-cli");
|
||||
});
|
||||
|
||||
it("getRecentRunsForTask returns last N runs ordered by finished_at DESC", () => {
|
||||
const { taskId, companyId, agentId, workspaceId } = scaffold(db);
|
||||
|
||||
// Create some runs
|
||||
const run1 = db.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "claude_local" });
|
||||
const run1 = db.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "claude-pro" });
|
||||
db.finishRun({
|
||||
runId: run1.id,
|
||||
status: "succeeded",
|
||||
@@ -216,7 +216,7 @@ describe("task_sessions CRUD", () => {
|
||||
unattributedCost: false,
|
||||
});
|
||||
|
||||
const run2 = db.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "claude_local" });
|
||||
const run2 = db.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "claude-pro" });
|
||||
db.finishRun({
|
||||
runId: run2.id,
|
||||
status: "failed",
|
||||
@@ -242,22 +242,25 @@ describe("task_sessions CRUD", () => {
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// adapter exemption
|
||||
// connector definition lookup
|
||||
// ===========================================================================
|
||||
describe("adapter exemption", () => {
|
||||
it("claude_local connector has nativeContextManagement: true", () => {
|
||||
const def = getConnectorDefinition("claude_local" as ConnectorId);
|
||||
expect(def.module.nativeContextManagement).toBe(true);
|
||||
describe("connector definition lookup", () => {
|
||||
it("resolves claude-pro definition from the pi-mono catalog", () => {
|
||||
const def = getConnectorDefinition("claude-pro" as ConnectorId);
|
||||
expect(def.id).toBe("claude-pro");
|
||||
expect(def.provider).toBe("anthropic");
|
||||
});
|
||||
|
||||
it("codex_local connector has nativeContextManagement: true", () => {
|
||||
const def = getConnectorDefinition("codex_local" as ConnectorId);
|
||||
expect(def.module.nativeContextManagement).toBe(true);
|
||||
it("resolves openai-codex definition from the pi-mono catalog", () => {
|
||||
const def = getConnectorDefinition("openai-codex" as ConnectorId);
|
||||
expect(def.id).toBe("openai-codex");
|
||||
expect(def.provider).toBe("openai-codex");
|
||||
});
|
||||
|
||||
it("gemini_local connector has nativeContextManagement: false", () => {
|
||||
const def = getConnectorDefinition("gemini_local" as ConnectorId);
|
||||
expect(def.module.nativeContextManagement).toBe(false);
|
||||
it("resolves gemini-cli definition from the pi-mono catalog", () => {
|
||||
const def = getConnectorDefinition("gemini-cli" as ConnectorId);
|
||||
expect(def.id).toBe("gemini-cli");
|
||||
expect(def.provider).toBe("google-gemini-cli");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -298,7 +301,7 @@ describe("session management service", () => {
|
||||
companyId: "co-1",
|
||||
taskId: "task-1",
|
||||
agentId: "agent-1",
|
||||
adapterType: "gemini_local",
|
||||
adapterType: "gemini-cli",
|
||||
sessionParamsJson: null,
|
||||
sessionDisplayId: null,
|
||||
lastRunId: null,
|
||||
@@ -312,7 +315,7 @@ describe("session management service", () => {
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
const result = svc.evaluateRotation(session, "gemini_local");
|
||||
const result = svc.evaluateRotation(session, "gemini-cli");
|
||||
expect(result.shouldRotate).toBe(false);
|
||||
expect(result.reason).toBeNull();
|
||||
});
|
||||
@@ -324,7 +327,7 @@ describe("session management service", () => {
|
||||
companyId: "co-1",
|
||||
taskId: "task-1",
|
||||
agentId: "agent-1",
|
||||
adapterType: "gemini_local",
|
||||
adapterType: "gemini-cli",
|
||||
sessionParamsJson: null,
|
||||
sessionDisplayId: null,
|
||||
lastRunId: null,
|
||||
@@ -338,7 +341,7 @@ describe("session management service", () => {
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
const result = svc.evaluateRotation(session, "gemini_local");
|
||||
const result = svc.evaluateRotation(session, "gemini-cli");
|
||||
expect(result.shouldRotate).toBe(true);
|
||||
expect(result.reason).toBe("max_runs");
|
||||
});
|
||||
@@ -350,7 +353,7 @@ describe("session management service", () => {
|
||||
companyId: "co-1",
|
||||
taskId: "task-1",
|
||||
agentId: "agent-1",
|
||||
adapterType: "gemini_local",
|
||||
adapterType: "gemini-cli",
|
||||
sessionParamsJson: null,
|
||||
sessionDisplayId: null,
|
||||
lastRunId: null,
|
||||
@@ -364,7 +367,7 @@ describe("session management service", () => {
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
const result = svc.evaluateRotation(session, "gemini_local");
|
||||
const result = svc.evaluateRotation(session, "gemini-cli");
|
||||
expect(result.shouldRotate).toBe(true);
|
||||
expect(result.reason).toBe("max_tokens");
|
||||
});
|
||||
@@ -377,7 +380,7 @@ describe("session management service", () => {
|
||||
companyId: "co-1",
|
||||
taskId: "task-1",
|
||||
agentId: "agent-1",
|
||||
adapterType: "gemini_local",
|
||||
adapterType: "gemini-cli",
|
||||
sessionParamsJson: null,
|
||||
sessionDisplayId: null,
|
||||
lastRunId: null,
|
||||
@@ -391,24 +394,24 @@ describe("session management service", () => {
|
||||
createdAt: oldDate,
|
||||
updatedAt: oldDate,
|
||||
};
|
||||
const result = svc.evaluateRotation(session, "gemini_local");
|
||||
const result = svc.evaluateRotation(session, "gemini-cli");
|
||||
expect(result.shouldRotate).toBe(true);
|
||||
expect(result.reason).toBe("max_age_hours");
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// adapter exemption in service
|
||||
// uniform rotation under pi-mono
|
||||
// -------------------------------------------------------------------------
|
||||
describe("adapter exemption in evaluateRotation", () => {
|
||||
it("returns shouldRotate: false for nativeContextManagement=true even when thresholds exceeded", () => {
|
||||
describe("uniform rotation under pi-mono", () => {
|
||||
it("returns shouldRotate: true for all connectors when thresholds exceeded (pi-mono has no native exemption)", () => {
|
||||
const svc = createSessionManagementService({ db, logger: mockLogger });
|
||||
const session: TaskSessionRecord = {
|
||||
id: "sess-1",
|
||||
companyId: "co-1",
|
||||
taskId: "task-1",
|
||||
agentId: "agent-1",
|
||||
adapterType: "claude_local",
|
||||
adapterType: "claude-pro",
|
||||
sessionParamsJson: null,
|
||||
sessionDisplayId: null,
|
||||
lastRunId: null,
|
||||
@@ -422,9 +425,9 @@ describe("session management service", () => {
|
||||
createdAt: new Date(Date.now() - 200 * 60 * 60 * 1000).toISOString(), // Way old
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
const result = svc.evaluateRotation(session, "claude_local");
|
||||
expect(result.shouldRotate).toBe(false);
|
||||
expect(result.reason).toBeNull();
|
||||
const result = svc.evaluateRotation(session, "claude-pro");
|
||||
expect(result.shouldRotate).toBe(true);
|
||||
expect(result.reason).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -439,7 +442,7 @@ describe("session management service", () => {
|
||||
companyId: "co-1",
|
||||
taskId: "task-1",
|
||||
agentId: "agent-1",
|
||||
adapterType: "gemini_local",
|
||||
adapterType: "gemini-cli",
|
||||
sessionParamsJson: null,
|
||||
sessionDisplayId: null,
|
||||
lastRunId: null,
|
||||
@@ -453,7 +456,7 @@ describe("session management service", () => {
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
const result = svc.evaluateRotation(session, "gemini_local", { maxRuns: 5 });
|
||||
const result = svc.evaluateRotation(session, "gemini-cli", { maxRuns: 5 });
|
||||
expect(result.shouldRotate).toBe(true);
|
||||
expect(result.reason).toBe("max_runs");
|
||||
});
|
||||
@@ -465,7 +468,7 @@ describe("session management service", () => {
|
||||
companyId: "co-1",
|
||||
taskId: "task-1",
|
||||
agentId: "agent-1",
|
||||
adapterType: "gemini_local",
|
||||
adapterType: "gemini-cli",
|
||||
sessionParamsJson: null,
|
||||
sessionDisplayId: null,
|
||||
lastRunId: null,
|
||||
@@ -479,7 +482,7 @@ describe("session management service", () => {
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
const result = svc.evaluateRotation(session, "gemini_local", { maxTokens: 100000 });
|
||||
const result = svc.evaluateRotation(session, "gemini-cli", { maxTokens: 100000 });
|
||||
expect(result.shouldRotate).toBe(true);
|
||||
expect(result.reason).toBe("max_tokens");
|
||||
});
|
||||
@@ -491,7 +494,7 @@ describe("session management service", () => {
|
||||
companyId: "co-1",
|
||||
taskId: "task-1",
|
||||
agentId: "agent-1",
|
||||
adapterType: "gemini_local",
|
||||
adapterType: "gemini-cli",
|
||||
sessionParamsJson: null,
|
||||
sessionDisplayId: null,
|
||||
lastRunId: null,
|
||||
@@ -505,7 +508,7 @@ describe("session management service", () => {
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
const result = svc.evaluateRotation(session, "gemini_local");
|
||||
const result = svc.evaluateRotation(session, "gemini-cli");
|
||||
expect(result.shouldRotate).toBe(false);
|
||||
expect(result.reason).toBeNull();
|
||||
|
||||
@@ -526,7 +529,7 @@ describe("session management service", () => {
|
||||
taskId,
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "claude_local",
|
||||
adapterType: "claude-pro",
|
||||
sessionParamsJson: null,
|
||||
sessionDisplayId: null,
|
||||
});
|
||||
@@ -552,13 +555,13 @@ describe("session management service", () => {
|
||||
taskId,
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "gemini_local",
|
||||
adapterType: "gemini-cli",
|
||||
sessionParamsJson: null,
|
||||
sessionDisplayId: null,
|
||||
});
|
||||
|
||||
// Create a completed run for history
|
||||
const run = db.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "gemini_local" as ConnectorId });
|
||||
const run = db.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "gemini-cli" as ConnectorId });
|
||||
db.finishRun({
|
||||
runId: run.id,
|
||||
status: "succeeded",
|
||||
@@ -597,7 +600,7 @@ describe("session management service", () => {
|
||||
taskId,
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "gemini_local",
|
||||
adapterType: "gemini-cli",
|
||||
sessionParamsJson: JSON.stringify({ resumeId: "old-abc" }),
|
||||
sessionDisplayId: "gemini-old",
|
||||
});
|
||||
@@ -644,7 +647,7 @@ describe("session management service", () => {
|
||||
taskId,
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "gemini_local",
|
||||
adapterType: "gemini-cli",
|
||||
sessionParamsJson: null,
|
||||
sessionDisplayId: null,
|
||||
});
|
||||
@@ -663,7 +666,7 @@ describe("session management service", () => {
|
||||
taskId,
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "gemini_local",
|
||||
adapterType: "gemini-cli",
|
||||
sessionParamsJson: null,
|
||||
sessionDisplayId: null,
|
||||
});
|
||||
@@ -684,7 +687,7 @@ describe("session management service", () => {
|
||||
taskId,
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "gemini_local",
|
||||
adapterType: "gemini-cli",
|
||||
sessionParamsJson: null,
|
||||
sessionDisplayId: null,
|
||||
});
|
||||
@@ -773,7 +776,7 @@ describe("wiring integration", () => {
|
||||
db,
|
||||
{ id: "task-1" },
|
||||
{ inputTokens: 200, outputTokens: 100, runId: "run-1" },
|
||||
"gemini_local",
|
||||
"gemini-cli",
|
||||
);
|
||||
|
||||
expect(sessionService.accumulateTokenUsage).toHaveBeenCalledWith("sess-1", 200, 100, "run-1");
|
||||
@@ -789,10 +792,10 @@ describe("wiring integration", () => {
|
||||
db,
|
||||
{ id: "task-1" },
|
||||
{ inputTokens: 200, outputTokens: 100, runId: "run-1" },
|
||||
"gemini_local",
|
||||
"gemini-cli",
|
||||
);
|
||||
|
||||
expect(sessionService.evaluateRotation).toHaveBeenCalledWith(mockSession, "gemini_local");
|
||||
expect(sessionService.evaluateRotation).toHaveBeenCalledWith(mockSession, "gemini-cli");
|
||||
// evaluateRotation called AFTER accumulateTokenUsage
|
||||
const accOrder = sessionService.accumulateTokenUsage.mock.invocationCallOrder[0];
|
||||
const evalOrder = sessionService.evaluateRotation.mock.invocationCallOrder[0];
|
||||
@@ -815,7 +818,7 @@ describe("wiring integration", () => {
|
||||
db,
|
||||
{ id: "task-1" },
|
||||
{ inputTokens: 200, outputTokens: 100, runId: "run-1" },
|
||||
"gemini_local",
|
||||
"gemini-cli",
|
||||
);
|
||||
|
||||
expect(sessionService.rotateSession).toHaveBeenCalledWith(updatedSession);
|
||||
@@ -832,7 +835,7 @@ describe("wiring integration", () => {
|
||||
db,
|
||||
{ id: "task-1" },
|
||||
{ inputTokens: 200, outputTokens: 100, runId: "run-1" },
|
||||
"gemini_local",
|
||||
"gemini-cli",
|
||||
);
|
||||
|
||||
expect(sessionService.rotateSession).not.toHaveBeenCalled();
|
||||
@@ -847,7 +850,7 @@ describe("wiring integration", () => {
|
||||
db,
|
||||
null,
|
||||
{ inputTokens: 200, outputTokens: 100, runId: "run-1" },
|
||||
"gemini_local",
|
||||
"gemini-cli",
|
||||
);
|
||||
|
||||
expect(db.getActiveTaskSession).not.toHaveBeenCalled();
|
||||
@@ -863,7 +866,7 @@ describe("wiring integration", () => {
|
||||
db,
|
||||
{ id: "task-1" },
|
||||
{ inputTokens: 200, outputTokens: 100, runId: "run-1" },
|
||||
"gemini_local",
|
||||
"gemini-cli",
|
||||
);
|
||||
|
||||
expect(sessionService.accumulateTokenUsage).not.toHaveBeenCalled();
|
||||
@@ -879,7 +882,7 @@ describe("wiring integration", () => {
|
||||
db,
|
||||
{ id: "task-1" },
|
||||
{ inputTokens: null, outputTokens: null, runId: "run-1" },
|
||||
"gemini_local",
|
||||
"gemini-cli",
|
||||
);
|
||||
|
||||
expect(sessionService.accumulateTokenUsage).toHaveBeenCalledWith("sess-1", 0, 0, "run-1");
|
||||
|
||||
@@ -6,7 +6,7 @@ describe("buildConnectorSection", () => {
|
||||
const section = buildConnectorSection({
|
||||
allConnectors: [
|
||||
{
|
||||
id: "codex_local",
|
||||
id: "openai-codex",
|
||||
label: "Codex",
|
||||
status: "ready",
|
||||
enabledModels: ["gpt-5.4", "gpt-5.4-mini"],
|
||||
@@ -22,7 +22,7 @@ describe("buildConnectorSection", () => {
|
||||
const section = buildConnectorSection({
|
||||
allConnectors: [
|
||||
{
|
||||
id: "codex_local",
|
||||
id: "openai-codex",
|
||||
label: "Codex",
|
||||
status: "ready",
|
||||
enabledModels: ["gpt-5.4"],
|
||||
@@ -38,7 +38,7 @@ describe("buildConnectorSection", () => {
|
||||
const section = buildConnectorSection({
|
||||
allConnectors: [
|
||||
{
|
||||
id: "codex_local",
|
||||
id: "openai-codex",
|
||||
label: "Codex",
|
||||
status: "ready",
|
||||
enabledModels: [],
|
||||
|
||||
@@ -17,36 +17,35 @@ import type {
|
||||
|
||||
describe("skill-types", () => {
|
||||
describe("TOKEN_BUDGETS", () => {
|
||||
it("has entries for all supported adapter types", () => {
|
||||
expect(TOKEN_BUDGETS).toHaveProperty("claude_local");
|
||||
expect(TOKEN_BUDGETS).toHaveProperty("codex_local");
|
||||
expect(TOKEN_BUDGETS).toHaveProperty("gemini_local");
|
||||
expect(TOKEN_BUDGETS).toHaveProperty("claude_compatible");
|
||||
it("has entries for all supported connector types", () => {
|
||||
expect(TOKEN_BUDGETS).toHaveProperty("claude-pro");
|
||||
expect(TOKEN_BUDGETS).toHaveProperty("anthropic");
|
||||
expect(TOKEN_BUDGETS).toHaveProperty("gemini-cli");
|
||||
expect(TOKEN_BUDGETS).toHaveProperty("google");
|
||||
});
|
||||
|
||||
it("has numeric token caps for each adapter", () => {
|
||||
expect(TOKEN_BUDGETS.claude_local).toBe(50000);
|
||||
expect(TOKEN_BUDGETS.codex_local).toBe(30000);
|
||||
expect(TOKEN_BUDGETS.gemini_local).toBe(50000);
|
||||
expect(TOKEN_BUDGETS.claude_compatible).toBe(50000);
|
||||
it("has numeric token caps for each connector", () => {
|
||||
expect(TOKEN_BUDGETS["claude-pro"]).toBe(50000);
|
||||
expect(TOKEN_BUDGETS["anthropic"]).toBe(50000);
|
||||
expect(TOKEN_BUDGETS["gemini-cli"]).toBe(50000);
|
||||
expect(TOKEN_BUDGETS["google"]).toBe(50000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveTokenBudget", () => {
|
||||
it("returns exact match for known adapter types", () => {
|
||||
expect(resolveTokenBudget("claude_local")).toBe(50000);
|
||||
expect(resolveTokenBudget("codex_local")).toBe(30000);
|
||||
expect(resolveTokenBudget("gemini_local")).toBe(50000);
|
||||
it("returns exact match for known connector types", () => {
|
||||
expect(resolveTokenBudget("claude-pro")).toBe(50000);
|
||||
expect(resolveTokenBudget("anthropic")).toBe(50000);
|
||||
expect(resolveTokenBudget("gemini-cli")).toBe(50000);
|
||||
});
|
||||
|
||||
it("resolves claude_compat_* variants to claude_compatible budget", () => {
|
||||
expect(resolveTokenBudget("claude_compat_2")).toBe(50000);
|
||||
expect(resolveTokenBudget("claude_compat_10")).toBe(50000);
|
||||
expect(resolveTokenBudget("claude_compatible")).toBe(50000);
|
||||
it("resolves anthropic-family connectors to the larger injection budget", () => {
|
||||
expect(resolveTokenBudget("amazon-bedrock")).toBe(50000);
|
||||
expect(resolveTokenBudget("google-vertex")).toBe(50000);
|
||||
});
|
||||
|
||||
it("returns 30000 default for unknown adapter types", () => {
|
||||
expect(resolveTokenBudget("unknown_adapter")).toBe(30000);
|
||||
it("returns 30000 default for unknown connector types", () => {
|
||||
expect(resolveTokenBudget("unknown_connector")).toBe(30000);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||
import { AppDatabase } from "../../src/main/database";
|
||||
import { SqliteSessionStore } from "../../src/main/agent-runtime/sqlite-session-store";
|
||||
|
||||
/**
|
||||
* Verifies the SqliteSessionStore round-trips pi `AgentMessage` values
|
||||
* correctly through the new `session_entries` table (migration 27).
|
||||
*/
|
||||
describe("SqliteSessionStore", () => {
|
||||
let tempDir: string;
|
||||
let db: AppDatabase;
|
||||
const TASK_SESSION_ID = "task-session-test-1";
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), "agentcompany-session-store-"));
|
||||
db = new AppDatabase(tempDir);
|
||||
db.init();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
db.close();
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("appends and reloads pi user/assistant messages preserving order", () => {
|
||||
const store = new SqliteSessionStore(db, TASK_SESSION_ID);
|
||||
const userMessage: AgentMessage = {
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
timestamp: Date.now(),
|
||||
} as AgentMessage;
|
||||
const assistantMessage: AgentMessage = {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "hi" }],
|
||||
stopReason: "stop",
|
||||
provider: "anthropic",
|
||||
model: { id: "claude-opus-4-5" },
|
||||
usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, totalTokens: 15, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
|
||||
timestamp: Date.now(),
|
||||
} as unknown as AgentMessage;
|
||||
store.appendMessage(userMessage);
|
||||
store.appendMessage(assistantMessage);
|
||||
const loaded = store.loadMessages();
|
||||
expect(loaded).toHaveLength(2);
|
||||
expect((loaded[0] as { role: string }).role).toBe("user");
|
||||
expect((loaded[1] as { role: string }).role).toBe("assistant");
|
||||
});
|
||||
|
||||
it("appends a compaction marker that does not appear as a message", () => {
|
||||
const store = new SqliteSessionStore(db, TASK_SESSION_ID);
|
||||
store.appendMessage({ role: "user", content: [{ type: "text", text: "first" }], timestamp: Date.now() } as AgentMessage);
|
||||
store.appendCompaction("Earlier conversation summarised: planning phase complete.");
|
||||
const loaded = store.loadMessages();
|
||||
expect(loaded).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("replaceMessages supersedes prior rows and inserts new ones in order", () => {
|
||||
const store = new SqliteSessionStore(db, TASK_SESSION_ID);
|
||||
store.appendMessage({ role: "user", content: [{ type: "text", text: "old-1" }], timestamp: Date.now() } as AgentMessage);
|
||||
store.appendMessage({ role: "assistant", content: [{ type: "text", text: "old-2" }], timestamp: Date.now() } as AgentMessage);
|
||||
|
||||
const compacted: AgentMessage[] = [
|
||||
{ role: "user", content: [{ type: "text", text: "context summary" }], timestamp: Date.now() } as AgentMessage,
|
||||
];
|
||||
store.replaceMessages(compacted, "Summary of prior turns.");
|
||||
|
||||
const active = store.loadMessages();
|
||||
expect(active).toHaveLength(1);
|
||||
expect((active[0] as { content: { text: string }[] }).content[0]?.text).toBe("context summary");
|
||||
|
||||
// Check: pre-replace messages must still exist as superseded audit rows.
|
||||
const all = db.sessionEntries.listAll(TASK_SESSION_ID);
|
||||
const supersededCount = all.filter((row) => row.isSuperseded).length;
|
||||
expect(supersededCount).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -285,7 +285,7 @@ const fakeRun = (id: string): RunRecord => ({
|
||||
taskId: "t-1",
|
||||
agentId: "ag-1",
|
||||
workspaceId: null,
|
||||
connectorId: "claude_local",
|
||||
connectorId: "claude-pro",
|
||||
status: "queued",
|
||||
summary: "",
|
||||
errorMessage: null,
|
||||
@@ -311,7 +311,7 @@ const fakeAgent = (id: string, name = "Agent"): AgentRecord => ({
|
||||
department: "engineering",
|
||||
status: "idle",
|
||||
reportsTo: null,
|
||||
connectorId: "claude_local",
|
||||
connectorId: "claude-pro",
|
||||
workspaceId: null,
|
||||
model: null,
|
||||
capabilities: "",
|
||||
|
||||
@@ -28,7 +28,7 @@ function createTestCompany(db: AppDatabase): string {
|
||||
|
||||
function createTestConnector(db: AppDatabase): void {
|
||||
db.saveConnector({
|
||||
id: "codex_local",
|
||||
id: "openai-codex",
|
||||
command: "codex",
|
||||
model: "gpt-5",
|
||||
envBindingText: "",
|
||||
@@ -55,7 +55,7 @@ function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string
|
||||
title: name,
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Executes tasks",
|
||||
|
||||
Reference in new issue
Block a user