feat(web): reshape workshop from 'tune a harness' to 'design a domain node'
The board is now a Claude-powered ZeroClaw agent with expert skills that teams talk to directly (its own dashboard / Telegram / voice). The frontend was built on the older sensor+harness model; this repoints it. - State/DTO: AddLayers remapped to the 5 design layers (Domain/Skills/Policies/ Harness/Loops); added session.domain (+ mirrored through TeamSnapshot, sync, and the SQLite store with a guarded migration); dropped Harness/Provider/RunMode; persist v2 migrate resets stale state. - Removed the harness/sim/failover machinery (HarnessProviderSelect, HarnessTuner, HarnessTomlPreview, ResiliencePanel, TriggerButtons, harness.ts, useSerial, LiveFeed/serial classifier) and every sim-vs-live branch. - Onboarding: EnvSetup rebuilt into 'Meet your node' — Open-your-node hero (new reusable OpenYourNode CTA), say-hi-to-your-agent, a free-text DomainPicker (domain drives L1-L5), Telegram/voice pointers, and the open->locked lockdown policy step. Domain is required to proceed. - Modules repointed: M1 Domain & events (L1), M2 Skills & Policies (L2/L3, skills reference + actuation-gate framing), M3 Harness (reasoning+tiering) & Loops (cadence) (L4/L5). BuildFlash reframed as a guided 'ask your node to build X' that hands off to the node dashboard. - Judge rubric -> Domain fit/Skills/Policies/Harness/Loops; Lecture + Landing re-storied to the Claude-node + talk-to-your-node narrative. Verified: web tsc + 181 tests, api tsc + 58 tests, dead-ref sweep clean. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
3be44ec215
commit
6d94833bb9
+24
-55
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { render, screen, fireEvent, act, waitFor } from '@testing-library/react'
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { render, screen, act, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import { Module1 } from './Module1'
|
||||
@@ -32,76 +32,45 @@ describe('Module1', () => {
|
||||
|
||||
it('renders the phase strip set to m1 and the heading', () => {
|
||||
renderPage()
|
||||
expect(screen.getByRole('heading', { name: /sense.*reason/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('heading', { name: /domain.*events/i })).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByTestId('phase-strip').querySelector('[data-phase="m1"]'),
|
||||
).toHaveAttribute('data-state', 'active')
|
||||
})
|
||||
|
||||
it('renders the actor map and a live feed', () => {
|
||||
it('renders the actor map and the live board feed', () => {
|
||||
renderPage()
|
||||
expect(screen.getByTestId('actor-map')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('live-feed')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('live-board-feed')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('gates Proceed until frames are observed and L1 is filled', async () => {
|
||||
it('streams real board activity into the feed', async () => {
|
||||
renderPage()
|
||||
act(() =>
|
||||
emit({ type: 'node:activity', teamId: 'x', kind: 'flash', label: 'Flashing sketch to the MCU', ts: '' }),
|
||||
)
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('live-board-activity')).toHaveTextContent(/flashing sketch/i),
|
||||
)
|
||||
})
|
||||
|
||||
it('gates Proceed until the board is online and L1 is filled', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderPage()
|
||||
const proceed = screen.getByRole('button', { name: /proceed/i })
|
||||
expect(proceed).toBeDisabled()
|
||||
|
||||
await user.type(screen.getByLabelText(/goal/i), 'keep the structure safe')
|
||||
expect(proceed).toBeDisabled() // still no frames
|
||||
await user.type(screen.getByLabelText(/layer 1/i), 'structural resonance')
|
||||
expect(proceed).toBeDisabled() // board not online yet
|
||||
|
||||
act(() => emit({ type: 'node:status', teamId: 'x', online: true }))
|
||||
await waitFor(() => expect(proceed).toBeEnabled())
|
||||
})
|
||||
|
||||
describe('with the feed running', () => {
|
||||
beforeEach(() => vi.useFakeTimers())
|
||||
afterEach(() => vi.useRealTimers())
|
||||
|
||||
it('streams frames into stats and enables Proceed when L1 is set', async () => {
|
||||
useSession.getState().setAddLayer('L1', { goal: 'keep safe' })
|
||||
renderPage()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /start feed/i }))
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(2000)
|
||||
})
|
||||
|
||||
expect(useSession.getState().stats.calls).toBeGreaterThan(0)
|
||||
expect(screen.getByRole('button', { name: /proceed/i })).toBeEnabled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('live mode (real node feed)', () => {
|
||||
it('shows the live board feed instead of the sim IMU feed', async () => {
|
||||
useSession.getState().setMode('live')
|
||||
renderPage()
|
||||
expect(await screen.findByTestId('live-board-feed')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('live-feed')).toBeNull()
|
||||
// real board activity streams in from the node
|
||||
act(() =>
|
||||
emit({ type: 'node:activity', teamId: 'x', kind: 'flash', label: 'Flashing sketch to the MCU', ts: '' }),
|
||||
)
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('live-board-activity')).toHaveTextContent(/flashing sketch/i),
|
||||
)
|
||||
})
|
||||
|
||||
it('enables Proceed once the board is online and L1 is set', async () => {
|
||||
useSession.getState().setMode('live')
|
||||
useSession.getState().setAddLayer('L1', { goal: 'keep safe' })
|
||||
renderPage()
|
||||
const proceed = await screen.findByRole('button', { name: /proceed/i })
|
||||
expect(proceed).toBeDisabled()
|
||||
act(() => emit({ type: 'node:status', teamId: 'x', online: true }))
|
||||
await waitFor(() => expect(proceed).toBeEnabled())
|
||||
})
|
||||
})
|
||||
|
||||
it('persists the Layer-1 goal to the store', async () => {
|
||||
it('persists the Layer-1 domain text to the store as a string', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderPage()
|
||||
await user.type(screen.getByLabelText(/goal/i), 'detect resonance')
|
||||
expect((useSession.getState().add.L1 as { goal: string }).goal).toBe('detect resonance')
|
||||
await user.type(screen.getByLabelText(/layer 1/i), 'detect resonance')
|
||||
expect(useSession.getState().add.L1).toBe('detect resonance')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user