feat(web): wire Module 1/2 live feed to the real node (not Web Serial)
In live mode the sense loop runs on the board (ZeroClaw on the Uno Q), so the browser observes it rather than reading frames over Web Serial. - useNodeFeed(teamId, enabled): subscribes to the node's SSE activity feed (openTeamActivity) and seeds liveness with a one-shot getNodeStatus (the per-team stream only emits status on change). Inert in sim mode. - LiveBoardFeed: shows the board's real perception->reason->act activity + a live tally (agent runs / flashes / errors) + online indicator. - Module 1/2 are now mode-aware: sim keeps the synthetic IMU feed + browser classification (the teaching sandbox); live shows LiveBoardFeed from the actual board. Proceed gates on board online (M1) / real board activity (M2) in live, unchanged in sim. Note: this consumes the board's real activity stream (the signal the Uno Q emits today). Streaming raw IMU frames for browser-side classification in live mode would need a board-side sensor emitter (ZeroClaw firmware) + an API frame relay — a separate piece. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
71a255cf15
commit
a26185d47b
@@ -1,9 +1,20 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { render, screen } 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 { Module2 } from './Module2'
|
||||
import { useSession } from '@/store/session'
|
||||
import type { WsEvent } from '@/types'
|
||||
|
||||
let emit: (e: WsEvent) => void = () => {}
|
||||
vi.mock('@/lib/api', async (orig) => ({
|
||||
...(await orig<typeof import('@/lib/api')>()),
|
||||
openTeamActivity: (_teamId: string, on: (e: WsEvent) => void) => {
|
||||
emit = on
|
||||
return () => {}
|
||||
},
|
||||
getNodeStatus: vi.fn().mockResolvedValue({ teamId: 't', online: false }),
|
||||
}))
|
||||
|
||||
function renderPage() {
|
||||
return render(
|
||||
@@ -62,6 +73,24 @@ describe('Module2', () => {
|
||||
expect(proceed).toBeEnabled()
|
||||
})
|
||||
|
||||
describe('live mode (real node feed)', () => {
|
||||
it('replaces triggers with the live board feed and gates on real activity', async () => {
|
||||
useSession.getState().setMode('live')
|
||||
useSession.getState().setAddLayer('L2', 'escalate on critical')
|
||||
useSession.getState().setAddLayer('L3', 'drive damper on critical')
|
||||
renderPage()
|
||||
|
||||
expect(await screen.findByTestId('live-board-feed')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('trigger-buttons')).toBeNull() // no synthetic triggers in live mode
|
||||
const proceed = screen.getByRole('button', { name: /proceed/i })
|
||||
expect(proceed).toBeDisabled()
|
||||
|
||||
// the board acts (via Build & flash) → activity streams from the node
|
||||
act(() => emit({ type: 'node:activity', teamId: 'x', kind: 'response', label: 'Agent finished', ts: '' }))
|
||||
await waitFor(() => expect(proceed).toBeEnabled())
|
||||
})
|
||||
})
|
||||
|
||||
it('marks m2 complete on Proceed', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderPage()
|
||||
|
||||
Reference in New Issue
Block a user