Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
f77ef9d
chore(porch): 83 init spir
pseudoseed Aug 24, 2026
b5a63ec
chore(porch): 83 skip pre-approved plan
pseudoseed Aug 24, 2026
e7cf3a0
[Spec 83] Initial implementation plan
pseudoseed Aug 24, 2026
ca399e1
chore(porch): 83 plan build-complete
pseudoseed Aug 24, 2026
bd09173
[Spec 83] Plan with multi-agent review
pseudoseed Aug 24, 2026
9b28ceb
chore(porch): 83 plan-approval gate-requested
pseudoseed Aug 24, 2026
7616815
chore(porch): 83 plan-approval gate-approved
pseudoseed Aug 24, 2026
bcade73
chore(porch): 83 implement phase-transition
pseudoseed Aug 24, 2026
7b85dc9
chore(porch): 83 acknowledge context refresh
pseudoseed Aug 24, 2026
2c9777a
[Spec 83][Phase: Workspace and /v2/ static serving] feat: Serve apps/…
pseudoseed Aug 24, 2026
ee11c01
chore(porch): 83 implement build-complete
pseudoseed Aug 24, 2026
1166d2f
chore(porch): 83 advance plan phase → phase_2
pseudoseed Aug 24, 2026
8489c45
[Spec 83][Phase: Workspace and /v2/ static serving] fix: copy-v2 unde…
pseudoseed Aug 24, 2026
9ea40bc
chore(porch): 83 acknowledge context refresh
pseudoseed Aug 24, 2026
15d55d5
[Spec 83][Phase: Frame validation and reducer] feat: Reduce v2 frames…
pseudoseed Aug 24, 2026
5250a63
chore(porch): 83 implement build-complete
pseudoseed Aug 24, 2026
c56adc5
[Spec 83][Phase: Frame validation and reducer] fix: Dark survives gon…
pseudoseed Aug 24, 2026
9e8472a
chore(porch): 83 implement re-iter (iter 2)
pseudoseed Aug 24, 2026
ff6b551
chore(porch): 83 implement build-complete
pseudoseed Aug 24, 2026
6ab3b3f
chore(porch): 83 advance plan phase → phase_3
pseudoseed Aug 24, 2026
da0943f
chore(porch): 83 acknowledge context refresh
pseudoseed Aug 24, 2026
c27a6a9
chore(porch): 83 implement build-complete
pseudoseed Aug 24, 2026
f34ce49
chore(porch): 83 implement re-iter (iter 2)
pseudoseed Aug 24, 2026
faa1084
chore(porch): 83 implement build-complete
pseudoseed Aug 24, 2026
10aaf6b
chore(porch): 83 implement re-iter (iter 3)
pseudoseed Aug 24, 2026
8fc8d76
chore(porch): 83 implement build-complete
pseudoseed Aug 24, 2026
0c82aa1
chore(porch): 83 implement force-advance (safety ceiling reached at i…
pseudoseed Aug 24, 2026
1b2a5b9
chore(porch): 83 advance plan phase → phase_4
pseudoseed Aug 24, 2026
05b981d
[Spec 83][Phase: Bootstrap, stream reader, reconnect] feat: Open /v2/…
pseudoseed Aug 24, 2026
799f8bb
chore(porch): 83 acknowledge context refresh
pseudoseed Aug 24, 2026
78cf53b
chore(porch): 83 implement build-complete
pseudoseed Aug 24, 2026
755d54a
chore(porch): 83 implement re-iter (iter 2)
pseudoseed Aug 24, 2026
eee0b5a
chore(porch): 83 implement build-complete
pseudoseed Aug 24, 2026
39f7ff6
chore(porch): 83 advance plan phase → phase_5
pseudoseed Aug 24, 2026
3ff35f2
[Spec 83][Phase: Site view] feat: Draw the live hierarchy
pseudoseed Aug 24, 2026
8b5f661
chore(porch): 83 acknowledge context refresh
pseudoseed Aug 24, 2026
56e995a
chore(porch): 83 implement build-complete
pseudoseed Aug 24, 2026
c3d7153
[Spec 83][Phase: Playwright fixture proof] test: Drive /v2/ against a…
pseudoseed Aug 24, 2026
2eae90c
chore(porch): 83 implement re-iter (iter 2)
pseudoseed Aug 24, 2026
bed7e16
chore(porch): 83 implement build-complete
pseudoseed Aug 24, 2026
75ce35b
[Spec 83][Phase: Playwright fixture proof] test: Tighten resume, colo…
pseudoseed Aug 24, 2026
3d4e32f
chore(porch): 83 implement re-iter (iter 3)
pseudoseed Aug 24, 2026
229df0b
chore(porch): 83 implement build-complete
pseudoseed Aug 24, 2026
a077ce9
chore(porch): 83 all plan phases complete → review
pseudoseed Aug 24, 2026
3950531
chore(porch): 83 acknowledge context refresh
pseudoseed Aug 24, 2026
e175b1d
[Spec 83][Phase: review] fix: Space workspace name from status stamp
pseudoseed Aug 24, 2026
b13201e
chore(porch): 83 review build-complete
pseudoseed Aug 24, 2026
9c85e7f
Merge remote-tracking branch 'origin/main' into builder/spir-83
pseudoseed Aug 24, 2026
d6081d8
[Spec 83][Phase: review] fix: Address review iter 1
pseudoseed Aug 24, 2026
11e98b4
chore(porch): 83 pr gate-requested
pseudoseed Aug 24, 2026
c8ba39e
chore(porch): 83 pr gate-approved
pseudoseed Aug 24, 2026
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
4 changes: 4 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ jobs:
working-directory: apps/web
run: pnpm test

- name: Run v2 unit tests
working-directory: apps/v2
run: pnpm test

- name: Type-check vscode extension
working-directory: apps/vscode
run: pnpm check-types
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,13 @@ apps/vscode/out/
*.vsix
*.tsbuildinfo
test-results/
playwright-report/

# Dashboard build output (copied into codev for publishing)
packages/codev/dashboard-dist/
apps/web/dist/
packages/codev/v2-dist/
apps/v2/dist/

# three.js vendored at build time from the `three` devDependency (copy-three.mjs);
# copied into templates/vendor/ and shipped in the npm tarball, not committed.
Expand Down
221 changes: 221 additions & 0 deletions apps/v2/__tests__/SiteView.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import { cleanup, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it } from 'vitest';
import { Page } from '../src/App.js';
import { initialAppState, type AppState } from '../src/lib/stream.js';
import type { ClientNode } from '../src/lib/validate.js';

function node(over: Partial<ClientNode> & Pick<ClientNode, 'id' | 'kind'>): ClientNode {
return {
parentId: null,
name: over.id,
status: 'running',
flags: { heldMail: false },
lastDataAt: null,
buckets: Array.from({ length: 20 }, () => 0),
...over,
};
}

function liveState(): AppState {
const s = initialAppState();
s.connection = 'live';
s.bootstrap = 'scoped';
s.reducer.nodes = new Map([
['workspace:/a', node({ id: 'workspace:/a', kind: 'workspace', name: 'alpha' })],
['architect:1', node({ id: 'architect:1', kind: 'architect', parentId: 'workspace:/a', name: 'arch' })],
[
'builder:1',
node({ id: 'builder:1', kind: 'builder', parentId: 'workspace:/a', name: 'b1', status: 'running' }),
],
[
'builder:2',
node({
id: 'builder:2',
kind: 'builder',
parentId: 'workspace:/a',
name: 'b2',
status: 'gate-waiting',
}),
],
]);
s.reducer.counts = { workspaces: 22, builders: { total: 58, byStatus: { running: 10 } }, gateWaiting: 3 };
return s;
}

describe('SiteView display (scenarios 6, 7, 21)', () => {
afterEach(() => cleanup());
it('renders unknown status as the raw string, not RUN', () => {
const s = liveState();
s.reducer.nodes.set(
'builder:x',
node({ id: 'builder:x', kind: 'builder', parentId: 'workspace:/a', name: 'bx', status: 'reticulating' }),
);
render(<Page state={s} hostname="box" />);
expect(screen.getByText('reticulating')).toBeTruthy();
expect(screen.queryByText('reticulating')?.className).toContain('stamp-unknown');
});

it('keeps a live sibling when one workspace is dark', () => {
const s = liveState();
s.reducer.darkPaths.set('workspace:/gone', { reason: 'unreadable', at: 't0' });
render(<Page state={s} hostname="box" />);
expect(screen.getByText('alpha')).toBeTruthy();
expect(screen.getByText('gone')).toBeTruthy();
expect(screen.getByText(/unreadable/)).toBeTruthy();
const dark = document.querySelector('[data-dark="true"]');
expect(dark?.className).toContain('dim-sub');
expect(document.querySelector('[data-id="workspace:/a"]')?.className).not.toContain('dim-sub');
});

it('nodes: [] is the empty-site copy, not the unreachable banner', () => {
const s = initialAppState();
s.connection = 'live';
s.bootstrap = 'empty';
render(<Page state={s} hostname="box" />);
expect(screen.getByTestId('empty-site').textContent).toMatch(/No workspaces/);
expect(screen.queryByTestId('unreachable')).toBeNull();
});

it('unreachable is a connection banner and not the empty-site copy', () => {
const s = initialAppState();
s.connection = 'unreachable';
s.connectionWhy = 'transport';
render(<Page state={s} hostname="box" />);
expect(screen.getByTestId('unreachable').textContent).toMatch(/Cannot reach Tower/);
expect(screen.queryByTestId('empty-site')).toBeNull();
});

it('http mismatch is the mismatch page, not an empty tree', () => {
const s = liveState();
s.httpMismatch = { status: 400 };
render(<Page state={s} hostname="box" />);
expect(screen.getByTestId('mismatch').textContent).toMatch(/HTTP 400/);
expect(screen.queryByTestId('empty-site')).toBeNull();
expect(screen.queryByText('alpha')).toBeNull();
});

it('nodes: [] plus one dark is a dark plot from the id (scenario 21)', () => {
const s = initialAppState();
s.connection = 'live';
s.bootstrap = 'scoped';
s.reducer.darkPaths.set('workspace:/tmp/gone', { reason: 'unknown', at: 't1' });
render(<Page state={s} hostname="box" />);
expect(screen.queryByTestId('empty-site')).toBeNull();
expect(screen.getByText('gone')).toBeTruthy();
expect(screen.getByText(/unknown/)).toBeTruthy();
});

it('puts counts in the footer as machine totals, not a tree rollup', () => {
render(<Page state={liveState()} hostname="box" />);
const foot = screen.getByTestId('machine-totals');
expect(foot.textContent).toMatch(/Machine totals/);
expect(foot.textContent).toMatch(/22 workspaces/);
expect(foot.textContent).toMatch(/58 builders/);
expect(foot.textContent).not.toMatch(/drawn|this tree|shown/i);
});

it('sits the builder under the workspace beside the architect', () => {
const { container } = render(<Page state={liveState()} hostname="box" />);
const ws = container.querySelector('[data-kind="workspace"]');
const kinds = [...(ws?.querySelectorAll('[data-kind]') ?? [])].map((el) => el.getAttribute('data-kind'));
expect(kinds).toContain('architect');
expect(kinds).toContain('builder');
const arch = ws?.querySelector('[data-kind="architect"]');
expect(arch?.querySelector('[data-kind="builder"]')).toBeNull();
});

it('nests an architect-parented builder inside that architect', () => {
const s = liveState();
s.reducer.nodes.set(
'builder:nested',
node({ id: 'builder:nested', kind: 'builder', parentId: 'architect:1', name: 'nested' }),
);
const { container } = render(<Page state={s} hostname="box" />);
const arch = container.querySelector('[data-kind="architect"][data-id="architect:1"]');
expect(arch?.querySelector('[data-id="builder:nested"]')).toBeTruthy();
const wsLevel = container.querySelector('[data-kind="workspace"] > .stake-list');
expect(wsLevel?.querySelector('[data-id="builder:nested"]')).toBeNull();
});

it('renders an unresolvable parent at machine level, labelled', () => {
const s = liveState();
s.reducer.nodes.set(
'builder:lost',
node({ id: 'builder:lost', kind: 'builder', parentId: 'workspace:/missing', name: 'lost' }),
);
render(<Page state={s} hostname="box" />);
const box = screen.getByTestId('unattached');
expect(box.textContent).toMatch(/parent not in tree/);
expect(box.querySelector('[data-id="builder:lost"]')).toBeTruthy();
expect(document.querySelector('[data-id="workspace:/a"] [data-id="builder:lost"]')).toBeNull();
});

it('renders workspace name and status as separately readable text', () => {
const { container } = render(<Page state={liveState()} hostname="box" />);
const header = container.querySelector('[data-id="workspace:/a"] .ws-plot-name');
expect(header?.querySelector('.ws-plot-label')?.textContent).toBe('alpha');
expect(header?.querySelector('.stamp-run')?.textContent).toBe('RUN');
expect(header?.querySelector('.ws-plot-label')?.textContent).not.toContain('RUN');
});

it('renders workspace held mail beside name and status, not concatenated', () => {
const s = liveState();
s.reducer.nodes.set(
'workspace:/a',
node({ id: 'workspace:/a', kind: 'workspace', name: 'alpha', flags: { heldMail: true } }),
);
const { container } = render(<Page state={s} hostname="box" />);
const header = container.querySelector('[data-id="workspace:/a"] .ws-plot-name');
expect(header?.querySelector('.ws-plot-label')?.textContent).toBe('alpha');
expect(header?.querySelector('.held-mail')?.textContent).toBe('mail');
expect(header?.querySelector('.stamp-run')?.textContent).toBe('RUN');
expect(header?.querySelector('.ws-plot-label')?.textContent).not.toMatch(/mail|RUN/);
});

it('dims an offline workspace and shows architect heldMail plus status', () => {
const s = liveState();
s.reducer.nodes.set(
'workspace:/a',
node({ id: 'workspace:/a', kind: 'workspace', name: 'alpha', status: 'offline' }),
);
s.reducer.nodes.set(
'architect:1',
node({
id: 'architect:1',
kind: 'architect',
parentId: 'workspace:/a',
name: 'arch',
status: 'offline',
flags: { heldMail: true },
}),
);
const { container } = render(<Page state={s} hostname="box" />);
expect(container.querySelector('[data-id="workspace:/a"]')?.className).toContain('dim-sub');
expect(container.querySelector('[data-kind="architect"]')?.className).toContain('dim-sub');
expect(container.querySelector('[data-kind="architect"] .held-mail')).toBeTruthy();
expect(container.querySelector('[data-kind="architect"] .stamp-offline')).toBeTruthy();
});

it('keeps machine totals on an empty snapshot that still has counts', () => {
const s = initialAppState();
s.connection = 'live';
s.bootstrap = 'scoped';
s.reducer.counts = { workspaces: 22, builders: { total: 58, byStatus: {} }, gateWaiting: 3 };
render(<Page state={s} hostname="box" />);
expect(screen.getByTestId('empty-site')).toBeTruthy();
expect(screen.getByTestId('machine-totals').textContent).toMatch(/Machine totals/);
expect(screen.getByTestId('machine-totals').textContent).toMatch(/22 workspaces/);
});

it('uses rust only on gate-waiting treatment', () => {
const { container } = render(<Page state={liveState()} hostname="box" />);
const rust = [...container.querySelectorAll('.stamp-gate, .needs-attn')];
expect(rust.length).toBeGreaterThan(0);
expect(container.querySelector('.stamp-gate')?.textContent).toBe('GATE');
expect(container.querySelectorAll('.needs-attn')).toHaveLength(1);
expect(container.querySelector('.machine-footer')?.className).not.toContain('stamp-gate');
expect(container.innerHTML).not.toMatch(/#gate-rail|Find node|Add machine|#terminal-bank/);
expect(container.querySelector('#gate-rail')).toBeNull();
expect(container.querySelector('#terminal-bank')).toBeNull();
});
});
16 changes: 16 additions & 0 deletions apps/v2/__tests__/Sparkline.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { cleanup, render } from '@testing-library/react';
import { afterEach, describe, expect, it } from 'vitest';
import { Sparkline } from '../src/components/Sparkline.js';

describe('Sparkline', () => {
afterEach(() => cleanup());
it('renders 20 bars', () => {
const { container } = render(<Sparkline values={Array.from({ length: 20 }, (_, i) => i)} />);
expect(container.querySelectorAll('.spark i')).toHaveLength(20);
});

it('renders a flat trace when values are missing', () => {
const { container } = render(<Sparkline />);
expect(container.querySelectorAll('.spark i')).toHaveLength(20);
});
});
28 changes: 28 additions & 0 deletions apps/v2/__tests__/StatusStamp.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { cleanup, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it } from 'vitest';
import { StatusStamp } from '../src/components/StatusStamp.js';

describe('StatusStamp (scenario 6)', () => {
afterEach(() => cleanup());
it('renders GATE in rust and not RUN', () => {
const { container } = render(<StatusStamp status="gate-waiting" />);
expect(screen.getByText('GATE')).toBeTruthy();
expect(container.querySelector('.stamp-gate')).toBeTruthy();
expect(container.querySelector('.stamp-run')).toBeNull();
});

it('renders STALLED in ochre', () => {
const { container } = render(<StatusStamp status="stalled" />);
expect(screen.getByText('STALLED')).toBeTruthy();
expect(container.querySelector('.stamp-stalled')).toBeTruthy();
});

it('renders an unknown status as the raw string, not RUN', () => {
const { container } = render(<StatusStamp status="reticulating" />);
expect(screen.getByText('reticulating')).toBeTruthy();
expect(container.querySelector('.stamp-run')).toBeNull();
expect(container.querySelector('.stamp-gate')).toBeNull();
expect(container.querySelector('.stamp-stalled')).toBeNull();
expect(container.querySelector('.stamp-unknown')).toBeTruthy();
});
});
Loading
Loading