Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,55 @@ jobs:
- name: Build all packages
run: pnpm build

workspace-quality-surfaces:
name: Workspace Quality Surfaces
if: github.event_name == 'pull_request' || github.repository == 'raphaeltm/simple-agent-manager'
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile --ignore-scripts

- name: Require test coverage scripts for every tested workspace
run: pnpm quality:workspace-test-surfaces

- name: Build Storybook production bundle
run: pnpm --filter @simple-agent-manager/ui build-storybook

- name: Build public docs and check internal links
run: pnpm --filter @simple-agent-manager/www build && pnpm --filter @simple-agent-manager/www check:links

- name: Install Chromium
working-directory: packages/ui
run: ./node_modules/.bin/playwright install --with-deps chromium

- name: Audit Storybook in desktop and mobile browsers
run: pnpm --filter @simple-agent-manager/ui test:storybook

- name: Test public site in desktop and mobile browsers
run: pnpm --filter @simple-agent-manager/www test:browser

- name: Require complete Storybook and public browser evidence
run: pnpm quality:browser-evidence

- name: Upload workspace browser evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: workspace-browser-evidence
path: .codex/tmp/playwright-screenshots/
if-no-files-found: error
retention-days: 7

playwright-visual:
name: Playwright Visual Tests
needs: [changes]
Expand Down Expand Up @@ -369,6 +418,8 @@ jobs:

- name: Dependency governance pinning check
run: pnpm quality:dependency-governance
- name: Workspace test surface completeness
run: pnpm quality:workspace-test-surfaces
- name: CI workflow wiring tests
run: pnpm exec vitest run --config scripts/quality/vitest.config.ts scripts/quality/ci-worker-suite.test.ts

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ build/
# Test coverage
coverage/
packages/cli/coverage.out
**/test-results/
**/storybook-static/

