Status
PASS
1449 executed tests
Passed
1449
100.0% pass rate
Failed
0
Duration
16.9s
9.3s vitest + 7.6s android
TS Line Coverage
35.9%
6,240/17,398 TypeScript lines
Total Tests
1449
Pass Rate
100.0%
Line Coverage
35.9%
◈
Terminal Output Parsing
PASS
Claude Code와 Codex CLI의 터미널 출력을 정확히 해석하는가?
248 passed
0 failed
3 files
259ms
output-parser.test.ts
205
/
205
196ms
OutputParser
205 / 205
basic option parsing › parses clean numbered options with newlines
basic option parsing › detects (recommended) marker
basic option parsing › detects ✔ selected marker
basic option parsing › handles both recommended and selected on different options
basic option parsing › parses full Claude model selector with labels and middle dot
ANSI-stripped options › parses concatenated text with · delimiter
ANSI-stripped options › extracts version numbers from · delimited labels
chunked input debounce › does NOT emit immediately on first chunk
chunked input debounce › batches multiple chunks into single emission
chunked input debounce › resets debounce timer when new chunk with options arrives
chunked input debounce › simulates real /model flow: split across chunks
model ID date rejection › does NOT parse 20251001 from model ID as option number
model ID date rejection › rejects large numbers that look like dates
version number rejection › does NOT parse version "4.6" followed by digit as option
version number rejection › does NOT parse "6)" from "4.6)" as option 6
stale buffer overwrite › newer options overwrite older ones with same index
permission prompts › detects Yes/No/Always pattern
permission prompts › detects (Y)es/(N)o pattern
permission prompts › emits immediately with no debounce
permission prompts › sets yes_no_always prompt type
permission prompts › sets yes_no prompt type
permission prompts › suppresses false idle from user prompt echo after permission (interactive cooldown)
permission prompts › allows real idle after interactive cooldown expires
diff prompts › detects (V)iew/(A)pply/(D)eny pattern
diff prompts › detects lowercase (a)pply/(d)eny/(v)iew pattern
diff prompts › emits immediately with no debounce
diff prompts › suppresses false idle from user prompt echo after diff (interactive cooldown)
various option counts › handles 1 option
various option counts › handles 2 options
various option counts › handles 5+ options
various option counts › handles bullet-style options
option updates › emits new options when data changes after first emission
spinner cancels option timer › spinner cancels pending option debounce
idle vs option debounce › idle prompt is ignored when option debounce is pending
idle vs option debounce › idle prompt fires normally when no option debounce is pending
interactive prompt during spinner › stops spinner when permission prompt arrives
interactive prompt during spinner › stops spinner when diff prompt arrives
interactive prompt during spinner › stops spinner when option prompt arrives
interactive prompt during spinner › stops spinner when idle prompt arrives in small chunk
interactive prompt during spinner › ignores idle prompt in large chunk during spinner (screen redraw)
cleanOptionLabel (via parseOptions) › deduplicates exact CamelCase matches
cleanOptionLabel (via parseOptions) › preserves labels without · delimiter
cleanOptionLabel (via parseOptions) › removes (recommended) from label text
cleanOptionLabel (via parseOptions) › removes ✔ from label text
cleanOptionLabel (via parseOptions) › extracts version from identity before middle dot
metadata events › emits project_name from startup banner
metadata events › emits project_name only once (caches)
metadata events › parses absolute path for project_name
metadata events › emits model_info with model and plan
metadata events › emits model_info without plan
metadata events › emits model_info only once for same model (caches)
metadata events › re-emits model_info when model changes
metadata events › parses ANSI-stripped model_info
metadata events › detects API billing plan
metadata events › emits status_line with duration and tokens
metadata events › parses zero-minute status line
metadata events › emits tool_action from ⏺ pattern
metadata events › extracts various tool names
user prompt › emits user_prompt after first idle has been seen
user prompt › does NOT emit user_prompt before first idle
user prompt › filters out mode banner text
user prompt › filters out numbered option lines
user prompt › filters out keyboard hint text
user prompt › filters out "esc to interrupt" text
user prompt › filters out autocomplete suggestions
user prompt › filters out box-drawing decorative lines
usage info › parses usage percentage
usage info › parses usage cost
usage info › parses session percentage with hour limit
usage info › parses reset time with timezone
usage info › parses time remaining
spinner lifecycle › emits spinner_start on spinner char (after idle seen)
spinner lifecycle › does NOT emit spinner_start before first idle
spinner lifecycle › emits spinner_stop after debounce (2000ms)
spinner lifecycle › does NOT emit duplicate spinner_start on repeated chars
spinner lifecycle › resets spinner debounce timer on repeated chars
spinner lifecycle › ignores spinner chars in large text blocks (>80 non-ws)
spinner lifecycle › recognizes all spinner characters
idle detection › emits idle after IDLE_DEBOUNCE_MS (300ms)
idle detection › sets seenFirstIdle on first idle prompt
idle detection › cancels idle timer when spinner starts
idle detection › recognizes > as idle prompt char
mode detection › detects plan mode
mode detection › detects accept edits mode
mode detection › detects default mode after Shift+Tab
mode detection › detects default mode via idle prompt after Shift+Tab
mode detection › emits default mode on Shift+Tab timeout (2s)
mode detection › detects ANSI-stripped plan mode text
mode detection › detects ANSI-stripped accept edits text
mode detection › clears pending mode switch when mode banner is detected
reset › clears cached project name and model name
reset › disables spinner detection (clears seenFirstIdle)
reset › allows re-detection after new idle prompt post-reset
reset › allows re-detection of project name after reset
reset › allows re-detection of model info after reset
buffer management › truncates buffer when exceeding 8192 chars
buffer management › keeps the last 4096 chars after truncation
option navigation › cursor movement during option selection does NOT trigger idle
option navigation › cursor movement does NOT trigger user_prompt
option navigation › normal cursor movement between options emits no idle or user_prompt
option navigation › real idle prompt still works after option navigation
option navigation › effort/hint text during navigation does not trigger events
option navigation › IDLE_PROMPT requires space/tab/NBSP after ❯ (not newline)
option navigation › USER_PROMPT requires space/tab after ❯ (not newline)
permission reclassification › reclassifies numbered Yes/No options as permission_prompt
permission reclassification › does NOT reclassify regular options as permission
permission reclassification › does NOT reclassify cursor-selection UI with "Enter to confirm" as permission
permission reclassification › detects cursor-selection UI even when ANSI stripping removes spaces
permission reclassification › includes navigable and cursorIndex in reclassified permission_prompt
permission reclassification › infers shortcuts for reclassified permission options
permission reclassification › infers shortcut "a" for "don't ask again" labels
permission reclassification › infers shortcut "a" for "allow all sessions" labels
ghost text suggestion › detects SGR 90 (bright black) ghost text on prompt line
ghost text suggestion › detects ghost text via Strategy 1 (Try "..." in clean text)
ghost text suggestion › unwraps Try "..." wrapper with smart quotes
ghost text suggestion › unwraps Try "..." wrapper with straight quotes
ghost text suggestion › detects ghost text on first idle (❯ and suggestion in same chunk)
ghost text suggestion › clears suggestion on spinner start
ghost text suggestion › debounces rapid updates
ghost text suggestion › ignores dim (SGR 2) text without ❯ prompt context
ghost text suggestion › detects dim (SGR 2) ghost text on ❯ prompt line
ghost text suggestion › detects ghost text with cursor-forward spacing (Strategy 1)
ghost text suggestion › filters UI chrome fragments
ghost text suggestion › filters box-drawing lines as UI chrome
ghost text suggestion › filters file paths (false positive from screen redraws)
ghost text suggestion › detects 256-color gray ghost text
ghost text suggestion › handles multi-segment ghost text on prompt line
ghost text suggestion › ignores ghost text on non-prompt lines
ghost text suggestion › detects 24-bit RGB gray ghost text
ghost text suggestion › ignores 24-bit RGB non-gray colors (e.g. blue prompt char)
ghost text suggestion › filters out short gray text like prompt char itself
ghost text suggestion › suppresses ghost text detection during spinner (processing)
ghost text suggestion › rejects digit+operator fragments like diff markers "96 +"
ghost text UI chrome filtering › filters out "Tip:" segments on prompt line
ghost text UI chrome filtering › filters out "(ctrl+...)" shortcut hints on prompt line
ghost text UI chrome filtering › filters concatenated UI chrome even in scheduleSuggestion
ghost text UI chrome filtering › keeps ghost text when UI chrome is also present on the line
filters out extended thinking indicators › rejects "(thought for 1s)" as ghost text via isUiChrome
filters out extended thinking indicators › rejects "(thought for 1s)>" via scheduleSuggestion
filters out extended thinking indicators › rejects longer thinking durations like "(thought for 15s)"
filters out extended thinking indicators › rejects "(thought for 1m 30s)" multipart duration
filters out extended thinking indicators › rejects "✻ Cooked for 1m 26s" sparkle indicator
filters out extended thinking indicators › rejects "✻ Cooked for 5s" short duration variant
filters out extended thinking indicators › rejects "Cooked for 10s" without sparkle (cross-chunk)
parenthesized placeholder filtering › rejects "(no content)" via SGR 2 (dim)
parenthesized placeholder filtering › rejects "(no content)" via SGR 90 (bright black)
parenthesized placeholder filtering › rejects "(loading...)"
parenthesized placeholder filtering › rejects "(empty)"
parenthesized placeholder filtering › rejects "(waiting for response)"
parenthesized placeholder filtering › allows "fix the broken (auth) module" — parens in middle
parenthesized placeholder filtering › allows "(optional) refactor the code" — paren prefix with text after
stacked ANSI + cross-chunk ghost text › detects ghost text with stacked ANSI escapes (gray + italic)
stacked ANSI + cross-chunk ghost text › detects ghost text with combined SGR params (2;90 = dim+bright-black)
stacked ANSI + cross-chunk ghost text › detects cross-chunk ghost text (❯ and gray text in separate feeds)
stacked ANSI + cross-chunk ghost text › does NOT cross-chunk detect when chunk contains ⎿ output fence
stacked ANSI + cross-chunk ghost text › does NOT cross-chunk detect when new chunk has \n (different line)
ghost option from stale buffer › excludes stale numbered list items before actual option prompt
ghost option from stale buffer › bypasses chunk size guard when ❯ cursor is present in large chunk
ghost option from stale buffer › still works with scrambled TUI order after backward scan
ghost option from stale buffer › excludes file path fragments from Read() tool in permission prompt
option index ordering › returns options sorted by index even when TUI lines arrive out of order
split ANSI sequence buffering › handles ANSI SGR codes split across chunks
split ANSI sequence buffering › handles bare ESC at end of chunk
split ANSI sequence buffering › does not buffer complete ANSI sequences
large chunk guard for option detection › does NOT detect numbered items in large response chunks as options
large chunk guard for option detection › still detects real options in small TUI chunks
CUP-positioned options › parses options using CUP (\x1b[row;colH) instead of newlines
CUP-positioned options › parses options using CUD (\x1b[B) for vertical movement
trailing TUI chrome stripping › strips "Esc to cancel" from last option label
trailing TUI chrome stripping › strips "Enter to confirm" from last option label
trailing TUI chrome stripping › does NOT strip "Esc" when it appears within legitimate label text
CJK suggestion detection › accepts ghost text containing CJK characters
CJK suggestion detection › accepts ghost text that is purely CJK (no ASCII words)
AskUserQuestion with separators and descriptions › handles AskUserQuestion with separator and descriptions
AskUserQuestion with separators and descriptions › handles options starting from non-zero index (buffer truncation)
hierarchical CC prompt shapes › captures 5-option plan approval with long labels
hierarchical CC prompt shapes › captures OpenClaw scope selection (numbered list)
hierarchical CC prompt shapes › captures OpenClaw token-action step (short numbered list)
permission scroll does not trigger idle (Bug 1) › scroll chunk with ❯ option text does NOT cause idle when navigable
permission scroll does not trigger idle (Bug 1) › /model combined chunk: confirmation + idle prompt clears options
permission scroll does not trigger idle (Bug 1) › /model separate chunks: ANSI reposition timer does not block idle
permission scroll does not trigger idle (Bug 1) › cursor move chunk with option text does NOT falsely trigger idle
permission scroll does not trigger idle (Bug 1) › genuine idle prompt exits navigable state and emits idle
TUI cursor-overwrite label correction (Bug 2) › fixes contaminated permission option label using correction line
TUI cursor-overwrite label correction (Bug 2) › leaves labels unchanged when no correction line is present
genuine idle detection (semantic) › "❯ \n" is genuine idle — only prompt character, no label text
genuine idle detection (semantic) › "❯ Beta" is NOT idle — has label text after prompt char
genuine idle detection (semantic) › "❯ A" is NOT idle — single-char label still counts
genuine idle detection (semantic) › ">" also treated as idle prompt character
effort level parsing › detects "High effort" selection pattern
effort level parsing › detects "Medium effort" selection pattern
effort level parsing › detects "Low effort" selection pattern
effort level parsing › detects "with high effort" confirmation line
effort level parsing › detects effort in model info line
effort level parsing › caches effort level — no re-emit on same value
effort level parsing › emits on effort level change
effort level parsing › resets effort level on reset()
effort level parsing › does not match "effort" in unrelated context
effort level parsing › does not match effort inside numbered option lines
effort level parsing › getter returns current effort level
effort level parsing › detects "Max effort" (Opus 4.7 variant)
effort level parsing › detects "xhigh effort" (Opus 4.7 variant)
effort level parsing › detects "default effort" (per-model default variant)
effort level parsing › detects "fast effort" (Opus 4.6 variant)
effort level parsing › detects effort in /model confirmation line with max
codex-output-parser.test.ts
28
/
28
35ms
CodexOutputParser
28 / 28
idle detection › emits idle on ❯ prompt with source=prompt
idle detection › emits idle on > prompt
idle detection › debounces rapid idle signals
spinner detection › emits spinner_start on braille spinner chars after first idle
spinner detection › does not emit spinner_start before first idle
spinner detection › emits spinner_stop + idle on timeout with source=timeout
spinner detection › emits spinner_stop when idle prompt appears
spinner detection › detects "Thinking" text as processing
permission prompt detection › emits permission_prompt on Allow/Deny pattern
permission prompt detection › emits permission_prompt on y/n pattern
permission prompt detection › stops spinner when approval detected
permission prompt detection › extracts Allow once / Always allow options
tool action detection › detects Running: command pattern
tool action detection › detects file operation patterns
tool action detection › detects Editing file pattern
tool action detection › dedups repeated emits of the same (tool, args) within window
tool action detection › emits a different command immediately
tool action detection › re-emits the same command after the dedup window elapses
tool action detection › re-emits the same command after a turn boundary (idle resets dedup)
model info detection › detects gpt model name
model info detection › detects o-series model
model info detection › does not re-emit same model
project name detection › detects working directory
project name detection › detects project from path
project name detection › only detects project name once
buffer management › truncates buffer at 8192 chars
buffer management › handles incomplete ANSI sequences
getProjectName › returns null when no project detected
cursor-sync.test.ts
15
/
15
28ms
Cursor Synchronization
15 / 15
terminal keyboard cursor tracking › emits cursor_update when full option redraw arrives with ❯ at new position
terminal keyboard cursor tracking › triggers buffer re-parse on small non-❯ chunk during navigable state
terminal keyboard cursor tracking › does not emit cursor_update when cursor position unchanged
genuine idle distinction › treats "❯ \n" as genuine idle (clears navigable state)
genuine idle distinction › does NOT treat "❯ Beta" as idle (cursor on option label)
genuine idle distinction › does NOT treat "❯ Allow once" as idle (permission cursor move)
cursor authority in StateMachine › accepts optimistic update immediately
cursor authority in StateMachine › suppresses stale PTY value within 200ms of optimistic update
cursor authority in StateMachine › accepts PTY value after 200ms grace period
cursor authority in StateMachine › always accepts PTY when no recent optimistic update
cursor authority in StateMachine › resets authority on state transition out of AWAITING
cursor authority in StateMachine › default source parameter is pty
select_option proportional delay › delay scales with step count
option re-emission with cursorIndex › AWAITING_OPTION update triggers state_changed with options
option re-emission with cursorIndex › updateCursorIndex emits snapshot with new cursor value
◇
State Machine & Adapters
PASS
에이전트의 상태 전이와 타입별 명령 라우팅이 정확한가?
175 passed
0 failed
3 files
163ms
state-machine.test.ts
57
/
57
51ms
StateMachine
57 / 57
basic transitions › starts in DISCONNECTED
basic transitions › SessionStart → IDLE
basic transitions › UserPromptSubmit → PROCESSING
basic transitions › Stop → IDLE from PROCESSING
basic transitions › SessionEnd → DISCONNECTED from any state
permission flow › permission_prompt → AWAITING_PERMISSION
permission flow › user respond → PROCESSING from AWAITING_PERMISSION
option flow › option_prompt → AWAITING_OPTION
option flow › select_option → PROCESSING from AWAITING_OPTION
diff flow › diff_prompt → AWAITING_DIFF
diff flow › respond → PROCESSING from AWAITING_DIFF
interrupt › interrupt → IDLE from PROCESSING
interrupt › interrupt → IDLE from AWAITING_PERMISSION
strict transitions › allows IDLE → AWAITING_PERMISSION (prompt without spinner)
strict transitions › blocks invalid transition: DISCONNECTED → PROCESSING
strict transitions › allows wildcard session_end from any state
stuck timeout › PROCESSING for >5min → auto-recovery to IDLE
stuck timeout › AWAITING_PERMISSION does NOT timeout (waits for user)
stuck timeout › AWAITING_OPTION does NOT timeout (waits for user)
stuck timeout › AWAITING_DIFF does NOT timeout (waits for user)
stuck timeout › timer resets on state change before timeout
stuck timeout › no timeout in IDLE state
parser events › spinner_start → PROCESSING from IDLE
parser events › spinner_stop → IDLE from PROCESSING
parser events › idle → IDLE from PROCESSING
parser events › mode_change updates permission mode
snapshot › emits state_changed on transitions
snapshot › includes tool info in snapshot
snapshot › clears tool info on PostToolUse
snapshot › includes project name and model
billingType detection › defaults to unknown
billingType detection › detects subscription from "Claude Max" plan
billingType detection › detects subscription from "Max" (case-insensitive)
billingType detection › detects api from "api.anthropic.com"
billingType detection › detects api (case-insensitive)
billingType detection › stays unknown for unrecognized plan
billingType detection › stays unknown when plan is absent
billingType detection › persists billingType across subsequent model_info without plan
billingType detection › emits state_changed when billingType is set
spinner_start recovery from awaiting states › spinner_start transitions from AWAITING_OPTION to PROCESSING
spinner_start recovery from awaiting states › spinner_start transitions from AWAITING_PERMISSION to PROCESSING
spinner_start recovery from awaiting states › spinner_start transitions from AWAITING_DIFF to PROCESSING
spinner_start recovery from awaiting states › clears options and navigable state on spinner_start from AWAITING_OPTION
spinner_start recovery from awaiting states › stores navigable and cursorIndex from permission_prompt
spinner_start recovery from awaiting states › cursor index tracking via updateCursorIndex
cursor authority (optimistic vs pty) › optimistic source updates cursor immediately
cursor authority (optimistic vs pty) › suppresses stale PTY within 200ms of optimistic
cursor authority (optimistic vs pty) › accepts PTY after 200ms grace period
cursor authority (optimistic vs pty) › emits state_changed on optimistic update
cursor authority (optimistic vs pty) › does NOT emit state_changed when stale PTY is suppressed
codex_* hook events › codex_session_start → IDLE
codex_* hook events › codex_user_prompt_submit → PROCESSING
codex_* hook events › codex_tool_start sets currentTool from tool_name
codex_* hook events › codex_tool_end clears currentTool
codex_* hook events › codex_stop → IDLE from PROCESSING
codex_* hook events › codex_turn_complete is a snapshot-emit no-op for state
codex_* hook events › full codex lifecycle preserves state transitions
adapter.test.ts
93
/
93
86ms
createAdapter factory
5 / 5
creates ClaudeCodeAdapter for "claude-code"
creates OpenClawAdapter for "openclaw"
passes gatewayUrl to OpenClawAdapter
creates CodexCliAdapter for "codex-cli"
throws for unknown agent type
ClaudeCodeAdapter
17 / 17
capabilities › reports all Claude Code capabilities as true
handleCommand routing › handles respond → returns true
handleCommand routing › handles switch_mode → returns true
handleCommand routing › handles interrupt → returns true
handleCommand routing › handles escape → returns true
handleCommand routing › defers select_option to bridge → returns false
handleCommand routing › defers navigate_option to bridge → returns false
handleCommand routing › defers send_prompt to bridge → returns false
handleCommand routing › defers voice to bridge → returns false
handleCommand routing › defers query_usage to bridge → returns false
switch_mode debounce › debounces rapid switch_mode calls (< 100ms apart)
lifecycle › isAlive returns false before start
lifecycle › getTtyPath returns undefined before start
lifecycle › getProjectName returns null before start
lifecycle › getHttpServer returns an object
onRawData callback › can register callback without error
onDiag handler › can register handler without error
OpenClawAdapter
17 / 17
capabilities › reports OpenClaw capabilities correctly
handleCommand routing › handles respond → returns true (RPC)
handleCommand routing › handles select_option → returns true (RPC)
handleCommand routing › handles navigate_option → returns true (no-op)
handleCommand routing › handles send_prompt → returns false without session key
handleCommand routing › handles interrupt → returns true (RPC)
handleCommand routing › handles escape → returns true (RPC)
handleCommand routing › defers switch_mode → returns false (not supported)
handleCommand routing › defers voice to bridge → returns false
handleCommand routing › defers query_usage to bridge → returns false
lifecycle › isAlive returns false before start
lifecycle › getTtyPath returns undefined (no PTY)
lifecycle › getProjectName returns null before start
lifecycle › getHttpServer returns an object
lifecycle › attachTerminal is a no-op
onRawData callback › can register callback without error
onDiag handler › can register handler without error
OpenClawAdapter gateway protocol
13 / 13
sends connect request with correct format on connect.challenge
becomes alive after hello-ok response
emits spinner_start on chat delta event
emits idle on chat final event
emits idle on chat aborted event
emits idle on chat error event
emits permission_prompt on exec.approval.requested
sends exec.approval.resolve on respond after approval request
sends exec.approval.resolve with deny on respond "n"
sends chat.send on send_prompt with active session
sends chat.abort on interrupt with active session and runId
emits SessionEnd on shutdown event
clears pendingApprovalId on exec.approval.resolved
CodexCliAdapter
16 / 16
capabilities › reports Codex CLI capabilities correctly
handleCommand routing › handles respond → returns true
handleCommand routing › handles interrupt → returns true
handleCommand routing › handles escape → returns true
handleCommand routing › does not handle switch_mode → returns false
handleCommand routing › defers select_option to bridge → returns false
handleCommand routing › defers navigate_option to bridge → returns false
handleCommand routing › defers send_prompt to bridge → returns false
handleCommand routing › defers voice to bridge → returns false
handleCommand routing › defers query_usage to bridge → returns false
lifecycle › isAlive returns false before start
lifecycle › getTtyPath returns undefined before start
lifecycle › getProjectName returns null before start
lifecycle › getHttpServer returns an object
onRawData callback › can register callback without error
onDiag handler › can register handler without error
CodexCliAdapter start lifecycle
2 / 2
emits SessionStart and connected on start
feeds PTY data to output parser and emits activity
MonitorAdapter
18 / 18
capabilities › reports monitor capabilities correctly
handleCommand routing › rejects respond (no PTY)
handleCommand routing › rejects interrupt (no PTY)
handleCommand routing › rejects escape (no PTY)
handleCommand routing › rejects switch_mode (no PTY)
handleCommand routing › rejects select_option (no PTY)
handleCommand routing › rejects send_prompt (no PTY)
handleCommand routing › defers voice to bridge
handleCommand routing › defers query_usage to bridge
lifecycle › isAlive always returns true
lifecycle › getTtyPath returns undefined (no PTY)
lifecycle › getProjectName returns null
lifecycle › getHttpServer returns an object
lifecycle › writeInput is a no-op (no PTY)
lifecycle › attachTerminal is a no-op
lifecycle › onRawData is a no-op
start and event emission › emits connected event on start
start and event emission › exposes hook server for external wiring
ClaudeCodeAdapter start lifecycle
5 / 5
emits SessionStart and connected on start
uses claude as default command
feeds PTY data to output parser and emits activity
emits SessionEnd and disconnected on PTY exit
registers rawData callback without error
protocol-contract.test.ts
25
/
25
26ms
Protocol Contract — StateUpdateEvent
5 / 5
minimal state_update has required fields
full state_update has all optional fields as correct types
state_update serializes to valid JSON
state enum values are lowercase strings
promptType values match known set
Protocol Contract — UsageEvent
3 / 3
has all required numeric fields
rate limit fields are present when available
tokenStatus matches known values
Protocol Contract — SessionsListEvent
2 / 2
sessions have required fields
agentType is optional string
Protocol Contract — ButtonStateEvent
1 / 1
buttons have required fields
Protocol Contract — EncoderStateEvent
1 / 1
encoders have required fields
Protocol Contract — TimelineEventMsg
1 / 1
entry has required fields
Protocol Contract — ConnectionEvent
1 / 1
has required fields
Protocol Contract — DisplayStateEvent
1 / 1
has boolean displayOn
Protocol Contract — PluginCommand shapes
6 / 6
respond command has value
select_option has numeric index
navigate_option has direction
send_prompt has text
utility command has action
clear_session_focus is fieldless
Protocol Contract — BridgeEvent discriminated union
2 / 2
all event types in union have type field
SERIAL_FORWARDED_EVENTS covers expected types
Protocol Contract — Backward Compatibility
2 / 2
state_update can be parsed with only required fields (old client)
usage_update required fields are sufficient for display
◆
Timeline Pipeline
PASS
이벤트 타임라인의 저장, 중복 제거, 세션 간 릴레이가 올바른가?
93 passed
0 failed
3 files
1.3s
timeline.test.ts
53
/
53
19ms
cleanDetailText
12 / 12
returns empty/falsy input unchanged
strips markdown bold
strips markdown headings
strips code fences
strips inline backticks
strips markdown links
strips blockquotes
strips list markers
collapses multiple blank lines
filters system JSON blobs (connectionId)
extracts error from JSON blob
compacts other JSON
cleanRawText
2 / 2
strips bold, headings, links, backticks
returns empty/falsy unchanged
cleanNopMarkers
3 / 3
removes NOP markers
collapses resulting blank lines
returns empty/falsy unchanged
extractSemanticCore
4 / 4
strips duration suffix for chat_end
keeps full text for chat_end without separator
keeps full text for non-chat_end types
trims whitespace
isRepetitiveEntry
11 / 11
detects exact duplicate chat_end entries
detects keyword-similar entries
returns -1 for non-matching entries
ignores entries outside window
only applies to chat_end, chat_start, and error types
dedupes repeated error entries within 1h window
matches chat_start entries
dedupes automated entries regardless of content
does not dedup automated vs non-automated
uses 8h window for automated entries
expires automated dedup after 8h
parseLogLine
21 / 21
returns null for null/undefined/non-object
returns null for empty message
returns null for model start/complete (suppressed)
parses memory/recall (legacy structured)
parses tool execution (legacy structured)
filters gateway/ws subsystem
filters infrastructure noise
filters channel infra reconnect noise
filters connection status JSON blobs
filters transient fetch timeouts
filters edit mismatch errors (agent retries)
filters failover cascade noise
parses genuine errors
parses error from message pattern (not level)
filters model/inference patterns (suppressed)
does not synthesize memory_recall from message text alone
does not synthesize tool_exec from message text alone
parses ISO timestamp correctly via error pattern
falls back to Date.now() for invalid timestamp on error message
truncates long tool raw to 500 chars
filters whatsapp noise from channel infra subsystem
timeline-integration.test.ts
35
/
35
32ms
BridgeTimelineStore
11 / 11
adds and retrieves entries
filters history by timestamp
calls listeners on new entries
upsert updates existing entry with same ts+type
upsert keeps timeline attribution fields
upsert adds new entry when no match exists
updateEntryStatus updates approval status
getLastEntry returns most recent of given type
getLastEntry returns null when type not found
removeListener stops notifications
enforces MAX_ENTRIES (200) with FIFO
deduplicateEntry pipeline
5 / 5
exact duplicate within 8s → skip
same type, different content → add
different type, same raw → add
chat_response identical raw 6s apart → skip (PTY/Stop race)
chat_response near-duplicate beyond 8s → repetitive merge
BridgeTimelineStore.setAttributor — history replay attribution
10 / 10
addEntry passes the entry through the attributor before storage
caller-set fields take precedence over attributor (idempotent)
listener (broadcast) receives the same attributed entry
upsertEntry propagates summaryKind to existing entry (LLM enrichment regression)
upsertEntry routes through the attributor too
late upsert preserves the original entry attribution after task rotation (regression)
merge path (repetitive dedup) does not re-attribute after task rotation
upsert with no existing match falls through to attributor (insert path)
caller-set taskId on upsert overrides existing entry attribution
history replay returns entries with taskId/runId set (regression)
Stop hook + PTY fallback double-emit (regression)
1 / 1
identical chat_response from two emit paths is collapsed by store
Timeline → WS broadcast pipeline
4 / 4
store entries trigger broadcast via listener
upsert entries broadcast with upsert flag
exact duplicate within 5s is skipped by store
different content within 5s is NOT deduped
TimelineEntry types
4 / 4
common entry types have expected shape
entries with status field
entries with detail field
entries with automated flag
session-timeline-relay.test.ts
5
/
5
1.2s
SessionTimelineRelay
5 / 5
connects to sibling and relays timeline_event
relays timeline_history entries
removes subscription when session disappears
ignores daemon sessions
handles upsert timeline events
◉
Daemon & Infrastructure
PASS
데몬 싱글톤, 세션 레지스트리, 사용량 릴레이가 안정적인가?
60 passed
0 failed
4 files
1.3s
daemon-lifecycle.test.ts
19
/
19
51ms
daemon.json lifecycle
6 / 6
writeDaemonInfo creates daemon.json with correct content
readDaemonInfo returns info when PID is alive
readDaemonInfo returns null and removes file when PID is dead
readDaemonInfo returns null when file does not exist
removeDaemonInfo deletes daemon.json
removeDaemonInfo is safe when file already gone
session registry with real files
7 / 7
register creates sessions.json
register replaces existing entry with same id
deregister removes session by id
listActive prunes dead PIDs
multiple sessions coexist
findExistingDaemon returns daemon session
findExistingDaemon returns null when no daemon
findDaemonPort
4 / 4
returns port from daemon.json (priority 1)
falls back to sessions.json daemon entry
returns null when no daemon anywhere
daemon.json takes precedence over sessions.json
probeDaemonHealth
2 / 2
returns health JSON from running server
returns null for closed port
session-registry.test.ts
10
/
10
12ms
Session Registry Logic
10 / 10
pruneDeadSessions › keeps alive sessions
pruneDeadSessions › removes sessions with dead PIDs
pruneDeadSessions › keeps old sessions if PID is alive
pruneDeadSessions › handles mix of alive and dead sessions
port allocation logic › returns base port when no ports are used
port allocation logic › returns next port when base is taken
port allocation logic › skips used ports
port allocation logic › finds gaps in used ports
port allocation logic › throws when all ports are taken
atomic write › write-then-rename produces valid JSON
usage-relay.test.ts
12
/
12
626ms
Usage relay — HTTP (Tier 1)
3 / 3
fetches usage from sibling GET /usage
returns null usage when sibling has no data
rejects stale data (>5 min old)
Usage relay — WebSocket (Tier 2)
2 / 2
receives usage_update from sibling WS on connect
does not send usage_update when sibling has no data
HookServer GET /usage (relay source)
2 / 2
returns cached usage via onApiUsage getter
returns fresh usage after update
429 prevention — relay-first strategy
2 / 2
sibling with data prevents direct API call
multiple siblings — first with data wins
ApiUsageData shape
3 / 3
has all required fields
serializes to valid JSON and back
handles null optional fields
bridge-core.test.ts
19
/
19
579ms
BridgeCore Orchestration
19 / 19
buildStateEvent › builds state_update with IDLE state
buildStateEvent › includes cached ollamaStatus and gatewayAvailable
buildStateEvent › computes promptType for permission options
buildStateEvent › computes promptType yes_no_always for 3+ permission options
usage management › updateApiUsage caches and broadcasts
usage management › buildUsage includes API usage data
usage management › buildUsage works without API data
usage management › inferredBillingType propagates to StateMachine
sendInitialState › sends state_update, usage_update, connection, display_state on connect
sendInitialState › includes timeline_history when entries exist
state change broadcast › state_changed emits to WS clients when wired by caller
wireTimeline › timeline entries broadcast as timeline_event
hasClients guard › wsServer.getClientCount reflects connected clients
hasClients guard › external client count provider extends hasClients
voice assistant state › updateVoiceAssistantState caches and triggers state broadcast
voice assistant state › disabled voice assistant state is not included in event
session registry › registerSession writes to sessions.json
session registry › deregisterSession removes from sessions.json
broadcast coordination › broadcast sends to WS and SSE callback
◎
Integration Tests
PASS
전체 파이프라인이 실제 서버 환경에서 동작하는가?
29 passed
0 failed
2 files
933ms
server-integration.test.ts
15
/
15
633ms
Server Integration
15 / 15
SessionStart hook → IDLE state broadcast
full session lifecycle: SessionStart → UserPromptSubmit → Stop
PreToolUse → PostToolUse cycle broadcasts tool info
SessionEnd → DISCONNECTED
GET /health returns valid JSON
GET /usage returns data when getter is set
GET /usage returns null when no getter
GET /devices returns empty when no getter
POST to unknown route returns 404
hook endpoint responds immediately
SSE client receives state_update events
broadcasts to multiple WS clients
WS command from client triggers callback
rapid hook events do not corrupt state
token counts accumulate through hook events
tier3-integration.test.ts
14
/
14
299ms
mDNS crash recovery
3 / 3
invalidateMdnsInstance does not throw when no instance exists
EADDRNOTAVAIL pattern matches expected error messages
non-mDNS errors are NOT matched by recovery pattern
Display sleep/wake broadcast
5 / 5
DisplayMonitor initial state is ON
display_state event has correct shape
display_state broadcasts to WS clients
display_state broadcasts to multiple clients
SSE also receives display_state
Voice transcription endpoint
4 / 4
returns 503 when no voice manager is set
returns 400 for empty audio data
returns transcription from voice manager
returns 500 when voice manager throws
WsServer broadcast hooks (serial relay)
2 / 2
onBroadcast hook receives all broadcast events
broadcast hook errors do not crash server
▣
Stream Deck Plugin UI
PASS
플러그인의 연결 관리, 옵션 레이아웃, 렌더링이 정확한가?
113 passed
0 failed
5 files
1.8s
connection-manager.test.ts
16
/
16
15ms
ConnectionManager
16 / 16
starts disconnected
start() begins bridge connection to daemon
start() installs a port provider so daemon.json is re-read every attempt
emits connected when bridge connects
emits disconnected when bridge disconnects
forwards state_update from bridge
send() delegates to bridge
send() drops command when not connected
getBridgePort returns bridge port
getConnectionSnapshot exposes connection and discovery state
retryNow probes daemon discovery and reconnects immediately when disconnected
switchToOpenClaw() › sends switch_agent command to bridge
switchToClaude() › sends switch_agent command to bridge
isGatewayAvailable() › returns false by default
isGatewayAvailable() › returns true when bridge reports gateway available
isGatewayAvailable() › returns false when bridge reports gateway unavailable
connection-integration.test.ts
6
/
6
1.7s
ConnectionManager Integration — Real WebSocket
6 / 6
client connects and receives broadcast events
multiple clients receive same broadcast
client sends command to server
handles rapid connect/disconnect without crashes
server detects client disconnect
bridge → gateway priority: second server takes over on disconnect
option-scenario.test.ts
9
/
9
6ms
option-renderer panels
3 / 3
E2 Focus panel renders with adaptive font
E3 List panel renders 4 visible rows with 14px font
E4 Detail panel shows word-wrapped label (12px, left-aligned)
colorForOption — "don't ask again" / "allow all sessions"
6 / 6
returns blue for "Yes, and don't ask again for: tail:*"
returns blue for "Yes, and don’t ask again" (smart quote)
returns blue for "Yes, allow all sessions in project"
returns green for plain "Yes" (shortcut y)
returns green for "Apply" (shortcut a, but no "don't ask" pattern)
returns blue for "Always allow"
renderer-snapshots.test.ts
63
/
63
63ms
voice-renderer snapshots
12 / 12
renderVoiceReady
renderVoiceRecording frame 0
renderVoiceRecording frame 30 (>1min)
renderVoiceTranscribing frame 0
renderVoiceTranscribing frame 10 (different dot phase)
renderVoiceError with message
renderVoiceError default
renderVoiceDisabled
renderVoiceAssistantListening frame 0
renderVoiceAssistantProcessing frame 5
renderVoiceAssistantSpeaking frame 0
renderWideVoiceText returns correct panel count
utility-renderer snapshots
4 / 4
renderSetupUtility
renderUtilityGeneric with icon
renderUtilityGeneric text-only (no icon)
renderUtilityMedia
usage-dial-renderer snapshots
7 / 7
renderUsageOverview
renderUsageDetail 5h
renderUsageDetail 7d
renderUsageSession
renderUsageExtra enabled
renderUsageExtra disabled
renderUsageDisconnected
response-renderer snapshots
7 / 7
renderResponseIdle
renderResponseProcessing
renderResponseDisconnected
renderResponseDisabled
renderResponseSuggestion
renderResponseInteractive
renderSetupPrompt
option-renderer snapshots
7 / 7
renderContextPanel permission
renderContextPanel diff
renderFocusPanel with recommended option
renderListPanel 3 options
renderListPanel 6 options (scroll indicator)
renderDetailPanel
renderWideOptionList returns correct panel count
button-renderer snapshots
7 / 7
basic text button
button with subtitle
disabled button
loading button
long text that needs abbreviation
svgToDataUrl returns data URI
labelNeedsHaiku detects long labels
session-slot-renderer snapshots
8 / 8
disconnected hero is icon-rich
disconnected non-center slot is empty
disconnected cluster quadrant tl
disconnected cluster quadrant tr
disconnected cluster quadrant bl
disconnected cluster quadrant br
connected no-session card is icon-rich
active idle session uses orbiting focus border
timeline-renderer snapshots
5 / 5
empty timeline
single entry
multiple entries fisheye
detail mode
with session status
qr-renderer snapshots
3 / 3
extractUrlLabel extracts host:port
qrPathData deterministic
renderQrButtonSvg
agent-logos snapshots
3 / 3
claude-code watermark
openclaw watermark
CLAUDE_LOGO_PATH is defined
text-utils-and-labels.test.ts
19
/
19
11ms
text-utils: CJK width measurement
4 / 4
Latin text is ~0.55em per char
Korean text is 1em per char (double-width)
mixed text measures correctly
isWide detects Hangul, CJK, fullwidth
text-utils: wrapTextByWidth
3 / 3
short text returns single line
long Latin text wraps to multiple lines
Korean text wraps at correct pixel width
button-renderer: abbreviation
10 / 10
short label renders without abbreviation indicator
"Yes, I trust this folder" fits with wrapping (no abbreviation needed)
"Yes, allow and don't ask again" wraps into 3 lines
very long label triggers abbreviation with ~ indicator
short permission labels like "No" render unchanged
labelNeedsHaiku returns false for short labels
labelNeedsHaiku returns false when heuristic abbreviation fits
labelNeedsHaiku returns true for very long unknown labels
labelNeedsHaiku returns false when Haiku cache has result
BUTTON_MAX_CHARS is reasonable
button-renderer: CJK labels
2 / 2
Korean label does not overflow (produces valid SVG)
mixed CJK/Latin label renders
▤
TUI Dashboard
PASS
터미널 대시보드의 상태 렌더링과 테라리움 애니메이션이 올바른가?
49 passed
0 failed
3 files
64ms
tui-dashboard.test.ts
8
/
8
19ms
TUI dashboard models
8 / 8
stores modelCatalog from state_update
renders OAuth catalog and Ollama models in wide layout
renders disconnected OAuth state
shows current session summary and control hints
renders agent list secondary line as model dash compact state
renders sibling models in session bridge mode and omits uptime label
renders help overlay when helpVisible is on
shows numbered session badges
tui-renderer-snapshots.test.ts
29
/
29
23ms
blockGauge snapshots
7 / 7
0% — all empty, green
50% — half filled, green
75% — yellow threshold
95% — red threshold
100% — all filled, red
clamps negative to 0
clamps above 100
resetTimeStr
5 / 5
returns empty for undefined
returns ↻now for past time
formats minutes
formats hours and minutes
formats days and hours
formatUptime
3 / 3
seconds
minutes
hours and minutes
formatTokens
3 / 3
below 1000 unchanged
1k-10k with decimal
10k+ rounded
activityDensityBar
3 / 3
empty timestamps — all dim
recent burst — bright right side
spread timestamps — distributed density
getLayout
3 / 3
wide for 120+ cols
standard for 80-119 cols
narrow for <80 cols
shouldShowTerrarium
3 / 3
true for adequate size
false for too narrow
false for too short
spinner
2 / 2
returns braille characters at different frames
spinner cycles through 10 frames
tui-terrarium-snapshots.test.ts
12
/
12
21ms
TUI terrarium snapshots
12 / 12
initTerrarium creates context with expected structure
setOctopi configures octopus instances
setCrayfish configures crayfish state
setJellyfish configures jellyfish instances
renderTerrariumFrame empty terrarium (small)
renderTerrariumFrame empty terrarium (large)
renderTerrariumFrame with idle octopus
renderTerrariumFrame with processing octopus
renderTerrariumFrame with routing crayfish
renderTerrariumFrame with sick crayfish
renderTerrariumFrame too small returns empty
updateTerrarium advances bubble positions
▥
Serial Protocol
PASS
ESP32와의 시리얼 바이트스트림이 정확히 프레이밍되는가?
22 passed
0 failed
1 files
12ms
esp32-serial-node.test.ts
22
/
22
12ms
SERIAL_FORWARDED_EVENTS
4 / 4
includes all display events
includes timeline events (unique to serial)
does NOT include plugin-only events
required serial events are present
ESP32 port detection patterns
5 / 5
matches CH340 ports (86 Box)
matches native USB JTAG ports (IPS 3.5", Round AMOLED)
matches Linux serial ports
excludes Bluetooth and WLAN
excludes non-ESP32 ports
handleSerialLine (source)
4 / 4
parses device_info message and updates deviceInfo
skips debug lines (non-JSON)
recovers from malformed JSON
ignores JSON without type field
prepareForSerial (source)
5 / 5
strips agentCapabilities from state_update
strips billingType and remoteUrl from state_update
preserves essential state_update fields
strips legacy usage fields from usage_update
passes through other events unchanged
WiFi provision protocol
1 / 1
provision message has all required fields
serial buffer management
3 / 3
line splitting handles multiple lines in one chunk
handles partial message across chunks
truncates buffer when exceeding 8KB limit
▦
Display Rendering
PASS
외부 디스플레이에 전송할 이미지 데이터가 올바른가?
3 passed
0 failed
1 files
3ms
pixoo-sprites.test.ts
3
/
3
3ms
getOctopusPaletteForSession
3 / 3
keeps the first additional session near the original terracotta tone
darkens later sessions while preserving channel ordering
clamps very large session indices to the darkest supported tone
▧
Hook Installation
PASS
Claude Code hook의 설치, 제거, 마이그레이션이 안전한가?
21 passed
0 failed
1 files
17ms
install.test.ts
21
/
21
17ms
Hook Installer
21 / 21
buildHookEntry › creates matcher-group format with AGENTDECK_PORT env var
buildHookEntry › includes daemon.json port resolution with fallback to 9120
buildHookEntry › reads PORT from AGENTDECK_PORT env var first, then daemon.json, then 9120
buildHookEntry › emits newline-separated shell so if/then/for/do keywords are not mis-terminated by `;`
applyHooks › installs hooks to empty settings in matcher-group format
applyHooks › preserves non-AgentDeck hooks
applyHooks › replaces old flat-format hooks
applyHooks › replaces old matcher-format hooks
applyHooks › is idempotent — running twice produces same result
applyHooks › preserves existing non-hook settings
removeHooks › removes all AgentDeck hooks (new format)
removeHooks › removes old flat-format AgentDeck hooks
removeHooks › preserves non-AgentDeck hooks
removeHooks › handles empty settings gracefully
migrateHooks › migrates old hardcoded port to env var
migrateHooks › migrates flat format to matcher-group format
migrateHooks › skips already-migrated hooks (new format)
migrateHooks › skips non-AgentDeck hooks
migrateHooks › migrates multiple events at once
migrateHooks › migrates hardcoded port inside matcher-group
migrateHooksIfNeeded (file-based) › upgrades old :-9120 fallback hooks to daemon.json-reading format
○
Other Tests
Uncategorized test files
454 passed
0 failed
31 files
apme-category-e2e.test.ts
3
/
3
55ms
APME category-aware turn evaluation (E2E)
3 / 3
conversation turn: prompt → classify → judge → turn category + composite persisted
research turn: rule-based classifier picks research, judge uses research rubric axes
daemon backfill pass: turns with response but no outcome get committed + null composite
apme-classifier.test.ts
18
/
18
65ms
classify()
12 / 12
planning — plan mode used
planning — short session, no file changes
research — web searches dominant
research — grep/glob dominant, no file changes
coding — edit+write dominant with file changes
debugging — tests + edits + bash
refactoring — many edits, no new files
review — mostly reads, minimal edits
ops — bash dominant, few edits
conversation — very short, no tools
multi_agent — multiple Agent delegations
unknown — no clear pattern
computeSignals()
3 / 3
counts tool calls and identifies dominant tool
detects plan mode from step payload
detects web searches
classifyRun()
2 / 2
returns signals and category for a coding run
returns unknown for empty runs
task_category schema migration
1 / 1
runs table has task_signals, task_category, task_category_source columns
apme-collector.test.ts
10
/
10
144ms
ApmeCollector
10 / 10
openRun → ingestHook → closeRun persists a run with steps
updateUsage and updateModel reflect in the run row
listRuns filters by agent and orders by started_at desc
default rubric v1 is seeded on init
insertEval + listEvalsForRun round-trip
scorecard view aggregates runs per model
multi-turn cycle captures prompts and responses (wireAgentApme path)
setLastClosedTurnResponse fills missing response on closed turn
setTurnResponse tags efficiency_json.response_kind (text / tool_only / empty)
no-op gracefully when store is disabled
apme-http.test.ts
13
/
13
277ms
APME HTTP routes
13 / 13
returns 503 when APME is not initialized
GET /apme/runs lists recent runs with evals and overall score
GET /apme/runs filters by agent
GET /apme/run/:id returns run detail with steps and evals
GET /apme/run/:id returns 404 for unknown runs
GET /apme/run/:id includes per-task rollup with attached evals
GET /apme/tasks/:id returns task detail with evals + turns
closeTaskExternal with manual signal records outcome + boundary_signal=manual
GET /apme/scorecard returns model aggregates
GET /apme/rubric/current returns seeded rubric v1
POST /apme/vibe records feedback and rejects unknown runs
POST /apme/recommend returns candidate list
unknown /apme/* path returns 404
apme-judge-probe.test.ts
20
/
20
91ms
probeJudgeBackend — MLX
4 / 4
returns unavailable when /v1/models is unreachable
returns unavailable when /v1/models advertises no chat-capable model
returns unavailable when chat ping fails (model not loaded)
returns ready when catalog AND chat ping both succeed
probeJudgeBackend — OpenClaw
4 / 4
returns unavailable when /health is unreachable
returns unavailable when /health is up but /models is not (gateway not initialised)
returns unavailable when configured model is not in /models catalog
returns ready when /health + /models both up and model matches
probeJudgeBackend — Anthropic API (stub backend)
2 / 2
returns unavailable when ANTHROPIC_API_KEY is not set
returns unavailable even when ANTHROPIC_API_KEY is set (callApi is a stub)
callJudgeWithMeta — effective backend labelling across fallback
3 / 3
FM happy path labels eval as foundationModels
FM→MLX fallback (fallbackToMlx=true) labels eval as mlx, not foundationModels
FM failure without fallbackToMlx propagates the error (no silent label drift)
sanitizeForMlx — runtime fallback hygiene
4 / 4
strips FM endpoint and model when forcing to MLX
strips OpenClaw endpoint and model when forcing to MLX
returns the same cfg shape when already-clean MLX is passed
clears a custom MLX endpoint when source backend is MLX but model is overridden
probeJudgeBackend — Foundation Models
3 / 3
returns unavailable when Swift daemon FM endpoint cannot be resolved
returns unavailable when FM endpoint responds with error body
returns ready when FM endpoint responds with text
apme-runner.test.ts
31
/
31
405ms
detectLanguage
4 / 4
recognizes TypeScript from package.json
recognizes Swift from .xcodeproj
recognizes Kotlin from build.gradle.kts
returns null for unknown paths
parseJudgeJson
5 / 5
extracts scores and reasoning from a clean JSON blob
rescales 0-10 axis to 0-1
tolerates prose wrapping and code fences
returns null when there is no JSON at all
returns null when overall is missing
effectiveJudgeModelTag
4 / 4
uses cfg.model for MLX when no pin is set
uses llm.mlx pin over cfg.model for MLX backend
ignores llm.mlx pin for non-MLX backends
foundationModels uses the Swift-parity judgeModelLabel
callJudge foundationModels routing
3 / 3
returns FM text on ok response
throws when FM reports unavailable (default = no fallback)
falls back to MLX only when fallbackToMlx is explicitly true
shouldJudge gating
4 / 4
never runs when sampleRate is 0
skips clear passes when onlyWhenDisagreement is true
runs on failures when onlyWhenDisagreement is true
runs for clear passes when onlyWhenDisagreement is false
ApmeRunner.runOne
4 / 4
records deterministic failures and invokes judge with rubric prompt
skips layer 2 on a clean pass when onlyWhenDisagreement is true
enqueueTurn skips judge for tool_only and empty turns
swallows judge errors without inserting partial rows
runDeterministic (end-to-end spawn)
3 / 3
runs sh-based commands against the project path and reports pass
captures exit code 1 as tests_pass=0
skips entirely when the worktree has no changes
onTaskEvaluated
3 / 3
fires after enqueueTask resolves and carries the composite score + outcome
derives outcome=fail for low composite scores
preserves a manually-set outcome — judge does not overwrite `abandoned`
buildJudgePrompt
1 / 1
includes rubric prompt + task + context fields
apme-settings.test.ts
16
/
16
18ms
loadApmeConfig — defaults + merge behaviour
12 / 12
returns full defaults when settings.json does not exist
returns full defaults when settings.json has no apme section
returns defaults on malformed JSON (must not throw)
merges user-set fields on top of defaults
clamps sampleRate to [0, 1]
falls back to mlx when judge.backend is unknown string
downgrades judge.backend="api" to "mlx" on the Node bridge (callApi is a stub)
also wipes endpoint/model when downgrading backend="api" → "mlx"
also wipes endpoint/model when falling back from unknown backend → "mlx"
preserves user-set endpoint/model when keeping their chosen backend
respects explicit enabled=false (opt-out)
clamps deterministic.timeoutSec to [5, 1800]
loadApmeConfig — DEFAULT_APME_CONFIG sanity
4 / 4
default backend is local MLX (cost-sensitive policy)
default sampleRate is 1.0 (judge every closed run; cost is local)
default enabled is true (zero-config activation)
default autoTune is true (rubric self-improves over time)
apme-task-boundary.test.ts
12
/
12
195ms
ApmeCollector task boundaries
10 / 10
first UserPromptSubmit opens a task and attaches the turn
TodoWrite with every todo completed closes the active task
TodoWrite with partial completion does NOT close the task
next UserPromptSubmit after boundary opens a new task
splitRun closes the active task with boundary=clear
closeRun closes the active task with boundary=session_end
onTaskClosed fires with the task metadata
onTaskOpened fires with sessionId + agentType + projectName + taskIndex
onTaskClosed payload includes session, agent, project, and timing
empty task (no turns between two boundaries) is dropped
ApmeRunner task eval
2 / 2
enqueueTask invokes judge with turns and persists summary + scores
skips task eval when all turns are tool_only / empty
apme-telemetry-envelope.test.ts
39
/
39
355ms
claudeHookToSpans
6 / 6
emits turn_start with prompt for UserPromptSubmit
detects /clear and emits a task_boundary span (not turn_start)
emits tool_call for PreToolUse with gen_ai.tool.name
emits tool_result for PostToolUse
falls through to raw_step for unknown events (Stop, SessionEnd, …)
handles legacy { prompt: ... } shape on UserPromptSubmit
claudePtyParserEventToSpans
4 / 4
maps tool_start to a tool_call span
maps tool_end to a tool_result span
emits a raw_step for spinner_start/idle/spinner_stop
drops unknown parser events
claudePtyResponseToSpan
3 / 3
returns a turn_response span for non-trivial text
marks fallback_to_last_closed when requested
returns null for empty / single-character text (filters silence)
timelineEntryToSpans
12 / 12
translates chat_start to turn_start with detail as prompt
translates chat_response to turn_response
translates chat_end with response detail to turn_response with fallback flag
drops chat_response / chat_end with empty body
translates tool_request with first-token tool name
codex_user_prompt_submit → turn_start span
codex_user_prompt_submit with /clear → task_boundary span
codex_tool_start → tool_call span with tool name
codex_tool_end → tool_result span
codex_stop / codex_session_start / codex_turn_complete → raw_step
translates tool_exec the same as tool_request (legacy + Codex paths)
translates tool_resolved to a tool_result span
ApmeCollector.ingestSpan dispatch
13 / 13
turn_start span opens a turn and records the prompt
turn_response span persists the response on the active turn
tool_call span increments tool_calls on the active turn
chat_end fallback does not clobber a prior closed turn response
Codex timeline path opens turn and counts tools (timelineEntryToSpans)
task_boundary span with signal=clear splits the run
task_boundary span with signal=manual closes the active task
task_boundary span with signal=idle_gap closes the active task
task_boundary span with signal=todo_complete closes the active task
task_boundary span with an unknown signal is dropped (no task close, no throw)
raw_step span inserts a steps row without lifecycle effects
session_meta span updates the model id on the run
timeline adapter feeds the same dispatch path correctly
eval-schema constants
1 / 1
exports a stable schema version string
apme-tuner.test.ts
18
/
18
189ms
correlation
4 / 4
returns 1 for perfectly aligned arrays
returns -1 for anti-aligned arrays
returns null when variance is zero
returns null when arrays are too short
parseProposal
3 / 3
extracts prompt + weights from a clean JSON blob
rejects proposals with too-short prompts
rejects proposals with no weights
extractOverall
3 / 3
parses overall score from a JSON blob
rescales 0-10 values
returns null when overall is missing
collectDisagreements
1 / 1
picks up tests-pass-judge-fail and user-reject-judge-approve cases
vibeCorrelation
2 / 2
returns high positive correlation when judge and user agree
returns null when there are too few labeled samples
ApmeTuner.tune
5 / 5
accepts a proposal that improves vibe correlation
rejects a proposal that makes correlation worse
rejects when the judge returns unparseable text
refuses to run when there are fewer than 3 disagreement samples
refuses when autoTune is disabled
bridge-core-sessions.test.ts
2
/
2
84ms
BridgeCore sessions_list
2 / 2
broadcastSessionsList enriches sessions before broadcast
sendInitialState sends enriched sessions_list to the connecting client
claude-transcript-reader.test.ts
7
/
7
9ms
readLastTurn
7 / 7
extracts assistant text and user prompt from a text-only turn
flags tool-only turns (tool_use blocks, no text blocks)
concatenates text across multiple assistant records for one turn
picks the LAST user prompt when multiple turns are in the file
returns null for missing file
skips malformed lines and continues parsing
accepts legacy string content on both roles
codex-turn-manager.test.ts
9
/
9
115ms
CodexTurnManager (hook-primary path)
6 / 6
happy path: UPS → tool_start → tool_end → stop emits one chat
codex_stop does not reset subsequent turn_index numbering
codex_stop finalizes APME turn (endedAt set, tool_calls flushed)
next prompt opens a fresh chat_start
hook freshness window suppresses PTY parser idle close
long-bash: hook tool_start + tool_end keep the same turn open
CodexTurnManager (PTY-only fallback when hooks absent)
3 / 3
spinner_start opens turn, prompt-source idle closes after deferral
timeout-source idle without prior tool_action does not latch
tool_action then timeout-idle latches; spinner_start closes prev + opens new
gateway-parity-fixtures.test.ts
33
/
33
8ms
Gateway parity fixtures
33 / 33
fixture set is non-empty
'auth-pairing-required-error.json' carries a valid frame discriminator
'chat-delta.json' carries a valid frame discriminator
'chat-final-with-tools.json' carries a valid frame discriminator
'connect-challenge.json' carries a valid frame discriminator
'connect-hello-ok-device-token.json' carries a valid frame discriminator
'connect-ok.json' carries a valid frame discriminator
'exec-approval-requested.json' carries a valid frame discriminator
'health-event.json' carries a valid frame discriminator
'logs-tail-response.json' carries a valid frame discriminator
'models-list-response.json' carries a valid frame discriminator
'rpc-error.json' carries a valid frame discriminator
'session-message.json' carries a valid frame discriminator
'session-tool.json' carries a valid frame discriminator
'sessions-changed.json' carries a valid frame discriminator
'tick.json' carries a valid frame discriminator
'auth-pairing-required-error.json' conforms to its frame shape
'chat-delta.json' conforms to its frame shape
'chat-final-with-tools.json' conforms to its frame shape
'connect-challenge.json' conforms to its frame shape
'connect-hello-ok-device-token.json' conforms to its frame shape
'connect-ok.json' conforms to its frame shape
'exec-approval-requested.json' conforms to its frame shape
'health-event.json' conforms to its frame shape
'logs-tail-response.json' conforms to its frame shape
'models-list-response.json' conforms to its frame shape
'rpc-error.json' conforms to its frame shape
'session-message.json' conforms to its frame shape
'session-tool.json' conforms to its frame shape
'sessions-changed.json' conforms to its frame shape
'tick.json' conforms to its frame shape
chat-final fixture carries the final-state fields the adapter depends on
exec.approval.requested fixture exposes options for the user prompt
openclaw-hook.test.ts
8
/
8
7ms
openclaw-hook → telemetry spans
8 / 8
OPENCLAW_IDLE_GAP_MS is conservative (60–180 s) so multi-turn collab stays together
chat.send produces exactly one turn_start span carrying the prompt text
chat.final with a response + tools yields turn_response + per-tool tool_result spans
chat.delta emits no spans — deltas are streaming chunks, not eval signals
chat.aborted emits a manual task_boundary so the user gesture closes the task immediately
chat.error emits nothing — the agent may retry, idle timer keeps running
idle-gap task_boundary span carries boundary_signal=idle_gap
all emitted spans propagate traceId for run correlation
opencode-client.test.ts
13
/
13
21ms
OpenCodeClient
12 / 12
health › should call /global/health
listSessions › should call /session with directory and limit
createSession › should POST to /session
sendMessage › should POST to /session/{id}/message with parts
sendMessage › should include model option when provided
abortSession › should POST to /session/{id}/abort
respondPermission › should POST allow response
respondPermission › should POST deny response
HTTP error handling › should throw on non-ok response
disconnect › should prevent reconnection after disconnect
url accessor › should return server URL
url accessor › should strip trailing slash
OpenCodeAdapter event mapping
1 / 1
should export OpenCodeSSEEvent type
opencode-hook.test.ts
8
/
8
7ms
opencode-hook → telemetry spans
8 / 8
tool part with status=running produces a tool_call span
tool part with status=completed produces a tool_result span
todowrite completion with all todos completed emits a task_boundary span (todo_complete)
todowrite completion with a pending todo does NOT emit a task_boundary
todowrite reads todos out of state.output JSON string when input is absent
non-tool part returns an empty span list
message.updated for user role with prompt emits a turn_start
message.updated for assistant role with response emits a turn_response
passive-observer.test.ts
4
/
4
7ms
passive-observer parsers
4 / 4
parses ps output without depending on fixed command columns
summarizes Claude transcripts and redacts tool secrets
summarizes Codex rollout metadata, context, and pending tool calls
maps lsof field output to Codex rollout files by pid
project-name.test.ts
14
/
14
74ms
resolveProjectName
10 / 10
returns git toplevel basename from a nested subdir
falls back to nearest package.json name when no git
skips package.json with empty name and keeps walking
falls back to cwd basename when no git and no package.json
returns 'unknown' for a basename-less root
AGENTDECK_PROJECT_NAME env var wins over everything
envOverride option takes precedence over env var
preserves scoped package name verbatim
git subprocess error (non-repo) does not throw
malformed package.json is ignored (walks to parent)
gitToplevelBasename
2 / 2
returns null outside a git worktree
returns repo basename from nested subdir
nearestPackageJsonName
2 / 2
returns null when no ancestor has package.json
returns nearest ancestor name
session-aggregator.test.ts
7
/
7
24ms
session-aggregator
7 / 7
uses ownState for the current session without fetching /health
fetches sibling /health and merges state and modelName
falls back to base session info when sibling /health fails (no cache)
returns cached state when sibling /health fails after a previous success
clearSiblingStateCache removes cached entry
buildEnrichedSessionsList includes own session and excludes daemon
buildEnrichedSessionsList returns own session in single-session mode without /health calls
tui-hud-entries.test.ts
16
/
16
24ms
buildHudEntries — primary anchoring
7 / 7
promotes the matching-port sibling to primary instead of appending a duplicate
patches the anchored sibling with the primary's live fields (matches macOS / Android)
uses the primary's projectName for the #N suffix grouping after anchoring
preserves the deterministic #N order when primary anchors a sibling slot
appends a synthetic primary when no sibling shares its agentType
skips the synthetic primary when a sibling shares agentType but no port match (duplicate guard)
never appends primary when agentType is daemon or openclaw
buildHudEntries — virtual OpenClaw
2 / 2
inserts a virtual OpenClaw row when gateway is available and sessions has none
does not insert a virtual row when sessions already contains an openclaw entry
buildHudEntries — hotkey eligibility (sibling-only)
1 / 1
primary and virtual rows do not consume hotkey slots
formatTaskEvalSuffix — task_end badge for each outcome class
6 / 6
renders ✓ for success
renders ✗ for fail
renders △ for partial
renders ⊘ for abandoned (manual cancel)
renders ? as score placeholder when judge has not produced a number yet
returns empty string while the eval is still pending (outcome undefined)
codex-install.test.ts
29
/
29
34ms
codex-mini-toml: applyManagedBlock
3 / 3
appends fence when absent
replaces existing fence block
apply twice is idempotent
codex-mini-toml: removeManagedBlock
2 / 2
leaves user content
is idempotent without fence
codex-mini-toml: hasTopLevelKeyOutsideFence
3 / 3
detects user notify key
ignores notify key inside table
ignores notify key inside fence
codex-mini-toml: hasTableOutsideFence
5 / 5
detects user [otel] table
detects user [otel.exporter] dotted table
detects array-of-table header [[hooks.Stop]]
ignores [otel] inside fence
ignores [otelfoo] (word boundary)
codex-mini-toml: quoted
3 / 3
escapes backslash and double quote
escapes newline and tab
passes simple ASCII
codex-install: managedBlockBody
3 / 3
roundtrip preserves user TOML byte-for-byte (load-bearing)
matches Codex schema (lifecycle hooks + endpoints + notify dummy)
omits conflicting optional channels when asked
codex-install: install / uninstall (file I/O)
10 / 10
creates config with fence when file is absent
preserves user content when installing into existing config
skips when user already has [features] table outside fence
skips when user already has [hooks] table outside fence
omits notify when user has top-level notify
omits OTel when user has [otel] table
uninstall strips fence and preserves user content
uninstall is idempotent when no config exists
honours AGENTDECK_NO_CODEX_HOOKS=1 opt-out
install is idempotent (same port → no rewrite)
bridge-client.test.ts
3
/
3
3.4s
BridgeClient — port provider
3 / 3
skips connect when provider returns null
connects once provider returns a live port
rebinds to a new port when provider value changes
session-slot-button.test.ts
12
/
12
8ms
computeCenterSlot
6 / 6
SD+ 4x2 → bottom-center
SD MK2 5x3 → true geometric center
SD XL 8x4 → middle row, mid column
SD Mini 3x2 → bottom-center
single key device → slot 0
clamps degenerate zero rows/cols to slot 0
computeCenterCluster
6 / 6
SD+ 4x2 → 2x2 cluster on geometric center (slots 1,2,5,6)
SD XL 8x4 → 2x2 cluster on geometric center (slots 11,12,19,20)
SD MK2 5x3 → single full hero on slot 7
SD Mini 3x2 (odd cols) → single full hero on slot 4
single-key device → single full hero on slot 0
degenerate zero dimensions → single hero on slot 0
session-slot-manager.test.ts
8
/
8
13ms
SessionSlotManager detail layout
8 / 8
re-points detail focus onto the codex fold representative when the focused thread is absorbed
exits detail view when the focused session is gone with no fold successor
folds codex companion threads by project before slot assignment
renders connected no-session list as status cards instead of text-only empty buttons
puts processing tool info before OpenClaw presets
keeps a processing status tile even before tool metadata arrives
aliases the model name on detail MODEL surfaces (status card + OpenClaw preset)
uses actual parser options and reserves MORE only when awaiting overflow exists
format-utils.test.ts
18
/
18
8ms
adjustUsagePercent
11 / 11
returns undefined when percent is null
returns undefined when percent is undefined
returns percent unchanged when resetsAt is null
returns percent unchanged when resetsAt is undefined
returns percent unchanged when resetsAt is in the future
returns 0 when resetsAt is in the past (window expired)
returns 0 when resetsAt equals now (edge case)
handles invalid date string gracefully (returns percent)
handles empty string resetsAt (returns percent)
returns percent unchanged when resetsAt is far in the past (>1h)
still returns 0 just after the 1h threshold boundary
formatResetTime
7 / 7
returns "now" when resetsAt is in the past
returns undefined for null input
returns minutes-only for < 1h remaining
returns hours and minutes for < 24h remaining
returns days and hours for >= 24h remaining
omits minutes when exactly on the hour
passes through pre-formatted strings (no T)
llm-settings.test.ts
10
/
10
9ms
llm-settings
10 / 10
returns defaults when settings.json is missing
reads llm.mlx pin from settings.json
treats "qwen3-30b" and "default" as placeholders (unpinned)
falls back to apme.judge.model when llm.mlx is absent
strips /chat/completions suffix from legacy endpoints
prefers llm.mlx over apme.judge when both set
resolveMlxModel: pin > probe > fallback
pickMlxModel: 4-layer priority (pin > fallback > first > null)
mlxChatUrl reflects endpoint setting
caches result for TTL window
session-utils.test.ts
13
/
13
20ms
agentTypeRank
1 / 1
ranks openclaw first, then claude-code, codex-cli, opencode, others
naturalLabelCompare
2 / 2
orders numeric chunks naturally (Agent 2 before Agent 10)
treats undefined as empty string
sortSessions
6 / 6
places openclaw before claude-code regardless of project
sorts by project name within the same agent type
sorts numbered project names naturally (#2 before #10)
breaks ties on startedAt ascending (oldest first)
falls back to id natural-compare when startedAt ties to the same ms
does not mutate the input array
assignDisplayNames
4 / 4
passes single sessions through without #N suffix
adds #1/#2 suffixes for duplicate (project, agentType) tuples in input order
numbers same project across different agentTypes independently
produces the same #N assignment as a deterministic sort + display pipeline
svg-renderers/model-alias.test.ts
7
/
7
4ms
aliasModelName
3 / 3
shortens claude family-major-minor
drops trailing date suffix on claude releases
passes gpt and unknown strings through unchanged
formatModelEffort
4 / 4
returns aliased model when no effort to show
appends non-default effort when it fits
truncates aliased model name to fit budget with effort suffix
returns empty string for missing model
timeline-summarizer.test.ts
13
/
13
5ms
extractTopicHintWithKind — Korean polite-closer robustness
7 / 7
returns the cleaned topic when the response opens with "네, …"
returns the original (kind=fallback) when the entire response is just a polite closer
returns null for empty / very short text (still)
skips lone heading markers and code fences
a heading WITH text becomes the topic (heading is the title)
strips list bullet markers
extractTopicHint convenience returns just the hint string
promptSnippetFallback
6 / 6
returns first sentence trimmed
returns whole string when no sentence terminator
truncates to maxLen with ellipsis
cuts at first newline if no sentence terminator
returns null for empty / very short input
handles Korean prompts
timeline-task-hierarchy.test.ts
40
/
40
15ms
deduplicateEntry — task hierarchy
3 / 3
always adds task_start even with identical raw within 8s
always adds task_end with same boundarySignal back-to-back
still dedupes ordinary chat_start within 8s
timelineIconKey
5 / 5
maps task entries to "task"
maps tool_request status to success/error/awaiting
chat_start in flight is "running"; chat_end is "success"
error → error; user_action → user; memory_recall → memory
every key has an e-ink glyph of constant 4-char width
isInFlightTask
5 / 5
task_start without matching task_end is in flight
task_start whose task_end (same taskId) appeared is finished
mismatched taskId on task_end does not close it
task_start without taskId is never considered in flight
non-task_start entries are never in flight
isRotatingEntry
5 / 5
chat_start always rotates (icon-key running)
orphan task_start rotates via in-flight predicate
closed task_start does not rotate
static rows do not rotate
eval_result and task_end never rotate
parseTimelineMarkdown
9 / 9
returns single text line for plain text
parses headings 1-6 with required space
parses bullets and numbered lists
handles code fence — verbatim lines, not interpreted
blank line → blank kind
parses tables with header separator
parses tables without separator (no header)
table block ends at first non-table line
quote lines parse
parseInlineSpans
9 / 9
returns empty for empty string
returns single plain span for plain text
parses **bold**
parses *italic* (single star)
parses `code` inline
parses [text](url) link
unclosed ** falls back to plain
multiple spans in one line
first-match-wins: ** consumed before * (no double-italic split)
prepareMarkdownDetail
4 / 4
preserves markdown markers (chat-response detail goes to client)
still filters system JSON blobs
collapses 3+ blank lines but keeps double-blank paragraph break
contrasts with cleanDetailText which strips markdown (non-chat path)
▣
Android
PASS
JUnit + Robolectric · 13 files · 175 tests
data.DashboardOrientationTest
3
/
3
3ms
defaults keep e-ink fixed and tablets auto-rotating
legacy unspecified preference still counts as auto
manual rotate toggles fixed modes from auto using current posture
net.ProtocolTest
33
/
33
7.2s
parse state_update with all permission modes
parse connection connected with sessionId
parse sessions_list
PluginCommands selectOption generates valid JSON
parse timeline_event with fractional timestamps
parse button_state
PluginCommands utility with and without value
parse invalid json returns null
parse timeline_history
parse timeline_event upsert
parse state_update with processing state and tool info
PluginCommands respond escapes special characters
PluginCommands respond generates valid JSON
parse state_update with permission options
parse connection disconnected
parse missing type returns null
parse deck_slot_map
parse usage_update with extra usage
parse state_update with ollama status
PluginCommands navigateOption
parse state_update with agent capabilities
parse unknown type returns null
parse user_prompt
BridgeTimelineEntry converts to TimelineEntry
parse state_update with model catalog
parse state_update ignores unknown fields
parse state_update with idle state
parse display_state sleep and wake
parse voice_state
parse encoder_state
PluginCommands interrupt and escape
parse usage_update with rate limits
parse timeline_event
state.SessionMetricsTest
8
/
8
8ms
reset clears all metrics
onMessageReceived updates lastMessageAt
onConnected sets connectedSince
onDisconnected clears connectedSince
clean reconnect after disconnect does not increment
initial state has no connection
reconnect increments reconnectCount when still connected
onMessageReceived increments count
state.TimelineDisplayScenarioTest
5
/
5
7ms
multi-agent dashboard timeline projects meaningful session rows
synthetic chat_start is suppressed once completion arrives
chat_end with summaryKind survives even when chat_response paired
codex otel low-signal tool entries are suppressed
same timestamp summaries stay separate by agent and project
state.TimelineStoreTest
47
/
47
33ms
addEntry drops openclaw placeholder with status-only detail
timelineDisplayGroups keeps meaningful chat_start alongside response
addEntry allows same type+summary after 5s
addEntries filters otel noise from bulk replay
clear empties the store
upsertEntry updates existing entry within 1s tolerance
upsertEntry without taskId falls back to ts-window match
timelineDisplayGroups keeps in-flight chat_start until completion
groupConsecutive empty list returns empty
addEntry drops openclaw placeholder tool rows
addEntry keeps meaningful raw on otel-active session
timelineLifecycleBounds pairs response with prior start
timelineDisplayGroups hides chat_end when chat_response already represents the same turn
upsertEntry adds new entry if no match
addEntry stores entry
groupConsecutive does not merge tool_request across sessions
groupConsecutive groups same summary within 60s
timelineDisplayGroups keeps independent sessions visible
updateLastOfType modifies the last matching entry
upsertEntry refuses to insert otel noise via add fallback
addEntries merges and deduplicates
addEntry drops openclaw placeholder with arbitrary future status
upsertEntry preserves existing detail if new detail is null
addEntry keeps real openclaw tool rows
groupConsecutive splits tool_request after 10s gap
addEntries sorts by timestamp
upsertEntry preserves existing summaryKind when new entry omits it
groupConsecutive does not merge chat_end across projects
upsertEntry preserves timeline attribution
groupConsecutive splits different types
addEntry allows different type within 5s
addEntry drops openclaw placeholder with failed status
groupConsecutive single entry
addEntry deduplicates within 5s window
upsertEntry propagates summaryKind progression heuristic to llm
timelineDisplayGroups collapses synthetic chat_start once response arrives
addEntry allows different summary within 5s
groupConsecutive groups chat_end by type only within 60s
addEntry drops codex otel low-signal tool noise
addEntry caps at MAX_ENTRIES
groupConsecutive does not merge distinct run ids on same session
timelineDisplayGroups keeps in-flight start when later completion has distinct run id
addEntry keeps openclaw placeholder summary when detail has content
groupConsecutive requires same summary for other types
updateLastOfType no-op if type not found
groupConsecutive groups tool_request within 10s
upsertEntry merges task_end by taskId beyond the 1s tolerance window
state.TimelineTaskHierarchyTest
24
/
24
17ms
mismatched taskId on task_end does not close it
stripMarkdownInline drops markers including table syntax
detail redundancy fires when prefix matches summary
detail redundancy does not fire when content is genuinely new
same project two sessions are not the same context anymore
closed task_start does not rotate
non task_start entries are never in flight
task_start whose task_end (same taskId) appeared is finished
static rows do not rotate
stripMarkdownInline preserves non-table pipes
task hierarchy is never elided by display projection
task_start without matching task_end is in flight
chat_start always rotates via icon-key running
task entries never group with each other
iconKey resolves tool_request status to success error awaiting
taskId is the strongest grouping key
iconKey resolves to Task for task entries
eval_result and task_end never rotate
stripMarkdownInline handles single-cell table row
markdown parser parity with shared - basics
task_start without taskId is never considered in flight
orphan task_start rotates via in-flight predicate
markdown parser code fence is verbatim
eink glyphs are constant 4-char width
terrarium.TerrariumStateTest
7
/
7
267ms
claude processing does not bleed into OpenClaw crayfish on aggregate view
stale gateway error is ignored when gateway is unavailable
authenticated gateway shows OpenClaw at rest
gateway error surfaces sick OpenClaw
reachable gateway without auth hides OpenClaw and workers
OpenClaw processing routes its own crayfish
daemon aggregate keeps OpenClaw crayfish calm while claude works
terrarium.renderer.EinkAnimationTimingTest
3
/
3
5ms
animation frame advance is elapsed-time based and bounded
fish simulation scales movement for partial color frames
color e-ink animation uses video-like cadence
ui.eink.EinkAttentionPanelTest
3
/
3
14ms
non-focused awaiting session hides unavailable live prompt fields
primary awaiting session is surfaced when not represented by siblings
featured attention prefers focused awaiting session
ui.eink.SessionDisplayOrderingTest
6
/
6
4ms
naturalLabelCompare orders Agent 2 before Agent 10
compareSessionsForDisplay breaks ties on startedAt ascending (oldest first)
compareSessionsForDisplay sorts openclaw before claude-code regardless of project
compareSessionsForDisplay tie-breaks on natural id when startedAt is identical
agentTypeRank places openclaw first
compareSessionsForDisplay is stable across re-sorts of any input order
ui.monitor.OpenClawDisplayLinesTest
4
/
4
3ms
empty when no default tagged
empty catalog yields empty list
keeps only default model when present
empty when default is unavailable
ui.monitor.SubscriptionLineTest
12
/
12
10ms
subscriptionTrailing flags expired for past dates
malformed until renders renewal needed
future ISO8601 with offset renders date suffix
parseUntilInstant accepts ISO8601
bare date in past renders renewal needed
future ISO8601 with fractional seconds renders date suffix
subscriptionTrailing returns date for future
parseUntilInstant accepts blank as null
past until renders renewal needed
bare date string parses as UTC midnight
subscriptionTrailing returns null for blank or null
null until renders name only
util.TimeFormatUtilsTest
20
/
20
9ms
gaugeBar clamps below 0
formatCount thousands show K
formatBytes kilobytes
formatDurationCompact seconds
formatDurationCompact exact minutes no seconds
formatDurationCompact sub-second
formatCount int overload works
gaugeBar clamps above 100
formatBytes small values
formatCount small numbers unchanged
gaugeBar 50 percent is half filled
gaugeBar 0 percent is all empty
formatBytes megabytes
formatResetTime returns original on parse failure
gaugeBar custom width
formatBytes gigabytes
formatUptime zero returns 0 colon 00
formatCount millions show M
formatDurationCompact minutes
gaugeBar 100 percent is all filled
▩
Robot Framework
PASS
ESP32 Hardware Tests · 1 suites · 4 scenarios · 7 tests · 4 boards
| Board | Build | Flash+Boot | Boot Time | FW Size | Boot Heap | Latency |
|---|---|---|---|---|---|---|
| 86Box | 20.9s | — | — | — | — | — |
| IPS 3.5" | 17.2s | — | — | — | — | — |
| Round | 17.1s | — | — | — | — | — |
| TC001 | 15.5s | — | — | — | — | — |
01_build.robot
7
/
7
Build And Verify
4 boards
4 / 4
Given the "${board}" firmware is built
Then the firmware binary should exist for "${board}"
And the firmware size should be sane for "${board}"
And the partitions binary should exist for "${board}"
✓ 86Box✓ IPS 3.5"✓ Round✓ TC001
Box 86 Build And Verify
IPS 3.5 Build And Verify
Round AMOLED Build And Verify
Ulanzi TC001 Build And Verify
Boot Test Environment Builds Successfully
Source Files Are Present
PlatformIO Configuration Parses Cleanly
▦
Scenario Coverage
User scenario mapping against actual test results
| Score | Scenario | Priority | Unit | Integ. | Platform | E2E |
|---|---|---|---|---|---|---|
| 6/8 |
Session Lifecycle
Start session -> processing -> idle -> stop. Core state machine transitions that drive all UI.
|
critical | 4/4 | 1/2 | 1/2 | — |
| 3/5 |
Permission Flow
Agent requests permission -> user sees options -> approves/denies -> state transitions back.
|
critical | 2/2 | 0/2 | 1/1 | — |
| 7/7 |
Multi-agent Monitoring
Daemon hub orchestrates multiple session bridges, unified dashboard for all agents.
|
high | 3/3 | 4/4 | — | — |
| 7/7 |
Device Connection
mDNS discovery -> auth token validation -> WebSocket/serial state sync with dashboard clients.
|
high | 3/3 | 3/3 | 1/1 | — |
| 4/5 |
Timeline Aggregation
Events from multiple sessions -> daemon relay -> dedup -> unified timeline for all clients.
|
high | 2/2 | 1/1 | 1/2 | — |
| 1/1 |
Voice Recording & Transcription
Audio capture -> whisper transcription -> text delivery to Claude PTY or clipboard.
|
medium | — | 1/1 | — | — |
| 2/3 |
Encoder & Button Interaction
Dial rotate -> option scroll -> push select. Button press -> action trigger. Label rendering.
|
medium | 2/3 | — | — | — |
| 4/5 |
Usage Tracking & Rate Limits
API usage fetch -> rate limit gauge -> sibling relay (429 prevention) -> dashboard display.
|
medium | 0/1 | 2/2 | 2/2 | — |
| 2/2 |
Daemon Singleton Guard
PID check -> port probe -> prevent double daemon. Auto-fallback on port conflict.
|
medium | 1/1 | 1/1 | — | — |
| 1/1 |
Hook Installation & Migration
Install Claude Code hooks -> migrate v2.0 flat format to v2.1 matcher groups -> port injection.
|
medium | 1/1 | — | — | — |
▤
Coverage
v8 provider · 17,398 lines tracked
Lines ≥17%: 35.9%
Functions ≥15%: 32.2%
Branches ≥14%: 31.6%
Statements ≥16%: 34.4%
bridge
Lines
Stmts
Funcs
Branch
4553/12604 lines covered
plugin
Lines
Stmts
Funcs
Branch
1010/3612 lines covered
shared
Lines
Stmts
Funcs
Branch
483/944 lines covered
hooks
Lines
Stmts
Funcs
Branch
194/238 lines covered
| File | Stmts | Branch | Funcs | Lines | |
|---|---|---|---|---|---|
| bridge/src/adb-reverse.ts | 0% | 0% | 0% | 0% | |
| bridge/src/check-deps.ts | 0% | 0% | 0% | 0% | |
| bridge/src/cli.ts | 0% | 0% | 0% | 0% | |
| bridge/src/daemon-server.ts | 0% | 0% | 0% | 0% | |
| bridge/src/daemon-ws-client.ts | 0% | 0% | 0% | 0% | |
| bridge/src/daemon.ts | 0% | 0% | 0% | 0% | |
| bridge/src/diag-analyzer.ts | 0% | 0% | 0% | 0% | |
| bridge/src/event-journal.ts | 0% | 0% | 0% | 0% | |
| bridge/src/hook-migration.ts | 0% | 0% | 0% | 0% | |
| bridge/src/index.ts | 0% | 0% | 0% | 0% | |
| bridge/src/log-stream.ts | 0% | 100% | 0% | 0% | |
| bridge/src/pty-ringbuffer.ts | 0% | 0% | 0% | 0% | |
| bridge/src/session-focus-relay.ts | 0% | 0% | 0% | 0% | |
| bridge/src/terminal-status.ts | 0% | 0% | 0% | 0% | |
| bridge/src/tts.ts | 0% | 0% | 0% | 0% | |
| bridge/src/types.ts | 0% | 0% | 0% | 0% | |
| bridge/src/utility-proxy.ts | 0% | 0% | 0% | 0% | |
| bridge/src/version-check.ts | 0% | 0% | 0% | 0% | |
| bridge/src/voice-assistant.ts | 0% | 0% | 0% | 0% | |
| bridge/src/voice.ts | 0% | 0% | 0% | 0% | |
| bridge/src/wake-word.ts | 0% | 0% | 0% | 0% | |
| bridge/src/whisper-server-manager.ts | 0% | 0% | 0% | 0% | |
| bridge/src/wifi-config.ts | 0% | 0% | 0% | 0% | |
| bridge/src/apme/dashboard-html.ts | 0% | 100% | 0% | 0% | |
| bridge/src/apme/types.ts | 0% | 0% | 0% | 0% | |
| bridge/src/d200h/hid-protocol.ts | 0% | 0% | 0% | 0% | |
| bridge/src/d200h/image-renderer.ts | 0% | 0% | 0% | 0% | |
| bridge/src/modules/adb-module.ts | 0% | 0% | 0% | 0% | |
| bridge/src/modules/d200h-module.ts | 0% | 0% | 0% | 0% | |
| bridge/src/modules/d200h-renderer.ts | 0% | 0% | 0% | 0% | |
| bridge/src/modules/index.ts | 0% | 0% | 0% | 0% | |
| bridge/src/modules/mdns-module.ts | 0% | 100% | 0% | 0% | |
| bridge/src/modules/pixoo-module.ts | 0% | 0% | 0% | 0% | |
| bridge/src/modules/serial-module.ts | 0% | 0% | 0% | 0% | |
| bridge/src/modules/types.ts | 0% | 0% | 0% | 0% | |
| bridge/src/pixoo/pixoo-settings.ts | 0% | 0% | 0% | 0% | |
| bridge/src/tui/screen.ts | 0% | 0% | 0% | 0% | |
| bridge/src/types/picovoice.d.ts | 0% | 0% | 0% | 0% | |
| bridge/src/adapters/opencode-adapter.ts | 0% | 0% | 0% | 1% | |
| bridge/src/apme/index.ts | 3% | 0% | 0% | 3% | |
| bridge/src/pixoo/pixoo-font.ts | 3% | 0% | 0% | 4% | |
| bridge/src/apme/hw-sampler.ts | 6% | 4% | 20% | 4% | |
| bridge/src/ollama-probe.ts | 5% | 0% | 0% | 5% | |
| bridge/src/mdns.ts | 5% | 3% | 9% | 5% | |
| bridge/src/tui/dashboard.ts | 5% | 11% | 3% | 6% | |
| bridge/src/antigravity-local.ts | 7% | 4% | 20% | 6% | |
| bridge/src/mlx-probe.ts | 6% | 0% | 0% | 6% | |
| bridge/src/pixoo/pixoo-camera.ts | 6% | 0% | 0% | 7% | |
| bridge/src/apme/outcome.ts | 8% | 2% | 8% | 8% | |
| bridge/src/pixoo/pixoo-renderer.ts | 6% | 0% | 0% | 8% | |
| bridge/src/pixoo/pixoo-client.ts | 11% | 0% | 0% | 11% | |
| bridge/src/pixoo/pixoo-sprites.ts | 10% | 0% | 8% | 11% | |
| bridge/src/pixoo/pixoo-bridge.ts | 11% | 0% | 0% | 13% | |
| bridge/src/esp32-serial.ts | 12% | 12% | 4% | 13% | |
| bridge/src/logger.ts | 18% | 10% | 17% | 15% | |
| bridge/src/timeline-summarizer.ts | 13% | 0% | 0% | 15% | |
| bridge/src/gateway-probe.ts | 12% | 0% | 0% | 16% | |
| bridge/src/display-monitor.ts | 20% | 9% | 18% | 22% | |
| bridge/src/usage-api.ts | 26% | 12% | 50% | 28% | |
| bridge/src/codex-auth.ts | 36% | 8% | 25% | 37% | |
| bridge/src/bridge-core.ts | 37% | 39% | 26% | 39% | |
| bridge/src/usage-tracker.ts | 44% | 55% | 23% | 44% | |
| bridge/src/opencode-client.ts | 40% | 38% | 57% | 44% | |
| bridge/src/model-catalog.ts | 39% | 17% | 36% | 46% | |
| bridge/src/passive-observer.ts | 45% | 40% | 42% | 50% | |
| bridge/src/pty-manager.ts | 54% | 31% | 53% | 55% | |
| bridge/src/hook-server.ts | 53% | 29% | 53% | 55% | |
| bridge/src/ws-server.ts | 60% | 31% | 58% | 59% | |
| bridge/src/adapters/openclaw.ts | 60% | 43% | 55% | 62% | |
| bridge/src/auth.ts | 59% | 37% | 75% | 63% | |
| bridge/src/apme/recommend.ts | 58% | 32% | 67% | 64% | |
| bridge/src/tui/terrarium.ts | 62% | 48% | 64% | 64% | |
| bridge/src/usage-event.ts | 68% | 72% | 100% | 67% | |
| bridge/src/tui/renderer.ts | 70% | 57% | 92% | 71% | |
| bridge/src/apme/classifier.ts | 65% | 66% | 74% | 71% | |
| bridge/src/adapters/index.ts | 71% | 67% | 100% | 71% | |
| bridge/src/adapters/claude-code.ts | 73% | 40% | 50% | 73% | |
| bridge/src/session-registry.ts | 70% | 46% | 71% | 73% | |
| bridge/src/tui/ansi.ts | 75% | 49% | 83% | 75% | |
| bridge/src/apme/store.ts | 64% | 68% | 77% | 77% | |
| bridge/src/state-machine.ts | 80% | 62% | 81% | 80% | |
| bridge/src/adapters/monitor.ts | 81% | 60% | 87% | 81% | |
| bridge/src/apme/runner.ts | 76% | 63% | 69% | 82% | |
| bridge/src/adapters/pty-adapter.ts | 82% | 81% | 65% | 82% | |
| bridge/src/adapters/codex-cli.ts | 83% | 100% | 75% | 83% | |
| bridge/src/session-timeline-relay.ts | 80% | 62% | 71% | 84% | |
| bridge/src/apme/adapters/codex-hook.ts | 79% | 60% | 100% | 85% | |
| bridge/src/apme/collector.ts | 80% | 68% | 88% | 85% | |
| bridge/src/session-aggregator.ts | 85% | 58% | 86% | 85% | |
| bridge/src/apme/adapters/codex-turn-manager.ts | 78% | 58% | 95% | 86% | |
| bridge/src/apme/http.ts | 84% | 79% | 88% | 86% | |
| bridge/src/apme/tuner.ts | 87% | 73% | 96% | 89% | |
| bridge/src/output-parser.ts | 89% | 83% | 100% | 93% | |
| bridge/src/apme/adapters/timeline.ts | 95% | 80% | 100% | 94% | |
| bridge/src/codex-output-parser.ts | 90% | 84% | 93% | 94% | |
| bridge/src/apme/classify-turn.ts | 91% | 80% | 100% | 95% | |
| bridge/src/apme/adapters/opencode-hook.ts | 96% | 83% | 100% | 95% | |
| bridge/src/apme/adapters/openclaw-hook.ts | 92% | 70% | 100% | 96% | |
| bridge/src/utils/project-name.ts | 94% | 88% | 100% | 96% | |
| bridge/src/timeline-store.ts | 97% | 89% | 100% | 98% | |
| bridge/src/apme/claude-transcript-reader.ts | 91% | 76% | 100% | 100% | |
| bridge/src/apme/settings.ts | 97% | 86% | 100% | 100% | |
| bridge/src/apme/adapters/claude-hook.ts | 100% | 75% | 100% | 100% | |
| bridge/src/apme/adapters/claude-pty.ts | 100% | 88% | 100% | 100% | |
| bridge/src/tui/gauge.ts | 98% | 96% | 100% | 100% | |
| hooks/src/install.ts | 60% | 68% | 75% | 63% | |
| hooks/src/codex-install.ts | 84% | 62% | 87% | 87% | |
| hooks/src/codex-mini-toml.ts | 97% | 91% | 100% | 100% | |
| plugin/src/agent-link.ts | 0% | 0% | 0% | 0% | |
| plugin/src/encoder-registry.ts | 0% | 0% | 0% | 0% | |
| plugin/src/encoder-takeover.ts | 0% | 0% | 0% | 0% | |
| plugin/src/label-summarizer.ts | 0% | 0% | 0% | 0% | |
| plugin/src/plugin.ts | 0% | 0% | 0% | 0% | |
| plugin/src/project-picker.ts | 0% | 0% | 0% | 0% | |
| plugin/src/project-scanner.ts | 0% | 0% | 0% | 0% | |
| plugin/src/timeline-store.ts | 0% | 0% | 0% | 0% | |
| plugin/src/voice-local.ts | 0% | 0% | 0% | 0% | |
| plugin/src/actions/iterm-dial.ts | 0% | 0% | 0% | 0% | |
| plugin/src/actions/option-dial.ts | 0% | 0% | 0% | 0% | |
| plugin/src/actions/session-slot-button.ts | 0% | 0% | 0% | 0% | |
| plugin/src/actions/utility-dial.ts | 0% | 0% | 0% | 0% | |
| plugin/src/actions/voice-dial.ts | 0% | 0% | 0% | 0% | |
| plugin/src/renderers/agent-logos.ts | 0% | 0% | 0% | 0% | |
| plugin/src/renderers/session-slot-renderer.ts | 0% | 0% | 0% | 0% | |
| plugin/src/renderers/text-utils.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/apme.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/brightness.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/darkmode.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/diag.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/index.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/macos.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/media.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/mic.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/permission-mode.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/timer.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/tower.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/types.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/volume.ts | 0% | 0% | 0% | 0% | |
| plugin/src/bridge-client.ts | 55% | 36% | 67% | 60% | |
| plugin/src/renderers/timeline-renderer.ts | 58% | 34% | 75% | 63% | |
| plugin/src/session-slot-manager.ts | 66% | 54% | 68% | 66% | |
| plugin/src/log.ts | 59% | 50% | 33% | 67% | |
| plugin/src/utility-modes/usage.ts | 65% | 47% | 43% | 73% | |
| plugin/src/renderers/usage-dial-renderer.ts | 75% | 55% | 91% | 80% | |
| plugin/src/renderers/button-renderer.ts | 82% | 72% | 100% | 82% | |
| plugin/src/renderers/response-renderer.ts | 85% | 40% | 92% | 87% | |
| plugin/src/connection-manager.ts | 86% | 79% | 85% | 90% | |
| plugin/src/renderers/voice-renderer.ts | 88% | 57% | 87% | 90% | |
| plugin/src/renderers/option-renderer.ts | 92% | 69% | 100% | 93% | |
| plugin/src/layout-manager.ts | 95% | 92% | 100% | 95% | |
| plugin/src/renderers/utility-renderer.ts | 98% | 57% | 100% | 98% | |
| plugin/src/renderers/qr-renderer.ts | 98% | 90% | 80% | 98% | |
| plugin/src/center-slot.ts | 100% | 100% | 100% | 100% | |
| shared/src/adapter.ts | 0% | 100% | 100% | 0% | |
| shared/src/command-builders.ts | 0% | 100% | 0% | 0% | |
| shared/src/design-tokens.ts | 0% | 100% | 100% | 0% | |
| shared/src/eval-schema.ts | 0% | 100% | 0% | 0% | |
| shared/src/gateway-protocol.ts | 0% | 100% | 100% | 0% | |
| shared/src/index.ts | 0% | 0% | 0% | 0% | |
| shared/src/net-utils.ts | 0% | 0% | 0% | 0% | |
| shared/src/telemetry-envelope.ts | 0% | 0% | 0% | 0% | |
| shared/src/voice-paths.ts | 0% | 0% | 0% | 0% | |
| shared/src/svg-renderers/index.ts | 0% | 0% | 0% | 0% | |
| shared/src/svg-renderers/text-utils.ts | 2% | 0% | 0% | 2% | |
| shared/src/svg-renderers/session-slot-renderer.ts | 10% | 5% | 11% | 9% | |
| shared/src/svg-renderers/agent-logos.ts | 14% | 0% | 0% | 14% | |
| shared/src/state-colors.ts | 15% | 0% | 0% | 17% | |
| shared/src/format-utils.ts | 42% | 41% | 29% | 42% | |
| shared/src/session-utils.ts | 41% | 41% | 47% | 43% | |
| shared/src/timeline-summarizer.ts | 73% | 63% | 57% | 76% | |
| shared/src/timeline.ts | 74% | 70% | 100% | 80% | |
| shared/src/timeline-icons.ts | 88% | 82% | 75% | 85% | |
| shared/src/llm-settings.ts | 100% | 98% | 100% | 100% | |
| shared/src/protocol.ts | 100% | 100% | 100% | 100% | |
| shared/src/states.ts | 100% | 100% | 100% | 100% | |
| shared/src/timeline-markdown.ts | 99% | 92% | 100% | 100% |