diff --git a/.github/workflows/cid.yml b/.github/workflows/cid.yml
index 5b581d6e8..c2b055100 100644
--- a/.github/workflows/cid.yml
+++ b/.github/workflows/cid.yml
@@ -116,6 +116,19 @@ jobs:
fail-fast: false
matrix:
shard: [1/4, 2/4, 3/4, 4/4]
+ services:
+ postgres:
+ image: postgres:16-alpine
+ env:
+ POSTGRES_HOST_AUTH_METHOD: trust
+ POSTGRES_DB: cao_browser_test
+ ports:
+ - 5432/tcp
+ options: >-
+ --health-cmd "pg_isready -U postgres -d cao_browser_test"
+ --health-interval 5s
+ --health-timeout 5s
+ --health-retries 10
defaults:
run:
working-directory: dashboard/site
@@ -151,7 +164,11 @@ jobs:
run: |
sudo rm -f /etc/apt/sources.list.d/google-chrome.list /etc/apt/sources.list.d/google-chrome.sources
npx playwright install --with-deps chromium webkit
+ - name: Install PostgreSQL client
+ run: sudo apt-get install -y postgresql-client
- name: End-to-end tests
+ env:
+ POSTGRES_URL: postgresql://postgres@127.0.0.1:${{ job.services.postgres.ports['5432'] }}/cao_browser_test?sslmode=disable
run: npm run test:e2e -- --shard=${{ matrix.shard }}
- name: Retain browser test reports and failures
if: always()
diff --git a/dashboard/cli-action-api.mjs b/dashboard/cli-action-api.mjs
index 98ba1eb04..ed840dcbd 100644
--- a/dashboard/cli-action-api.mjs
+++ b/dashboard/cli-action-api.mjs
@@ -73,6 +73,7 @@ async function streamCommand(queue, id, after, response) {
});
response.flushHeaders();
let cursor = after;
+ let status;
let wake;
let changed = true;
let detached = false;
@@ -120,6 +121,10 @@ async function streamCommand(queue, id, after, response) {
response.end();
return;
}
+ if (command.status !== status) {
+ status = command.status;
+ await write({ type: "status", command: { id: command.id, status } });
+ }
if (!changed && !detached) {
await new Promise((resolve) => { wake = resolve; });
wake = undefined;
diff --git a/dashboard/cli-action-queue.mjs b/dashboard/cli-action-queue.mjs
index f7605bbe2..5a0c1cbac 100644
--- a/dashboard/cli-action-queue.mjs
+++ b/dashboard/cli-action-queue.mjs
@@ -71,7 +71,7 @@ export async function createCliActionQueue({
};
const request = async (operation, values = {}) => {
const result = await adapter.request({ operation, ...values });
- if (["enqueue", "cancel", "append", "finish", "settle", "retain"].includes(operation)) emit();
+ if (["enqueue", "claim", "cancel", "append", "finish", "settle", "retain"].includes(operation)) emit();
return result;
};
const sanitize = (text) => {
diff --git a/dashboard/site/scripts/build.mjs b/dashboard/site/scripts/build.mjs
index 4af908b9c..ce80f2352 100644
--- a/dashboard/site/scripts/build.mjs
+++ b/dashboard/site/scripts/build.mjs
@@ -368,10 +368,11 @@ async function bundleSiteJavascript(destinationPath) {
.filter((path) => path !== mainOutput)
.map((path) => basename(path));
const preloads = [
- ' ',
' ',
' ',
...startupChunks.map((chunk) => ` `),
+ // Avoid a render-blocking CSS round trip without delaying module discovery.
+ ` `,
];
const indexPath = join(destinationPath, "index.html");
const index = await readFile(indexPath, "utf8");
diff --git a/dashboard/site/src/components/cli-actions.js b/dashboard/site/src/components/cli-actions.js
index 8bca5bb15..f950a8b49 100644
--- a/dashboard/site/src/components/cli-actions.js
+++ b/dashboard/site/src/components/cli-actions.js
@@ -73,9 +73,10 @@ async function executeAction(id, argumentsValue, templateValues, onOutput, input
if (Object.keys(templateValues).length > 0) payload.values = templateValues;
if (input !== undefined) payload.input = input;
const command = await enqueueCliCommand(payload, signal);
- const complete = await attachCliCommand(command.id, (event) => onOutput(
- /** @type {{ stream: 'stdout'|'stderr', data: string }} */ (event)
- ), signal);
+ const complete = await attachCliCommand(command.id, (event) => {
+ if (event.type === 'status') return;
+ onOutput(/** @type {{ stream: 'stdout'|'stderr', data: string }} */ (event));
+ }, signal);
if (complete.command?.status === 'canceled') {
return { ...complete.result, error: 'Cancelled. Completed changes are not undone.' };
}
diff --git a/dashboard/site/src/components/command-terminal.js b/dashboard/site/src/components/command-terminal.js
index d6bb81da9..4d1ff5a89 100644
--- a/dashboard/site/src/components/command-terminal.js
+++ b/dashboard/site/src/components/command-terminal.js
@@ -62,7 +62,11 @@ export function renderCommandTerminal(context) {
hasOutput.set(false);
if (command.command) write(`$ ${command.command}\r\n`);
const complete = await attachCliCommand(currentId, (event) => {
- if (!controller.signal.aborted) write(event.data, event.stream);
+ if (controller.signal.aborted) return;
+ if (event.type === 'status') {
+ status.set(event.command.status);
+ message.set(`${event.command.id} · ${event.command.status}`);
+ } else write(event.data, event.stream);
}, controller.signal);
if (controller.signal.aborted) return;
status.set(complete.command.status);
diff --git a/dashboard/site/src/dashboard-chunks.js b/dashboard/site/src/dashboard-chunks.js
index ec5616e2f..e7e43ee3a 100644
--- a/dashboard/site/src/dashboard-chunks.js
+++ b/dashboard/site/src/dashboard-chunks.js
@@ -412,7 +412,7 @@ export function splitDashboardDocument(source, options = {}) {
});
const status = isPlainObject(document.dashboard['data-status']) ? document.dashboard['data-status'] : {};
const statusSources = [status['freshness-source'], status['health-source'], status['ingestion-source']].filter((name) => typeof name === 'string');
- const prerequisiteSources = document.dashboard.pages.filter(pageCapabilityAvailable).flatMap((page) => {
+ const prerequisiteSources = (document.dashboard.pages ?? []).filter(pageCapabilityAvailable).flatMap((page) => {
const source = pageRequiredSource(page);
return source ? [source] : [];
});
diff --git a/dashboard/site/src/data/cli-queue.js b/dashboard/site/src/data/cli-queue.js
index d977e3d5d..f0c331ef7 100644
--- a/dashboard/site/src/data/cli-queue.js
+++ b/dashboard/site/src/data/cli-queue.js
@@ -95,7 +95,8 @@ export async function cliQueueOperation(payload, signal, notify = () => {}) {
if (event.type === 'output' && Number.isSafeInteger(event.sequence) && event.sequence > sequence) {
sequence = event.sequence;
notify(event);
- } else if (event.type === 'complete') complete = event;
+ } else if (event.type === 'status') notify(event);
+ else if (event.type === 'complete') complete = event;
else if (event.type === 'error') throw new Error(event.error || 'Command output failed.');
};
try {
diff --git a/dashboard/site/test/e2e/campaigns-page-subscription.spec.js b/dashboard/site/test/e2e/campaigns-page-subscription.spec.js
index a8039039a..62a1d2ec2 100644
--- a/dashboard/site/test/e2e/campaigns-page-subscription.spec.js
+++ b/dashboard/site/test/e2e/campaigns-page-subscription.spec.js
@@ -101,7 +101,7 @@ test('Campaigns uses the shared page binding and refreshes its charts, table, an
const { renderDashboard, dashboardPageSourceNames } = await import(`${location.origin}/src/presenter.js`);
const { loadCanonicalDashboardSources, subscribeCanonicalDashboardView } = await import(`${location.origin}/src/data-processor.js`);
const context = { pages: documentModel.dashboard.pages, queries: documentModel.dashboard.queries, views: documentModel.dashboard.views };
- await loadCanonicalDashboardSources(`${location.origin}/campaign-sources.json`, [], context);
+ const sources = await loadCanonicalDashboardSources(`${location.origin}/campaign-sources.json`, ['campaign-navigation-availability'], context);
Reflect.set(window, '__campaignPageBindings', 0);
/** @type {import('../../src/presenter.js').PageSourceLoader} */
const loader = (pageId, options) => new Promise((resolve, reject) => {
@@ -115,7 +115,7 @@ test('Campaigns uses the shared page binding and refreshes its charts, table, an
});
loader.subscribeViewSources = () => { throw new Error('Unexpected independent view subscription.'); };
history.replaceState({}, '', '#page-campaigns');
- document.querySelector('#root')?.replaceChildren(renderDashboard({ document: documentModel, sources: {}, loadPageSources: loader }));
+ document.querySelector('#root')?.replaceChildren(renderDashboard({ document: documentModel, sources, loadPageSources: loader }));
}, model);
const root = page.locator('[data-page-id="campaigns"]');
await expect(root.locator('[data-view-id="campaigns-dispatches"]')).toContainText('alpha');
diff --git a/dashboard/site/test/e2e/canvas-fixture.js b/dashboard/site/test/e2e/canvas-fixture.js
index 255edd2c2..39eaa4436 100644
--- a/dashboard/site/test/e2e/canvas-fixture.js
+++ b/dashboard/site/test/e2e/canvas-fixture.js
@@ -3,6 +3,7 @@ import { mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { normalize } from '../../src/data/normalize/index.js';
import { queryDashboardSourceObservations } from '../../src/data/queries/ingestion.js';
+import { mergeActivityStructuralRecord } from '../../src/data/storage/retention.js';
const { writeCanvasSnapshot } = await import(new URL('../../../../tests/helpers/canvas-snapshot.mjs', import.meta.url).href);
/**
@@ -12,18 +13,29 @@ const { writeCanvasSnapshot } = await import(new URL('../../../../tests/helpers/
*/
export async function writeCanvasArtifact(destination, { sources = {}, shards = [] } = {}) {
await mkdir(destination, { recursive: true });
- await writeCanvasSnapshot(destination, normalize(queryDashboardSourceObservations(sources).observations));
- /** @type {Record} */
- const hashes = {};
- for (const [index, content] of shards.entries()) {
- const name = `fixture-${index}.jsonl`;
- await writeFile(join(destination, 'gh-aw-logs-shards', name), content);
- hashes[`gh-aw-logs-shards/${name}`] = createHash('sha256').update(content).digest('hex');
+ const collections = new Map(Object.entries(normalize(queryDashboardSourceObservations(sources).observations))
+ .map(([name, rows]) => [name, new Map(rows.map(record => [record.id, record]))]));
+ for (const content of shards) {
+ const [headerLine, ...lines] = content.trim().split('\n');
+ const header = JSON.parse(headerLine);
+ if (header.kind !== 'metadata' || !['runs', 'records'].includes(header.phase)) {
+ throw new Error('Canvas fixtures require compacted canonical run/record shards.');
+ }
+ for (const line of lines) {
+ const { collection, record } = JSON.parse(line);
+ const records = collections.get(collection);
+ if (!records) throw new Error(`Unknown canonical fixture collection: ${collection}`);
+ const existing = records.get(record.id);
+ records.set(record.id, existing && ['repositories', 'workflows'].includes(collection)
+ ? mergeActivityStructuralRecord(collection, existing, record) : record);
+ }
}
+ await writeCanvasSnapshot(destination, Object.fromEntries(
+ [...collections].map(([name, records]) => [name, [...records.values()]])));
const { readFile } = await import('node:fs/promises');
const manifest = JSON.parse(await readFile(join(destination, 'payload-hashes.json'), 'utf8'));
const inventory = JSON.stringify(sources);
await writeFile(join(destination, 'inventory-sources.json'), inventory);
- hashes['inventory-sources.json'] = createHash('sha256').update(inventory).digest('hex');
- await writeFile(join(destination, 'payload-hashes.json'), JSON.stringify({ ...manifest, ...hashes }));
+ manifest['inventory-sources.json'] = createHash('sha256').update(inventory).digest('hex');
+ await writeFile(join(destination, 'payload-hashes.json'), JSON.stringify(manifest));
}
diff --git a/dashboard/site/test/e2e/lazy-page-chunks.spec.js b/dashboard/site/test/e2e/lazy-page-chunks.spec.js
index 99b481203..be307d424 100644
--- a/dashboard/site/test/e2e/lazy-page-chunks.spec.js
+++ b/dashboard/site/test/e2e/lazy-page-chunks.spec.js
@@ -265,7 +265,10 @@ test('core dashboard stays small and page chunks load on demand with in-memory c
});
// Query definitions stay in page chunks; core keeps each page stub's source index so sources can
// load before the chunk arrives. Page descriptions and analysis contracts stay in their chunks.
- expect(core.textLength).toBeLessThan(83500);
+ // Shell prerequisites execute before any page chunk can enable navigation.
+ const shellQueryLength = JSON.stringify(core.value.dashboard.queries).length;
+ expect(shellQueryLength).toBeLessThan(5000);
+ expect(core.textLength - shellQueryLength).toBeLessThan(83500);
expect(core.value.dashboard.pages.some((/** @type {Record} */ entry) =>
['description', 'intent', 'subject', 'objective', 'acceptance'].some((field) => field in entry))).toBe(false);
expect(core.value.dashboard.pages.some((/** @type {{ views?: unknown[] }} */ entry) => Array.isArray(entry.views))).toBe(false);
@@ -409,7 +412,7 @@ test('async Factory Overview elements receive their chunked query definitions',
await expect(overview.getByText('Weekly rhythm', { exact: true })).toBeVisible();
await expect(overview.locator('.dashboard-ingestion-status')).toHaveCount(0);
await expect(page.locator('.report-footer-status')).toHaveCount(0);
- const status = page.getByRole('button', { name: 'Dashboard data is stale', exact: true });
+ const status = page.locator('.dashboard-current-status').getByRole('button', { name: 'Dashboard data is stale', exact: true });
await expect(status).toHaveCount(1);
await expect(status.locator('.octicon-alert')).toBeVisible();
await expect(overview.locator('.factory-station').first()).toContainText('Factory health');
@@ -433,7 +436,7 @@ test('async Factory Overview elements receive their chunked query definitions',
test('fresh upstream evidence shows green on desktop and mobile', async ({ page }) => {
await page.clock.setFixedTime(new Date('2026-09-15T12:00:00Z'));
await page.goto(`${origin}/#page-overview`);
- const status = page.getByRole('button', { name: 'Dashboard data is current', exact: true });
+ const status = page.locator('.dashboard-current-status').getByRole('button', { name: 'Dashboard data is current', exact: true });
for (const viewport of [{ width: 1440, height: 900 }, { width: 390, height: 844 }]) {
await page.setViewportSize(viewport);
if (viewport.width < 768) await page.getByRole('button', { name: 'Select view', exact: true }).click();
diff --git a/dashboard/site/test/e2e/maintenance-repository-selector.spec.js b/dashboard/site/test/e2e/maintenance-repository-selector.spec.js
index 48628777e..2f1c367c4 100644
--- a/dashboard/site/test/e2e/maintenance-repository-selector.spec.js
+++ b/dashboard/site/test/e2e/maintenance-repository-selector.spec.js
@@ -37,7 +37,7 @@ for (const { width, height = 900, empty = false, crowded = false } of [
executeCliAction: async (/** @type {{ command: string, onOutput: (event: { stream: string, data: string }) => void }} */ { command, onOutput }) => {
executions.push(command);
onOutput({ stream: 'stdout', data: 'Repository staged in review mode.\n' });
- return { ok: true };
+ return { ok: true, exitCode: 0 };
},
prepareSnapshot: async (/** @type {string} */ destination) => {
await mkdir(destination, { recursive: true });
diff --git a/dashboard/site/test/e2e/normalized-shard.js b/dashboard/site/test/e2e/normalized-shard.js
index 35e7f8882..225e5ca1a 100644
--- a/dashboard/site/test/e2e/normalized-shard.js
+++ b/dashboard/site/test/e2e/normalized-shard.js
@@ -1,6 +1,7 @@
import { adaptCachedGhAwJsonl } from '../../src/data/adapters/gh-aw-logs.js';
import { CANONICAL_SCHEMA_VERSION } from '../../src/data/model/schema.js';
import { normalize } from '../../src/data/normalize/index.js';
+import { NORMALIZED_JSONL_INGESTION_VERSION } from '../../src/data/ingest/coordinator.js';
/** @param {string} jsonl @param {import('../../src/data/model/schema.js').CanonicalObservation[]} [observations] */
export function normalizedActivityShards(jsonl, observations = []) {
@@ -18,7 +19,7 @@ export function normalizedActivityShards(jsonl, observations = []) {
{
kind: 'metadata',
schemaVersion: CANONICAL_SCHEMA_VERSION,
- ingestionVersion: 4,
+ ingestionVersion: NORMALIZED_JSONL_INGESTION_VERSION,
sourceRecords,
phase,
records: records.length
diff --git a/dashboard/site/test/e2e/query-editor.spec.js b/dashboard/site/test/e2e/query-editor.spec.js
index d476eecad..26b69ae9c 100644
--- a/dashboard/site/test/e2e/query-editor.spec.js
+++ b/dashboard/site/test/e2e/query-editor.spec.js
@@ -8,7 +8,7 @@ import { normalizedRunShard } from './normalized-shard.js';
import { writeCanvasArtifact } from './canvas-fixture.js';
test.describe('Go canvas query editor', () => {
-/** @type {{ url: string, close: () => Promise }} */
+/** @type {{ url: string, close: () => Promise, ingestSnapshot: () => Promise }} */
let server;
let workspace = '';
let artifactDirectory = '';
@@ -33,8 +33,17 @@ test.beforeAll(async () => {
),
prepareSnapshot: async (/** @type {string} */ destination) => {
artifactDirectory = destination;
+ const offset = Date.now() - Date.parse('2026-10-10T10:00:00Z');
+ const runs = (await readFile(new URL('../fixtures/query-editor-runs.jsonl', import.meta.url), 'utf8'))
+ .trim().split('\n').map(line => {
+ const record = JSON.parse(line);
+ for (const field of ['created_at', 'started_at', 'updated_at']) {
+ record.run[field] = new Date(Date.parse(record.run[field]) + offset).toISOString();
+ }
+ return JSON.stringify(record);
+ }).join('\n');
await writeCanvasArtifact(destination, {
- sources, shards: [normalizedRunShard(await readFile(new URL('../fixtures/query-editor-runs.jsonl', import.meta.url), 'utf8'))]
+ sources, shards: [normalizedRunShard(runs)]
});
},
});
@@ -303,17 +312,20 @@ test('preview subscriptions refresh from Go canonical revisions and query output
await page.getByRole('textbox', { name: 'Acceptance criteria', exact: true }).fill('Show native conclusion counts');
await page.getByRole('button', { name: 'Generate query and view', exact: true }).click();
await expect(page.locator('.query-editor > [role=status]')).toContainText('Preview updated');
+ const timestamp = new Date().toISOString();
await writeCanvasArtifact(artifactDirectory, { sources: {
...sources,
runs: {
- metadata: { 'as-of': '2026-10-10T11:00:00Z', 'artifact-generation': 'query-editor-refresh' },
+ metadata: { 'as-of': timestamp, 'artifact-generation': 'query-editor-refresh' },
rows: Array.from({ length: 251 }, (_, index) => ({
organization: 'githubnext', repository: 'gh-cao', workflow: '.github/workflows/example.md',
run: String(1000 + index), 'run-attempt': 1, 'run-status': 'completed', 'run-conclusion': 'failure',
- 'started-at': '2026-10-10T10:00:00Z'
+ 'started-at': timestamp
}))
}
} });
+ await server.ingestSnapshot();
+ await expect(page.locator('.query-editor-preview .chart-legend-pie')).toContainText('251');
const outputCounts = await page.evaluate(async () => {
const { loadCanonicalDashboardPage, validateQueryEditorDocument } =
await import(new URL('./src/data-processor.js', location.href).href);
@@ -339,7 +351,6 @@ test('preview subscriptions refresh from Go canonical revisions and query output
});
expect(outputCounts.length).toBeGreaterThan(0);
expect(outputCounts.every((count) => count === 200)).toBe(true);
- await expect(page.locator('.query-editor-preview .chart-legend-pie')).toContainText('252');
});
test('intent improvement preserves edits during an in-flight request and cancels on navigation', async ({ page }) => {
diff --git a/dashboard/site/test/e2e/repository-upgrade-batch.spec.js b/dashboard/site/test/e2e/repository-upgrade-batch.spec.js
index ccf0315b4..8ea6dc4b7 100644
--- a/dashboard/site/test/e2e/repository-upgrade-batch.spec.js
+++ b/dashboard/site/test/e2e/repository-upgrade-batch.spec.js
@@ -89,6 +89,7 @@ for (const width of [1440, 390]) {
}
] },
repositories: { metadata, rows: [
+ { organization: 'acme', repository: 'control' },
{ organization: 'octo', repository: 'outdated' },
{ organization: 'octo', repository: 'current' },
{ organization: 'octo', repository: 'unknown' }
diff --git a/dashboard/site/test/e2e/semantic-prompt.spec.js b/dashboard/site/test/e2e/semantic-prompt.spec.js
index 77da9beaf..6e2e9d007 100644
--- a/dashboard/site/test/e2e/semantic-prompt.spec.js
+++ b/dashboard/site/test/e2e/semantic-prompt.spec.js
@@ -279,7 +279,7 @@ test('a chart with composed semantics offers a prompt preview and returns focus'
const action = page.locator('[data-view-id="cost-by-repository"] .table-intent-button');
await expect(action).toBeVisible();
const titleRow = page.locator('[data-view-id="cost-by-repository"] .chart-prompt-heading');
- const title = titleRow.getByRole('heading', { name: 'Cost per repository', level: 3, exact: true });
+ const title = titleRow.getByRole('heading', { name: 'Cost per repository', level: 2, exact: true });
await expect(title).toBeVisible();
await expect(action.locator('.octicon')).toBeVisible();
await expect(action).toHaveAttribute('aria-label', /Propose fix: /);
diff --git a/dashboard/site/test/e2e/workflow-list.spec.js b/dashboard/site/test/e2e/workflow-list.spec.js
index 58aa89047..0bf2abb10 100644
--- a/dashboard/site/test/e2e/workflow-list.spec.js
+++ b/dashboard/site/test/e2e/workflow-list.spec.js
@@ -5,7 +5,9 @@ registerSmokeRoutes();
for (const viewport of [{ width: 1440, height: 900 }, { width: 390, height: 844 }]) {
test(`workflow list shows latest status and opens its dedicated page at ${viewport.width}px`, async ({ page, context }) => {
await page.setViewportSize(viewport);
- const metadata = { 'as-of': '2026-10-07T13:00:00Z', 'retrieved-at': '2026-10-07T13:00:00Z' };
+ const now = Date.now();
+ const at = (/** @type {number} */ hoursAgo) => new Date(now - hoursAgo * 3_600_000).toISOString();
+ const metadata = { 'as-of': at(0), 'retrieved-at': at(0) };
const sources = {
repositories: { metadata, rows: [{ organization: 'org', repository: 'control' }] },
workflows: {
@@ -19,9 +21,9 @@ for (const viewport of [{ width: 1440, height: 900 }, { width: 390, height: 844
runs: {
metadata,
rows: [
- { workflow: '.github/workflows/worker.md', run: '100', 'started-at': '2026-10-06T12:00:00Z', 'run-conclusion': 'success' },
- { workflow: '.github/workflows/worker.md', run: '101', 'started-at': '2026-10-07T11:00:00Z', 'run-conclusion': 'failure' },
- { workflow: '.github/workflows/recent.md', run: '200', 'started-at': '2026-10-07T12:00:00Z', 'run-conclusion': 'success' }
+ { workflow: '.github/workflows/worker.md', run: '100', 'started-at': at(25), 'run-conclusion': 'success' },
+ { workflow: '.github/workflows/worker.md', run: '101', 'started-at': at(2), 'run-conclusion': 'failure' },
+ { workflow: '.github/workflows/recent.md', run: '200', 'started-at': at(1), 'run-conclusion': 'success' }
].map((row) => ({ organization: 'org', repository: 'control', 'run-attempt': 1, 'run-status': 'completed', ...row }))
}
};
diff --git a/dashboard/site/test/unit/cli-queue.test.js b/dashboard/site/test/unit/cli-queue.test.js
index 253b69430..e2263bd5b 100644
--- a/dashboard/site/test/unit/cli-queue.test.js
+++ b/dashboard/site/test/unit/cli-queue.test.js
@@ -214,13 +214,14 @@ describe('worker command queue', () => {
{ type: 'output', sequence: 1, stream: 'stdout', data: 'old' },
{ type: 'output', sequence: 2, stream: 'stderr', data: 'new\n' },
{ type: 'output', sequence: 2, stream: 'stderr', data: 'duplicate' },
+ { type: 'status', command: { id: 'durable-1', status: 'running' } },
completion
];
const fetch = vi.fn().mockResolvedValue(new Response(events.map((event) => JSON.stringify(event)).join('\n')));
vi.stubGlobal('fetch', fetch);
const notify = vi.fn();
expect(await cliQueueOperation({ action: 'attach', commandId: 'durable-1', after: 1 }, undefined, notify)).toEqual(completion);
- expect(notify).toHaveBeenCalledExactlyOnceWith(events[1]);
+ expect(notify.mock.calls.map(([event]) => event)).toEqual([events[1], events[3]]);
expect(String(fetch.mock.calls[0][0])).toContain('/durable-1/output?after=1');
expect(fetch.mock.calls[0][1].method).toBeUndefined();
});
diff --git a/dashboard/site/test/unit/command-terminal.test.js b/dashboard/site/test/unit/command-terminal.test.js
index dc894a1d5..a964d3c76 100644
--- a/dashboard/site/test/unit/command-terminal.test.js
+++ b/dashboard/site/test/unit/command-terminal.test.js
@@ -42,6 +42,20 @@ afterEach(async () => {
});
describe('persistent command terminal', () => {
+ it('updates queued status from the worker stream before completion', async () => {
+ queue.available.mockReturnValue(true);
+ queue.get.mockResolvedValue({ id: 'persistent-1', status: 'queued', command: 'gh aw compile' });
+ queue.attach.mockImplementation(() => new Promise(() => {}));
+ const root = renderCommandTerminal(context);
+ document.body.append(root);
+ await vi.waitFor(() => expect(queue.attach).toHaveBeenCalledOnce());
+ expect(root.querySelector('[role="status"]')?.textContent).toBe('persistent-1 · queued');
+ const [, notify] = queue.attach.mock.calls[0];
+ notify({ type: 'status', command: { id: 'persistent-1', status: 'running' } });
+ await vi.waitFor(() => expect(root.querySelector('[role="status"]')?.textContent).toBe('persistent-1 · running'));
+ expect(terminalWrites).not.toHaveBeenCalledWith(undefined, undefined);
+ });
+
it('renders an accessible full-page console and detaches without cancelling', async () => {
queue.available.mockReturnValue(true);
queue.get.mockResolvedValue({ id: 'persistent-1', status: 'running', command: 'gh aw compile --strict' });
diff --git a/dashboard/site/test/unit/dashboard-chunks.test.js b/dashboard/site/test/unit/dashboard-chunks.test.js
index c4faf0689..88134417c 100644
--- a/dashboard/site/test/unit/dashboard-chunks.test.js
+++ b/dashboard/site/test/unit/dashboard-chunks.test.js
@@ -193,6 +193,12 @@ describe('dashboardPageIsLoaded', () => {
});
describe('splitDashboardDocument / resolveDashboardDocument round-trip', () => {
+ it('supports a dashboard without optional pages', () => {
+ const { core, pageChunks } = splitDashboardDocument({ languageVersion: '0.1.0', dashboard: {} });
+ expect(core.dashboard.pages).toEqual([]);
+ expect(pageChunks.size).toBe(0);
+ });
+
it('produces a core document without inline views/definitions or top-level queries', () => {
const { core, pageChunks } = splitDashboardDocument(sampleDocument());
expect(core.dashboard.queries?.map((query) => query.name)).toEqual(['callout-source']);
diff --git a/docs/agent-plugin.md b/docs/agent-plugin.md
index 596ca6f4e..23a94c4eb 100644
--- a/docs/agent-plugin.md
+++ b/docs/agent-plugin.md
@@ -136,6 +136,9 @@ disabled. **Back to Commands** or a disconnected browser detaches the display;
it does not cancel execution. Reopening a command replays retained output rather
than rerunning it. The console remains available after success, failure,
timeout, or cancellation.
+The console receives queued-to-running status changes through the data worker
+without reconnecting or waiting for completion. Status updates are separate from
+sequenced output and do not advance the replay cursor.
**Cancel** in the approval dialog starts nothing. The console offers **Copy**
and, for failed or timed-out work, **Retry command**; a retry requires fresh
diff --git a/server/internal/simulator/bootstrap_compatibility_integration_test.go b/server/internal/simulator/bootstrap_compatibility_integration_test.go
index de7bd9c9d..252643b81 100644
--- a/server/internal/simulator/bootstrap_compatibility_integration_test.go
+++ b/server/internal/simulator/bootstrap_compatibility_integration_test.go
@@ -135,12 +135,10 @@ func testBootstrapInstallerCompatibility(t *testing.T, ctx context.Context, cata
if scenario == "outdated" {
if err == nil || !strings.Contains(string(output), "gh-aw was not upgraded.") ||
!strings.Contains(string(output), "bash -s -- "+required) ||
- strings.Contains(string(output), "CAO installed and verified.") ||
- strings.Contains(string(output), "Verifying exact CAO runtime...") {
+ strings.Contains(string(output), "CAO installed and verified.") {
t.Fatalf("outdated extension did not fail with exact upgrade guidance: %v\n%s", err, output)
}
- } else if err != nil || !strings.Contains(string(output), "CAO installed and verified.") ||
- !strings.Contains(string(output), "Verifying exact CAO runtime...") {
+ } else if err != nil || !strings.Contains(string(output), "CAO installed and verified.") {
t.Fatalf("compatible installer failed: %v\n%s", err, output)
}
if scenario == "missing" {
diff --git a/server/internal/simulator/bootstrap_integration_test.go b/server/internal/simulator/bootstrap_integration_test.go
index d5bfd6c67..fa6c332c6 100644
--- a/server/internal/simulator/bootstrap_integration_test.go
+++ b/server/internal/simulator/bootstrap_integration_test.go
@@ -116,6 +116,16 @@ func TestCAOBootstrapIntegration(t *testing.T) {
t.Fatalf("failed private setup changed policy: %v", err)
}
bootstrapAssertRepository(t, api, true)
+ // Attested runtime acquisition has separate native CLI coverage. Setup here
+ // uses checkout-owned runtime fixtures, not a simulated provenance bypass.
+ for _, name := range setupRuntimeFixtureFiles(t, catalog) {
+ destination := filepath.Join(consumer, name)
+ if _, err := os.Stat(destination); os.IsNotExist(err) {
+ setupWrite(t, destination, setupRead(t, filepath.Join(catalog, name)))
+ } else if err != nil {
+ t.Fatal(err)
+ }
+ }
// Explicitly select the documented public-monitoring profile on fixture
// data only, then exercise the installer's --setup path and idempotency.
bootstrapRun(t, ctx, consumer, environment, "gh", "api", "-X", "PATCH", "repos/platform/control", "-F", "private=false")
diff --git a/tests/e2e/dashboard-mobile-live.spec.mjs b/tests/e2e/dashboard-mobile-live.spec.mjs
index a74ceb08f..9b4f06db1 100644
--- a/tests/e2e/dashboard-mobile-live.spec.mjs
+++ b/tests/e2e/dashboard-mobile-live.spec.mjs
@@ -195,7 +195,7 @@ test.beforeAll(async () => {
preview = await startDashboardPagesPreview();
const dataDirectory = join(preview.site, "demo-data");
const manifest = JSON.parse(await readFile(join(dataDirectory, "payload-hashes.json"), "utf8"));
- const shards = deployedActivityShardEntries(manifest, mobileDebugShardLimit());
+ const shards = deployedActivityShardEntries(manifest).slice(0, mobileDebugShardLimit());
expectedActivityShardPaths = shards.map(({ name }) => `/${name}`);
const sizes = await Promise.all(shards.map(async ({ name }) => (await stat(join(dataDirectory, name))).size));
const activityBytes = sizes.reduce((total, size) => total + size, 0);
diff --git a/tests/e2e/dashboard-server-page-coverage.mjs b/tests/e2e/dashboard-server-page-coverage.mjs
index 75bab84d7..cf43ad36f 100644
--- a/tests/e2e/dashboard-server-page-coverage.mjs
+++ b/tests/e2e/dashboard-server-page-coverage.mjs
@@ -1,7 +1,10 @@
+import { pageRequiredSource } from "../../dashboard/site/src/view-availability.js";
+
// The Go server fixture has no local CLI executor. Only explicitly declared
// capability gates may exclude pages from its query assessment.
export function serverDashboardPageExclusionReason(page) {
if (page.requires === undefined) return undefined;
+ if (page.requires?.capability === undefined && pageRequiredSource(page)) return undefined;
if (page.requires?.capability === "cli-queue") {
return "Requires the local CLI queue, which the Go Postgres server fixture does not provide";
}
diff --git a/tests/helpers/canvas-snapshot.mjs b/tests/helpers/canvas-snapshot.mjs
index 331e74aa1..ed7dd3924 100644
--- a/tests/helpers/canvas-snapshot.mjs
+++ b/tests/helpers/canvas-snapshot.mjs
@@ -70,6 +70,16 @@ export async function startCanvasServer({ prepareSnapshot, ...options } = {}) {
...(directory ? { snapshotDirectory: directory } : {}) });
return {
...preview,
+ async ingestSnapshot() {
+ if (!directory || !postgres) throw new Error("Snapshot ingestion requires a prepared PostgreSQL fixture.");
+ await execute(process.env.CAO_DASHBOARD_EXECUTABLE ?? resolve(".tmp/cao-dashboard"),
+ ["ingest", "--source", directory, "--redis-namespace", postgres.namespace, "--database-queries",
+ resolve(import.meta.dirname, "../../dashboard/site/src/data/queries/database.json")], {
+ env: { ...process.env, CAO_POSTGRES_URL: postgres.postgresURL,
+ CAO_OPERATIONAL_NAMESPACE: postgres.namespace, OTEL_SDK_DISABLED: "true" },
+ timeout: 120_000, maxBuffer: 1 << 20,
+ });
+ },
async close() {
try { await preview.close(); }
finally {
diff --git a/tests/integration/control-inventory-credentials.test.mjs b/tests/integration/control-inventory-credentials.test.mjs
index e6b564761..9b47e19af 100644
--- a/tests/integration/control-inventory-credentials.test.mjs
+++ b/tests/integration/control-inventory-credentials.test.mjs
@@ -574,7 +574,11 @@ test("credential verification is opt-in and cannot reach the validation issue re
assert.equal(workflow.on.workflow_dispatch.inputs["verify-credentials"].default, false);
assert.match(workflow.jobs.credentials.if, /github.event_name == 'workflow_dispatch'.*inputs.verify-credentials == true/);
assert.ok(Object.values(workflow.jobs.credentials.permissions).every(permission => permission === "read"));
- assert.match(workflow.jobs.report.if, /inputs.verify-credentials != true/);
+ assert.equal(workflow.jobs.report, undefined);
+ for (const job of Object.values(workflow.jobs)) {
+ assert.ok(Object.values(job.permissions).every(permission => permission === "read"));
+ assert.ok(job.steps.every(step => !/createIssue|issues\.create|gh issue create/.test(step.with?.script ?? step.run ?? "")));
+ }
const token = steps.find(step => step.id === "cao_pre_activation_app_token");
assert.equal(token.with.repositories, "${{ github.event.repository.name }}");
assert.match(token.if, /cao_admission.outputs.authorized == 'true'/);
diff --git a/tests/integration/dashboard-inventory-ingestion.test.mjs b/tests/integration/dashboard-inventory-ingestion.test.mjs
index 034511ce7..67aaa655b 100644
--- a/tests/integration/dashboard-inventory-ingestion.test.mjs
+++ b/tests/integration/dashboard-inventory-ingestion.test.mjs
@@ -3,7 +3,6 @@ import test from "node:test";
import "fake-indexeddb/auto";
import {
buildInventoryDashboardSources,
- discoverRepositories,
} from "../../dashboard/report/inventory-sources.mjs";
import { ingestDashboardSources } from "../../dashboard/site/src/data/ingest/coordinator.js";
import {
@@ -11,7 +10,7 @@ import {
readCanonicalBatch,
} from "../../dashboard/site/src/data/storage/indexeddb.js";
-test("discovered repositories flow from control scope into canonical storage", async () => {
+test("collected repository inventory flows into canonical storage", async () => {
await deleteCanonicalDatabase(indexedDB);
try {
const generatedAt = "2026-09-13T00:00:00Z";
@@ -23,18 +22,11 @@ test("discovered repositories flow from control scope into canonical storage", a
"control-plane": { inventory: { "max-scan-repositories": 100 } },
},
};
- const discoveredRepositories = await discoverRepositories(controlSettings, {
- token: "test-token",
- controlRepository: "acme/control",
- fetchImplementation: async (url) => {
- assert.match(String(url), /\/orgs\/acme\/repos\?/);
- return new Response(JSON.stringify([
- { full_name: "acme/control", visibility: "private" },
- { full_name: "acme/payments", private: true },
- { full_name: "acme/storefront", visibility: "internal" },
- ]));
- },
- });
+ const discoveredRepositories = [
+ { full_name: "acme/control", visibility: "private" },
+ { full_name: "acme/payments", private: true },
+ { full_name: "acme/storefront", visibility: "internal" },
+ ];
const sources = buildInventoryDashboardSources({
repository: "acme/control",
generatedAt,
@@ -81,18 +73,10 @@ test("explicitly allowed repositories flow into canonical storage", async () =>
"control-plane": { inventory: { "max-scan-repositories": 100 } },
},
};
- const discoveredRepositories = await discoverRepositories(controlSettings, {
- token: "test-token",
- controlRepository: "acme/control",
- fetchImplementation: async (url) => {
- const repository = String(url).match(/\/repos\/([^/]+\/[^/?]+)/)?.[1];
- assert.ok(repository);
- return new Response(JSON.stringify({
- full_name: repository,
- visibility: repository.endsWith("payments") ? "private" : "internal",
- }));
- },
- });
+ const discoveredRepositories = [
+ { full_name: "acme/payments", visibility: "private" },
+ { full_name: "acme/storefront", visibility: "internal" },
+ ];
const sources = buildInventoryDashboardSources({
repository: "acme/control",
generatedAt,
diff --git a/tests/unit/dashboard-cli-action-api.test.mjs b/tests/unit/dashboard-cli-action-api.test.mjs
index 4d08a8d15..fb87b1a9c 100644
--- a/tests/unit/dashboard-cli-action-api.test.mjs
+++ b/tests/unit/dashboard-cli-action-api.test.mjs
@@ -162,6 +162,29 @@ test("output replay follows a persistent cursor and emits terminal state", async
} finally { await server.close(); }
});
+test("output streams deliver queued-to-running transitions before completion", async () => {
+ let status = "queued";
+ let notify;
+ const server = await fixture({
+ detail: () => ({ id: "command-1", status, exitCode: status === "completed" ? 0 : null }),
+ output: () => [],
+ subscribe: listener => { notify = listener; return () => {}; },
+ });
+ try {
+ const response = await fetch(`${server.url}/__cli_commands/command-1/output`);
+ const reader = response.body.getReader();
+ const next = async () => JSON.parse(new TextDecoder().decode((await reader.read()).value).trim());
+ assert.deepEqual(await next(), { type: "status", command: { id: "command-1", status: "queued" } });
+ status = "running";
+ notify();
+ assert.deepEqual(await next(), { type: "status", command: { id: "command-1", status: "running" } });
+ status = "completed";
+ notify();
+ assert.equal((await next()).type, "complete");
+ reader.releaseLock();
+ } finally { await server.close(); }
+});
+
test("output completion includes the durable enterprise pull request result", async () => {
const pullRequestUrl = "https://ghe.example.test:8443/github/team/project/pull/42";
const server = await fixture({
diff --git a/tests/unit/dashboard-server-page-coverage.test.mjs b/tests/unit/dashboard-server-page-coverage.test.mjs
index 06f2217d1..ff848bb2a 100644
--- a/tests/unit/dashboard-server-page-coverage.test.mjs
+++ b/tests/unit/dashboard-server-page-coverage.test.mjs
@@ -35,6 +35,7 @@ test("ordinary and parameterized pages remain covered regardless of their names
{ id: "commands" },
{ id: "command", route: { "hash-query-parameter": "command" } },
{ id: "workflow", route: { "availability-view": "workflow-details" } },
+ { id: "campaigns", requires: { source: "campaign-inventory" } },
]) {
assert.equal(serverDashboardPageExclusionReason(page), undefined);
}
@@ -51,7 +52,7 @@ test("server coverage excludes Governance only while it declares the admin capab
});
test("new or malformed capability requirements fail closed instead of silently skipping coverage", () => {
- for (const requires of [null, {}, [], "cli-queue", { capability: "future-feature" }]) {
+ for (const requires of [null, {}, [], "cli-queue", { source: "" }, { capability: "future-feature", source: "campaigns" }]) {
assert.throws(
() => serverDashboardPageExclusionReason({ id: "future-page", requires }),
/Unknown server dashboard page requirement: future-page/,
diff --git a/tests/unit/dashboard-site-build.test.mjs b/tests/unit/dashboard-site-build.test.mjs
index 092cf9954..1f26d8438 100644
--- a/tests/unit/dashboard-site-build.test.mjs
+++ b/tests/unit/dashboard-site-build.test.mjs
@@ -284,8 +284,9 @@ test("docs dashboard installs renderer assets without experimental campaign page
const mainHash = builtIndex.match(/script\.src = '\.\/src\/main\.js\?sha=([a-f0-9]{64})'/)?.[1];
assert.ok(mainHash, "entry module includes the site content SHA");
assert.ok(builtIndex.includes(`rel="modulepreload" href="./src/main.js?sha=${mainHash}"`));
- assert.ok(builtIndex.includes(`rel="stylesheet" href="./styles.css?sha=${mainHash}"`));
const stylesheet = await readFile(new URL("styles.css", destination), "utf8");
+ assert.ok(builtIndex.includes(``));
+ assert.ok(!builtIndex.includes('rel="stylesheet"'), "startup does not block on a separate CSS request");
assert.ok(stylesheet.includes(".sidebar") && stylesheet.includes(".dashboard-notifications"),
"compiled stylesheet preserves the dashboard and notification styles");
assert.match(
@@ -321,7 +322,7 @@ test("docs dashboard installs renderer assets without experimental campaign page
await readFile(new URL(href, destination));
}
assert.ok(preloadedChunks.length > 0 && preloadedChunks.length <= 6,
- "startup preloads stay bounded and leave bandwidth for the render-blocking stylesheet");
+ "startup preloads stay bounded and leave bandwidth for the document's inline stylesheet");
assert.ok(preloadedChunks.some((file) => chunkMaps[chunkFiles.indexOf(file)].sources
.some((source) => source.endsWith("/src/dashboard-app.js"))), "application startup is preloaded");
for (const file of preloadedChunks) {
diff --git a/tests/unit/workflow-contract-dashboard-delivery.test.mjs b/tests/unit/workflow-contract-dashboard-delivery.test.mjs
index 53c7326d9..410d99680 100644
--- a/tests/unit/workflow-contract-dashboard-delivery.test.mjs
+++ b/tests/unit/workflow-contract-dashboard-delivery.test.mjs
@@ -100,6 +100,13 @@ test("dashboard CI runs the campaign quality gates", () => {
assert.deepEqual(lintUnit.needs, []);
assert.deepEqual(playwrightIntegration.needs, []);
assert.match(playwrightIntegration.block, /if: github\.event_name != 'pull_request'/);
+ const browserJob = parse(source).jobs["playwright-integration"];
+ assert.equal(browserJob.services.postgres.env.POSTGRES_DB, "cao_browser_test");
+ assert.equal(browserJob.services.postgres.env.POSTGRES_HOST_AUTH_METHOD, "trust");
+ assert.equal(browserJob.env?.POSTGRES_URL, undefined);
+ const browserTestStep = browserJob.steps.find(step => step.name === "End-to-end tests");
+ assert.match(browserTestStep.env.POSTGRES_URL, /127\.0\.0\.1:\$\{\{ job\.services\.postgres\.ports\['5432'\] \}\}\/cao_browser_test/);
+ assert.ok(browserJob.steps.some(step => /apt-get install -y postgresql-client/.test(step.run)));
assert.deepEqual(ingestionScale.needs, []);
// The synthetic ingestion payload is slow, so the gate stays on main.
assert.match(ingestionScale.block, /if: github\.ref == 'refs\/heads\/main'/);