Promote AgentCompany-New to the repository root and retire the old app
The legacy application - its src, tests, landing site, promo assets, docs, release tooling, CI workflows, and root configs - leaves the tree; it stays reachable in history. Everything under AgentCompany-New moves up unchanged: same package name, so the production userData/database path is untouched, and e2e/build scripts already resolved paths relative to the project. The developer cred.txt stays gitignored at its new location. Full verify:ci passes from the new root.
This commit is contained in:
1 parent
2350f744e3
commit
07d0b4587d
1904 files changed
+1133
-282075
No files matched your search
@@ -1,212 +0,0 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { _electron as electron, expect, test } from "@playwright/test";
|
||||
import type { ElectronApplication, Page } from "@playwright/test";
|
||||
import { createElectronLaunchEnv, getElectronExecutablePath, getMainEntryPath } from "./launch";
|
||||
import {
|
||||
E2E_READY_CONNECTOR_ID,
|
||||
E2E_READY_CONNECTOR_MODEL_ID,
|
||||
E2E_PROVIDER_API_KEY,
|
||||
readOnboardingProfileState,
|
||||
} from "./onboarding-fixture";
|
||||
|
||||
function buildLaunchEnv(profileDir: string) {
|
||||
return createElectronLaunchEnv({
|
||||
AGENTCOMPANY_PROFILE_DIR: profileDir,
|
||||
AGENTCOMPANY_DISABLE_UPDATES: "true",
|
||||
AGENTCOMPANY_DISABLE_SINGLE_INSTANCE: "true",
|
||||
AGENTCOMPANY_ALLOW_INSECURE_VAULT: "true",
|
||||
});
|
||||
}
|
||||
|
||||
async function launchDesktopApp(profileDir: string): Promise<ElectronApplication> {
|
||||
return electron.launch({
|
||||
executablePath: getElectronExecutablePath(),
|
||||
args: [getMainEntryPath()],
|
||||
cwd: process.cwd(),
|
||||
env: buildLaunchEnv(profileDir),
|
||||
});
|
||||
}
|
||||
|
||||
async function advanceFromWelcome(page: Page) {
|
||||
await expect(page.getByText("AgentCompany")).toBeVisible();
|
||||
await expect(page.getByText("Build your AI company here.")).toBeVisible();
|
||||
await page.getByRole("button", { name: /Get Started/i }).click();
|
||||
}
|
||||
|
||||
async function advancePastNodeCheck(page: Page) {
|
||||
const continueButton = page.getByRole("button", { name: /^Continue$/i });
|
||||
await expect(page.getByText("Check Node.js")).toBeVisible();
|
||||
await expect(continueButton).toBeEnabled({ timeout: 15_000 });
|
||||
await continueButton.click();
|
||||
}
|
||||
|
||||
async function configureReadyProvider(page: Page) {
|
||||
await expect(page.getByText("Anthropic API")).toBeVisible();
|
||||
await page.getByRole("button", { name: /Anthropic API/i }).click();
|
||||
const apiKeyInput = page.locator('input[type="password"]').first();
|
||||
await expect(apiKeyInput).toBeVisible();
|
||||
await apiKeyInput.fill(E2E_PROVIDER_API_KEY);
|
||||
await page.getByRole("button", { name: /^Save$/i }).click();
|
||||
await expect(page.getByText("Saved.")).toBeVisible({ timeout: 15_000 });
|
||||
await expect(page.getByText("Connected").first()).toBeVisible({ timeout: 15_000 });
|
||||
}
|
||||
|
||||
async function completeOnboardingToLaunch(
|
||||
page: Page,
|
||||
input: {
|
||||
companyName: string;
|
||||
workspaceDir: string;
|
||||
goalTitle: string;
|
||||
toggleSecondModel?: boolean;
|
||||
},
|
||||
) {
|
||||
await advanceFromWelcome(page);
|
||||
await advancePastNodeCheck(page);
|
||||
|
||||
await expect(page.getByText("Connect a provider")).toBeVisible();
|
||||
await configureReadyProvider(page);
|
||||
const providersContinue = page.getByRole("button", { name: /^Continue$/i });
|
||||
await expect(providersContinue).toBeEnabled({ timeout: 30_000 });
|
||||
await providersContinue.click();
|
||||
|
||||
await expect(page.getByLabel("Company name")).toBeVisible();
|
||||
await page.getByLabel("Company name").fill(input.companyName);
|
||||
await page.getByLabel("Workspace folder").fill(input.workspaceDir);
|
||||
await expect(page.getByLabel("Provider")).toHaveValue(E2E_READY_CONNECTOR_ID);
|
||||
await expect(page.getByLabel("Model")).toHaveValue(E2E_READY_CONNECTOR_MODEL_ID);
|
||||
await page.getByRole("button", { name: /^Continue$/i }).click();
|
||||
|
||||
await expect(page.getByText("Choose team models")).toBeVisible();
|
||||
const enabledCountLabel = page.getByText(/\d+ \/ \d+ models enabled/i).first();
|
||||
await expect(enabledCountLabel).toBeVisible();
|
||||
const initialEnabledCount = await enabledCountLabel.textContent();
|
||||
const switches = page.getByRole("switch");
|
||||
await expect(switches.nth(1)).toBeVisible();
|
||||
if (input.toggleSecondModel) {
|
||||
await switches.nth(1).click();
|
||||
await expect.poll(async () => enabledCountLabel.textContent()).not.toBe(initialEnabledCount);
|
||||
}
|
||||
await page.getByRole("button", { name: /^Continue$/i }).click();
|
||||
|
||||
await expect(page.getByText("How should it run?")).toBeVisible();
|
||||
await page.getByRole("button", { name: /^Continue$/i }).click();
|
||||
|
||||
await expect(page.getByLabel("Goal")).toBeVisible();
|
||||
await page.getByLabel("Goal").fill(input.goalTitle);
|
||||
await page.getByRole("button", { name: /Launch Company/i }).click();
|
||||
|
||||
await expect(page.getByRole("heading", { name: new RegExp(`${input.companyName}.*is live`, "i") })).toBeVisible();
|
||||
await expect(page.getByText("Queued in orchestration")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /Open Control Center/i })).toBeVisible();
|
||||
}
|
||||
|
||||
test("first launch onboarding advances into runtime setup and shell-ready flow", async () => {
|
||||
const entry = getMainEntryPath();
|
||||
expect(existsSync(entry)).toBeTruthy();
|
||||
|
||||
const profileDir = await mkdtemp(join(tmpdir(), "agentcompany-e2e-"));
|
||||
const workspaceDir = await mkdtemp(join(tmpdir(), "agentcompany-workspace-"));
|
||||
|
||||
let app: ElectronApplication | null = await launchDesktopApp(profileDir);
|
||||
|
||||
try {
|
||||
const page = await app.firstWindow();
|
||||
await completeOnboardingToLaunch(page, {
|
||||
companyName: "Orbit Foundry",
|
||||
workspaceDir,
|
||||
goalTitle: "Launch the company",
|
||||
});
|
||||
await page.getByRole("button", { name: /Open Control Center/i }).click();
|
||||
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.getByRole("button", { name: /Orbit Foundry/i })).toBeVisible();
|
||||
await app.close();
|
||||
app = null;
|
||||
|
||||
const state = readOnboardingProfileState(profileDir);
|
||||
expect(state.companyCount).toBe(1);
|
||||
expect(state.currentCompanyName).toBe("Orbit Foundry");
|
||||
expect(state.leadAgentConnectorId).toBe(E2E_READY_CONNECTOR_ID);
|
||||
expect(state.leadAgentModel).toBe(E2E_READY_CONNECTOR_MODEL_ID);
|
||||
expect(state.taskCount).toBeGreaterThanOrEqual(1);
|
||||
expect(state.runCount).toBeGreaterThanOrEqual(1);
|
||||
} finally {
|
||||
await app?.close();
|
||||
await rm(profileDir, { recursive: true, force: true });
|
||||
await rm(workspaceDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("platform launch: app opens, window renders, screenshot captured", async () => {
|
||||
const entry = getMainEntryPath();
|
||||
expect(existsSync(entry)).toBeTruthy();
|
||||
|
||||
const profileDir = await mkdtemp(join(tmpdir(), "agentcompany-launch-"));
|
||||
const screenshotDir = join(process.cwd(), "build", "screenshots");
|
||||
await mkdir(screenshotDir, { recursive: true });
|
||||
|
||||
const app = await launchDesktopApp(profileDir);
|
||||
|
||||
try {
|
||||
const page = await app.firstWindow();
|
||||
await expect(page.getByText("AgentCompany")).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.getByText("Build your AI company here.")).toBeVisible();
|
||||
|
||||
const viewport = page.viewportSize();
|
||||
const dimensions = viewport ?? await page.evaluate(() => ({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
}));
|
||||
expect(dimensions.width).toBeGreaterThan(400);
|
||||
expect(dimensions.height).toBeGreaterThan(300);
|
||||
|
||||
const platformName = process.platform === "darwin" ? "macos"
|
||||
: process.platform === "win32" ? "windows"
|
||||
: "linux";
|
||||
const screenshotPath = join(screenshotDir, `launch-${platformName}.png`);
|
||||
await page.screenshot({ path: screenshotPath, fullPage: true });
|
||||
expect(existsSync(screenshotPath)).toBeTruthy();
|
||||
} finally {
|
||||
await app.close();
|
||||
await rm(profileDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("onboarding model pool persists across relaunch for a ready connector", async () => {
|
||||
const entry = getMainEntryPath();
|
||||
expect(existsSync(entry)).toBeTruthy();
|
||||
|
||||
const profileDir = await mkdtemp(join(tmpdir(), "agentcompany-model-pool-"));
|
||||
const workspaceDir = await mkdtemp(join(tmpdir(), "agentcompany-model-pool-workspace-"));
|
||||
|
||||
let app: ElectronApplication | null = await launchDesktopApp(profileDir);
|
||||
|
||||
try {
|
||||
const page = await app.firstWindow();
|
||||
await completeOnboardingToLaunch(page, {
|
||||
companyName: "Persisted Model Pool Co",
|
||||
workspaceDir,
|
||||
goalTitle: "Keep model pool choices after relaunch",
|
||||
toggleSecondModel: true,
|
||||
});
|
||||
await page.getByRole("button", { name: /Open Control Center/i }).click();
|
||||
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible({ timeout: 30_000 });
|
||||
await app.close();
|
||||
app = null;
|
||||
|
||||
const persistedState = readOnboardingProfileState(profileDir);
|
||||
expect(persistedState.currentCompanyName).toBe("Persisted Model Pool Co");
|
||||
expect(persistedState.disabledModelCount).toBe(1);
|
||||
|
||||
app = await launchDesktopApp(profileDir);
|
||||
const relaunchedPage = await app.firstWindow();
|
||||
await expect(relaunchedPage.getByRole("heading", { name: "Dashboard" })).toBeVisible({ timeout: 30_000 });
|
||||
await expect(relaunchedPage.getByRole("button", { name: /Persisted Model Pool Co/i })).toBeVisible();
|
||||
} finally {
|
||||
await app?.close();
|
||||
await rm(profileDir, { recursive: true, force: true });
|
||||
await rm(workspaceDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,226 @@
|
||||
import { _electron as electron, expect, test, type Page } from "@playwright/test";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
|
||||
test("onboards, hires, and creates work in the Electron shell", async () => {
|
||||
const profileDir = join(tmpdir(), `agentcompany-new-e2e-${randomUUID()}`);
|
||||
mkdirSync(profileDir, { recursive: true });
|
||||
const app = await electron.launch({
|
||||
args: [resolve("out/main/index.js")],
|
||||
env: {
|
||||
...process.env,
|
||||
AGENTCOMPANY_NEW_PROFILE_DIR: profileDir,
|
||||
AGENTCOMPANY_NEW_AUTORUN: "0",
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const page = await app.firstWindow();
|
||||
await expect(page.getByRole("heading", { name: "Welcome" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Get started" }).click();
|
||||
await page.getByLabel("API key").fill("e2e-secret");
|
||||
await page.getByLabel("Model").fill("gpt-5.4");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await page.getByLabel("Company name").fill("Acme Operations");
|
||||
await page.getByLabel("Your name").fill("Dana Lee");
|
||||
await page.getByLabel("Description").fill("Coordinate agents with auditable approvals.");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await page.getByLabel("Goal").fill("Ship a verified control plane.");
|
||||
await page.getByLabel("Acceptance criteria").fill("The CEO creates the kickoff operating loop.");
|
||||
await page.getByRole("button", { name: "Launch" }).click();
|
||||
|
||||
await expect(page.getByRole("button", { name: /Acme Operations/ })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Work", exact: true }).click();
|
||||
await expect(page.getByText("Projects", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("Execution plan: Ship a verified control plane").first()).toBeVisible();
|
||||
await expect(page.getByText("#execution-plan-ship-a-verified-control-plane").first()).toBeVisible();
|
||||
await page.getByRole("button", { name: "Goal", exact: true }).click();
|
||||
await page.getByLabel("Title").fill("Open the Berlin market");
|
||||
await page.getByLabel("Description").fill("Win the first ten customers.");
|
||||
await page.locator(".modal").getByRole("button", { name: "Create" }).click();
|
||||
await expect(page.getByText("Operationalize goal: Open the Berlin market").first()).toBeVisible();
|
||||
await page.getByRole("button", { name: "Team" }).click();
|
||||
await page.getByRole("button", { name: "Hire" }).click();
|
||||
await page.getByLabel("Name").fill("Alex Chen");
|
||||
await page.getByLabel("Role").fill("CTO");
|
||||
await page.getByLabel("Department").fill("Engineering");
|
||||
await page.getByLabel("Monthly budget").fill("250");
|
||||
await page.locator(".modal").getByRole("button", { name: "Hire" }).click();
|
||||
await expect(page.getByText("Alex Chen").first()).toBeVisible();
|
||||
const hiredAgent = await page.evaluate(async () => {
|
||||
const api = (window as unknown as { agentCompany: { loadSnapshot: () => Promise<unknown> } }).agentCompany;
|
||||
const result = await api.loadSnapshot() as {
|
||||
ok: true;
|
||||
data: {
|
||||
agents: Array<{ name: string; modelProviderId: string | null; modelId: string | null; workspaceId: string | null; heartbeatEnabled: boolean; heartbeatIntervalSec: number }>;
|
||||
workspaces: Array<{ id: string; isPrimary: boolean }>;
|
||||
};
|
||||
};
|
||||
const agent = result.data.agents.find((item) => item.name === "Alex Chen");
|
||||
const primaryWorkspace = result.data.workspaces.find((item) => item.isPrimary) ?? null;
|
||||
return {
|
||||
modelProviderId: agent?.modelProviderId ?? null,
|
||||
modelId: agent?.modelId ?? null,
|
||||
workspaceId: agent?.workspaceId ?? null,
|
||||
heartbeatEnabled: agent?.heartbeatEnabled ?? false,
|
||||
heartbeatIntervalSec: agent?.heartbeatIntervalSec ?? 0,
|
||||
primaryWorkspaceId: primaryWorkspace?.id ?? null,
|
||||
};
|
||||
});
|
||||
expect(hiredAgent.modelProviderId).toBeTruthy();
|
||||
expect(hiredAgent.modelId).toBe("gpt-5.4");
|
||||
expect(hiredAgent.workspaceId).toBe(hiredAgent.primaryWorkspaceId);
|
||||
expect(hiredAgent.heartbeatEnabled).toBe(true);
|
||||
expect(hiredAgent.heartbeatIntervalSec).toBe(120);
|
||||
await page.getByRole("button", { name: "Work", exact: true }).click();
|
||||
await page.getByRole("button", { name: "New task" }).click();
|
||||
await page.getByLabel("Title").fill("Production deploy");
|
||||
await page.getByLabel("Description").fill("Deploy a high-risk change.");
|
||||
await page.getByLabel("Risk").selectOption("high");
|
||||
await page.locator(".modal").getByRole("button", { name: "Create" }).click();
|
||||
await expect(page.getByText("Production deploy").first()).toBeVisible();
|
||||
await page.getByRole("button", { name: "Search" }).click();
|
||||
await page.getByRole("textbox", { name: "Search query" }).fill("production");
|
||||
await expect(page.getByText("Production deploy").first()).toBeVisible();
|
||||
await page.getByRole("dialog", { name: "Search" }).getByRole("button").filter({ hasText: "Production deploy" }).first().click();
|
||||
|
||||
await page.getByRole("button", { name: "Approvals" }).click();
|
||||
await expect(page.getByText("Production deploy").first()).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Messages" }).click();
|
||||
await page.getByRole("button", { name: "New space" }).click();
|
||||
await page.getByLabel("Name").fill("release");
|
||||
await page.getByLabel("Topic").fill("Release coordination");
|
||||
await page.locator(".modal").getByRole("button", { name: "Create" }).click();
|
||||
await expect(page.getByText("release").first()).toBeVisible();
|
||||
await page.getByRole("textbox", { name: "Message" }).fill("/escalate @Board review release plan");
|
||||
await page.getByRole("button", { name: "Send" }).click();
|
||||
await expect(page.getByText("@Board").first()).toBeVisible();
|
||||
await expect(page.getByText("/escalate").first()).toBeVisible();
|
||||
await page.getByRole("button", { name: "Open thread" }).first().click();
|
||||
await expect(page.getByRole("textbox", { name: "Thread reply" })).toBeVisible();
|
||||
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.
|
||||
await page.getByRole("button", { name: "Connectors", exact: true }).click();
|
||||
await expect(page.getByRole("heading", { name: "Connectors" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Refresh" })).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Settings" }).click();
|
||||
await expect(page.getByText("AI CEO", { exact: true })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Providers" }).click();
|
||||
await page.getByLabel("OpenAI background execution").check();
|
||||
await page.getByRole("button", { name: "Save OpenAI policy" }).click();
|
||||
|
||||
// The operator request console lives under Advanced.
|
||||
await page.getByRole("button", { name: "Advanced" }).click();
|
||||
await expect(page.getByText("Requests", { exact: true }).first()).toBeVisible();
|
||||
await expect(page.getByLabel("Request provider")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Background" })).toBeVisible();
|
||||
await page.getByLabel("Request prompt").fill("Draft a provider control memo.");
|
||||
await page.getByLabel("Request instructions").fill("Use direct language.");
|
||||
await expect(page.getByLabel("Composio API key")).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Providers" }).click();
|
||||
await page.getByLabel("OpenAI data policy").selectOption("zero_data_retention");
|
||||
await expect(page.getByLabel("OpenAI background execution")).not.toBeChecked();
|
||||
await page.getByLabel("OpenAI monthly request limit").fill("25");
|
||||
await page.getByRole("button", { name: "Save OpenAI policy" }).click();
|
||||
await expect(page.getByText("0 / 25").first()).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Skills" }).click();
|
||||
await page.getByRole("button", { name: "Import" }).click();
|
||||
await page.getByLabel("Directory path").fill(createSkillDirectory());
|
||||
await page.locator(".modal").getByRole("button", { name: "Import" }).click();
|
||||
await expect(page.getByText("Release Gate").first()).toBeVisible();
|
||||
await expect(page.getByText("Never run").first()).toBeVisible();
|
||||
await page.getByRole("button", { name: "Run tests" }).click();
|
||||
await expect(page.getByText("1/1 passed").first()).toBeVisible();
|
||||
const legacyPath = createLegacyDatabase();
|
||||
await page.getByRole("button", { name: "Advanced" }).click();
|
||||
await page.getByRole("button", { name: "Report" }).click();
|
||||
await page.getByLabel("Source database path").fill(legacyPath);
|
||||
await page.locator(".modal").getByRole("button", { name: "Create" }).click();
|
||||
await expect(page.getByText("organizations: 1").first()).toBeVisible();
|
||||
await page.getByRole("button", { name: "Snapshot" }).click();
|
||||
await expect(page.getByText("Rollback snapshots", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText(/\d+ workspaces/).first()).toBeVisible();
|
||||
await page.getByRole("button", { name: "Set", exact: true }).click();
|
||||
await expect(page.getByRole("button", { name: "active", exact: true })).toBeDisabled();
|
||||
|
||||
// Virtual office page
|
||||
await page.getByRole("button", { name: "Office" }).click();
|
||||
await expect(page.locator("canvas")).toBeVisible();
|
||||
|
||||
// @mention with canonical handle in chat
|
||||
await page.getByRole("button", { name: "Messages" }).click();
|
||||
await page.getByText("general").first().click();
|
||||
await page.getByRole("textbox", { name: "Message" }).fill("@alexchen what is the status?");
|
||||
await page.getByRole("button", { name: "Send" }).click();
|
||||
await expect(page.getByText("Alex Chen").first()).toBeVisible();
|
||||
|
||||
await verifyPagesDoNotOverflow(page);
|
||||
} finally {
|
||||
await app.close();
|
||||
}
|
||||
});
|
||||
|
||||
function createLegacyDatabase(): string {
|
||||
const legacyPath = join(tmpdir(), `agentcompany-legacy-${randomUUID()}.db`);
|
||||
const legacy = new DatabaseSync(legacyPath);
|
||||
legacy.exec("create table organizations(id text primary key); insert into organizations(id) values ('legacy-org');");
|
||||
legacy.close();
|
||||
return legacyPath;
|
||||
}
|
||||
|
||||
function createSkillDirectory(): string {
|
||||
const root = join(tmpdir(), `agentcompany-skill-${randomUUID()}`);
|
||||
mkdirSync(join(root, "checks"), { recursive: true });
|
||||
writeFileSync(join(root, "SKILL.md"), "Review release evidence and produce a concise gate decision.\n", "utf8");
|
||||
writeFileSync(join(root, "checks", "acceptance.txt"), "release evidence accepted\n", "utf8");
|
||||
writeFileSync(join(root, "skill.manifest.json"), JSON.stringify({
|
||||
name: "Release Gate",
|
||||
version: "1.0.0",
|
||||
entrypoint: "SKILL.md",
|
||||
capabilities: ["release-review"],
|
||||
required_tools: ["git"],
|
||||
compatible_models: ["gpt-5.4"],
|
||||
risk_level: "low",
|
||||
tests: ["checks/acceptance.txt"],
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
}, null, 2), "utf8");
|
||||
return root;
|
||||
}
|
||||
|
||||
async function verifyPagesDoNotOverflow(page: Page): Promise<void> {
|
||||
for (const size of [{ width: 1280, height: 860 }, { width: 980, height: 700 }]) {
|
||||
await page.setViewportSize(size);
|
||||
for (const nav of ["Home", "Team", "Work", "Messages", "Files", "Approvals", "Connectors", "Office", "Settings"]) {
|
||||
await page.getByRole("button", { name: nav, exact: true }).click();
|
||||
await expectPageToFit(page);
|
||||
}
|
||||
for (const tab of ["General", "Providers", "Skills", "Advanced"]) {
|
||||
await page.getByRole("button", { name: "Settings" }).click();
|
||||
await page.getByRole("button", { name: tab, exact: true }).click();
|
||||
await expectPageToFit(page);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function expectPageToFit(page: Page): Promise<void> {
|
||||
const dimensions = await page.evaluate(() => ({
|
||||
documentScrollWidth: document.documentElement.scrollWidth,
|
||||
documentClientWidth: document.documentElement.clientWidth,
|
||||
bodyScrollWidth: document.body.scrollWidth,
|
||||
bodyClientWidth: document.body.clientWidth,
|
||||
}));
|
||||
expect(dimensions.documentScrollWidth).toBeLessThanOrEqual(dimensions.documentClientWidth + 1);
|
||||
expect(dimensions.bodyScrollWidth).toBeLessThanOrEqual(dimensions.bodyClientWidth + 1);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { createElectronLaunchEnv } from "./launch";
|
||||
|
||||
/**
|
||||
* Playwright globalSetup: build the Electron app from source before E2E tests.
|
||||
* Uses execFileSync and enables shell mode on Windows so the pnpm shim resolves correctly.
|
||||
*/
|
||||
export default function globalSetup() {
|
||||
execFileSync("pnpm", ["electron-vite", "build"], {
|
||||
stdio: "inherit",
|
||||
cwd: process.cwd(),
|
||||
timeout: 120_000,
|
||||
env: createElectronLaunchEnv({}),
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
|
||||
const EXPERIMENTAL_WARNING_FLAG = "--disable-warning=ExperimentalWarning";
|
||||
|
||||
export function getElectronExecutablePath() {
|
||||
return require("electron");
|
||||
}
|
||||
|
||||
export function getMainEntryPath() {
|
||||
return resolve(repoRoot, "out/main/index.js");
|
||||
}
|
||||
|
||||
export function createElectronLaunchEnv(
|
||||
overrides: Record<string, string>,
|
||||
) : Record<string, string> {
|
||||
const env: Record<string, string> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
if (typeof value === "string") {
|
||||
env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(env, overrides);
|
||||
|
||||
if (env.FORCE_COLOR && env.NO_COLOR) {
|
||||
delete env.NO_COLOR;
|
||||
}
|
||||
|
||||
const existingNodeOptions = env.NODE_OPTIONS?.trim();
|
||||
if (!existingNodeOptions) {
|
||||
env.NODE_OPTIONS = EXPERIMENTAL_WARNING_FLAG;
|
||||
return env;
|
||||
}
|
||||
|
||||
if (!existingNodeOptions.includes(EXPERIMENTAL_WARNING_FLAG)) {
|
||||
env.NODE_OPTIONS = `${existingNodeOptions} ${EXPERIMENTAL_WARNING_FLAG}`;
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { _electron as electron, expect, test, type Page } from "@playwright/test";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { buildFloorPlan } from "../../src/renderer/src/pages/office/floor-plan";
|
||||
import { cameraFor } from "../../src/renderer/src/pages/office/simulation";
|
||||
|
||||
test("the office floor opens agent details on click", async () => {
|
||||
const profileDir = join(tmpdir(), `agentcompany-new-office-${randomUUID()}`);
|
||||
mkdirSync(profileDir, { recursive: true });
|
||||
const app = await electron.launch({
|
||||
args: [resolve("out/main/index.js")],
|
||||
env: { ...process.env, AGENTCOMPANY_NEW_PROFILE_DIR: profileDir, AGENTCOMPANY_NEW_AUTORUN: "0" },
|
||||
});
|
||||
|
||||
try {
|
||||
const page = await app.firstWindow();
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await page.getByRole("button", { name: "Get started" }).click();
|
||||
await page.getByLabel("API key").fill("office-e2e-key");
|
||||
await page.getByLabel("Model").fill("gpt-5.4");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await page.getByLabel("Company name").fill("Pixel Works");
|
||||
await page.getByLabel("Your name").fill("Morgan Reyes");
|
||||
await page.getByLabel("Description").fill("Office e2e coverage.");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await page.getByLabel("Goal").fill("Verify the office floor.");
|
||||
await page.getByLabel("Acceptance criteria").fill("Clicking an agent opens their card.");
|
||||
await page.getByRole("button", { name: "Launch" }).click();
|
||||
await expect(page.getByRole("button", { name: /Pixel Works/ })).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Office", exact: true }).click();
|
||||
const canvas = page.locator("canvas");
|
||||
await expect(canvas).toBeVisible();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Onboarding seeds exactly one agent (the CEO), so the floor plan and
|
||||
// camera are fully determined by the same pure modules the app uses.
|
||||
const box = await canvas.boundingBox();
|
||||
expect(box).not.toBeNull();
|
||||
const plan = buildFloorPlan(0);
|
||||
const camera = cameraFor(plan.world, box!.width, box!.height, plan.entrance);
|
||||
const clickX = box!.x + plan.ceoSeat.x - camera.x;
|
||||
const clickY = box!.y + plan.ceoSeat.y - 20 - camera.y;
|
||||
|
||||
await page.mouse.click(clickX, clickY);
|
||||
const card = page.getByRole("dialog", { name: "Agent CEO" });
|
||||
await expect(card).toBeVisible();
|
||||
await expect(card.getByText("CEO", { exact: true })).toBeVisible();
|
||||
await expect(card.getByText("Chief Executive Officer")).toBeVisible();
|
||||
await expect(card.getByText("Executive", { exact: true })).toBeVisible();
|
||||
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(card).not.toBeVisible();
|
||||
|
||||
// Clicking empty corridor floor selects nothing.
|
||||
await page.mouse.click(box!.x + 500 - camera.x, box!.y + 320 - camera.y);
|
||||
await expect(page.getByRole("dialog", { name: /Agent / })).not.toBeVisible();
|
||||
|
||||
// WASD input is accepted without disturbing the page.
|
||||
for (const key of ["KeyD", "KeyS", "KeyA", "KeyW"]) {
|
||||
await page.keyboard.down(key);
|
||||
await page.waitForTimeout(120);
|
||||
await page.keyboard.up(key);
|
||||
}
|
||||
await expect(canvas).toBeVisible();
|
||||
|
||||
// Hired agents take open-floor desks in hire order and are clickable too.
|
||||
await hireAgent(page, "Ava Patel", "Designer", "Design");
|
||||
await hireAgent(page, "Ken Ito", "Engineer", "Engineering");
|
||||
await page.getByRole("button", { name: "Office", exact: true }).click();
|
||||
await expect(canvas).toBeVisible();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
const populated = buildFloorPlan(2);
|
||||
const populatedBox = await canvas.boundingBox();
|
||||
const populatedCamera = cameraFor(populated.world, populatedBox!.width, populatedBox!.height, populated.entrance);
|
||||
const firstDesk = populated.desks[0].seat;
|
||||
await page.mouse.click(
|
||||
populatedBox!.x + firstDesk.x - populatedCamera.x,
|
||||
populatedBox!.y + firstDesk.y - 20 - populatedCamera.y,
|
||||
);
|
||||
const avaCard = page.getByRole("dialog", { name: "Agent Ava Patel" });
|
||||
await expect(avaCard).toBeVisible();
|
||||
await expect(avaCard.getByText("Design", { exact: true })).toBeVisible();
|
||||
|
||||
const outDir = resolve("test-results/screenshots");
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
await page.screenshot({ path: join(outDir, "19-office-populated.png"), fullPage: false });
|
||||
} finally {
|
||||
await app.close();
|
||||
}
|
||||
});
|
||||
|
||||
async function hireAgent(page: Page, name: string, role: string, department: string): Promise<void> {
|
||||
await page.getByRole("button", { name: "Team", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Hire", exact: true }).click();
|
||||
await page.getByLabel("Name").fill(name);
|
||||
await page.getByLabel("Role").fill(role);
|
||||
await page.getByLabel("Department").fill(department);
|
||||
await page.getByLabel("Monthly budget").fill("100");
|
||||
await page.locator(".modal").getByRole("button", { name: "Hire" }).click();
|
||||
await expect(page.getByText(name).first()).toBeVisible();
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import { join } from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import type { ConnectorId } from "../../src/shared/types";
|
||||
|
||||
export const E2E_READY_CONNECTOR_ID = "anthropic" as ConnectorId;
|
||||
export const E2E_READY_CONNECTOR_MODEL_ID = "claude-sonnet-4-5";
|
||||
export const E2E_PROVIDER_API_KEY = "sk-ant-e2e-key-do-not-use-000000000000";
|
||||
|
||||
export interface OnboardingProfileState {
|
||||
companyCount: number;
|
||||
currentCompanyName: string | null;
|
||||
leadAgentConnectorId: string | null;
|
||||
leadAgentModel: string | null;
|
||||
taskCount: number;
|
||||
runCount: number;
|
||||
disabledModelCount: number;
|
||||
}
|
||||
|
||||
function first<T>(db: DatabaseSync, sql: string, ...params: Array<string | number | null>): T | null {
|
||||
return (db.prepare(sql).get(...params) as T | undefined) ?? null;
|
||||
}
|
||||
|
||||
export function readOnboardingProfileState(profileDir: string): OnboardingProfileState {
|
||||
const db = new DatabaseSync(join(profileDir, "profile.sqlite"));
|
||||
try {
|
||||
const companyCount =
|
||||
first<{ count: number }>(db, "select count(*) as count from companies")?.count ?? 0;
|
||||
const currentCompanyId =
|
||||
first<{ value: string }>(db, "select value from settings where key = 'current_company_id'")?.value ?? null;
|
||||
const currentCompany = currentCompanyId
|
||||
? first<{ id: string; name: string }>(db, "select id, name from companies where id = ?", currentCompanyId)
|
||||
: first<{ id: string; name: string }>(db, "select id, name from companies order by created_at asc limit 1");
|
||||
const companyId = currentCompany?.id ?? null;
|
||||
const leadAgent = companyId
|
||||
? first<{ connector_id: string; model: string | null }>(
|
||||
db,
|
||||
"select connector_id, model from agents where company_id = ? and reports_to is null limit 1",
|
||||
companyId,
|
||||
)
|
||||
: null;
|
||||
const disabledModelsJson = companyId
|
||||
? first<{ disabled_models_json: string }>(
|
||||
db,
|
||||
"select disabled_models_json from connector_model_pool_settings where company_id = ? and connector_id = ?",
|
||||
companyId,
|
||||
E2E_READY_CONNECTOR_ID,
|
||||
)?.disabled_models_json ?? "[]"
|
||||
: "[]";
|
||||
const disabledModels = JSON.parse(disabledModelsJson) as unknown[];
|
||||
return {
|
||||
companyCount,
|
||||
currentCompanyName: currentCompany?.name ?? null,
|
||||
leadAgentConnectorId: leadAgent?.connector_id ?? null,
|
||||
leadAgentModel: leadAgent?.model ?? null,
|
||||
taskCount: companyId
|
||||
? first<{ count: number }>(db, "select count(*) as count from tasks where company_id = ?", companyId)?.count ?? 0
|
||||
: 0,
|
||||
runCount: companyId
|
||||
? first<{ count: number }>(db, "select count(*) as count from runs where company_id = ?", companyId)?.count ?? 0
|
||||
: 0,
|
||||
disabledModelCount: disabledModels.length,
|
||||
};
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,9 @@
|
||||
import { join } from "node:path";
|
||||
import { defineConfig } from "@playwright/test";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: ".",
|
||||
testMatch: ["desktop.smoke.spec.ts"],
|
||||
timeout: 120_000,
|
||||
fullyParallel: false,
|
||||
timeout: 60_000,
|
||||
retries: 0,
|
||||
workers: 1,
|
||||
reporter: "list",
|
||||
outputDir: join(process.cwd(), "build", "test-results"),
|
||||
use: {
|
||||
trace: "on-first-retry",
|
||||
screenshot: "only-on-failure",
|
||||
},
|
||||
// Build from current source before running e2e tests.
|
||||
// Uses globalSetup (not webServer) because electron-vite build is a
|
||||
// one-shot build command, not a long-running server process.
|
||||
globalSetup: "./e2e-global-setup.ts",
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { _electron as electron, expect, test, type Page } from "@playwright/test";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
test("captures every page for visual review", async () => {
|
||||
const profileDir = join(tmpdir(), `agentcompany-new-shots-${randomUUID()}`);
|
||||
mkdirSync(profileDir, { recursive: true });
|
||||
const app = await electron.launch({
|
||||
args: [resolve("out/main/index.js")],
|
||||
env: { ...process.env, AGENTCOMPANY_NEW_PROFILE_DIR: profileDir, AGENTCOMPANY_NEW_AUTORUN: "0" },
|
||||
});
|
||||
const outDir = resolve("test-results/screenshots");
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
try {
|
||||
const page = await app.firstWindow();
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await page.screenshot({ path: join(outDir, "00-onboarding-welcome.png"), fullPage: false });
|
||||
|
||||
await page.getByRole("button", { name: "Get started" }).click();
|
||||
await page.screenshot({ path: join(outDir, "01-onboarding-provider.png"), fullPage: false });
|
||||
await page.getByLabel("API key").fill("local-dev-key");
|
||||
await page.getByLabel("Model").fill("gpt-5.4");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
await page.getByLabel("Company name").fill("OrbitOps");
|
||||
await page.getByLabel("Your name").fill("Yu Chen");
|
||||
await page.getByLabel("Description").fill("Multi-agent operations platform.");
|
||||
await page.screenshot({ path: join(outDir, "02-onboarding-company.png"), fullPage: false });
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
await page.screenshot({ path: join(outDir, "03-onboarding-mode.png"), fullPage: false });
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
await page.getByLabel("Goal").fill("Ship the v1 launch by July.");
|
||||
await page.getByLabel("Acceptance criteria").fill("Pricing model + landing page + 3 pilot signups.");
|
||||
await page.screenshot({ path: join(outDir, "04-onboarding-goal.png"), fullPage: false });
|
||||
await page.getByRole("button", { name: "Launch" }).click();
|
||||
|
||||
await expect(page.getByRole("button", { name: /OrbitOps/ })).toBeVisible();
|
||||
await snap(page, outDir, "10-home");
|
||||
|
||||
await go(page, "Team", outDir, "11-team");
|
||||
await go(page, "Work", outDir, "12-work");
|
||||
await go(page, "Messages", outDir, "13-messages");
|
||||
await go(page, "Files", outDir, "14-files");
|
||||
await go(page, "Approvals", outDir, "15-approvals");
|
||||
await go(page, "Connectors", outDir, "16-connectors");
|
||||
await go(page, "Settings", outDir, "17-settings");
|
||||
await go(page, "Office", outDir, "18-office");
|
||||
|
||||
for (const tab of ["General", "Providers", "Skills", "Advanced"]) {
|
||||
await page.getByRole("button", { name: "Settings" }).click();
|
||||
await page.getByRole("button", { name: tab }).first().click();
|
||||
await snap(page, outDir, `settings-${tab.toLowerCase()}`);
|
||||
}
|
||||
|
||||
await page.setViewportSize({ width: 980, height: 700 });
|
||||
await go(page, "Home", outDir, "20-narrow-home");
|
||||
await go(page, "Messages", outDir, "21-narrow-messages");
|
||||
await go(page, "Files", outDir, "22-narrow-files");
|
||||
} finally {
|
||||
await app.close();
|
||||
}
|
||||
});
|
||||
|
||||
async function go(page: Page, nav: string, dir: string, name: string): Promise<void> {
|
||||
await page.getByRole("button", { name: nav, exact: true }).click();
|
||||
await snap(page, dir, name);
|
||||
}
|
||||
|
||||
async function snap(page: Page, dir: string, name: string): Promise<void> {
|
||||
await page.waitForTimeout(120);
|
||||
await page.screenshot({ path: join(dir, `${name}.png`), fullPage: false });
|
||||
}
|
||||
Reference in new issue
Block a user