open-design/e2e/ui/automations-page.test.ts
Amy 1c2a1c4459
Add launch review regression coverage and stabilize daemon tests (#3207)
* Add launch review E2E regression coverage

* Harden daemon launch review regressions

* Stabilize daemon runtime tests

* fix(tests): restore e2e preflight typing

Generated-By: looper 0.8.1 (runner=fixer, agent=codex)

* fix(tests): make fake plugin runtime ESM-safe

Generated-By: looper 0.8.1 (runner=fixer, agent=codex)

* Stabilize e2e fake agent and regression tests

* fix(tests): repair fake agent cjs runtime

Generated-By: looper 0.8.1 (runner=fixer, agent=codex)

* fix(review): harden plugin authoring checks

Generated-By: looper 0.9.2 (runner=fixer, agent=codex)

* fix(tests): bind plugin authoring run to seeded conversation

Generated-By: looper 0.9.2 (runner=fixer, agent=codex)
2026-05-29 02:39:33 +00:00

1149 lines
36 KiB
TypeScript

import { expect, test } from '@playwright/test';
import type { Page } from '@playwright/test';
const STORAGE_KEY = 'open-design:config';
const OPEN_SETTINGS_LABEL = /Open settings|打开设置|開啟設定/i;
test.describe.configure({ timeout: 30_000 });
function baseConfig(): Record<string, unknown> {
return {
mode: 'daemon',
apiKey: '',
apiProtocol: 'openai',
apiVersion: '',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
apiProviderBaseUrl: 'https://api.openai.com/v1',
agentId: 'codex',
skillId: null,
designSystemId: null,
onboardingCompleted: true,
mediaProviders: {},
agentModels: {},
agentCliEnv: {},
};
}
async function seedAutomationsBase(page: Page) {
await page.addInitScript(({ key, value }) => {
window.localStorage.setItem(key, JSON.stringify(value));
}, { key: STORAGE_KEY, value: baseConfig() });
await page.route('**/api/health', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: '{"ok":true}',
});
});
await page.route('**/api/agents', async (route) => {
await route.fulfill({
json: {
agents: [
{
id: 'codex',
name: 'Codex CLI',
bin: 'codex',
available: true,
version: '0.130.0',
models: [{ id: 'default', label: 'Default' }],
},
],
},
});
});
await page.route('**/api/plugins', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ plugins: [] }),
});
});
await page.route('**/api/mcp/servers', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ servers: [], templates: [] }),
});
});
}
async function waitForLoadingToClear(page: Page) {
await expect(page.getByText('Loading Open Design…')).toHaveCount(0, { timeout: 15_000 });
}
async function gotoEntryHome(page: Page) {
await page.goto('/', { waitUntil: 'domcontentloaded' });
await waitForLoadingToClear(page);
const privacyDialog = page.getByRole('dialog').filter({ hasText: 'Help us improve Open Design' });
if (await privacyDialog.isVisible().catch(() => false)) {
await privacyDialog.getByRole('button', { name: /not now/i }).click();
}
await expect(page.getByRole('button', { name: OPEN_SETTINGS_LABEL })).toBeVisible();
}
async function gotoAutomations(page: Page) {
await gotoEntryHome(page);
await page.getByTestId('entry-nav-tasks').click();
const view = page.getByTestId('tasks-view');
await expect(view.getByRole('heading', { name: 'Automations', exact: true })).toBeVisible();
return view;
}
test.describe('Automations page', () => {
test('renders the page hero, summary metrics, filters, and saved rows', async ({ page }) => {
await seedAutomationsBase(page);
let routines = [
{
id: 'routine-active-1',
name: 'Daily digest',
prompt: 'Summarize GitHub and design activity.',
schedule: { kind: 'daily', time: '09:00', timezone: 'UTC' },
target: { mode: 'create_each_run' },
enabled: true,
nextRunAt: Date.now() + 3600_000,
lastRun: null,
createdAt: Date.now(),
updatedAt: Date.now(),
},
{
id: 'routine-paused-1',
name: 'Weekly release notes',
prompt: 'Draft release notes.',
schedule: { kind: 'weekly', weekday: 1, time: '09:00', timezone: 'UTC' },
target: { mode: 'create_each_run' },
enabled: false,
nextRunAt: null,
lastRun: null,
createdAt: Date.now(),
updatedAt: Date.now(),
},
];
await page.route('**/api/projects', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ projects: [] }),
});
});
await page.route('**/api/routines', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ routines }),
});
});
await page.route('**/api/automation-templates', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ templates: [] }),
});
});
await page.route('**/api/automation-proposals?status=pending-review', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ proposals: [] }),
});
});
await page.route('**/api/automation-source-packets?limit=3', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ packets: [] }),
});
});
const view = await gotoAutomations(page);
await expect(view.getByText('Plan recurring conversations for project work, Orbit digests, and live artifacts.')).toBeVisible();
await expect(view.getByLabel('Automation summary')).toContainText('Active');
await expect(view.getByLabel('Automation summary')).toContainText('Paused');
await expect(view.getByLabel('Automation summary')).toContainText('Templates');
await expect(view.getByLabel('Your automations')).toContainText('Daily digest');
await expect(view.getByLabel('Your automations')).toContainText('Weekly release notes');
const templateFilters = view.getByRole('tablist', { name: 'Template filters' });
const allTab = templateFilters.getByRole('tab', { name: /^All/i });
const skillsTab = templateFilters.getByRole('tab', { name: /Skills/i });
await expect(allTab).toHaveAttribute('aria-selected', 'true');
await skillsTab.click();
await expect(skillsTab).toHaveAttribute('aria-selected', 'true');
await expect(view.getByRole('status')).toContainText('No templates in this category yet.');
});
test('creates an automation from the page and runs it into a project conversation', async ({ page }) => {
await seedAutomationsBase(page);
const projects = [{ id: 'proj-1', name: 'Routine Test Project' }];
let routines: Array<Record<string, unknown>> = [];
await page.route('**/api/projects', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ projects }),
});
});
await page.route('**/api/routines', async (route) => {
const method = route.request().method();
if (method === 'GET') {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ routines }),
});
return;
}
if (method === 'POST') {
const payload = route.request().postDataJSON() as Record<string, unknown>;
const routine = {
id: 'routine-1',
name: payload.name,
prompt: payload.prompt,
schedule: payload.schedule,
target: payload.target,
enabled: true,
nextRunAt: Date.now() + 3600_000,
lastRun: null,
createdAt: Date.now(),
updatedAt: Date.now(),
};
routines = [routine];
await route.fulfill({
status: 201,
contentType: 'application/json',
body: JSON.stringify({ routine }),
});
return;
}
await route.fulfill({ status: 404, body: '{}' });
});
await page.route('**/api/automation-templates', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ templates: [] }),
});
});
await page.route('**/api/automation-proposals?status=pending-review', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ proposals: [] }),
});
});
await page.route('**/api/automation-source-packets?limit=3', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ packets: [] }),
});
});
await page.route('**/api/routines/routine-1/run', async (route) => {
const startedAt = Date.now();
const lastRun = {
runId: 'run-1',
status: 'queued',
trigger: 'manual',
startedAt,
projectId: 'proj-run',
conversationId: 'conv-run',
agentRunId: 'agent-run-1',
};
routines = [{ ...routines[0], lastRun }];
await route.fulfill({
status: 202,
contentType: 'application/json',
body: JSON.stringify({
routine: routines[0],
run: lastRun,
projectId: 'proj-run',
conversationId: 'conv-run',
agentRunId: 'agent-run-1',
}),
});
});
const view = await gotoAutomations(page);
await view.getByRole('button', { name: 'New automation' }).click();
const modal = page.getByTestId('automation-modal');
await modal.getByLabel('Automation title').fill('Weekly digest');
await modal.getByTestId('automation-modal-prompt').fill('Summarize GitHub and design activity.');
await modal.getByRole('button', { name: 'Create' }).click();
await expect(view.getByText('Weekly digest')).toBeVisible();
const row = view.locator('.automation-row', { hasText: 'Weekly digest' }).first();
await row.getByRole('button', { name: 'Run' }).click();
await expect(page).toHaveURL(/\/projects\/proj-run/);
});
test('places a newly created automation at the top of the list and highlights it', async ({ page }) => {
await seedAutomationsBase(page);
const projects = [{ id: 'proj-1', name: 'Routine Test Project' }];
let routines: Array<Record<string, unknown>> = [
{
id: 'routine-existing-1',
name: 'Older digest',
prompt: 'Summarize older activity.',
schedule: { kind: 'daily', time: '09:00', timezone: 'UTC' },
target: { mode: 'create_each_run' },
enabled: true,
nextRunAt: Date.now() + 3600_000,
lastRun: null,
createdAt: Date.now() - 120_000,
updatedAt: Date.now() - 120_000,
},
];
await page.route('**/api/projects', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ projects }),
});
});
await page.route('**/api/routines', async (route) => {
const method = route.request().method();
if (method === 'GET') {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ routines }),
});
return;
}
if (method === 'POST') {
const payload = route.request().postDataJSON() as Record<string, unknown>;
const now = Date.now();
const routine = {
id: 'routine-newest-1',
name: payload.name,
prompt: payload.prompt,
schedule: payload.schedule,
target: payload.target,
enabled: true,
nextRunAt: now + 3600_000,
lastRun: null,
createdAt: now,
updatedAt: now,
};
routines = [...routines, routine];
await route.fulfill({
status: 201,
contentType: 'application/json',
body: JSON.stringify({ routine }),
});
return;
}
await route.fulfill({ status: 404, body: '{}' });
});
await page.route('**/api/automation-templates', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ templates: [] }),
});
});
await page.route('**/api/automation-proposals?status=pending-review', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ proposals: [] }),
});
});
await page.route('**/api/automation-source-packets?limit=3', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ packets: [] }),
});
});
const view = await gotoAutomations(page);
await expect(view.getByText('Older digest')).toBeVisible();
await view.getByRole('button', { name: 'New automation' }).click();
const modal = page.getByTestId('automation-modal');
await modal.getByLabel('Automation title').fill('Newest digest');
await modal.getByTestId('automation-modal-prompt').fill('Summarize the newest activity.');
await modal.getByRole('button', { name: 'Create' }).click();
const rows = view.locator('.automation-row');
await expect(rows.first()).toContainText('Newest digest');
await expect(view.getByTestId('automation-row-routine-newest-1')).toHaveClass(/is-focused/);
});
test('keeps saved automations ordered by newest createdAt first', async ({ page }) => {
await seedAutomationsBase(page);
const now = Date.now();
const routines = [
{
id: 'routine-oldest-1',
name: 'Oldest digest',
prompt: 'Summarize the oldest activity.',
schedule: { kind: 'daily', time: '09:00', timezone: 'UTC' },
target: { mode: 'create_each_run' },
enabled: true,
nextRunAt: now + 3600_000,
lastRun: null,
createdAt: now - 300_000,
updatedAt: now - 300_000,
},
{
id: 'routine-middle-1',
name: 'Middle digest',
prompt: 'Summarize the middle activity.',
schedule: { kind: 'daily', time: '10:00', timezone: 'UTC' },
target: { mode: 'create_each_run' },
enabled: true,
nextRunAt: now + 7200_000,
lastRun: null,
createdAt: now - 120_000,
updatedAt: now - 120_000,
},
{
id: 'routine-newest-1',
name: 'Newest digest',
prompt: 'Summarize the newest activity.',
schedule: { kind: 'daily', time: '11:00', timezone: 'UTC' },
target: { mode: 'create_each_run' },
enabled: true,
nextRunAt: now + 10_800_000,
lastRun: null,
createdAt: now - 10_000,
updatedAt: now - 10_000,
},
];
await page.route('**/api/projects', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ projects: [] }),
});
});
await page.route('**/api/routines', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ routines }),
});
});
await page.route('**/api/automation-templates', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ templates: [] }),
});
});
await page.route('**/api/automation-proposals?status=pending-review', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ proposals: [] }),
});
});
await page.route('**/api/automation-source-packets?limit=3', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ packets: [] }),
});
});
const view = await gotoAutomations(page);
const rowTitles = view.locator('.automation-row .automation-row__title');
await expect(rowTitles).toHaveText([
'Newest digest',
'Middle digest',
'Oldest digest',
]);
});
test('keeps the automation modal open with the typed values when creation fails', async ({ page }) => {
await seedAutomationsBase(page);
await page.route('**/api/projects', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ projects: [] }),
});
});
await page.route('**/api/routines', async (route) => {
const method = route.request().method();
if (method === 'GET') {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ routines: [] }),
});
return;
}
if (method === 'POST') {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'provider unavailable' }),
});
return;
}
await route.fulfill({ status: 404, body: '{}' });
});
await page.route('**/api/automation-templates', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ templates: [] }),
});
});
await page.route('**/api/automation-proposals?status=pending-review', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ proposals: [] }),
});
});
await page.route('**/api/automation-source-packets?limit=3', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ packets: [] }),
});
});
const view = await gotoAutomations(page);
await view.getByRole('button', { name: 'New automation' }).click();
const modal = page.getByTestId('automation-modal');
await modal.getByLabel('Automation title').fill('Weekly digest');
await modal.getByTestId('automation-modal-prompt').fill('Summarize GitHub and design activity.');
await modal.getByRole('button', { name: 'Create' }).click();
await expect(modal.getByLabel('Automation title')).toHaveValue('Weekly digest');
await expect(modal.getByTestId('automation-modal-prompt')).toHaveValue('Summarize GitHub and design activity.');
await expect(modal.getByText('provider unavailable')).toBeVisible();
await expect(view.getByText('No automations yet')).toBeVisible();
});
test('shows a page error and keeps the row usable when Run fails', async ({ page }) => {
await seedAutomationsBase(page);
const routines = [
{
id: 'routine-run-error-1',
name: 'Daily digest',
prompt: 'Summarize GitHub and design activity.',
schedule: { kind: 'daily', time: '09:00', timezone: 'UTC' },
target: { mode: 'create_each_run' },
enabled: true,
nextRunAt: Date.now() + 3600_000,
lastRun: null,
createdAt: Date.now(),
updatedAt: Date.now(),
},
];
await page.route('**/api/projects', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ projects: [] }),
});
});
await page.route('**/api/routines', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ routines }),
});
});
await page.route('**/api/automation-templates', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ templates: [] }),
});
});
await page.route('**/api/automation-proposals?status=pending-review', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ proposals: [] }),
});
});
await page.route('**/api/automation-source-packets?limit=3', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ packets: [] }),
});
});
await page.route('**/api/routines/routine-run-error-1/run', async (route) => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'provider unavailable' }),
});
});
const view = await gotoAutomations(page);
const row = view.locator('.automation-row', { hasText: 'Daily digest' }).first();
await row.getByRole('button', { name: 'Run' }).click();
await expect(view.getByRole('alert')).toContainText('provider unavailable');
await expect(row.getByRole('button', { name: 'Run' })).toBeVisible();
await expect(row.getByRole('button', { name: 'Pause' })).toBeVisible();
});
test('pauses, expands history, and deletes an automation from the saved list', async ({ page }) => {
await seedAutomationsBase(page);
let routines = [
{
id: 'routine-1',
name: 'Daily digest',
prompt: 'Summarize GitHub and design activity.',
schedule: { kind: 'daily', time: '09:00', timezone: 'UTC' },
target: { mode: 'create_each_run' },
enabled: true,
nextRunAt: Date.now() + 3600_000,
lastRun: null,
createdAt: Date.now(),
updatedAt: Date.now(),
},
];
await page.route('**/api/projects', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ projects: [] }),
});
});
await page.route('**/api/routines', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ routines }),
});
});
await page.route('**/api/automation-templates', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ templates: [] }),
});
});
await page.route('**/api/automation-proposals?status=pending-review', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ proposals: [] }),
});
});
await page.route('**/api/automation-source-packets?limit=3', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ packets: [] }),
});
});
await page.route('**/api/routines/routine-1', async (route) => {
const method = route.request().method();
if (method === 'PATCH') {
const payload = route.request().postDataJSON() as { enabled?: boolean };
const routine = routines[0];
if (!routine) throw new Error('missing routine fixture');
const updated = { ...routine, enabled: Boolean(payload.enabled), updatedAt: Date.now() };
routines = [updated];
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ routine: updated }),
});
return;
}
if (method === 'DELETE') {
routines = [];
await route.fulfill({ status: 204, body: '' });
return;
}
await route.fulfill({ status: 404, body: '{}' });
});
await page.route('**/api/routines/routine-1/runs?limit=10', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
runs: [
{
id: 'run-1',
routineId: 'routine-1',
trigger: 'manual',
status: 'succeeded',
projectId: 'proj-run',
conversationId: 'conv-run',
agentRunId: 'agent-run-1',
startedAt: Date.now() - 60_000,
completedAt: Date.now() - 15_000,
summary: 'Updated digest',
error: null,
errorCode: null,
},
],
}),
});
});
const view = await gotoAutomations(page);
const row = view.locator('.automation-row', { hasText: 'Daily digest' }).first();
await row.getByRole('button', { name: 'Pause' }).click();
await expect(row.getByRole('button', { name: 'Resume' })).toBeVisible();
await row.getByRole('button', { name: 'History' }).click();
await expect(page.getByLabel('Automation run history')).toBeVisible();
await row.getByRole('button', { name: 'Hide history' }).click();
await expect(page.getByLabel('Automation run history')).toHaveCount(0);
page.once('dialog', (dialog) => {
void dialog.accept();
});
await row.getByRole('button', { name: 'Delete automation' }).click({ force: true });
await expect(view.getByText('No automations yet')).toBeVisible();
});
test('shows a page error and keeps the row usable when Pause fails', async ({ page }) => {
await seedAutomationsBase(page);
const routines = [
{
id: 'routine-pause-error-1',
name: 'Daily digest',
prompt: 'Summarize GitHub and design activity.',
schedule: { kind: 'daily', time: '09:00', timezone: 'UTC' },
target: { mode: 'create_each_run' },
enabled: true,
nextRunAt: Date.now() + 3600_000,
lastRun: null,
createdAt: Date.now(),
updatedAt: Date.now(),
},
];
await page.route('**/api/projects', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ projects: [] }),
});
});
await page.route('**/api/routines', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ routines }),
});
});
await page.route('**/api/automation-templates', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ templates: [] }),
});
});
await page.route('**/api/automation-proposals?status=pending-review', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ proposals: [] }),
});
});
await page.route('**/api/automation-source-packets?limit=3', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ packets: [] }),
});
});
await page.route('**/api/routines/routine-pause-error-1', async (route) => {
if (route.request().method() === 'PATCH') {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'pause failed upstream' }),
});
return;
}
await route.fulfill({ status: 404, body: '{}' });
});
const view = await gotoAutomations(page);
const row = view.locator('.automation-row', { hasText: 'Daily digest' }).first();
await row.getByRole('button', { name: 'Pause' }).click();
await expect(view.getByRole('alert')).toContainText('pause failed upstream');
await expect(row.getByRole('button', { name: 'Pause' })).toBeVisible();
await expect(row.getByRole('button', { name: 'Run' })).toBeVisible();
});
test('shows a page error and keeps the row visible when Delete fails', async ({ page }) => {
await seedAutomationsBase(page);
const routines = [
{
id: 'routine-delete-error-1',
name: 'Daily digest',
prompt: 'Summarize GitHub and design activity.',
schedule: { kind: 'daily', time: '09:00', timezone: 'UTC' },
target: { mode: 'create_each_run' },
enabled: true,
nextRunAt: Date.now() + 3600_000,
lastRun: null,
createdAt: Date.now(),
updatedAt: Date.now(),
},
];
await page.route('**/api/projects', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ projects: [] }),
});
});
await page.route('**/api/routines', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ routines }),
});
});
await page.route('**/api/automation-templates', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ templates: [] }),
});
});
await page.route('**/api/automation-proposals?status=pending-review', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ proposals: [] }),
});
});
await page.route('**/api/automation-source-packets?limit=3', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ packets: [] }),
});
});
await page.route('**/api/routines/routine-delete-error-1', async (route) => {
if (route.request().method() === 'DELETE') {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'delete failed upstream' }),
});
return;
}
await route.fulfill({ status: 404, body: '{}' });
});
const view = await gotoAutomations(page);
const row = view.locator('.automation-row', { hasText: 'Daily digest' }).first();
page.once('dialog', (dialog) => {
void dialog.accept();
});
await row.getByRole('button', { name: 'Delete automation' }).click({ force: true });
await expect(view.getByRole('alert')).toContainText('delete failed upstream');
await expect(row).toBeVisible();
await expect(row.getByRole('button', { name: 'Delete automation' })).toBeVisible();
});
test('edits an automation title from the saved list and keeps the updated row visible', async ({ page }) => {
await seedAutomationsBase(page);
let routines = [
{
id: 'routine-edit-1',
name: 'Daily digest',
prompt: 'Summarize GitHub and design activity.',
schedule: { kind: 'daily', time: '09:00', timezone: 'UTC' },
target: { mode: 'create_each_run' },
enabled: true,
nextRunAt: Date.now() + 3600_000,
lastRun: null,
createdAt: Date.now(),
updatedAt: Date.now(),
},
];
await page.route('**/api/projects', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ projects: [] }),
});
});
await page.route('**/api/routines', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ routines }),
});
});
await page.route('**/api/automation-templates', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ templates: [] }),
});
});
await page.route('**/api/automation-proposals?status=pending-review', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ proposals: [] }),
});
});
await page.route('**/api/automation-source-packets?limit=3', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ packets: [] }),
});
});
await page.route('**/api/routines/routine-edit-1', async (route) => {
if (route.request().method() === 'PATCH') {
const payload = route.request().postDataJSON() as { name?: string; prompt?: string };
const routine = routines[0];
if (!routine) throw new Error('missing routine fixture');
const updated = {
...routine,
name: payload.name ?? routine.name,
prompt: payload.prompt ?? routine.prompt,
updatedAt: Date.now(),
};
routines = [
updated,
];
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ routine: updated }),
});
return;
}
await route.fulfill({ status: 404, body: '{}' });
});
const view = await gotoAutomations(page);
const row = view.locator('.automation-row', { hasText: 'Daily digest' }).first();
await row.getByRole('button', { name: 'Edit' }).click();
const modal = page.getByTestId('automation-modal');
await expect(modal.getByLabel('Automation title')).toHaveValue('Daily digest');
await modal.getByLabel('Automation title').fill('Daily digest edited');
await modal.getByRole('button', { name: /^Save/i }).click();
await expect(view.getByText('Daily digest edited')).toBeVisible();
});
test('switches template filters and updates the visible template cards', async ({ page }) => {
await seedAutomationsBase(page);
await page.route('**/api/projects', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ projects: [] }),
});
});
await page.route('**/api/routines', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ routines: [] }),
});
});
await page.route('**/api/automation-templates', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ templates: [] }),
});
});
await page.route('**/api/automation-proposals?status=pending-review', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ proposals: [] }),
});
});
await page.route('**/api/automation-source-packets?limit=3', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ packets: [] }),
});
});
const view = await gotoAutomations(page);
const tabs = view.getByRole('tablist', { name: 'Template filters' });
await expect(view.getByText(/Refresh project memory from recent work\./i)).toBeVisible();
await tabs.getByRole('tab', { name: /Orbit/i }).click();
await expect(view.getByRole('status')).toHaveCount(0);
await expect(view.getByText(/Refresh project memory from recent work\./i)).toHaveCount(0);
await tabs.getByRole('tab', { name: /Memory/i }).click();
await expect(view.getByText(/Refresh project memory from recent work\./i)).toBeVisible();
await expect(view.getByRole('status')).toHaveCount(0);
});
test('renders the routine target and last-run status in the row summary', async ({ page }) => {
await seedAutomationsBase(page);
const projects = [{ id: 'proj-shared-1', name: 'Shared Release Project' }];
const routines = [
{
id: 'routine-summary-1',
name: 'Release digest',
prompt: 'Summarize release issues and recent commits.',
schedule: { kind: 'weekly', weekday: 3, time: '14:30', timezone: 'UTC' },
target: { mode: 'reuse', projectId: 'proj-shared-1' },
enabled: true,
nextRunAt: Date.now() + 86_400_000,
lastRun: {
id: 'run-summary-1',
status: 'failed',
trigger: 'manual',
startedAt: Date.now() - 7_200_000,
error: 'Provider request timed out after 30s',
summary: 'Provider request timed out after 30s',
},
createdAt: Date.now() - 300_000,
updatedAt: Date.now() - 60_000,
},
];
await page.route('**/api/projects', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ projects }),
});
});
await page.route('**/api/routines', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ routines }),
});
});
await page.route('**/api/automation-templates', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ templates: [] }),
});
});
await page.route('**/api/automation-proposals?status=pending-review', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ proposals: [] }),
});
});
await page.route('**/api/automation-source-packets?limit=3', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ packets: [] }),
});
});
const view = await gotoAutomations(page);
const row = view.locator('.automation-row', { hasText: 'Release digest' }).first();
await expect(row).toContainText('Shared Release Project');
await expect(row).toContainText('Failed');
});
});