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:
ZacharyZhang-NY committed 2026-04-17 02:36:53 -04:00
1 parent 4de9cd0339
commit e94939a562
80 files changed
+2210 -413

No files matched your search

+3 -3
View File
@@ -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",
+4 -4
View File
@@ -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",
+9 -9
View File
@@ -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",
+1 -1
View File
@@ -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: "{}",
+4 -4
View File
@@ -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: "{}",
+1 -1
View File
@@ -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",
+10 -10
View File
@@ -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);
+2 -2
View File
@@ -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");
});
});
+7 -35
View File
@@ -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("");
});
});
+3 -3
View File
@@ -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",
+19 -20
View File
@@ -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);
+40 -27
View File
@@ -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,
});
+7 -3
View File
@@ -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");
+1 -1
View File
@@ -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",
});
+146
View File
@@ -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",
});
});
});
+80
View File
@@ -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");
});
});
+143
View File
@@ -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);
});
});
+3 -3
View File
@@ -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
View File
@@ -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",
+9 -9
View File
@@ -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", "");
+14 -14
View File
@@ -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);
+27 -27
View File
@@ -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,
+2 -2
View File
@@ -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",
+4 -4
View File
@@ -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: "{}",
+22 -6
View File
@@ -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();
});
+61
View File
@@ -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");
});
});
+6 -6
View File
@@ -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",
+2 -2
View File
@@ -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",
+64
View File
@@ -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",
+59 -56
View File
@@ -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");
+3 -3
View File
@@ -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: [],
+19 -20
View File
@@ -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);
});
});
+80
View File
@@ -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);
});
});
+2 -2
View File
@@ -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: "",
+2 -2
View File
@@ -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",