1.5.1: refined office, chat in the app language, current OpenAI models, own connector keys
Build Release Installers / Release for the tag (push) Successful in 16s
Build Release Installers / Windows installer (NSIS x64) (push) Successful in 1m48s
Build Release Installers / Linux packages (AppImage + deb x64) (push) Successful in 2m21s
CI / Typecheck, lint, tests, e2e (push) Successful in 3m52s
Build Release Installers / macOS installers (dmg + zip, x64 + arm64) (push) Successful in 7m15s

- Home: the goal title box is two line boxes tall (2.8em), so a third line no longer peeks out.
- Chat messages, reply fallbacks and assignment DMs follow the app display language; tasks and documents keep the working language.
- OpenAI and GPT-account model lists match OpenAI's current line-up; default gpt-6.1-sol.
- Onboarding connectors step takes the operator's Composio and logo.dev keys, then lists the toolkits the Composio project enables; without a key Connectors leaves the sidebar.
- Google Antigravity removed end to end (migration 111), with the remote-revocation path only it used.
- Office: 2x pixel people with outlines, rim light, walk, typing and blink; dithered real-clock lighting; detailed furniture and floors.
- Per-agent model picker lists the provider's whole model list, live when connected.
- Chat wake context breaks same-millisecond ties by insertion order; the mid-run inbox dedupes by message id.
- The stored "clock" theme reads back instead of falling to "system".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-09 21:12:19 -04:00
1 parent 48dfddffef
commit 245ff66336
79 files changed
+1301 -1035

No files matched your search