# Generated Go binaries
packages/cli/bin/
Expand Down
2 changes: 2 additions & 0 deletions apps/tail-worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
"deploy:staging": "wrangler deploy --env staging",
"deploy:production": "wrangler deploy --env production",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@cloudflare/workers-types": "catalog:",
"@vitest/coverage-v8": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:",
"wrangler": "catalog:"
Expand Down
10 changes: 7 additions & 3 deletions apps/www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
"build:blog-mermaid": "pnpm exec esbuild src/scripts/blog-mermaid.ts --bundle --format=esm --platform=browser --target=es2022 --outfile=public/scripts/blog-mermaid.js",
"build:docs-mermaid": "pnpm exec esbuild src/scripts/docs-mermaid.ts --bundle --format=esm --platform=browser --target=es2022 --outfile=public/scripts/docs-mermaid.js",
"build:tracker": "npx tsx scripts/build-tracker.ts",
"test": "vitest run --exclude tests/playwright/**",
"test": "pnpm build:tracker && vitest run --exclude 'tests/playwright/**'",
"test:coverage": "pnpm build:tracker && vitest run --coverage --exclude 'tests/playwright/**'",
"test:browser": "playwright test",
"build": "pnpm build:assets && astro build",
"preview": "astro preview",
"check:links": "python3 scripts/check-doc-links.py"
Expand All @@ -21,8 +23,10 @@
"mermaid": "11.14.0"
},
"devDependencies": {
"@axe-core/playwright": "4.12.1",
"@playwright/test": "1.62.1",
"@vitest/coverage-v8": "catalog:",
"esbuild": "0.28.1",
"typescript": "catalog:",
"@playwright/test": "1.62.1"
"typescript": "catalog:"
}
}
1 change: 1 addition & 0 deletions apps/www/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default defineConfig({
? undefined
: {
command: 'pnpm build && pnpm preview --host 127.0.0.1 --port 4321',
env: { PUBLIC_BASE_DOMAIN: 'localhost' },
url: 'http://127.0.0.1:4321/self-host/',
reuseExistingServer: false,
timeout: 120_000,
Expand Down
26 changes: 26 additions & 0 deletions apps/www/tests/playwright/fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { expect, test as base } from '@playwright/test';

const PRODUCTION_ANALYTICS_ORIGIN = 'https://api.simple-agent-manager.org';

export const test = base.extend<{ productionAnalyticsIsolation: void }>({
productionAnalyticsIsolation: [
async ({ page }, use) => {
const productionAnalyticsRequests: string[] = [];
await page.route(`${PRODUCTION_ANALYTICS_ORIGIN}/api/t*`, async (route) => {
productionAnalyticsRequests.push(route.request().url());
await route.abort('blockedbyclient');
});

await use();

expect(
productionAnalyticsRequests,
'browser quality tests must not write synthetic analytics to production'
).toEqual([]);
},
{ auto: true },
],
});

export { expect };
export type { Page } from '@playwright/test';
32 changes: 32 additions & 0 deletions apps/www/tests/playwright/public-surface-a11y.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import AxeBuilder from '@axe-core/playwright';

import { expect, test } from './fixtures';

test('self-host surface has no overflow or serious axe violations', async ({ page }, testInfo) => {
await page.goto('/self-host/');
await expect(page.getByRole('heading', { name: 'Deploy your own SAM instance' })).toBeVisible();
if (!process.env.PLAYWRIGHT_BASE_URL) {
await expect(page.locator('script[data-api]')).toHaveAttribute(
'data-api',
'https://api.localhost/api/t'
);
}

const hasHorizontalOverflow = await page.evaluate(
() => document.documentElement.scrollWidth > document.documentElement.clientWidth
);
expect(hasHorizontalOverflow).toBe(false);

const axeResults = await new AxeBuilder({ page }).analyze();
const seriousViolations = axeResults.violations.filter(
(violation) => violation.impact === 'critical' || violation.impact === 'serious'
);
expect(seriousViolations, JSON.stringify(seriousViolations, null, 2)).toEqual([]);

const project = testInfo.project.name.toLowerCase().replace(/\W+/g, '-');
await page.screenshot({
path: `../../.codex/tmp/playwright-screenshots/www-self-host-${project}.png`,
fullPage: true,
animations: 'disabled',
});
});
2 changes: 1 addition & 1 deletion apps/www/tests/playwright/self-host-wizard-secrets.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, test, type Page } from '@playwright/test';
import { expect, test, type Page } from './fixtures';

const STORAGE_KEY = 'sam-self-host-wizard-v1';
const LEGACY_WEBHOOK_CANARY = 'legacy-webhook-secret-canary-0123456789abcdef';
Expand Down
67 changes: 67 additions & 0 deletions apps/www/tests/tracker-source.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/** @vitest-environment jsdom */
import { beforeEach, describe, expect, it, vi } from 'vitest';

async function readBeaconEvents(sendBeacon: ReturnType<typeof vi.fn>) {
const blob = sendBeacon.mock.calls.at(-1)?.[1] as Blob;
return JSON.parse(await blob.text()).events as Array<Record<string, unknown>>;
}

async function installTracker(url: string, referrer = '') {
window.history.pushState({}, '', url);
Object.defineProperty(document, 'referrer', { value: referrer, configurable: true });

const script = document.createElement('script');
script.setAttribute('data-api', 'https://api.example.com/api/t');
Object.defineProperty(document, 'currentScript', { value: script, configurable: true });

const sendBeacon = vi.fn().mockReturnValue(true);
Object.defineProperty(navigator, 'sendBeacon', { value: sendBeacon, configurable: true });
vi.spyOn(crypto, 'randomUUID').mockReturnValue('00000000-0000-4000-8000-000000000001');

vi.resetModules();
await import('../src/scripts/tracker');
return { sendBeacon };
}

describe('public website tracker source coverage', () => {
beforeEach(() => {
localStorage.clear();
sessionStorage.clear();
vi.restoreAllMocks();
});

it('covers redaction and navigation behavior before browser-asset compilation', async () => {
const { sendBeacon } = await installTracker(
'/projects/01KZ941C7W5JRFDA9RDZASV8EE/repos/raphaeltm/simple-agent-manager/blob/apps/www/src/scripts/tracker.ts?utm_source=newsletter&utm_medium=email&utm_campaign=launch&token=secret#frag',
'https://user:pass@example.com/oauth/callback/user@example.com?code=secret#frag'
);

expect(sendBeacon).toHaveBeenCalledTimes(1);
let [event] = await readBeaconEvents(sendBeacon);
expect(event).toMatchObject({
event: 'page_view',
page: '/projects/[redacted]/repos/[redacted]/simple-agent-manager/blob/apps/www/src/scripts/[redacted]',
referrer: 'https://example.com/oauth/[redacted]/[redacted]',
host: 'localhost',
utmSource: 'newsletter',
utmMedium: 'email',
utmCampaign: 'launch',
});
expect(JSON.stringify(event)).not.toContain('01KZ941C7W5JRFDA9RDZASV8EE');
expect(JSON.stringify(event)).not.toContain('token=secret');
expect(JSON.stringify(event)).not.toContain('#frag');
expect(JSON.stringify(event)).not.toContain('user@example.com');

document.dispatchEvent(new Event('astro:page-load'));
expect(sendBeacon).toHaveBeenCalledTimes(1);

window.history.pushState({}, '', '/invite/user@example.com/accept?code=secret#frag');
document.dispatchEvent(new Event('astro:page-load'));

expect(sendBeacon).toHaveBeenCalledTimes(2);
[event] = await readBeaconEvents(sendBeacon);
expect(event.page).toBe('/invite/[redacted]/accept');
expect(JSON.stringify(event)).not.toContain('user@example.com');
expect(JSON.stringify(event)).not.toContain('code=secret');
});
});
2 changes: 2 additions & 0 deletions infra/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"main": "index.ts",
"scripts": {
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest",
"typecheck": "tsc --noEmit"
},
Expand All @@ -17,6 +18,7 @@
},
"devDependencies": {
"@types/node": "catalog:",
"@vitest/coverage-v8": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
}
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
"quality:do-wall-time": "tsx scripts/quality/check-do-wall-time.ts",
"quality:observability-noise": "tsx scripts/quality/check-observability-noise.ts",
"quality:dependency-governance": "npx vitest run --config scripts/quality/vitest.config.ts scripts/quality/dependency-governance.test.ts",
"quality:workspace-test-surfaces": "tsx scripts/quality/check-workspace-test-surfaces.ts",
"quality:browser-evidence": "tsx scripts/quality/check-browser-evidence.ts",
"test:infra": "pnpm --filter @simple-agent-manager/infra test",
"prepare": "husky",
"quality:migration-ordering": "tsx scripts/quality/check-migration-ordering.ts",
Expand Down
2 changes: 2 additions & 0 deletions packages/cloud-init/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
"build": "tsup src/index.ts --format esm --dts",
"dev": "tsup src/index.ts --format esm --dts --watch",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@vitest/coverage-v8": "catalog:",
"tsup": "8.5.1",
"typescript": "catalog:",
"vitest": "catalog:",
Expand Down
4 changes: 2 additions & 2 deletions packages/terminal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"typecheck": "tsc --noEmit",
"lint": "eslint 'src/**/*.ts' 'src/**/*.tsx' 'tests/**/*.ts'"
"typecheck": "tsc --noEmit && tsc --project tsconfig.test.json --noEmit",
"lint": "eslint 'src/**/*.ts' 'src/**/*.tsx' 'tests/**/*.{ts,tsx}'"
},
"dependencies": {
"@xterm/xterm": "catalog:",
Expand Down
11 changes: 6 additions & 5 deletions packages/terminal/tests/unit/MultiTerminal.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { act, render, screen, fireEvent, waitFor } from '@testing-library/react';
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { MultiTerminal } from '../../src/MultiTerminal';

// Mock xterm.js
Expand Down Expand Up @@ -35,14 +36,14 @@
});

