moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,334 @@
import { test, expect, type Page } from '@playwright/test';
const SKIP_PARAMS = 'skipOnboarding=true&skipBoot=true&skipBriefing=true&tier=power';
function routeWithSkip(route: string): string {
const sep = route.includes('?') ? '&' : '?';
return `${route}${sep}${SKIP_PARAMS}`;
}
async function waitForShell(page: Page): Promise<void> {
await page.waitForSelector('.waggle-sidebar, [role="navigation"], main', { timeout: 15_000 });
await page.waitForTimeout(300);
}
async function gotoLauncher(page: Page): Promise<void> {
await page.goto(routeWithSkip('/launcher?watch=1'), { waitUntil: 'domcontentloaded' });
await waitForShell(page);
await expect(page.getByText('Tool Launcher')).toBeVisible({ timeout: 10_000 });
}
async function mockProcesses(page: Page): Promise<void> {
await page.route('**/api/tools/processes', route => route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ processes: [], total: 0 }),
}));
}
const HOOK_RENDER_CASES = [
{ id: 'claude-code', displayName: 'Claude Code', packageName: '@waggle/hive-mind-hooks-claude-code', configDir: '.claude', configFile: 'settings.json' },
{ id: 'codex', displayName: 'Codex CLI', packageName: '@waggle/hive-mind-hooks-codex', configDir: '.codex', configFile: 'hooks.json' },
{ id: 'codex-desktop', displayName: 'Codex Desktop', packageName: '@waggle/hive-mind-hooks-codex-desktop', configDir: '.codex', configFile: 'hooks.json' },
{ id: 'cursor', displayName: 'Cursor', packageName: '@waggle/hive-mind-hooks-cursor', configDir: '.cursor', configFile: 'hooks.json' },
{ id: 'hermes', displayName: 'Hermes Agent', packageName: '@waggle/hive-mind-hooks-hermes', configDir: '.hermes', configFile: 'config.yaml' },
{ id: 'openclaw', displayName: 'OpenClaw', packageName: '@waggle/hive-mind-hooks-openclaw', configDir: '.openclaw', configFile: 'openclaw.json' },
] as const;
test.describe('Launcher rendered states', () => {
test('sidecar-offline detection shows an inline retry action', async ({ page }) => {
let detectCalls = 0;
await mockProcesses(page);
await page.route('**/api/tools/detect', route => {
detectCalls += 1;
if (detectCalls === 1) {
return route.abort('failed');
}
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
platform: 'linux',
detectedAt: '2026-07-08T00:00:00.000Z',
tools: [
{
id: 'foo-cli',
displayName: 'Foo CLI',
launchable: true,
hookCapable: false,
builtin: false,
acceptsInlinePrompt: true,
installed: true,
installedPath: '/usr/local/bin/foo',
version: '1.0.0',
hooksInstalled: false,
hookPointerPath: null,
},
],
}),
});
});
await gotoLauncher(page);
await expect(page.getByText(/sidecar may be offline/i)).toBeVisible();
await page.getByRole('button', { name: /retry tool detection/i }).click();
await expect(page.getByText('Foo CLI')).toBeVisible();
await expect(page.getByText(/sidecar may be offline/i)).not.toBeVisible();
});
test('long hook stderr is summarized instead of flooding the rendered panel', async ({ page }) => {
await mockProcesses(page);
await page.route('**/api/tools/detect', route => route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
platform: 'linux',
detectedAt: '2026-07-08T00:00:00.000Z',
tools: [
{
id: 'codex',
displayName: 'Codex CLI',
launchable: true,
hookCapable: true,
builtin: true,
acceptsInlinePrompt: true,
installed: true,
installedPath: '/usr/local/bin/codex',
version: '1.0.0',
hooksInstalled: false,
hookPointerPath: null,
},
],
}),
}));
await page.route('**/api/tools/hooks', route => route.fulfill({
status: 400,
contentType: 'application/json',
body: JSON.stringify({
ok: false,
action: 'verify',
packageName: '@waggle/hive-mind-hooks-codex',
stdout: '',
stderr: [
'failure detail 1: missing hook pointer',
'failure detail 2: stale backup file',
'failure detail 3: cli not trusted',
'failure detail 4: config mismatch',
'failure detail 5: lifecycle skipped',
'failure detail 6: retry recommended',
'failure detail 7: noisy internal trace',
'failure detail 8: noisy internal trace',
].join('\n'),
code: 1,
error: 'verify failed',
}),
}));
await gotoLauncher(page);
await page.getByRole('button', { name: /^Verify$/ }).click();
await expect(page.getByText(/verify failed/i)).toBeVisible();
await expect(page.getByText('More output')).toBeVisible();
await expect(page.getByText(/2 additional hook output lines hidden/i)).toBeVisible();
await expect(page.getByText(/failure detail 8/i)).not.toBeVisible();
await expect(page.getByText('Recovery')).toBeVisible();
});
test('standard hook install output renders changed file, pointer, backup, and recovery labels', async ({ page }) => {
await mockProcesses(page);
await page.route('**/api/tools/detect', route => route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
platform: 'linux',
detectedAt: '2026-07-08T00:00:00.000Z',
tools: [
{
id: 'codex',
displayName: 'Codex CLI',
launchable: true,
hookCapable: true,
builtin: true,
acceptsInlinePrompt: true,
installed: true,
installedPath: '/usr/local/bin/codex',
version: '1.0.0',
hooksInstalled: false,
hookPointerPath: null,
},
],
}),
}));
await page.route('**/api/tools/hooks', route => route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
ok: true,
action: 'install',
packageName: '@waggle/hive-mind-hooks-codex',
stdout: [
'hive-mind/codex-hooks: install',
' - hooks.json: /home/.codex/hooks.json',
' - install pointer: /home/.codex/hive-mind-install.json',
' - backup: /home/.codex/hooks.json.hive-mind-backup.2026-07-08T19-00-00Z',
'Done. Run "codex" once and approve the hook command if prompted.',
].join('\n'),
stderr: '',
code: 0,
}),
}));
await gotoLauncher(page);
await page.getByRole('button', { name: /install hooks/i }).click();
await expect(page.getByText(/Codex CLI: install OK/i)).toBeVisible();
await expect(page.getByText('Changed file', { exact: true })).toBeVisible();
await expect(page.getByText('/home/.codex/hooks.json', { exact: true })).toBeVisible();
await expect(page.getByText('Install pointer', { exact: true })).toBeVisible();
await expect(page.getByText('/home/.codex/hive-mind-install.json', { exact: true })).toBeVisible();
await expect(page.getByText('Backup', { exact: true })).toBeVisible();
await expect(page.getByText(/hooks\.json\.hive-mind-backup/i)).toBeVisible();
await expect(page.getByText('Recovery', { exact: true })).toBeVisible();
await expect(page.getByText(/hive-mind\/codex-hooks: install/i)).not.toBeVisible();
});
test('all hook-capable tools render install, verify, and uninstall state transitions', async ({ page }) => {
const hookState = new Map(HOOK_RENDER_CASES.map(tool => [tool.id, false]));
await mockProcesses(page);
await page.route('**/api/tools/detect', route => route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
platform: 'linux',
detectedAt: '2026-07-09T00:00:00.000Z',
tools: HOOK_RENDER_CASES.map(tool => ({
id: tool.id,
displayName: tool.displayName,
launchable: true,
hookCapable: true,
builtin: true,
acceptsInlinePrompt: true,
installed: true,
installedPath: `/usr/local/bin/${tool.id}`,
version: '1.0.0',
hooksInstalled: hookState.get(tool.id) === true,
hookPointerPath: hookState.get(tool.id) === true
? `/home/${tool.configDir}/hive-mind-install.json`
: null,
})),
}),
}));
await page.route('**/api/tools/hooks', route => {
const body = route.request().postDataJSON() as { id: string; action: 'install' | 'verify' | 'uninstall' };
const tool = HOOK_RENDER_CASES.find(item => item.id === body.id);
if (!tool) {
return route.fulfill({
status: 400,
contentType: 'application/json',
body: JSON.stringify({ ok: false, action: body.action, error: 'unknown tool' }),
});
}
if (body.action === 'install') hookState.set(tool.id, true);
if (body.action === 'uninstall') hookState.set(tool.id, false);
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
ok: true,
action: body.action,
packageName: tool.packageName,
stdout: body.action === 'verify'
? 'All checks passed.'
: [
`hive-mind/${tool.id}-hooks: ${body.action}`,
` - ${tool.configFile}: /home/${tool.configDir}/${tool.configFile}`,
` - install pointer: /home/${tool.configDir}/hive-mind-install.json`,
' - backup removed: yes',
'Done.',
].join('\n'),
stderr: '',
code: 0,
}),
});
});
await gotoLauncher(page);
for (const tool of HOOK_RENDER_CASES) {
const card = page.getByTestId(`launcher-tool-${tool.id}`);
await expect(card.getByText(tool.displayName, { exact: true })).toBeVisible();
await card.getByRole('button', { name: /install hooks/i }).click();
await expect(page.getByText(`${tool.displayName}: install OK`, { exact: true })).toBeVisible();
await expect(card.getByText('Hooks active', { exact: true })).toBeVisible();
await card.getByRole('button', { name: /^Verify$/ }).click();
await expect(page.getByText(`${tool.displayName}: verify OK`, { exact: true })).toBeVisible();
await card.getByRole('button', { name: /uninstall hooks/i }).click();
await expect(page.getByText(`${tool.displayName}: uninstall OK`, { exact: true })).toBeVisible();
await expect(card.getByText('Hooks active', { exact: true })).not.toBeVisible();
await expect(card.getByRole('button', { name: /install hooks/i })).toBeVisible();
}
});
test('third-party adapter renders launch-only state and sends its prompt', async ({ page }) => {
let launchPayload: unknown = null;
await mockProcesses(page);
await page.route('**/api/tools/detect', route => route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
platform: 'linux',
detectedAt: '2026-07-08T00:00:00.000Z',
tools: [
{
id: 'foo-cli',
displayName: 'Foo CLI',
launchable: true,
hookCapable: false,
builtin: false,
acceptsInlinePrompt: true,
installed: true,
installedPath: '/usr/local/bin/foo',
version: '2.1.0',
hooksInstalled: false,
hookPointerPath: null,
},
],
}),
}));
await page.route('**/api/tools/launch', async route => {
launchPayload = route.request().postDataJSON();
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ ok: true, pid: 9876 }),
});
});
await gotoLauncher(page);
await expect(page.getByText('Foo CLI', { exact: true })).toBeVisible();
await expect(page.getByText(/v2\.1\.0/i)).toBeVisible();
await expect(page.getByText(/Sent to:/i)).not.toBeVisible();
await expect(page.getByText(/Launch only/i)).toBeVisible();
await expect(page.getByText(/Hook management is not supported for this tool yet/i)).toBeVisible();
await expect(page.getByRole('button', { name: /^Launch$/ })).toBeVisible();
await expect(page.getByRole('button', { name: /install hooks/i })).not.toBeVisible();
await expect(page.getByRole('button', { name: /^Verify$/ })).not.toBeVisible();
await page.getByLabel(/optional launch prompt/i).fill('summarize adapter context');
await expect(page.getByText('Sent to: Foo CLI', { exact: true })).toBeVisible();
await page.getByRole('button', { name: /^Launch$/ }).click();
await expect(page.getByText(/Launched Foo CLI with prompt \(pid 9876\)/i)).toBeVisible();
expect(launchPayload).toMatchObject({
id: 'foo-cli',
installedPath: '/usr/local/bin/foo',
prompt: 'summarize adapter context',
observe: true,
});
});
});