+1 -4
View File
@@ -16,7 +16,6 @@ const PROVIDER_SECRET_SAMPLES: Record<string, string> = {
"kimi-cn": "sk-live-provider",
"kimi-code": JSON.stringify({ access: "live-access", refresh: "live-refresh", expires: 1_900_000_000_000 }),
"github-copilot": JSON.stringify({ access: "live-access", refresh: "live-refresh", expires: 1_900_000_000_000 }),
"google-antigravity": JSON.stringify({ access: "live-access", refresh: "live-refresh", expires: 1_900_000_000_000, projectId: "acme-project" }),
"gpt-account": JSON.stringify({ access: "live-access", refresh: "live-refresh", expires: 1_900_000_000_000, accountId: "acct_1" }),
"azure-openai": JSON.stringify({
baseUrl: "https://acme.openai.azure.com",
@@ -88,7 +87,7 @@ describe("provider contracts", () => {
// Providers with no enumerable models endpoint keep returning the curated
// catalog; the rest fetch live (covered offline by provider-models-fetch.test).
const CURATED_MODEL_PROVIDERS = new Set([
"github-copilot", "google-antigravity", "gpt-account", "azure-openai", "aws-bedrock", "vertex-ai",
"github-copilot", "gpt-account", "azure-openai", "aws-bedrock", "vertex-ai",
"custom-openai", "custom-anthropic",
]);
@@ -115,8 +114,6 @@ describe("provider contracts", () => {
const secret = PROVIDER_SECRET_SAMPLES[provider.key];
const profile = adapter.parseAuthProfile(secret);
await expect(adapter.authorize({ secret })).resolves.toEqual({ authProfile: profile });
// Remote revocation makes a network call; provider-disconnect.test covers it.
if (!provider.revokesRemotely) await expect(adapter.revoke({ authProfile: profile, secret })).resolves.toBeUndefined();
await expect(adapter.refresh({ authProfile: profile })).rejects.toThrow("does not support token refresh");
}
});
+1 -1
View File
@@ -43,7 +43,7 @@ test("the pinned CEO conversation is the Messages front door", async () => {
await page.getByLabel("Acceptance criteria").fill("A working product, not documents about one.");
await page.getByRole("button", { name: "Launch" }).click();
await expect(page.getByRole("heading", { name: "Connectors" })).toBeVisible();
await page.getByRole("button", { name: "Finish" }).click();
await page.getByRole("button", { name: "Skip" }).click();
// The CEO conversation is the first row, badged, and already selected —
// opening Messages IS opening the CEO chat.
+23 -12
View File
@@ -56,7 +56,9 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
// The company exists after Launch; the wizard closes with the optional
// connectors step.
await expect(page.getByRole("heading", { name: "Connectors" })).toBeVisible();
await page.getByRole("button", { name: "Finish" }).click();
await expect(page.getByText("logo.dev publishable key")).toBeVisible();
await page.getByRole("button", { name: "Skip" }).click();
await expect(page.getByRole("button", { name: "Connectors", exact: true })).toHaveCount(0);
await expect(page.getByRole("button", { name: /Acme Operations/ })).toBeVisible();
await page.getByRole("button", { name: "Work", exact: true }).click();
@@ -74,6 +76,12 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
await page.getByLabel("Role").fill("CTO");
await page.getByLabel("Department").fill("Engineering");
await page.getByLabel("Monthly budget").fill("250");
// The model picker offers the provider's whole list, whatever is picked now.
const model = page.locator(".modal select[name=modelId]");
await model.selectOption("gpt-6.1-sol");
const offered = await model.locator("option").evaluateAll((options) => options.map((option) => (option as HTMLOptionElement).value));
expect(offered).toEqual(expect.arrayContaining(["", "gpt-6.1-sol", "gpt-6-astra", "gpt-5.4-mini"]));
await model.selectOption("");
await page.locator(".modal").getByRole("button", { name: "Hire" }).click();
await expect(page.getByText("Alex Chen").first()).toBeVisible();
const hiredAgent = await page.evaluate(async () => {
@@ -108,20 +116,19 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
await page.getByRole("button", { name: "Edit Alex Chen" }).click();
await page.locator(".modal").getByLabel("Name", { exact: true }).fill("Alex Turing");
await page.getByLabel("Avatar emoji").fill("🚀");
// The blank model field names the default of whichever provider is picked now.
const editModel = page.locator(".modal").getByLabel("Model", { exact: true });
await expect(editModel).toHaveAttribute("placeholder", "gpt-5.4");
// The blank model choice names the default of whichever provider is picked now.
const editModel = page.locator(".modal [name=modelId]");
await expect(editModel.locator("option").first()).toHaveText("gpt-5.4 (provider default)");
const editProvider = page.locator(".modal select[name=modelProviderId]");
const savedProvider = await editProvider.inputValue();
// Suggestions come from the picked provider only, and switching provider clears the model.
await expect(page.locator("#edit-model-suggestions option").first()).toHaveAttribute("value", /.+/);
await expect(page.locator("#edit-model-suggestions option[value^=claude]")).toHaveCount(0);
await editModel.fill("gpt-5.4");
// Choices come from the picked provider only, and switching provider clears the model.
await expect(editModel.locator("option[value^=claude]")).toHaveCount(0);
await editModel.selectOption("gpt-5.4");
await editProvider.selectOption("");
await expect(editModel).toHaveAttribute("placeholder", "");
await expect(editModel).toHaveValue("");
await expect(editModel).toBeDisabled();
await editProvider.selectOption(savedProvider);
await expect(editModel).toHaveValue("");
await page.locator(".modal").getByRole("button", { name: "Save" }).click();
await expect(page.getByText("Alex Turing").first()).toBeVisible();
await expect(page.locator(".table-row", { hasText: "Alex Turing" }).locator(".avatar", { hasText: "🚀" })).toBeVisible();
@@ -213,12 +220,15 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
await page.getByRole("button", { name: "Approvals" }).click();
await expect(page.getByText("Escalate release").first()).toBeVisible();
// The Connectors page shell renders whether or not the Composio catalog
// is reachable from the test machine.
// A saved Composio key brings Connectors into the sidebar; the page renders whether or not Composio answers.
await page.getByRole("button", { name: "Settings" }).click();
await page.getByRole("button", { name: "Integrations", exact: true }).click();
await page.locator("form").filter({ hasText: "Composio API key" }).getByLabel("Key").fill("ak_e2e");
await page.locator("form").filter({ hasText: "Composio API key" }).getByRole("button", { name: "Save" }).click();
await page.getByRole("button", { name: "Connectors", exact: true }).click();
await expect(page.getByRole("heading", { name: "Connectors" })).toBeVisible();
await expect(page.getByRole("button", { name: "Refresh" })).toHaveCount(0);
// Without a Composio key the account check fails, and the page says the statuses may be stale.
// A key Composio rejects fails the account check, and the page says the statuses may be stale.
await expect(page.getByRole("alert").filter({ hasText: "Account status could not be checked with Composio" })).toBeVisible();
// A connector failure is shown whole and can be copied.
const catalogError = page.getByRole("alert").filter({ has: page.getByRole("button", { name: "Retry" }) }).filter({ hasNotText: "Account status" }).first();
@@ -229,6 +239,7 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
expect((await app.evaluate(({ clipboard }) => clipboard.readText())).trim()).toBe(shownReason);
await page.getByRole("button", { name: "Settings" }).click();
await page.getByRole("button", { name: "General", exact: true }).click();
await expect(page.getByText("AI CEO", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "Providers" }).click();
for (const label of ["Kimi", "Kimi CN", "Kimi Code"]) {
+1 -1
View File
@@ -32,7 +32,7 @@ test("the library manages documents with versions and knowledge with history", a
await page.getByLabel("Goal").fill("Keep company knowledge.");
await page.getByLabel("Acceptance criteria").fill("Documents and knowledge are manageable.");
await page.getByRole("button", { name: "Launch" }).click();
await page.getByRole("button", { name: "Finish" }).click();
await page.getByRole("button", { name: "Skip" }).click();
await expect(page.getByRole("button", { name: /Stack Works/ })).toBeVisible();
const orgId = await page.evaluate(async () => {
+1 -1
View File
@@ -43,7 +43,7 @@ test("the office floor opens agent details on click", async () => {
await page.getByLabel("Acceptance criteria").fill("Clicking an agent opens their card.");
await page.getByRole("button", { name: "Launch" }).click();
await expect(page.getByRole("heading", { name: "Connectors" })).toBeVisible();
await page.getByRole("button", { name: "Finish" }).click();
await page.getByRole("button", { name: "Skip" }).click();
await expect(page.getByRole("button", { name: /Pixel Works/ })).toBeVisible();
await page.getByRole("button", { name: "Office", exact: true }).click();
+1 -1
View File
@@ -14,7 +14,7 @@ export async function onboard(page: Page, company: string): Promise<void> {
await page.getByLabel("Goal").fill("Ship.");
await page.getByLabel("Acceptance criteria").fill("It ships.");
await page.getByRole("button", { name: "Launch" }).click();
await page.getByRole("button", { name: "Finish" }).click();
await page.getByRole("button", { name: "Skip" }).click();
await expect(page.getByRole("button", { name: "Settings" })).toBeVisible();
}
+3 -3
View File
@@ -57,9 +57,9 @@ test("onboarding resumes where it was left after the app is closed", async () =>
app = await launch();
page = await app.firstWindow();
await expect(page.getByRole("heading", { name: "Connectors" })).toBeVisible();
// The step says when connector statuses could not be checked, as the Connectors page does.
await expect(page.getByRole("alert").filter({ hasText: "Account status could not be checked with Composio" })).toBeVisible();
await page.getByRole("button", { name: "Finish" }).click();
// Without a Composio key the step asks for one; skipping keeps Connectors out of the app.
await expect(page.getByText("Composio API key")).toBeVisible();
await page.getByRole("button", { name: "Skip" }).click();
await expect(page.getByRole("button", { name: "Settings" })).toBeVisible();
// A cancelled second company leaves nothing behind for the next attempt.
+1
View File
@@ -20,6 +20,7 @@ test("every page and onboarding step draws its text on the pixel font's grid", a
await expect(page.getByRole("heading", { name: "Welcome" })).toBeVisible();
expect(await offGridText(page)).toEqual([]);
await onboard(page, "Grid Co");
await page.evaluate(() => window.agentCompany.setIntegrationKey({ name: "composio", value: "ak_e2e" }));
for (const route of ["Home", "Work", "Team", "Office", "Messages", "Runs", "Files", "Library", "Approvals", "Connectors", "Settings"]) {
await page.getByRole("button", { name: route, exact: true }).click();
await page.waitForTimeout(400);
+2 -1
View File
@@ -46,7 +46,7 @@ test("captures every page for visual review", async () => {
await expect(page.getByRole("heading", { name: "Connectors" })).toBeVisible();
await page.screenshot({ path: join(outDir, "05-onboarding-connectors.png"), fullPage: false });
await page.getByRole("button", { name: "Finish" }).click();
await page.getByRole("button", { name: "Skip" }).click();
await expect(page.getByRole("button", { name: /OrbitOps/ })).toBeVisible();
await snap(page, outDir, "10-home");
@@ -56,6 +56,7 @@ test("captures every page for visual review", async () => {
await go(page, "Messages", outDir, "13-messages");
await go(page, "Files", outDir, "14-files");
await go(page, "Approvals", outDir, "15-approvals");
await page.evaluate(() => window.agentCompany.setIntegrationKey({ name: "composio", value: "ak_e2e" }));
await go(page, "Connectors", outDir, "16-connectors");
await go(page, "Settings", outDir, "17-settings");
await go(page, "Office", outDir, "18-office");
+15
View File
@@ -208,6 +208,21 @@ describe("chat wake context", () => {
h.connection.close();
});
test("a message stamped in the same millisecond as the reply still counts as new", async () => {
const h = await founderLine();
const threadId = h.say("Ship on Monday.", null);
await h.runWake();
h.say("Tie breaker.", threadId);
h.connection.db.exec(`
update chat_messages set created_at = (select created_at from chat_messages where sender_type = 'agent' order by rowid desc limit 1)
where body_md = 'Tie breaker.'
`);
await h.runWake();
const prompt = h.wakePrompts[1];
expect(prompt.slice(prompt.indexOf("New since your last reply"))).toContain(": Tie breaker.");
h.connection.close();
});
test("a long thread keeps every new message and the most recent history", async () => {
const h = await founderLine();
const threadId = h.say("Message 0", null);
+5
View File
@@ -138,6 +138,11 @@ describe("agent direct messages", () => {
const assignment = h.connection.db.prepare("select space_id, body_md from chat_messages where org_id = ?").get(h.orgId) as { space_id: string; body_md: string };
expect(assignment.space_id).toBe(spaces[0].id);
expect(assignment.body_md).toContain("take task_id=");
h.organization.updateUiPreferences({ language: "zh" });
dispatchAgentTool(deps, h.ctx, call("create_task", { title: "Ship docs", description: "d", assignee_agent_name: "Dana" }));
const zh = h.connection.db.prepare("select body_md from chat_messages where org_id = ? and body_md like '%Ship docs%'").get(h.orgId) as { body_md: string };
expect(zh.body_md).toContain("请接手 task_id=");
});
test("send_message requires exactly one destination", () => {
+33 -21
View File
@@ -1,7 +1,7 @@
import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { describe, expect, test, vi } from "vitest";
import { DatabaseConnection } from "../../src/main/database/connection";
import { OrganizationService } from "../../src/main/services/organization-service";
import { ProviderService } from "../../src/main/services/provider-service";
@@ -9,6 +9,8 @@ import { providerAdapters } from "../../src/main/services/provider-adapters";
import type { AgentTurnResult, ProviderToolTurnInput } from "../../src/main/services/provider-tool-turn";
import { ConnectorGrantsService } from "../../src/main/services/composio/connector-grants";
import { TaskExecutionService } from "../../src/main/services/task-execution-service";
import { SkillService } from "../../src/main/services/skill-service";
import { ChatService } from "../../src/main/services/chat-service";
import { RISK_LEVEL, TASK_PRIORITY, TASK_STATUS } from "../../src/shared/domain";
import { buildAgentInstructions, type TaskExecutionAgentRow } from "../../src/main/services/task-prompts";
import { AGENT_TOOLS } from "../../src/main/services/agent-tools";
@@ -33,13 +35,13 @@ function agentRow(overrides: Partial<TaskExecutionAgentRow>): TaskExecutionAgent
describe("agent instructions", () => {
test("first-hire guidance is one rule everywhere, and new leaders get a leader's voice", () => {
const ceo = buildAgentInstructions({ agent: agentRow({ name: "CEO", role: "CEO", role_level: "c_suite", manager_agent_id: null }), companyName: "O", isCeo: true, hasReports: false, connectorNames: [], language: "en" });
const ceo = buildAgentInstructions({ agent: agentRow({ name: "CEO", role: "CEO", role_level: "c_suite", manager_agent_id: null }), companyName: "O", isCeo: true, hasReports: false, connectorNames: [], language: "en", chatLanguage: "en" });
const hireTool = AGENT_TOOLS.find((tool) => tool.name === "hire_agent");
for (const text of [ceo, goalKickoffDescription("O", "Ship", "Ship it.", { roleLevel: "c_suite", hasReports: false }), hireTool?.description ?? ""]) {
expect(text).toContain(CEO_FIRST_HIRES);
}
const voice = (role_level: string, hasReports: boolean) =>
buildAgentInstructions({ agent: agentRow({ role_level }), companyName: "O", isCeo: false, hasReports, connectorNames: [], language: "en" });
buildAgentInstructions({ agent: agentRow({ role_level }), companyName: "O", isCeo: false, hasReports, connectorNames: [], language: "en", chatLanguage: "en" });
expect(voice("director", false)).toContain('Your department has no one in it yet: hire the managers (hire_agent, seniority "lead") and staff your department needs');
expect(voice("lead", false)).toContain("Your team has no one in it yet: hire the staff your team needs (hire_agent)");
expect(voice("lead", false)).toContain("You lead a team.");
@@ -58,6 +60,7 @@ describe("agent instructions", () => {
hasReports: false,
connectorNames: ["GitHub", "Slack"],
language: "en",
chatLanguage: "en",
});
expect(text).toContain("every connected service");
expect(text).toContain("grant_connector");
@@ -77,6 +80,7 @@ describe("agent instructions", () => {
hasReports: false,
connectorNames: ["Stripe"],
language: "en",
chatLanguage: "en",
});
expect(text).toContain("Connected external services you carry in this run: Stripe");
expect(text).toContain("composio_find_actions");
@@ -93,31 +97,24 @@ describe("agent instructions", () => {
hasReports: false,
connectorNames: [],
language: "en",
chatLanguage: "en",
});
expect(text).toContain("You currently hold no connector grants");
expect(text).toContain("message the CEO");
expect(text).not.toContain("grant before you assign");
});
test("Chinese app language directs agents to work in Chinese", () => {
const zh = buildAgentInstructions({
agent: agentRow({}),
companyName: "OrbitOps",
isCeo: false,
hasReports: false,
connectorNames: [],
language: "zh",
test("chat follows the app language; work follows the working language", () => {
const text = (language: "en" | "zh", chatLanguage: "en" | "zh") => buildAgentInstructions({
agent: agentRow({}), companyName: "OrbitOps", isCeo: false, hasReports: false, connectorNames: [], language, chatLanguage,
});
const mixed = text("en", "zh");
expect(mixed).toContain("The working language is English");
expect(mixed).toContain("聊天语言是简体中文");
const zh = text("zh", "zh");
expect(zh).toContain("公司的工作语言是简体中文");
const en = buildAgentInstructions({
agent: agentRow({}),
companyName: "OrbitOps",
isCeo: false,
hasReports: false,
connectorNames: [],
language: "en",
});
expect(en).not.toContain("简体中文");
expect(zh).not.toContain("The chat language is English");
expect(text("en", "en")).not.toContain("简体中文");
});
});
@@ -178,9 +175,11 @@ describe("agent instruction wiring", () => {
await execution.runTask({ orgId, taskId });
return capturedInstructions;
};
// The display language changes only what the operator sees.
// The display language sets the chat language, not the working language.
expect(await rerun()).toContain("The chat language is English");
organization.updateUiPreferences({ language: "zh" });
expect(await rerun()).toContain("The working language is English");
expect(capturedInstructions).toContain("聊天语言是简体中文");
expect(capturedInstructions).not.toContain("公司的工作语言是简体中文");
// The company's working language redirects its agents, and a task can keep its own.
connection.db.prepare("update organizations set work_language = 'zh' where id = ?").run(orgId);
@@ -188,6 +187,19 @@ describe("agent instruction wiring", () => {
connection.db.prepare("update tasks set work_language = 'en' where id = ?").run(taskId);
expect(await rerun()).toContain("The working language is English");
expect(capturedInstructions).not.toContain("公司的工作语言是简体中文");
// A chat reply's house style follows the chat language.
connection.db.prepare("update organizations set work_language = 'en' where id = ?").run(orgId);
const houseStyle = vi.spyOn(SkillService.prototype, "hasRuntimeSkills");
await rerun();
expect(houseStyle.mock.lastCall?.[3]).toBe("en");
const chat = new ChatService(connection.db);
const employeeId = (connection.db.prepare("select id from employees where org_id = ?").get(orgId) as { id: string }).id;
const spaceId = chat.ensureDirectSpace(orgId, { type: "employee", id: employeeId }, { type: "agent", id: agentId });
chat.sendMessage({ orgId, spaceId, threadId: null, senderType: "employee", senderId: employeeId, bodyMd: "ping", attachmentFileIds: [] });
const wakeRun = connection.db.prepare("select id from task_runs where org_id = ? and trigger = 'chat_mention' and state = 'queued'").get(orgId) as { id: string };
await execution.executeRun(orgId, wakeRun.id);
expect(houseStyle.mock.lastCall?.[3]).toBe("zh");
houseStyle.mockRestore();
});
test("a new department head's task prompt tells them to staff their department, not to work as an IC", async () => {
+1 -1
View File
@@ -36,7 +36,7 @@ describe("domain services", () => {
const connection = createConnection();
const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>;
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110]);
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111]);
connection.close();
});
@@ -0,0 +1,42 @@
import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { DatabaseConnection } from "../../src/main/database/connection";
import { dropGoogleAntigravitySchema } from "../../src/main/database/migrations/111-drop-google-antigravity";
import { OrganizationService } from "../../src/main/services/organization-service";
describe("dropping Google Antigravity", () => {
test("removes its providers, frees agents and the auditor, erases its secrets and key", () => {
const connection = new DatabaseConnection(join(tmpdir(), `ac-drop-ag-${randomUUID()}.db`));
const db = connection.db;
const organization = new OrganizationService(db);
let orgId = "";
let agentId = "";
connection.transaction(() => {
orgId = organization.createOrganization({ organizationName: "Acme", employeeName: "Yang", mission: "m", goal: "g" });
db.exec(`
create temp table ag as select * from providers where org_id = '${orgId}' and provider_key = 'openai';
update ag set id = 'prov_ag', provider_key = 'google-antigravity', label = 'Google Antigravity', credential_id = 'cred_ag';
`);
db.prepare("insert into credentials(id, org_id, provider_key, kind, ciphertext, display_hint, created_at) values ('cred_ag', ?, 'google-antigravity', 'oauth', x'0102', 'g', datetime('now'))").run(orgId);
db.exec("insert into providers select * from ag");
agentId = organization.createAgent({ orgId, name: "Ada", role: "Engineer", department: "Engineering", modelProviderId: "prov_ag", modelId: "gemini-3-flash", budgetCents: 1000, managerAgentId: null });
db.prepare("update organizations set audit_provider_id = 'prov_ag' where id = ?").run(orgId);
db.prepare("insert into integration_keys(name, ciphertext, updated_at) values ('google_antigravity_client_secret', x'01', datetime('now'))").run();
db.prepare("update app_state set onboarding_draft_json = ? where id = 1").run(JSON.stringify({ step: "mission", companyName: "Next", providerKey: "google-antigravity", providerModel: "gemini-3-flash", providerBaseUrl: "" }));
});
db.exec(dropGoogleAntigravitySchema);
expect(db.prepare("select count(*) as n from providers where provider_key = 'google-antigravity'").get()).toEqual({ n: 0 });
expect(db.prepare("select model_provider_id, model_id from agents where id = ?").get(agentId)).toEqual({ model_provider_id: null, model_id: null });
expect(db.prepare("select audit_provider_id from organizations where id = ?").get(orgId)).toEqual({ audit_provider_id: null });
expect(db.prepare("select length(ciphertext) as bytes from credentials where id = 'cred_ag'").get()).toEqual({ bytes: 0 });
expect(db.prepare("select count(*) as n from integration_keys").get()).toEqual({ n: 0 });
expect(db.prepare("select count(*) as n from providers where provider_key = 'openai'").get()).toEqual({ n: 1 });
const draft = JSON.parse((db.prepare("select onboarding_draft_json from app_state where id = 1").get() as { onboarding_draft_json: string }).onboarding_draft_json);
expect(draft).toEqual({ step: "provider", companyName: "Next", providerKey: "openai", providerModel: "gpt-6.1-sol", providerBaseUrl: "" });
connection.close();
});
});
+2 -4
View File
@@ -5,7 +5,7 @@ import { accumulateResponsesStream, parseResponsesOutput } from "../../src/main/
import { accumulateChatStream, parseChatChoice } from "../../src/main/services/provider-tool-turns/chat-format";
import { parseAnthropicContent } from "../../src/main/services/provider-tool-turns/anthropic-format";
import { parseBedrockOutput } from "../../src/main/services/provider-tool-turns/bedrock-format";
import { accumulateGeminiStream, parseGeminiResponse } from "../../src/main/services/provider-tool-turns/gemini-format";
import { parseGeminiResponse } from "../../src/main/services/provider-tool-turns/gemini-format";
import { mapAnthropicMessageStream, mapOpenAiChatCompletionStream, mapOpenAiResponseStream } from "../../src/main/services/provider-streaming";
async function* stream(items: JsonRecord[]): AsyncIterable<ProviderEventStreamMessage> {
@@ -46,12 +46,10 @@ describe("a turn the provider cut short is never treated as finished", () => {
expect(parseBedrockOutput({ stopReason: "end_turn", output: { message: { content: [{ text: "ok" }] } } }).text).toBe("ok");
});
test("Gemini: MAX_TOKENS fails, a stream without finishReason fails, STOP passes", async () => {
test("Gemini: MAX_TOKENS fails, STOP passes", () => {
const parts = [{ text: "ok" }];
expect(() => parseGeminiResponse({ candidates: [{ content: { parts }, finishReason: "MAX_TOKENS" }] })).toThrow("MAX_TOKENS");
expect(parseGeminiResponse({ candidates: [{ content: { parts }, finishReason: "STOP" }] }).text).toBe("ok");
await expect(accumulateGeminiStream(stream([{ response: { candidates: [{ content: { parts } }] } }]))).rejects.toThrow("no stop reason");
expect((await accumulateGeminiStream(stream([{ response: { candidates: [{ content: { parts }, finishReason: "STOP" }] } }]))).text).toBe("ok");
});
test("streamed replies: a cut-off stop is an error, not a message_stop", async () => {
-1
View File
@@ -34,7 +34,6 @@ describe("integration key store", () => {
expect(statuses.map((status) => [status.name, status.configured])).toEqual([
["composio", false],
["logo_dev", true],
["google_antigravity_client_secret", false],
]);
expect(statuses[1].updatedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
connection.close();
+63
View File
@@ -0,0 +1,63 @@
import { describe, expect, test } from "vitest";
import { finishSprite, PixelGrid } from "../../src/renderer/src/pages/office/pixel-grid";
import { personArt, ANCHOR } from "../../src/renderer/src/pages/office/person-art";
import { appearanceFor, frameFor } from "../../src/renderer/src/pages/office/sprites";
import { ditherLevels, sunPatches } from "../../src/renderer/src/pages/office/lighting";
const pixel = (grid: PixelGrid, x: number, y: number) => Array.from(grid.data.subarray((y * grid.w + x) * 4, (y * grid.w + x) * 4 + 4));
describe("office pixel art", () => {
test("a finished sprite is rim-lit top-left, shaded bottom-right, and outlined in a dark tone of what it borders", () => {
const grid = new PixelGrid(6, 6);
grid.rect(1, 1, 4, 4, "#808080");
finishSprite(grid);
expect(pixel(grid, 1, 1)).toEqual([146, 146, 146, 255]);
expect(pixel(grid, 4, 4)).toEqual([108, 108, 108, 255]);
expect(pixel(grid, 2, 2)).toEqual([128, 128, 128, 255]);
expect(pixel(grid, 0, 2)).toEqual([41, 41, 41, 255]);
expect(pixel(grid, 0, 0)[3]).toBe(0);
});
test("people stand on the anchor in every facing, and west mirrors east", () => {
const look = appearanceFor("agent-7", false);
for (const facing of ["south", "north", "east", "west"] as const) {
const grid = personArt(look, { facing, step: -1, typing: -1, blink: false, carrying: null }, false);
const feetRow = Array.from({ length: grid.w }, (_, x) => grid.alpha(x, ANCHOR.y - 1));
expect(feetRow.some((alpha) => alpha > 0)).toBe(true);
expect(Array.from({ length: grid.w }, (_, x) => grid.alpha(x, ANCHOR.y + 2)).every((alpha) => alpha === 0)).toBe(true);
}
const east = personArt(look, { facing: "east", step: 0, typing: -1, blink: false, carrying: "coffee" }, false);
const west = personArt(look, { facing: "west", step: 0, typing: -1, blink: false, carrying: "coffee" }, false);
expect(pixel(west, 3, 40)).toEqual(pixel(east, east.w - 4, 40));
});
test("poses: four walk steps, typing beats only while working, a short blink", () => {
const pose = { facing: "south" as const, walkPhase: 0, working: false, carrying: null, timeMs: 0, animSeed: 3 };
expect(frameFor(pose).step).toBe(-1);
expect(new Set([0.1, 1.7, 3.3, 4.9].map((walkPhase) => frameFor({ ...pose, walkPhase }).step))).toEqual(new Set([0, 1, 2, 3]));
expect(frameFor(pose).typing).toBe(-1);
expect(new Set([0, 180].map((timeMs) => frameFor({ ...pose, working: true, timeMs }).typing))).toEqual(new Set([0, 1]));
const blinks = Array.from({ length: 400 }, (_, i) => frameFor({ ...pose, timeMs: i * 25 }).blink).filter(Boolean).length;
expect(blinks).toBeGreaterThan(0);
expect(blinks).toBeLessThan(20);
});
});
describe("office lighting", () => {
test("the light map bands into whole levels with an ordered dither", () => {
const data = new Uint8ClampedArray(4 * 4 * 4).fill(138);
ditherLevels(data, 4);
const levels = new Set(Array.from(data).filter((_, i) => i % 4 !== 3));
expect(levels.size).toBe(2);
for (const value of levels) expect(Math.round((value / 255) * 12) / 12).toBeCloseTo(value / 255, 2);
});
test("sun falls through the windows by day, slanting with the hour, and not at night", () => {
const glass = [{ x: 100, y: 8, w: 40, h: 20 }];
expect(sunPatches(glass, new Date(2026, 9, 9, 23, 0))).toEqual([]);
const morning = sunPatches(glass, new Date(2026, 9, 9, 9, 0))[0]!;
const evening = sunPatches(glass, new Date(2026, 9, 9, 17, 0))[0]!;
expect(morning[2]!.x).toBeGreaterThan(evening[2]!.x);
expect(morning[2]!.y).toBeGreaterThan(morning[1]!.y);
});
});
+4 -64
View File
@@ -6,10 +6,8 @@ import { DatabaseConnection } from "../../src/main/database/connection";
import { OrganizationService } from "../../src/main/services/organization-service";
import { ProviderService } from "../../src/main/services/provider-service";
import { providerAdapters } from "../../src/main/services/provider-adapters";
import { PROVIDER_CATALOG } from "../../src/shared/provider-catalog";
import { ERASE_RETIRED_CREDENTIALS_SQL } from "../../src/main/database/migrations/079-erase-retired-credentials";
import { AuthBrokerService } from "../../src/main/services/auth-broker-service";
import { IntegrationKeyStore } from "../../src/main/services/integration-key-store";
import { loadConfiguredProvider } from "../../src/main/services/provider-internal";
import { resolveTurnCredential } from "../../src/main/services/provider-tool-turns/credential-resolver";
@@ -33,43 +31,14 @@ function company(providerKey: string, secret: string, model: string) {
}
describe("disconnecting a provider", () => {
test("only Google Antigravity's sign-in can be revoked at the provider; the catalog says so", () => {
expect(PROVIDER_CATALOG.filter((entry) => entry.revokesRemotely).map((entry) => entry.key)).toEqual(["google-antigravity"]);
});
test("an API key is erased from this computer on disconnect, and a replaced key is erased too", async () => {
vi.stubGlobal("fetch", async () => { throw new Error("no network call expected"); });
const h = company("openai", "sk-first", "gpt-5.4");
h.connection.transaction(() => h.providers.saveCredential({ orgId: h.orgId, providerId: h.providerId, secret: "sk-second", model: "gpt-5.4" }));
expect(h.ciphertexts()).toEqual([0, 9]);
await h.providers.revokeCredential({ orgId: h.orgId, providerId: h.providerId });
h.connection.transaction(() => h.providers.revokeCredential({ orgId: h.orgId, providerId: h.providerId }));
expect(h.ciphertexts()).toEqual([0, 0]);
expect(h.lastRevocation()).toMatchObject({ providerKey: "openai", remote: "not_supported" });
h.connection.close();
});
test("a Google sign-in is revoked at Google before it is erased here", async () => {
const revoked: string[] = [];
vi.stubGlobal("fetch", async (url: string, init: RequestInit) => {
revoked.push(`${url} ${String(init.body)}`);
return new Response("", { status: 200 });
});
const h = company("google-antigravity", JSON.stringify({ access: "ya29", refresh: "1//refresh", expires: 1_900_000_000_000, projectId: "p" }), "gemini-3-pro");
await h.providers.revokeCredential({ orgId: h.orgId, providerId: h.providerId });
expect(revoked).toEqual(["https://oauth2.googleapis.com/revoke token=1%2F%2Frefresh"]);
expect(h.ciphertexts()).toEqual([0]);
expect(h.lastRevocation()).toMatchObject({ remote: "revoked" });
h.connection.close();
});
test("when Google does not confirm, the sign-in is still disconnected and erased here, and the failure is said plainly", async () => {
vi.stubGlobal("fetch", async () => new Response(JSON.stringify({ error: "invalid_token" }), { status: 400, headers: { "content-type": "application/json" } }));
const h = company("google-antigravity", JSON.stringify({ access: "ya29", refresh: "1//refresh", expires: 1_900_000_000_000, projectId: "p" }), "gemini-3-pro");
await expect(h.providers.revokeCredential({ orgId: h.orgId, providerId: h.providerId }))
.rejects.toThrow("Disconnected on this computer, but Google Antigravity did not confirm revoking the sign-in");
expect(h.connection.db.prepare("select enabled, credential_id from providers where id = ?").get(h.providerId)).toEqual({ enabled: 0, credential_id: null });
expect(h.ciphertexts()).toEqual([0]);
expect(h.lastRevocation()).toMatchObject({ remote: "failed", remoteError: expect.stringContaining("400") });
expect(h.lastRevocation()).toEqual({ providerKey: "openai" });
h.connection.close();
});
@@ -77,43 +46,14 @@ describe("disconnecting a provider", () => {
let answer: (response: Response) => void = () => {};
vi.stubGlobal("fetch", () => new Promise<Response>((resolve) => { answer = resolve; }));
const h = company("claude-pro", JSON.stringify({ access: "a", refresh: "r", expires: 0 }), "claude-sonnet-4-5");
const renewal = resolveTurnCredential({ authBroker: new AuthBrokerService(h.connection.db, vault), integrationKeys: new IntegrationKeyStore(h.connection.db, vault), orgId: h.orgId, provider: loadConfiguredProvider(h.connection.db, h.orgId, h.providerId) });
await h.providers.revokeCredential({ orgId: h.orgId, providerId: h.providerId });
const renewal = resolveTurnCredential({ authBroker: new AuthBrokerService(h.connection.db, vault), orgId: h.orgId, provider: loadConfiguredProvider(h.connection.db, h.orgId, h.providerId) });
h.connection.transaction(() => h.providers.revokeCredential({ orgId: h.orgId, providerId: h.providerId }));
answer(new Response(JSON.stringify({ access_token: "new", refresh_token: "new-r", expires_in: 3600 }), { status: 200 }));
await expect(renewal).rejects.toThrow("disconnected or replaced while it was being renewed");
expect(h.ciphertexts()).toEqual([0]);
h.connection.close();
});
test("a reconnect made while Google answers keeps the new connection and erases only the old sign-in", async () => {
let answer: (response: Response) => void = () => {};
vi.stubGlobal("fetch", () => new Promise<Response>((resolve) => { answer = resolve; }));
const signIn = (refresh: string) => JSON.stringify({ access: "ya29", refresh, expires: 1_900_000_000_000, projectId: "p" });
const h = company("google-antigravity", signIn("1//old"), "gemini-3-pro");
const disconnecting = h.providers.revokeCredential({ orgId: h.orgId, providerId: h.providerId });
h.connection.transaction(() => h.providers.saveCredential({ orgId: h.orgId, providerId: h.providerId, secret: signIn("1//new"), model: "gemini-3-pro" }));
answer(new Response("", { status: 200 }));
await expect(disconnecting).rejects.toThrow("was reconnected while it was being disconnected");
expect(h.connection.db.prepare("select enabled from providers where id = ?").get(h.providerId)).toEqual({ enabled: 1 });
expect(h.ciphertexts()).toEqual([0, signIn("1//new").length]);
h.connection.close();
});
test("a reconnect and a refused revocation at once are both reported", async () => {
let answer: (response: Response) => void = () => {};
vi.stubGlobal("fetch", () => new Promise<Response>((resolve) => { answer = resolve; }));
const signIn = (refresh: string) => JSON.stringify({ access: "ya29", refresh, expires: 1_900_000_000_000, projectId: "p" });
const h = company("google-antigravity", signIn("1//old"), "gemini-3-pro");
const disconnecting = h.providers.revokeCredential({ orgId: h.orgId, providerId: h.providerId });
h.connection.transaction(() => h.providers.saveCredential({ orgId: h.orgId, providerId: h.providerId, secret: signIn("1//new"), model: "gemini-3-pro" }));
answer(new Response(JSON.stringify({ error: "invalid_token" }), { status: 400, headers: { "content-type": "application/json" } }));
const failure = (await disconnecting.catch((error: unknown) => error)) as Error;
expect(failure.message).toContain("was reconnected while it was being disconnected");
expect(failure.message).toContain("did not confirm revoking the sign-in (Google revocation failed: 400");
expect(failure.message).toContain("revoke it in your account");
h.connection.close();
});
test("credentials retired before this release are erased by the migration's statement", () => {
const h = company("openai", "sk-old", "gpt-5.4");
h.connection.db.prepare("update providers set credential_id = null where id = ?").run(h.providerId);
-3
View File
@@ -7,7 +7,6 @@ import { refreshCodex } from "../../src/main/services/oauth/openai-codex-flow";
import { refreshKimiCode } from "../../src/main/services/oauth/kimi-code-flow";
import { postJson, ProviderHttpError } from "../../src/main/services/provider-http";
import { accumulateResponsesStream } from "../../src/main/services/provider-tool-turns/responses-format";
import { accumulateGeminiStream } from "../../src/main/services/provider-tool-turns/gemini-format";
import { DatabaseConnection } from "../../src/main/database/connection";
import { OrganizationService } from "../../src/main/services/organization-service";
import { ProviderService } from "../../src/main/services/provider-service";
@@ -67,8 +66,6 @@ describe("provider errors keep their status and machine code", () => {
const codex = await accumulateResponsesStream(events({ type: "error", error: { type: "usage_limit_reached", message: "The usage limit has been reached" } })).catch((caught: unknown) => caught);
expect(codex).toMatchObject({ status: null, code: "usage_limit_reached" });
expect(classifyProviderFailure(codex, NOW).transient).toBe(true);
const gemini = await accumulateGeminiStream(events({ error: { code: 503, message: "busy", status: "UNAVAILABLE" } })).catch((caught: unknown) => caught);
expect(gemini).toMatchObject({ status: 503, code: "UNAVAILABLE" });
});
test("a streamed reply's error event keeps the provider's code on the error and the request", async () => {
-25
View File
@@ -3,7 +3,6 @@ import { describe, expect, test } from "vitest";
import { interpretPollResponse } from "../../src/main/services/oauth/github-copilot-flow";
import { interpretKimiPollResponse } from "../../src/main/services/oauth/kimi-code-flow";
import { extractAccountId } from "../../src/main/services/oauth/openai-codex-flow";
import { readProjectId, requireAntigravityClientSecret } from "../../src/main/services/oauth/google-antigravity-flow";
import { generatePkce } from "../../src/main/services/oauth/pkce";
import { providerAdapters } from "../../src/main/services/provider-adapters";
@@ -80,28 +79,6 @@ describe("openai codex account id extraction", () => {
});
});
describe("antigravity project discovery", () => {
test("reads a string or object project id", () => {
expect(readProjectId({ cloudaicompanionProject: "proj-a" })).toBe("proj-a");
expect(readProjectId({ cloudaicompanionProject: { id: "proj-b" } })).toBe("proj-b");
});
test("returns null when no project is present", () => {
expect(readProjectId({})).toBeNull();
expect(readProjectId({ cloudaicompanionProject: {} })).toBeNull();
});
});
describe("antigravity oauth configuration", () => {
test("passes a saved client secret through", () => {
expect(requireAntigravityClientSecret("live-secret")).toBe("live-secret");
});
test("requires a client secret saved in Settings", () => {
expect(() => requireAntigravityClientSecret(null)).toThrow("Settings > Integrations");
});
});
describe("pkce generation", () => {
test("produces an S256 challenge for the verifier", () => {
const { verifier, challenge } = generatePkce();
@@ -117,7 +94,6 @@ describe("oauth credential parsing", () => {
test("a stored sign-in is accepted only with what a run needs to use and renew it", () => {
expect(providerAdapters["github-copilot"].parseAuthProfile(signIn())).toEqual({ type: "access_token", accessToken: "live_token" });
expect(providerAdapters["gpt-account"].parseAuthProfile(signIn({ accountId: "acct_1" }))).toEqual({ type: "account_token", accessToken: "live_token" });
expect(providerAdapters["google-antigravity"].parseAuthProfile(signIn({ projectId: "proj-1" }))).toEqual({ type: "access_token", accessToken: "live_token" });
});
test("a pasted token, or a sign-in missing its refresh token, expiry or provider extras, is refused at save time", () => {
@@ -125,7 +101,6 @@ describe("oauth credential parsing", () => {
expect(() => providerAdapters["claude-pro"].parseAuthProfile(JSON.stringify({ access: "a", expires: 1 }))).toThrow("missing its refresh token");
expect(() => providerAdapters["kimi-code"].parseAuthProfile(JSON.stringify({ access: "a", refresh: "r" }))).toThrow("missing its expiry");
expect(() => providerAdapters["gpt-account"].parseAuthProfile(signIn())).toThrow("missing its ChatGPT account id");
expect(() => providerAdapters["google-antigravity"].parseAuthProfile(signIn())).toThrow("missing its Google Cloud project id");
});
});
+7 -60
View File
@@ -7,7 +7,6 @@ import type { AgentTurnMessage } from "../../src/main/services/provider-tool-tur
import { accumulateResponsesStream, parseResponsesOutput, responsesToolDefs, toResponsesInput } from "../../src/main/services/provider-tool-turns/responses-format";
import { accumulateChatStream, parseChatChoice, toChatMessages } from "../../src/main/services/provider-tool-turns/chat-format";
import { parseAnthropicContent, toAnthropicMessages } from "../../src/main/services/provider-tool-turns/anthropic-format";
import { accumulateGeminiStream, antigravityRequestBody } from "../../src/main/services/provider-tool-turns/gemini-format";
import { bedrockToolConfig, parseBedrockOutput, toBedrockMessages } from "../../src/main/services/provider-tool-turns/bedrock-format";
import { parseGeminiResponse, vertexRequestBody } from "../../src/main/services/provider-tool-turns/gemini-format";
import { signAwsRequest } from "../../src/main/services/provider-tool-turns/aws-sigv4";
@@ -19,7 +18,6 @@ import { ProviderService } from "../../src/main/services/provider-service";
import { providerAdapters } from "../../src/main/services/provider-adapters";
import { AuthBrokerService } from "../../src/main/services/auth-broker-service";
import { loadConfiguredProvider } from "../../src/main/services/provider-internal";
import { IntegrationKeyStore } from "../../src/main/services/integration-key-store";
const CONVO: AgentTurnMessage[] = [
{ role: "user", text: "do it" },
@@ -135,40 +133,6 @@ describe("anthropic encoding", () => {
});
});
describe("antigravity (gemini) encoding", () => {
test("wraps the request with the Antigravity system instruction and tool declarations", () => {
const body = antigravityRequestBody({ model: "claude-sonnet-4-5", projectId: "proj", instructions: "be helpful", messages: CONVO, tools: [{ name: "f", description: "d", parameters: { type: "object" } }], requestId: "agent-1" });
expect(body.project).toBe("proj");
expect(body.requestType).toBe("agent");
expect(body.userAgent).toBe("antigravity");
const request = body.request as JsonRecord;
const system = request.systemInstruction as { role: string; parts: Array<{ text: string }> };
expect(system.role).toBe("user");
expect(system.parts[0].text).toContain("You are Antigravity");
expect(system.parts[system.parts.length - 1].text).toBe("be helpful");
// claude-* models use the legacy `parameters` field
expect(request.tools).toEqual([{ functionDeclarations: [{ name: "f", description: "d", parameters: { type: "object" } }] }]);
});
test("gemini models use parametersJsonSchema and omit the tool-call id", () => {
const body = antigravityRequestBody({ model: "gemini-3.1-pro-high", projectId: "proj", instructions: null, messages: CONVO, tools: [{ name: "f", description: "d", parameters: { type: "object" } }], requestId: "agent-2" });
const request = body.request as JsonRecord;
expect(request.tools).toEqual([{ functionDeclarations: [{ name: "f", description: "d", parametersJsonSchema: { type: "object" } }] }]);
const contents = request.contents as Array<{ role: string; parts: Array<JsonRecord> }>;
const assistant = contents.find((c) => c.role === "model");
const fnCall = assistant?.parts.find((p) => "functionCall" in p)?.functionCall as JsonRecord;
expect(fnCall.id).toBeUndefined();
});
test("accumulates streamed candidates", async () => {
const result = await accumulateGeminiStream(stream([
{ response: { candidates: [{ content: { parts: [{ text: "hello" }, { functionCall: { name: "f", args: { x: 1 }, id: "g1" } }] }, finishReason: "STOP" }] } },
]));
expect(result.text).toBe("hello");
expect(result.toolCalls).toEqual([{ id: "g1", name: "f", arguments: { x: 1 } }]);
});
});
describe("bedrock converse encoding", () => {
test("encodes toolUse/toolResult and the tool config", () => {
expect(toBedrockMessages(CONVO)).toEqual([
@@ -190,9 +154,11 @@ describe("bedrock converse encoding", () => {
describe("vertex (gemini) encoding", () => {
test("builds a generateContent body with tools and parses the response", () => {
const body = vertexRequestBody({ model: "gemini-2.5-pro", instructions: "sys", messages: CONVO, tools: [{ name: "f", description: "d", parameters: { type: "object" } }] });
const body = vertexRequestBody({ instructions: "sys", messages: CONVO, tools: [{ name: "f", description: "d", parameters: { type: "object" } }] });
expect((body.systemInstruction as JsonRecord).parts).toEqual([{ text: "sys" }]);
expect(body.tools).toEqual([{ functionDeclarations: [{ name: "f", description: "d", parametersJsonSchema: { type: "object" } }] }]);
const assistant = (body.contents as Array<{ role: string; parts: JsonRecord[] }>).find((c) => c.role === "model");
expect((assistant?.parts.find((p) => "functionCall" in p)?.functionCall as JsonRecord).id).toBeUndefined();
const parsed = parseGeminiResponse({ candidates: [{ content: { parts: [{ text: "hi" }, { functionCall: { name: "f", args: { x: 1 }, id: "v1" } }] }, finishReason: "STOP" }] });
expect(parsed).toMatchObject({ text: "hi", toolCalls: [{ id: "v1", name: "f", arguments: { x: 1 } }] });
});
@@ -214,7 +180,7 @@ describe("aws sigv4", () => {
describe("tool-turn router", () => {
test("supports every agent-capable provider (incl. cloud and custom) and rejects unknown ones", () => {
for (const key of ["openai", "anthropic", "xai", "groq", "mistral", "cerebras", "openrouter", "kimi", "kimi-cn", "gpt-account", "github-copilot", "google-antigravity", "claude-pro", "kimi-code", "custom-openai", "custom-anthropic", "azure-openai", "aws-bedrock", "vertex-ai"]) {
for (const key of ["openai", "anthropic", "xai", "groq", "mistral", "cerebras", "openrouter", "kimi", "kimi-cn", "gpt-account", "github-copilot", "claude-pro", "kimi-code", "custom-openai", "custom-anthropic", "azure-openai", "aws-bedrock", "vertex-ai"]) {
expect(supportsToolTurn(key)).toBe(true);
expect(typeof selectToolTurn(key)).toBe("function");
}
@@ -229,7 +195,6 @@ describe("credential resolver", () => {
const organization = new OrganizationService(connection.db);
const providers = new ProviderService(connection.db, providerAdapters, testVault);
const authBroker = new AuthBrokerService(connection.db, testVault);
const integrationKeys = new IntegrationKeyStore(connection.db, testVault);
let orgId = "";
let openaiId = "";
let gptAccountId = "";
@@ -243,32 +208,14 @@ describe("credential resolver", () => {
});
return (async () => {
const apiKeyCred = await resolveTurnCredential({ authBroker, integrationKeys, orgId, provider: loadConfiguredProvider(connection.db, orgId, openaiId) });
const apiKeyCred = await resolveTurnCredential({ authBroker, orgId, provider: loadConfiguredProvider(connection.db, orgId, openaiId) });
expect(apiKeyCred).toEqual({ apiKey: "sk-live" });
const oauthCred = await resolveTurnCredential({ authBroker, integrationKeys, orgId, provider: loadConfiguredProvider(connection.db, orgId, gptAccountId) });
const oauthCred = await resolveTurnCredential({ authBroker, orgId, provider: loadConfiguredProvider(connection.db, orgId, gptAccountId) });
expect(oauthCred.accessToken).toBe("tok");
expect(oauthCred.accountId).toBe("acct_1");
connection.close();
})();
});
test("an expired Antigravity credential without a saved client secret points to Settings", async () => {
const connection = new DatabaseConnection(join(tmpdir(), `ac-cred-${randomUUID()}.db`));
const organization = new OrganizationService(connection.db);
const providers = new ProviderService(connection.db, providerAdapters, testVault);
const authBroker = new AuthBrokerService(connection.db, testVault);
const integrationKeys = new IntegrationKeyStore(connection.db, testVault);
let orgId = "";
let antigravityId = "";
connection.transaction(() => {
orgId = organization.createOrganization({ organizationName: "Acme", employeeName: "Dana", mission: "m", goal: "g" });
antigravityId = (connection.db.prepare("select id from providers where org_id = ? and provider_key = 'google-antigravity'").get(orgId) as { id: string }).id;
providers.saveCredential({ orgId, providerId: antigravityId, secret: JSON.stringify({ access: "old", refresh: "r", expires: 0, projectId: "p" }), model: "gemini-3-pro" });
});
await expect(resolveTurnCredential({ authBroker, integrationKeys, orgId, provider: loadConfiguredProvider(connection.db, orgId, antigravityId) }))
.rejects.toThrow("Settings > Integrations");
connection.close();
});
});
describe("responses tool definitions", () => {
@@ -306,7 +253,7 @@ describe("mid-run follow-up text rides on the tool-result turn", () => {
role: "user",
content: [{ toolResult: { toolUseId: "c1", content: [{ text: "ok" }] } }, { text: "New: stop" }],
});
const gemini = vertexRequestBody({ model: "gemini-3-pro", instructions: null, messages: convo, tools: [] }) as { contents: Array<{ role: string; parts: unknown[] }> };
const gemini = vertexRequestBody({ instructions: null, messages: convo, tools: [] }) as { contents: Array<{ role: string; parts: unknown[] }> };
expect(gemini.contents.at(-1)).toEqual({
role: "user",
parts: [{ functionResponse: { name: "f", response: { output: "ok" } } }, { text: "New: stop" }],
+2 -4
View File
@@ -5,7 +5,7 @@ import { accumulateResponsesStream, parseResponsesOutput, toResponsesInput } fro
import { accumulateChatStream, parseChatChoice, toChatMessages } from "../../src/main/services/provider-tool-turns/chat-format";
import { parseAnthropicContent, toAnthropicMessages } from "../../src/main/services/provider-tool-turns/anthropic-format";
import { parseBedrockOutput, toBedrockMessages } from "../../src/main/services/provider-tool-turns/bedrock-format";
import { accumulateGeminiStream, parseGeminiResponse, vertexRequestBody } from "../../src/main/services/provider-tool-turns/gemini-format";
import { parseGeminiResponse, vertexRequestBody } from "../../src/main/services/provider-tool-turns/gemini-format";
async function* stream(items: JsonRecord[]): AsyncIterable<ProviderEventStreamMessage> {
for (const data of items) yield { eventName: null, data, rawData: JSON.stringify(data) };
@@ -53,10 +53,8 @@ describe("reasoning survives tool turns", () => {
test("Gemini: thought signatures on parts go back, streamed or not", async () => {
const parts = [{ text: "plan", thought: true }, { functionCall: { name: "f", args: {} }, thoughtSignature: "SIG" }];
const vertex = vertexRequestBody({ model: "gemini-3-pro", instructions: null, messages: after(parseGeminiResponse({ candidates: [{ content: { parts }, finishReason: "STOP" }] })), tools: [] });
const vertex = vertexRequestBody({ instructions: null, messages: after(parseGeminiResponse({ candidates: [{ content: { parts }, finishReason: "STOP" }] })), tools: [] });
expect((vertex.contents as JsonRecord[])[1]).toEqual({ role: "model", parts });
const streamed = await accumulateGeminiStream(stream([...parts.map((part) => ({ response: { candidates: [{ content: { parts: [part] } }] } })), { response: { candidates: [{ finishReason: "STOP" }] } }]));
expect(streamed.replay).toEqual({ format: "gemini", content: parts });
});
test("Chat: streamed reasoning goes back under the field it came in, and OpenRouter details are merged into whole entries", async () => {
+41
View File
@@ -0,0 +1,41 @@
import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { DatabaseConnection } from "../../src/main/database/connection";
import { OrganizationService } from "../../src/main/services/organization-service";
import { ChatService } from "../../src/main/services/chat-service";
import { createRunMessagePoller } from "../../src/main/services/chat-wake-context";
describe("the mid-run inbox", () => {
test("injects each new message once, even after another company's deletion frees message row numbers", () => {
const connection = new DatabaseConnection(join(tmpdir(), `ac-inbox-${randomUUID()}.db`));
const db = connection.db;
const organization = new OrganizationService(db);
const chat = new ChatService(db);
const company = (name: string) => {
const orgId = organization.createOrganization({ organizationName: name, employeeName: "Yang", mission: "m", goal: "g" });
const agentId = organization.createAgent({ orgId, name: "Dana", role: "Engineer", department: "Engineering", modelProviderId: null, modelId: null, budgetCents: 1000, managerAgentId: null });
const employeeId = (db.prepare("select id from employees where org_id = ?").get(orgId) as { id: string }).id;
const spaceId = chat.ensureDirectSpace(orgId, { type: "employee", id: employeeId }, { type: "agent", id: agentId });
const say = (bodyMd: string, threadId: string | null) => {
const messageId = chat.sendMessage({ orgId, spaceId, threadId, senderType: "employee", senderId: employeeId, bodyMd, attachmentFileIds: [] });
return (db.prepare("select thread_id from chat_messages where id = ?").get(messageId) as { thread_id: string }).thread_id;
};
return { orgId, agentId, say };
};
const ours = connection.transaction(() => company("Ours"));
const threadId = ours.say("Start.", null);
const theirs = connection.transaction(() => company("Theirs"));
for (let i = 0; i < 5; i++) theirs.say(`Noise ${i}`, null);
const injected: string[] = [];
const poll = createRunMessagePoller(db, { orgId: ours.orgId, agentId: ours.agentId, sourceThreadId: threadId, runStartedAt: new Date(0).toISOString(), onInjected: (entries) => injected.push(...entries.map((entry) => entry.id)) });
expect(poll()).toBeNull();
connection.transaction(() => organization.deleteOrganization(theirs.orgId));
ours.say("Correction.", threadId);
expect(poll()).toContain("Correction.");
expect(poll()).toBeNull();
expect(injected).toHaveLength(1);
connection.close();
});
});
+1 -2
View File
@@ -48,7 +48,6 @@ describe("every agent turn has the same output cap and a wait that fits how it i
const { body } = await capture(key);
expect([key, body[field]]).toEqual([key, OUTPUT_RESERVE_TOKENS]);
}
expect(outputCap((await capture("google-antigravity", "gemini-3-pro")).body)).toBe(OUTPUT_RESERVE_TOKENS);
expect(outputCap((await capture("vertex-ai", "gemini-3-pro")).body)).toBe(OUTPUT_RESERVE_TOKENS);
expect(outputCap((await capture("aws-bedrock", "anthropic.claude-sonnet-4-5")).body)).toBe(OUTPUT_RESERVE_TOKENS);
});
@@ -64,7 +63,7 @@ describe("every agent turn has the same output cap and a wait that fits how it i
const { streamed, timeouts } = await capture(key);
expect([key, streamed, timeouts?.headersTimeoutMs]).toEqual([key, false, 300_000]);
}
for (const key of ["xai", "kimi-code", "gpt-account", "google-antigravity"]) {
for (const key of ["xai", "kimi-code", "gpt-account"]) {
const { streamed, timeouts } = await capture(key);
expect([key, streamed, timeouts?.headersTimeoutMs]).toEqual([key, true, undefined]);
}
+23
View File
@@ -0,0 +1,23 @@
import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { DatabaseConnection } from "../../src/main/database/connection";
import { OrganizationService } from "../../src/main/services/organization-service";
import { SnapshotService } from "../../src/main/services/snapshot-service";
describe("ui preferences", () => {
test("every saved theme reads back as saved, the clock theme included; an unknown one fails loudly", () => {
const connection = new DatabaseConnection(join(tmpdir(), `ac-ui-prefs-${randomUUID()}.db`));
const organization = new OrganizationService(connection.db);
const snapshot = new SnapshotService(connection.db);
connection.transaction(() => organization.createOrganization({ organizationName: "Acme", employeeName: "Yang", mission: "m", goal: "g" }));
for (const theme of ["system", "light", "dark", "clock"] as const) {
organization.updateUiPreferences({ theme });
expect(snapshot.load().uiPreferences.theme).toBe(theme);
}
connection.db.prepare("update app_state set theme = 'sepia' where id = 1").run();
expect(() => snapshot.load()).toThrow('Stored theme "sepia" is not a known theme.');
connection.close();
});
});