vi.mock('../../src/components/TabBar', () => ({
TabBar: vi.fn(({ sessions, onNewTab, onTabActivate, onTabClose }: any) => (

Check warning on line 39 in packages/terminal/tests/unit/MultiTerminal.test.tsx

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
<div data-testid="tab-bar">
{sessions.map((s: any) => (

Check warning on line 41 in packages/terminal/tests/unit/MultiTerminal.test.tsx

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
<button key={s.id} data-testid={`tab-${s.id}`} onClick={() => onTabActivate(s.id)}>
{s.name}
<button
data-testid={`close-${s.id}`}
onClick={(e: any) => {

Check warning on line 46 in packages/terminal/tests/unit/MultiTerminal.test.tsx

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
e.stopPropagation();
onTabClose(s.id);
}}
Expand Down Expand Up @@ -95,7 +96,7 @@
const msg = JSON.parse(data);
if (msg.type === 'create_session' && this.onmessage) {
setTimeout(() => {
this.onmessage!(

Check warning on line 99 in packages/terminal/tests/unit/MultiTerminal.test.tsx

View workflow job for this annotation

GitHub Actions / Lint

Forbidden non-null assertion
new MessageEvent('message', {
data: JSON.stringify({
type: 'session_created',
Expand All @@ -110,7 +111,7 @@
}, 10);
} else if (msg.type === 'list_sessions' && this.onmessage) {
setTimeout(() => {
this.onmessage!(

Check warning on line 114 in packages/terminal/tests/unit/MultiTerminal.test.tsx

View workflow job for this annotation

GitHub Actions / Lint

Forbidden non-null assertion
new MessageEvent('message', {
data: JSON.stringify({
type: 'session_list',
Expand All @@ -121,7 +122,7 @@
}, 10);
} else if (msg.type === 'reattach_session' && this.onmessage) {
setTimeout(() => {
this.onmessage!(

Check warning on line 125 in packages/terminal/tests/unit/MultiTerminal.test.tsx

View workflow job for this annotation

GitHub Actions / Lint

Forbidden non-null assertion
new MessageEvent('message', {
data: JSON.stringify({
type: 'session_reattached',
Expand Down Expand Up @@ -154,9 +155,9 @@
// Mock ResizeObserver (not available in jsdom)
Object.defineProperty(globalThis, 'ResizeObserver', {
value: class MockResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
observe() {}
unobserve() {}
disconnect() {}
},
writable: true,
configurable: true,
Expand Down
17 changes: 8 additions & 9 deletions packages/terminal/tests/unit/components/TabBar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { TabBar } from '../../../src/components/TabBar';
import type { TerminalSession } from '../../../src/types/multi-terminal';

Expand Down Expand Up @@ -81,8 +82,8 @@
status: 'connected' as const,
createdAt: new Date(),
lastActivityAt: new Date(),
isActive: false,
order: 0,
isActive: false,
order: 0,
workingDirectory: '/workspace',
}));
render(<TabBar {...defaultProps} sessions={maxedSessions} maxTabs={10} />);
Expand Down Expand Up @@ -111,7 +112,7 @@
const terminal2 = screen.getByText('Terminal 2');
const tab2 = terminal2.closest('[role="tab"]');
expect(tab2).toBeDefined();
fireEvent.click(tab2!);

Check warning on line 115 in packages/terminal/tests/unit/components/TabBar.test.tsx

View workflow job for this annotation

GitHub Actions / Lint

Forbidden non-null assertion

expect(defaultProps.onTabActivate).toHaveBeenCalledWith('session-2');
});
Expand Down Expand Up @@ -205,7 +206,7 @@
const tab = screen.getByText('Terminal 1');
fireEvent.doubleClick(tab);

const input = await screen.findByDisplayValue('Terminal 1') as HTMLInputElement;
const input = (await screen.findByDisplayValue('Terminal 1')) as HTMLInputElement;
expect(input.selectionStart).toBe(0);
expect(input.selectionEnd).toBe('Terminal 1'.length);
});
Expand All @@ -225,9 +226,7 @@
workingDirectory: '/workspace',
}));

const { container } = render(
<TabBar {...defaultProps} sessions={manySessions} />
);
const { container } = render(<TabBar {...defaultProps} sessions={manySessions} />);

// Mock scrollWidth > clientWidth
const tabContainer = container.querySelector('.tab-container');
Expand Down Expand Up @@ -386,7 +385,7 @@
rerender(
<TabBar
{...defaultProps}
sessions={[mockSessions[0]!, mockSessions[2]!]}

Check warning on line 388 in packages/terminal/tests/unit/components/TabBar.test.tsx

View workflow job for this annotation

GitHub Actions / Lint

Forbidden non-null assertion

Check warning on line 388 in packages/terminal/tests/unit/components/TabBar.test.tsx

View workflow job for this annotation

GitHub Actions / Lint

Forbidden non-null assertion
activeSessionId="session-1"
/>
);
Expand Down Expand Up @@ -473,4 +472,4 @@
expect(tabs.length).toBe(mockSessions.length);
});
});
});
});
Loading
Loading