
> cli-jaw@1.5.0 test
> tsx --experimental-test-module-mocks --test tests/*.test.ts tests/unit/*.test.ts --test-reporter=tap

✔ AcpClient buildSpawnArgs: auto includes full allow-all flags (0.400333ms)
✔ AcpClient buildSpawnArgs: yolo includes full allow-all flags (0.052708ms)
✔ AcpClient buildSpawnArgs: safe mode omits allow-all flags (0.043333ms)
✔ AcpClient handles agent requests (id + method) before notifications (0.100791ms)
✔ AcpClient emits notifications (method without id) (0.289417ms)
✔ AcpClient request resolves from matching response id (1.957458ms)
✔ AcpClient request rejects immediately when stdin is not writable (0.187625ms)
✔ AcpClient permission response accepts id-based options (0.10725ms)
✔ requestWithHeartbeat resolves and cleans up timers on response (0.193792ms)
✔ requestWithActivityTimeout rejects on idle timeout when no activity (100.719ms)
✔ _handleLine resets idle timer via _activityPing on valid JSON (0.146ms)
✔ P100-ES-001: employee_sessions 테이블 스키마가 db.js에 정의됨 (1.584959ms)
✔ P100-ES-002: getEmployeeSession 없는 ID 조회 시 undefined (39.682667ms)
✔ P100-ES-003: upsertEmployeeSession 저장 후 조회 일치 (1.012917ms)
✔ P100-ES-004: upsertEmployeeSession 같은 ID로 업데이트 시 덮어쓰기 (1.533291ms)
✔ P100-ES-005: clearAllEmployeeSessions 전체 삭제 후 조회 시 undefined (1.489333ms)
✔ P100-ES-006: clearAllEmployeeSessions가 main session 테이블을 건드리지 않음 (0.770042ms)
✔ P100-ES-007: Phase Merging prompt includes recommendation (0.563708ms)
✔ extractFromAcpUpdate keeps full thought detail while previewing the label (0.446542ms)
✔ extractFromAcpUpdate handles tool_call and tool_call_update fallback (0.325709ms)
✔ extractFromAcpUpdate handles agent_message_chunk content shapes (0.078208ms)
✔ extractFromAcpUpdate handles plan and unknown update types (0.630041ms)
✔ claude stream_event tool labels are deduped (1.181167ms)
✔ claude assistant fallback works when stream was not seen (0.308208ms)
✔ claude assistant blocks are ignored after stream event (0.074792ms)
✔ claude system compact events emit compacting and boundary labels (0.060958ms)
✔ extractSessionId handles all supported CLIs (0.053833ms)
✔ tool label extraction fixture matrix covers codex, gemini, and opencode variants (1.063792ms)
✔ claude non-tool events do not emit labels (0.244541ms)
✔ claude thinking_delta buffer is flushed on non-thinking event (0.251292ms)
✔ codex reasoning keeps full detail while preview label stays short (0.070083ms)
✔ claude input_json_delta buffer adds detail to tool label on block_stop (0.251209ms)
✔ extractFromEvent updates context for each CLI path (0.132125ms)
✔ extractToolLabel keeps backward compatibility and claude keys are deterministic (0.066541ms)
✔ forwarder skips telegram-origin responses (3.380292ms)
✔ forwarder skips error responses (0.365791ms)
✔ forwarder falls back to plain text when HTML send fails (1.022916ms)
✔ forwarder handles mixed origin/error events deterministically (0.447667ms)
✔ forwarder chunks long messages into multiple sends (1.65525ms)
✔ forwarder does nothing when type is not agent_done or chatId is missing (0.390459ms)
✔ markdownToTelegramHtml converts markdown while preserving escaped html (0.262667ms)
✔ escapeHtmlTg escapes angle brackets and ampersands (0.133125ms)
✔ chunkTelegramMessage splits by newline when possible (0.115208ms)
✔ chunkTelegramMessage falls back to hard split without newlines (0.359333ms)
✔ createForwarderLifecycle attach/detach is idempotent (0.098834ms)
✔ createForwarderLifecycle can attach again after detach (0.054042ms)
✔ heartbeat suppressed within gate window (0.837208ms)
✔ heartbeat emitted after gate window (0.142667ms)
✔ heartbeat suppressed when already sent (0.103167ms)
✔ heartbeat at exact gate boundary is suppressed (0.072666ms)
✔ heartbeat with custom gate value (0.087292ms)
✔ unhandled agent requests are always logged (not DEBUG-only) (0.427292ms)
✔ session/cancelled notifications are logged (0.077792ms)
✔ ACP unexpected exit is warned (0.050584ms)
✔ unexpected exit check is before killReason consumption (0.044333ms)
✔ ACP client emits stderr_activity event (0.049ms)
✔ stderr_activity preserves DEBUG logging (0.035042ms)
✔ spawn.ts accumulates stderrBuf from stderr_activity (0.06425ms)
✔ heartbeat is gated via shouldEmitHeartbeat helper (0.036292ms)
✔ AA-001: buildArgs preserves explicit Claude full model names (0.400541ms)
✔ AA-002: buildArgs preserves canonical alias models (0.063792ms)
✔ AA-003: buildArgs omits --model when value is default (0.04575ms)
✔ AA-004: buildArgs omits --model when value is empty (0.036375ms)
✔ AA-005: buildResumeArgs preserves explicit Claude full model names (0.0695ms)
✔ AA-006: buildResumeArgs preserves canonical alias models (0.033292ms)
✔ AA-007: buildResumeArgs omits --model when default (0.046875ms)
✔ AA-008: buildResumeArgs for codex preserves explicit model (0.034417ms)
✔ AG-001: claude default excludes --model (0.430708ms)
✔ AG-002: claude custom model includes --model (0.051083ms)
✔ AG-003: claude auto permission includes skip-permissions (0.038583ms)
✔ AG-004: claude non-auto permission excludes skip-permissions (0.032ms)
✔ AG-005: claude with system prompt includes --append-system-prompt (0.047833ms)
✔ AG-006: claude with effort includes --effort (0.031416ms)
✔ AG-007: codex auto includes bypass flag (0.0335ms)
✔ AG-008: codex safe excludes bypass flag (0.029666ms)
✔ AG-009: codex includes --json (0.034875ms)
✔ AG-010: gemini includes prompt payload via -p (0.085042ms)
✔ AG-011: gemini with model includes -m (0.042667ms)
✔ AG-012: gemini default model excludes -m (0.033292ms)
✔ AG-013: unknown CLI returns empty args (0.262583ms)
✔ AG-014: claude resume includes --resume + session id (0.064ms)
✔ AG-015: codex resume includes session id (0.03225ms)
✔ AG-016: gemini resume includes --resume (0.036042ms)
✔ AH-001: passes sync handler through (0.333542ms)
✔ AH-002: catches async error and calls next (0.104292ms)
✔ AH-003: preserves statusCode on errors (0.064583ms)
✔ AH-004: passes req/res/next to handler (0.060541ms)
✔ BAO-001: serve --open default is true (jaw serve opens browser) (0.708416ms)
✔ BAO-002: serve enables JAW_OPEN_BROWSER only when --open is set (0.184875ms)
✔ BAO-003: server auto-open is guarded by JAW_OPEN_BROWSER env (0.737417ms)
✔ BAO-004: server auto-open skips in test environments (0.176125ms)
✔ CDP-001: launchChrome has readiness polling (waitForCdpReady) (0.403542ms)
✔ CDP-002: launchChrome supports headless flag (0.051583ms)
✔ CDP-003: connectCdp has retry logic (0.112209ms)
✔ CDP-004: launchChrome checks port before spawn (4.143666ms)
✔ CDP-005: connection.ts delegates headless policy to launch policy helper (0.1645ms)
✔ CDP-006: API /start passes headless option (0.252792ms)
✔ CDP-007: CLI --headless option exists (0.130125ms)
✔ CDP-008: skills_ref SKILL.md uses cli-jaw not cli-claw (0.65875ms)
✔ CDP-009: connection.ts has net import for isPortListening (0.787417ms)
✔ CDP-010: launchChrome signature is backward compatible (0.347375ms)
✔ CDP-011: connectCdp uses timeout in connectOverCDP (0.165833ms)
✔ CDP-012: blind 2s sleep removed (0.092917ms)
✔ BLP-001: invalid mode falls back to manual (0.336042ms)
✔ BLP-002: manual mode keeps visible browser by default (0.066417ms)
✔ BLP-003: manual mode respects explicit headless requests (0.040208ms)
✔ BLP-004: agent mode always forces headless automation (0.036ms)
✔ BLP-005: debug mode denies browser launch and points to debug console (0.048458ms)
▶ Browser Port Routing (#49)
  ✔ BP-001: connection.ts declares activePort state variable (0.396792ms)
  ✔ BP-002: getActivePort() uses activePort > settings > deriveCdpPort fallback (0.106542ms)
  ✔ BP-003: launchChrome sets activePort on CDP ready and port reuse (0.127ms)
  ✔ BP-004: closeBrowser resets activePort to null (0.065541ms)
  ✔ BP-005: all exported functions default to getActivePort() (0.144625ms)
  ✔ BP-006: index.ts re-exports getActivePort (0.055666ms)
  ✔ BP-007: routes cdpPort(req) checks req param then getActivePort() (0.110458ms)
  ✔ BP-008: no route uses old cdpPort() without req (0.052084ms)
  ✔ BP-009: routes/browser.ts does not import deriveCdpPort (0.071083ms)
✔ Browser Port Routing (#49) (2.523958ms)
✔ BRS-001: browser start defaults to manual mode (0.834292ms)
✔ BRS-002: browser start preserves agent mode and headless flag (0.105625ms)
✔ BRS-003: browser start preserves debug mode for route-level rejection (0.075834ms)
✔ BRS-004: query port wins over body port (0.061625ms)
✔ BSP-001: browser skill documents --agent automation mode (0.408833ms)
✔ BSP-002: vision-click skill documents screenshot-based coordinate click (0.474ms)
✔ addBroadcastListener receives broadcast events (0.703ms)
✔ removeBroadcastListener stops receiving events (0.076459ms)
✔ broadcast works without WS server set (0.079708ms)
✔ broadcast sends to WS clients with readyState 1 (0.097541ms)
✔ multiple listeners all receive the same broadcast (0.063125ms)
✔ removing non-existent listener does not throw (0.03775ms)
✔ CM-001: canonical set contains exactly 5 aliases (0.662667ms)
✔ CM-002: isClaudeCanonicalModel accepts all canonical values (0.085916ms)
✔ CM-003: isClaudeCanonicalModel rejects non-canonical values (0.043125ms)
✔ CM-004: migrateLegacyClaudeValue maps historical 1M values to aliases (0.041417ms)
✔ CM-005: migrateLegacyClaudeValue maps historical non-1M full names to aliases (0.038541ms)
✔ CM-006: migrateLegacyClaudeValue preserves pinned Haiku (0.033792ms)
✔ CM-007: migrateLegacyClaudeValue preserves unknown explicit values (0.033834ms)
✔ CM-008: migrateLegacyClaudeValue is idempotent on canonical values (0.043ms)
✔ CM-009: legacy map covers exactly 4 historical values (0.059625ms)
✔ CM-010: Haiku is intentionally excluded from legacy map (0.070875ms)
✔ CM-011: getDefaultClaudeModel returns sonnet (0.664167ms)
✔ CM-012: getDefaultClaudeChoices returns all canonical values (0.057583ms)
✔ CM-013: getClaudeModelKind classifies correctly (0.065708ms)
✔ CLI_KEYS contains exactly 5 known entries (0.633375ms)
✔ DEFAULT_CLI is claude (0.062875ms)
✔ every CLI entry has required fields (0.084833ms)
✔ every CLI defaultModel is included in its models list (0.044375ms)
✔ registry defaults for gemini and opencode are updated (0.044125ms)
✔ buildDefaultPerCli returns correct shape (0.062083ms)
✔ buildDefaultPerCli returns a new object each call (0.080875ms)
✔ buildModelChoicesByCli returns models for each CLI (0.068542ms)
✔ buildModelChoicesByCli returns independent copies (0.046541ms)
✔ P3-001: clone creates all required directories (152.794ms)
✔ P3-002: clone sets workingDir to target path (156.311125ms)
✔ P3-003: clone does NOT copy jaw.db from source (199.0015ms)
✔ P3-004: clone --with-memory copies MEMORY.md (309.009041ms)
✔ P3-005: clone --link-ref creates symlink for skills_ref (877.737291ms)
✔ P3-006: clone to non-empty dir fails (704.616625ms)
✔ P3-007: clone from non-existent source fails (849.477208ms)
✔ P3-008: clone from invalid source (no settings.json) fails (539.712167ms)
✔ CC-001: makeCommandCtx function is exported (0.786416ms)
✔ CC-002: getMcp returns real loadUnifiedMcp, not empty object (0.111291ms)
✔ CC-003: remote interface restricts settings via allowlist (0.097792ms)
✔ CC-004: server.ts uses makeCommandCtx instead of inline object (0.083667ms)
✔ CC-005: bot.ts uses makeCommandCtx instead of inline object (0.101167ms)
✔ CC-006: resetSkills available in unified context (0.072667ms)
✔ CC-007: getPrompt returns actual file content, not unsupported message (0.075917ms)
✔ CC-007b: skill CLI reset core avoids cwd-based repair (0.078458ms)
✔ CC-008: telegram fallbackOrder patch delegates to applySettings (0.952709ms)
✔ CC-009: telegram rejects unsupported patches without calling applySettings (0.255292ms)
✔ CC-010: web context delegates settings patches directly (0.160209ms)
✔ CC-011: clearSession delegates to dependency callback (0.135875ms)
✔ CC-012: telegram allows cli settings patch (0.152875ms)
✔ CC-013: telegram allows perCli settings patch (0.149791ms)
✔ CC-014: telegram allows memory settings patch (0.136791ms)
✔ CC-015: telegram rejects workingDir patch (0.101584ms)
✔ parseCommand: non-slash input returns null (0.491209ms)
✔ parseCommand: bare "/" returns help command (0.351041ms)
✔ parseCommand: known command is parsed correctly (0.163875ms)
✔ parseCommand: compact command is parsed correctly (0.0595ms)
✔ parseCommand: command aliases work (0.048583ms)
✔ parseCommand: unknown command returns type unknown (0.051459ms)
✔ parseCommand: multi-word args are split (0.051041ms)
✔ executeCommand: null parsed returns null (0.088583ms)
✔ executeCommand: unknown command returns error result (0.0955ms)
✔ executeCommand: /quit returns exit code (0.150541ms)
✔ executeCommand: /clear returns clear_screen for cli (0.074834ms)
✔ executeCommand: /clear returns info for telegram (0.082708ms)
✔ executeCommand: unsupported interface returns error (0.079834ms)
✔ executeCommand: handler error is caught gracefully (0.163042ms)
✔ getCompletions: empty partial returns all cli commands (6.839292ms)
✔ getCompletions: partial filters results (0.104916ms)
✔ getCompletions: compact appears in session command list (0.042584ms)
✔ getCompletionItems returns structured objects (0.05275ms)
✔ getCompletions: telegram interface excludes cli-only commands (0.0495ms)
✔ getArgumentCompletionItems: cli command returns cli choices (0.176541ms)
✔ getArgumentCompletionItems: unknown command returns empty (0.030584ms)
✔ COMMANDS: every command has required fields (0.057667ms)
✔ COMMANDS: no duplicate names (0.035334ms)
✔ executeCommand: compact returns busy error when runtime is active (0.169083ms)
✔ CP-001: web visible includes help (0.525666ms)
✔ CP-002: telegram menu includes model and cli (full writable) (0.0925ms)
✔ CP-003: telegram visible includes model (full) (0.070875ms)
✔ CP-004: web executable commands are subset of visible (0.091708ms)
✔ CP-005: all interfaces return non-empty lists (0.073041ms)
✔ CP-006: telegram menu includes help (0.059042ms)
✔ CP-007: telegram menu excludes start/id/settings (0.06675ms)
✔ CP-008: telegram menu has exact expected command set (0.067083ms)
✔ CP-009: every telegram command has tgDescKey (0.066792ms)
✔ CP-010: model and cli are writable on telegram (1.459708ms)
✔ isCompactMarkerRow recognizes managed compact marker rows (0.448333ms)
✔ getRowsSinceLatestCompactForTest keeps only rows after latest compact marker (0.362958ms)
✔ buildManagedCompactSummaryForTest produces deterministic summary with instructions (0.279375ms)
✔ isCompactMarkerRow rejects non-assistant roles (0.051083ms)
✔ isCompactMarkerRow rejects missing trace prefix (0.046291ms)
✔ getRowsSinceLatestCompactForTest returns all rows when no marker present (0.059958ms)
✔ getRowsSinceLatestCompactForTest stops at first (latest) marker only (0.065666ms)
✔ buildManagedCompactSummaryForTest uses default instructions when empty (0.043833ms)
✔ buildManagedCompactSummaryForTest strips tool_call tags from summary (0.057125ms)
✔ buildManagedCompactSummaryForTest caps at 8 turns (0.960125ms)
✔ CfgM-001: migrateSettings normalizes legacy Claude perCli model values (0.481417ms)
✔ CfgM-002: migrateSettings normalizes legacy Claude activeOverrides model values (0.06875ms)
✔ CfgM-003: migrateSettings normalizes Claude memory.model when cli is claude (0.050583ms)
✔ CfgM-004: migrateSettings does NOT normalize memory.model when cli is not claude (0.03925ms)
✔ CfgM-005: migrateSettings preserves pinned Haiku in perCli (0.045083ms)
✔ CfgM-006: migrateSettings is idempotent on already-canonical values (0.055ms)
✔ CfgM-007: migrateSettings normalizes all 4 legacy values across perCli (0.066792ms)
✔ DC-001: decodes normal UTF-8 filename (0.40525ms)
✔ DC-002: passes through plain ASCII filename (0.053958ms)
✔ DC-003: defaults to upload.bin on null/undefined (0.048167ms)
✔ DC-004: rejects malformed percent-encoding (0.178833ms)
✔ DC-005: rejects overlong filename (0.062792ms)
✔ DV-001: semver parses valid version (0.73425ms)
✔ DV-002: semver returns null for invalid (0.065792ms)
✔ DV-003: lt comparison works (0.075417ms)
✔ DV-004: ws 8.19.0 is safe (outside >=8.0.0 <8.17.1) (0.053625ms)
✔ DV-005: ws 8.16.0 is vulnerable (inside >=8.0.0 <8.17.1) (0.044625ms)
✔ DV-006: ws 8.17.1 is safe (boundary excluded) (0.041875ms)
✔ DV-007: node-fetch 3.3.2 is safe (outside >=3.0.0 <3.1.1) (0.036084ms)
✔ DV-008: node-fetch 3.0.5 is vulnerable (inside >=3.0.0 <3.1.1) (0.039625ms)
✔ DV-009: node-fetch 2.6.6 is vulnerable (< 2.6.7) (0.046ms)
✔ DV-010: node-fetch 2.7.0 is safe (>= 2.6.7) (0.085333ms)
✔ DISCORD_SLASH_COMMANDS includes operational commands (0.542459ms)
✔ registerDiscordSlashCommands guards on guildId (0.125958ms)
✔ registerDiscordSlashCommands guards on application id (0.068667ms)
✔ slash commands are built with SlashCommandBuilder (0.051542ms)
✔ commands are registered as guild-scoped (not global) (0.048666ms)
✔ slash command handler uses parseCommand + executeCommand (0.038708ms)
✔ discord command context uses applyRuntimeSettingsPatch (0.049417ms)
✔ DISCORD_LIMITS defines 10 MiB cap (0.488ms)
✔ validateDiscordFileSize throws on oversized files (0.084ms)
✔ validateDiscordFileSize accepts files under limit (4.331125ms)
✔ validateDiscordFileSize rejects files over 10 MiB (0.996084ms)
✔ validateDiscordFileSize accepts exactly 10 MiB (1.190417ms)
✔ sendDiscordFile checks for text-based channel (0.135834ms)
✔ sendDiscordFile uses attachment format with basename (0.156333ms)
✔ chunkDiscordMessage splits at 2000 char limit (3.932792ms)
✔ chunkDiscordMessage prefers splitting at newlines (0.846333ms)
✔ forwarder skips origin=discord to prevent echo loop (0.263667ms)
✔ dcOrchestrate passes chatId to submitMessage (0.121042ms)
✔ queue handler correlates by requestId for request-level isolation (0.166958ms)
✔ orchestrateAndCollect call includes chatId (0.140583ms)
✔ doctor shows active channel (0.508375ms)
✔ doctor has Discord check (0.066917ms)
✔ doctor detects Discord disabled (0.050959ms)
✔ doctor detects missing Discord token (0.044542ms)
✔ doctor detects missing guild ID (0.049209ms)
✔ doctor detects missing channel IDs (0.064375ms)
✔ doctor --json includes activeChannel (0.057834ms)
✔ doctor --json includes discord status object (0.0465ms)
✔ buildDiscordStatus returns degradedReasons array (0.058625ms)
✔ doctor checks channel consistency (0.094ms)
✔ buildDiscordStatus distinguishes missing_token vs missing_guild_id vs missing_channel_ids (0.069125ms)
✔ doctor JSON output includes checks array (0.555666ms)
✔ doctor JSON output includes activeChannel field (0.09225ms)
✔ doctor JSON output includes discord object (0.08425ms)
✔ discord status has status field (0.054125ms)
✔ discord status has enabled field (0.054208ms)
✔ discord status has tokenPresent field (0.065083ms)
✔ discord status has guildConfigured field (0.055ms)
✔ discord status has channelIdsConfigured field (0.046875ms)
✔ discord status has degradedReasons array (0.99975ms)
✔ discord status has runtimeReady field (0.181458ms)
✔ discord status has channelConsistent field (0.08925ms)
✔ DI-001: gateway idle path passes _skipInsert: true to orchestrate (0.655916ms)
✔ DI-002: gateway continue path passes _skipInsert: true to orchestrateContinue (0.088625ms)
✔ DI-003: gateway reset path passes _skipInsert: true to orchestrateReset (0.062834ms)
✔ DI-004: pipeline PABCD path propagates _skipInsert to spawnAgent (0.062375ms)
✔ DI-005: pipeline PABCD spawn includes _skipInsert (0.074292ms)
✔ DI-006: tgOrchestrate passes _skipInsert: true to orchestrateAndCollect (0.943416ms)
✔ DI-007: processQueue passes _skipInsert: true to orchestrate calls (0.139667ms)
✔ DI-008: steerAgent passes _skipInsert: true to orchestrate calls (0.079583ms)
✔ DI-009: processQueue still has its own insertMessage.run (not removed) (0.070834ms)
✔ DI-010: steerAgent still has its own insertMessage.run (not removed) (0.100333ms)
✔ EMP-001: getEmployeePrompt is exported (0.686542ms)
✔ EMP-002: getEmployeePromptV2 is exported (0.084166ms)
✔ EMP-003: getEmployeePrompt returns string with employee name (10.992ms)
✔ EMP-004: getEmployeePrompt includes executor rules (no subtask output) (2.672ms)
✔ EMP-005: getEmployeePrompt includes browser control section (3.159334ms)
✔ EMP-006: getEmployeePrompt includes channel file delivery section (2.118167ms)
✔ EMP-007: getEmployeePrompt defaults role to general developer (2.515458ms)
✔ EMP-008: getEmployeePromptV2 returns longer string than base (4.764291ms)
✔ EMP-009: getEmployeePromptV2 includes phase gate (2.281584ms)
✔ EMP-020: Phase 2 injects dev-code-reviewer content (2.753458ms)
✔ EMP-021: Phase 4 injects dev-testing, NOT dev-code-reviewer (2.429334ms)
✔ EMP-022: Phase 3 does NOT inject reviewer or testing guides (2.362166ms)
✔ EMP-023: String phase "2" works same as number 2 (type coercion safety) (6.044625ms)
✔ EMP-024: research role injects read-only guide and phase 1 context (2.427ms)
✔ EMP-025: employee prompt uses employee-agent pipe mode wording (2.197333ms)
✔ EMP-011: parseSubtasks extracts subtask JSON from agent response (0.189584ms)
✔ EMP-012: parseSubtasks returns empty for no JSON (0.151125ms)
✔ EMP-013: getSubAgentPrompt should not be exported (renamed) (1.057042ms)
✔ EMP-014: getSubAgentPromptV2 should not be exported (renamed) (5.082709ms)
✔ P100-001: pipeline uses employeeSessionId-based resume and global clear (0.752ms)
✔ P100-002: spawn guards main session update when employee session is used (0.315542ms)
✔ P100-003: db exports global employee session clear statement (0.541542ms)
✔ EG-001: spawnAgent calls detectCli() before any spawn (0.609417ms)
✔ EG-002: standard CLI branch has child.on('error') listener (0.098375ms)
✔ EG-003: ACP branch has acp.on('error') listener (1.13975ms)
✔ EG-004: standard CLI spawn uses shell:true on win32 (0.13025ms)
✔ EG-005: stdSettled guard exists in both error and close handlers (0.146625ms)
✔ EG-006: acpSettled guard exists in both error and exit handlers (0.099125ms)
✔ EG-007: settled flag is set before resolve() in error handlers (0.111417ms)
✔ EG-008: quota-copilot checks env vars before keychain (0.064041ms)
✔ EG-009: quota-copilot keychain lookup is darwin-only (0.119083ms)
✔ EG-010: preflight failure returns child: null (0.109417ms)
✔ EG-011: quota-copilot uses ~/.cli-jaw/auth/copilot-token cache path (0.06125ms)
✔ EG-012: quota-copilot has gh auth token fallback (0.057125ms)
✔ EG-013: quota-copilot uses execFileSync instead of execSync (0.091666ms)
✔ EG-014: quota-copilot cache includes account binding (0.045334ms)
✔ EG-015: clearCopilotTokenCache resets _keychainFailed flag (0.043167ms)
[jaw:fallback] state reset
[jaw:fallback] state reset
[jaw:fallback] state reset
[jaw:fallback] state reset
[jaw:fallback] state reset
[jaw:fallback] state reset
✔ resetFallbackState clears all entries (1.74375ms)
✔ getFallbackState returns object snapshot (0.15425ms)
✔ FALLBACK_MAX_RETRIES is 3 (verified via module constants) (1.797584ms)
✔ fallback state tracks retriesLeft and fallbackCli fields (0.129458ms)
✔ resetFallbackState is idempotent (0.146166ms)
✔ fallback retry flow: state transitions described correctly in source (2.272ms)
✔ server.js calls resetFallbackState on settings save (1.872417ms)
✔ 429: isAgentBusy checks activeProcess + retryPendingTimer (0.330208ms)
✔ 429: clearRetryTimer accepts resumeQueue param and defaults true (0.469959ms)
✔ 429: killActiveAgent calls clearRetryTimer(false) and returns hadTimer (0.379333ms)
✔ 429: killAllAgents returns true when timer cancelled (0.31075ms)
✔ 429: resetFallbackState calls clearRetryTimer(true) (0.453167ms)
✔ 429: processQueue guards against retryPendingTimer (1.527583ms)
✔ 429: INVARIANT comment present (0.265042ms)
✔ 429: retryPendingResolve stored before setTimeout (0.250292ms)
✔ 429: steerHandler uses isAgentBusy not activeProcess (0.178209ms)
✔ 429: gateway uses isAgentBusy (0.140875ms)
✔ 429: event consumers handle agent_retry (0.632541ms)
✔ 429: i18n keys exist (0.700083ms)
▶ 429 retry: behavioral tests
  ✔ clearRetryTimer(false) is safe on empty state (1.370042ms)
  ✔ clearRetryTimer(true) is safe on empty state (4.996166ms)
  ✔ killActiveAgent returns false when nothing pending (4.773166ms)
  ✔ isAgentBusy reflects activeProcess state (3.202875ms)
  ✔ processQueue guards against retryPendingTimer at runtime (2.686667ms)
✔ 429 retry: behavioral tests (17.423166ms)
▶ 429 retry: edge case coverage
  ✖ timer pending blocks processQueue at runtime (2.865708ms)
  ✔ steer/stop during retry calls clearRetryTimer(false) — queue stays blocked (0.307458ms)
  ✔ 429 retry branch appears BEFORE fallback branch in both exit handlers (0.313833ms)
✖ 429 retry: edge case coverage (3.625667ms)
▶ 429 retry: runtime timer simulation
  ✔ isAgentBusy() is false when no process and no timer (1.280875ms)
  ✔ clearRetryTimer(false) is safe no-op when no timer — race condition defense (2.5005ms)
  ✔ killActiveAgent safely handles "nothing active" — timer already self-cleared (0.716833ms)
✔ 429 retry: runtime timer simulation (4.610041ms)
✔ FC-001: /flush shows current flush model (ok=true) (0.925917ms)
✔ FC-001b: /flush shows custom flush model when set (ok=true) (0.118583ms)
✔ FC-002: /flush <custom-model> changes model or fails if CLI unavailable (12.397459ms)
✔ FC-003: /flush <cli> <model> changes both or reports unavailable (14.959458ms)
✔ FC-004: /flush off resets to active CLI/model (0.268708ms)
✔ FC-004b: /flush reset also resets (0.153625ms)
✔ FC-005: flush model resolution falls back to active CLI model (0.131583ms)
✔ FC-006: /flush <cli> without model sets model to default (25.064458ms)
✔ FC-007: /flush <model> picks first available matched CLI (codex before copilot) (18.984333ms)
✔ FC-008: /flush <model> returns cliUnavailable when matched CLIs are not installed (10.612875ms)
✔ FC-009: /flush <legacy-full-name> infers claude via LEGACY_MODEL_CLI_HINTS (6.462833ms)
✔ FC-010: /flush claude <legacy-full-name> preserves explicit model literally (5.626541ms)
✔ /forward is available on Discord interface (0.655709ms)
✔ /forward is in DISCORD_SLASH_COMMANDS (0.166208ms)
✔ forward command has description key (0.317458ms)
✔ forward command has Telegram description key (0.234958ms)
✔ forward command is defined as on/off toggle (0.160292ms)
✔ frontend copilot meta exposes selectable efforts (0.98575ms)
✔ frontend copilot meta preserves effortNote hint (0.105334ms)
▶ Graceful Shutdown Signals
  ▶ serve.ts (Mocked)
    ✔ exiting guard prevents multiple kills (0.907542ms)
    ✔ exit code calculation with os.constants.signals (0.256042ms)
  ✔ serve.ts (Mocked) (1.674959ms)
  ▶ server.ts Shutdown Logic (Mocked)
    ✔ telegramBot timeout race correctly clears timer upon success (0.381458ms)
    ✔ telegramBot timeout race rejects after 2s if slow (0.337166ms)
  ✔ server.ts Shutdown Logic (Mocked) (0.845333ms)
✔ Graceful Shutdown Signals (2.825083ms)
✔ initTelegram is async function (0.7345ms)
✔ initTelegram awaits old bot stop (0.134958ms)
✔ bot.start() has .catch() for 409 handling (0.112083ms)
✔ 409 retry uses tgRetryTimer for dedup (0.136292ms)
✔ 409 retry calls void initTelegram() (0.11075ms)
✔ shutdown handler uses shutdownMessagingRuntime (0.115292ms)
✔ shutdown handler uses process.once and Promise.race (0.324625ms)
✔ bootstrap calls initActiveMessagingRuntime with error handling (0.136042ms)
✔ applyRuntimeSettingsPatch calls restartMessagingRuntime (unified restart) (0.538917ms)
✔ heartbeat queue: 3 jobs accumulate and drain sequentially (1.115625ms)
✔ heartbeat queue: dedupe prevents same job.id from queuing twice (0.150875ms)
✔ heartbeat queue: 5 different jobs all drain in order (0.190166ms)
✔ heartbeat queue: broadcasts pending count on queue and dequeue (0.20325ms)
✔ heartbeat checks sendChannelOutput result (0.690667ms)
✔ sendChannelOutput has configured fallback after lastActive/latestSeen (0.116958ms)
✔ sendChannelOutput returns explicit error when no target available (0.087375ms)
✔ target resolution follows explicit > lastActive > latestSeen > configured (0.131917ms)
✔ startHeartbeatCronLoop runs current minute immediately and arms next tick (1.168041ms)
✔ startHeartbeatCronLoop re-arms after each tick (0.155333ms)
✔ startHeartbeatCronLoop still arms next tick when runCurrent throws (0.392958ms)
✔ normalizeHeartbeatSchedule defaults to every 5 minutes (1.231917ms)
✔ normalizeHeartbeatSchedule preserves cron and timezone (53.575959ms)
✔ matchesHeartbeatCron respects target timezone (0.867125ms)
✔ matchesHeartbeatCron supports weekday aliases (0.233125ms)
✔ validateHeartbeatCron rejects malformed expressions (0.301583ms)
✔ describeHeartbeatSchedule includes timezone label (0.187167ms)
✔ getHeartbeatScheduleTimeZone falls back to system timezone (2.067291ms)
✔ getHeartbeatMinuteSlotKey uses zoned minute, not raw UTC minute (0.590834ms)
✔ validateHeartbeatScheduleInput returns normalized cron schedule (0.368583ms)
✔ validateHeartbeatScheduleInput rejects invalid timezone (0.291375ms)
✔ HP-001: list mode returns all visible commands (1.014709ms)
✔ HP-002: detail mode for known command (0.210625ms)
✔ HP-003: unknown command returns not ok (0.118167ms)
✔ HP-004: telegram interface shows readonly tag (0.111916ms)
✔ HP-005: cli interface help generation (0.122291ms)
✔ HR-001: ok wraps data in { ok: true, data } (1.427667ms)
✔ HR-002: ok with extra fields merges them (0.143417ms)
✔ HR-003: ok with null data (0.107458ms)
✔ HR-004: fail sets status and error message (0.141291ms)
✔ HR-005: fail with extra fields (0.101417ms)
✔ HR-006: fail defaults to 500 style (0.084916ms)
✔ t(): returns key as fallback when no locales loaded (2.895208ms)
✔ t(): loadLocales loads ko and en (0.786959ms)
✔ t(): returns Korean string for ko locale (0.083208ms)
✔ t(): returns English string for en locale (0.0675ms)
✔ t(): parameter interpolation works (0.089458ms)
✔ t(): multiple occurrences of same param are replaced (0.076375ms)
✔ t(): falls back to ko when unknown locale requested (0.060375ms)
✔ t(): falls back to key itself when key not found (0.062125ms)
✔ locale JSON: ko.json and en.json have same keys (1.174625ms)
✔ locale JSON: no empty values in ko.json (0.4765ms)
✔ locale JSON: no empty values in en.json (4.297042ms)
✔ COMMANDS: every command with descKey has matching locale key (1.512083ms)
✔ COMMANDS: every command still has desc string (fallback) (0.131584ms)
✔ ROLE_PRESETS: every labelKey has matching locale entry (1.043875ms)
✔ ROLE_PRESETS: research preset is available (0.090834ms)
✔ getPromptLocale: returns ko for non-existent file (0.175541ms)
✔ getPromptLocale: parses Language: Korean → ko (0.569292ms)
✔ getPromptLocale: parses Language: English → en (0.533334ms)
✔ getPromptLocale: parses Language: 한국어 → ko (55.492583ms)
✔ getPromptLocale: unknown language falls back to ko (0.931875ms)
✔ getPromptLocale: no Language field falls back to ko (0.970375ms)
✔ locale files: ko.json has at least 100 keys (0.756417ms)
✔ locale files: en.json has at least 100 keys (0.497625ms)
✔ normalizeLocale: en-US → en (0.264334ms)
✔ normalizeLocale: ko-KR → ko (0.102292ms)
✔ normalizeLocale: EN (uppercase) → en (0.071584ms)
✔ normalizeLocale: unsupported locale → default ko (0.058958ms)
✔ normalizeLocale: null/undefined → default ko (0.060958ms)
▶ isGitRepo
  ✔ ID-001: returns true for a git repo (906.531916ms)
  ✔ ID-002: returns false for /tmp (not a git repo) (260.382541ms)
✔ isGitRepo (1167.90475ms)
▶ captureFileSet
  ✔ ID-003: empty map for clean repo (1864.65825ms)
  ✔ ID-004: detects unstaged tracked changes (1869.912291ms)
  ✔ ID-005: detects untracked files (1027.07175ms)
  ✔ ID-006: returns empty map for non-git directory (631.46775ms)
  ✔ ID-006b: detects unicode file paths (core.quotepath=false) (992.355875ms)
✔ captureFileSet (6386.254125ms)
▶ diffFileSets
  ✔ ID-007: returns only new files in post map (1.294459ms)
  ✔ ID-008: detects mtime changes for same file (0.130333ms)
  ✔ ID-008b: returns empty when nothing changed (0.138ms)
  ✔ ID-008c: detects same-mtime but different content fingerprint (0.081709ms)
✔ diffFileSets (1.892125ms)
▶ detectIde
  ✔ ID-009: returns antigravity when ANTIGRAVITY_AGENT=1 (0.239958ms)
  ✔ ID-010: returns code when TERM_PROGRAM=vscode (0.162667ms)
  ✔ ID-011: returns null when no IDE env vars set (0.150375ms)
✔ detectIde (0.632ms)
▶ getIdeCli
  ✔ ID-012: respects ANTIGRAVITY_CLI_ALIAS env (0.085333ms)
  ✔ returns null for null input (0.041542ms)
✔ getIdeCli (0.197625ms)
▶ getDiffStat
  ✔ ID-013: returns stat for modified file (617.01625ms)
  ✔ ID-014: returns empty for non-existent file (172.904875ms)
  ✔ ID-015: returns empty for empty files array (126.997292ms)
✔ getDiffStat (917.136834ms)
▶ integration: file-set based diff detection
  ✔ ID-016: detects only agent-created files (544.871125ms)
  ✔ ID-016b: detects re-modification of same file via mtime (421.337208ms)
  ✔ ID-017: commit-less repo captures untracked files (181.262917ms)
✔ integration: file-set based diff detection (1147.70125ms)
▶ queue drain safety
  ✔ ID-018: FIFO push/shift correctness (0.215792ms)
  ✔ ID-019: queue drain after /ide off — shift still removes entry (0.075ms)
✔ queue drain safety (0.366625ms)
▶ ideHandler contract
  ✔ ID-020: correct codes for valid args, rejects invalid (20.481792ms)
✔ ideHandler contract (20.542875ms)
▶ ENOENT safety
  ✔ ID-021: openDiffInIde(ide=null) returns without crash (0.302ms)
  ✔ ID-022: openDiffInIde with non-existent CLI does not throw (2.445791ms)
✔ ENOENT safety (2.851916ms)
✔ IMP-001: all relative imports resolve to existing files (0.721334ms)
✔ init rejects invalid --channel value (0.838167ms)
✔ init requires --force when settings.json exists (0.119667ms)
✔ init validates Discord token is required (0.089625ms)
✔ init validates Discord guild ID is required (0.085ms)
✔ init validates at least one Discord channel ID (0.083459ms)
✔ init accepts --discord-token flag (0.095208ms)
✔ init accepts --discord-guild-id flag (0.081084ms)
✔ init accepts --discord-channel-ids flag (0.077875ms)
✔ init accepts --channel flag (0.131667ms)
✔ init auto-selects discord when only discord is enabled (0.152417ms)
✔ init outputs Discord status in summary (0.136625ms)
✔ P2-001: JAW_HOME respects CLI_JAW_HOME env var (430.412875ms)
✔ P2-002: JAW_HOME defaults to ~/.cli-jaw without env var (324.69375ms)
✔ P2-003: --home flag sets JAW_HOME for doctor subcommand (3207.174958ms)
✔ P2-004: --home=/path equals syntax works (2331.205583ms)
✔ P2-005: tilde expansion resolves correctly (187.236208ms)
✔ P23-001: postinstall legacy rename guard — custom home must not move ~/.cli-jaw (0.35675ms)
✔ P23-002: init.ts workingDir default uses JAW_HOME, not hardcoded path (0.286583ms)
✔ P23-003: mcp.ts fallback uses JAW_HOME, not homedir() (0.193875ms)
✔ P23-004: --home with subcommand as value produces error (129.228792ms)
✔ P23-005: --home without any value produces error (100.392291ms)
✔ P20-001: all 6 command files import JAW_HOME from config (22.425334ms)
✔ P20-002: inline JAW_HOME files do NOT import config.ts (1.102083ms)
✔ P4-001: default JAW_HOME produces "default" label (381.90375ms)
✔ P4-002: custom JAW_HOME produces hashed label (0.688292ms)
✔ P4-003: LABEL format is com.cli-jaw.<instanceId> (0.426833ms)
✔ P4-004: xmlEsc escapes &, <, > in paths (0.834417ms)
✔ P4-005: parseArgs handles --port=3458 syntax (603.957125ms)
✔ P4-006: parseArgs handles --port 3458 (space) syntax (540.377208ms)
✔ P4-007: LOG_DIR uses JAW_HOME not hardcoded path (877.789416ms)
✔ )**CJK gets ZWSP between punctuation and ** (1.763208ms)
✔ multiple )**CJK patterns all get ZWSP (0.186042ms)
✔ .**CJK gets ZWSP (0.121708ms)
✔ **텍스트**CJK without punctuation is untouched (0.135125ms)
✔ ** followed by space is untouched (0.13975ms)
✔ ** followed by punctuation is untouched (0.15775ms)
✔ text without asterisks passes through unchanged (0.101167ms)
✔ fenced code block content is not modified (0.469958ms)
✔ inline code content is not modified (0.159792ms)
✔ code block preserved but surrounding text is fixed (0.203042ms)
✔ *** sequence is not split by ZWSP (0.115666ms)
✔ telegram bold adjacent CJK renders correctly (0.34375ms)
✔ telegram bold with paren before CJK renders correctly (0.176625ms)
✔ telegram italic adjacent CJK (0.141917ms)
✔ runtime exports clearTargetState for restart cleanup (1.175542ms)
✔ runtime exports hydrateTargetsFromSettings for boot-time hydration (0.396084ms)
✔ server.ts hydrates targets from settings on boot (0.188667ms)
✔ restartMessagingRuntime clears stale targets (0.107ms)
✔ inactive channel patch does not restart active runtime (0.123958ms)
✔ loadSettings catch path applies env overrides (0.142833ms)
✔ applyEnvOverrides handles DISCORD_TOKEN (0.088084ms)
✔ applyEnvOverrides handles TELEGRAM_ALLOWED_CHAT_IDS (0.078709ms)
✔ applyRuntimeSettingsPatch is async and awaits restart (39.82525ms)
✔ applyRuntimeSettingsPatch rolls back on restart failure (0.404417ms)
✔ MD-001: /model shows active CLI perCli model by default (52.152584ms)
✔ MD-002: /model prefers activeOverrides model over perCli model (0.35ms)
✔ MD-003: /model uses session model when session active CLI matches (0.251542ms)
✔ MD-004: /model does not leak session model from different CLI (0.259291ms)
✔ MD-005: /model <new> then /model shows the updated model (0.41925ms)
✔ MD-006: /status shows activeOverrides model when set (0.424417ms)
✔ MD-007: /status shows perCli model by default (no overrides) (0.352667ms)
✔ MD-008: /status and /model show same model (cross-consistency) (0.41375ms)
✔ MD-009: /status effort uses activeOverrides over perCli (0.614542ms)
✔ MD-010: /status effort does not leak from different CLI session (0.413666ms)
✔ MD-011: /status handles space-containing model names (0.450958ms)
✔ OSI-001: state is isolated per scope (5.533042ms)
✔ OSI-002: resetState only affects its own scope (3.24025ms)
✔ OSI-003: scopeId is persisted in ctx (1.105542ms)
✔ OSR-001: ws source exports hydrateAgentPhases function (1.42675ms)
✔ OSR-002: hydrateAgentPhases handles phase and phaseLabel fields (0.335375ms)
✔ OSR-003: ws source tracks current orc scope and ignores foreign orc_state events (0.26675ms)
[orchestrator:subtask] fenced JSON parse failed { preview: '{broken' }
✔ ORP-001: fenced json subtasks parse (4.44975ms)
✔ ORP-002: multiple subtasks parse (0.599833ms)
✔ ORP-003: malformed json returns null (2.163584ms)
✔ ORP-004: no json block returns null (0.365375ms)
✔ ORP-005: null/empty input returns null (0.628ms)
✔ ORP-006: raw (unfenced) json with subtasks (0.873708ms)
✔ ORP-007: direct_answer only path (0.368292ms)
✔ ORP-008: direct_answer with subtasks returns null (0.309708ms)
✔ ORP-009: no direct_answer returns null (28.165917ms)
✔ ORP-010: null input returns null (0.721292ms)
✔ ORP-011: strips fenced json block (0.669041ms)
✔ ORP-012: strips raw json block (0.215916ms)
✔ ORP-013: preserves text without json (0.227ms)
[jaw:research] Research employee not found, using fallback Research employee
[jaw:pabcd] state=IDLE, spawning/resuming agent
[jaw:pabcd] state=IDLE, spawning/resuming agent
✔ RES-001: Korean ambiguous requests detected (24.546583ms)
✔ RES-002: English ambiguous requests detected (4.270833ms)
✔ RES-003: Clear implementation requests are NOT ambiguous (1.783416ms)
✔ RES-004: injects report into planning prompt (1.208416ms)
✔ RES-005: returns original prompt when report is empty (1.541208ms)
✔ RES-006: parseResearchReport extracts structured sections (44.608792ms)
✔ RES-007: dispatchResearchTask falls back to temporary Research worker (105.845333ms)
✖ RES-008: initial P request injects research before planning (123.946667ms)
✖ RES-009: clear implementation request skips pre-planning research (26.444584ms)
[jaw:pabcd] state=IDLE, spawning/resuming agent
[jaw:pabcd] state=IDLE, spawning/resuming agent
✔ OSR-001: reset during agent execution does not restore stale P state (65.570959ms)
✔ OSR-002: phase advance during agent execution preserves advanced state and ctx (14.412ms)
✔ ORT-001: "continue" matches (6.021625ms)
✔ ORT-002: "이어서 해줘" matches (0.34675ms)
✔ ORT-003: "계속 해줘" matches (0.216625ms)
✔ ORT-003b: "again/다시" matches (0.195291ms)
✔ ORT-004: non-continue intent returns false (0.212917ms)
✔ ORT-005: empty/null returns false (0.097542ms)
✔ reset: orchestrateReset terminates main process before clearing worker registry (2.246875ms)
✔ reset: orchestrateReset terminates each live worker before cancel/clear (0.273208ms)
✔ OWC-001: pipeline imports createWorklog and resolves worklog seed (1.563166ms)
✖ OWC-002: initial planning turn creates worklog before setState (20.741459ms)
✔ OWC-003: worker handoff keeps object-shaped worklog contract (0.229666ms)
✔ OWC-004: distribute gates worklog prompt on truthy path (0.177083ms)
✔ PG-001: assertSkillId accepts simple name (1.399ms)
✔ PG-002: assertSkillId accepts dot-dash name (0.317042ms)
✔ PG-003: assertSkillId rejects traversal (..) (0.851083ms)
✔ PG-004: assertSkillId rejects slash (50.88175ms)
✔ PG-005: assertSkillId rejects backslash (0.703583ms)
✔ PG-006: assertSkillId rejects empty (0.380458ms)
✔ PG-007: assertFilename accepts valid .md (2.316833ms)
✔ PG-008: assertFilename rejects wrong extension (0.1875ms)
✔ PG-009: assertFilename accepts multiple extensions (0.36375ms)
✔ PG-010: assertFilename rejects traversal in name (0.272167ms)
✔ PG-011: assertFilename rejects empty/null (0.810083ms)
✔ PG-012: assertFilename rejects overlong name (0.60625ms)
✔ PG-013: safeResolveUnder allows normal filename (0.194292ms)
✔ PG-014: safeResolveUnder blocks traversal (..) (0.228959ms)
✔ PG-015: safeResolveUnder blocks absolute path (1.075208ms)
✔ PG-016: safeResolveUnder blocks encoded traversal (..%2f) (0.192666ms)
✔ P31-001: startup migrates permissions safe -> auto (1.275833ms)
✔ P31-002: applySettingsPatch tracks previous workingDir before merge (0.201917ms)
✔ P31-003: workingDir change triggers artifact regeneration pipeline (0.141125ms)
✔ P31-004: regenerateB clears both template cache and prompt cache (0.152ms)
✔ P4H-001: launchd rejects unknown option (e.g. --dry-run) (524.562625ms)
✔ P4H-002: launchctl load/unload quotes plist path (0.218291ms)
✔ P4H-003: browser command uses dynamic server URL (no hardcoded 3457) (0.245917ms)
✔ P4H-004: memory command uses dynamic server URL (no hardcoded 3457) (0.121584ms)
✔ PBP-001: a1 system prompt prefers debug console for debug inspection (0.995792ms)
✔ PBP-002: a1 system prompt uses --agent for automated browser sessions (0.172291ms)
✔ PBP-003: employee prompt uses --agent for automated browser sessions (0.183792ms)
✔ PBP-004: employee prompt forbids visible test browser for debug inspection (0.174291ms)
✔ prompt guard: system prompt contains pipe-mode prohibition block (1.125916ms)
✔ prompt guard: prohibition covers forDisk and employee prompt paths (0.322959ms)
✔ PLM-001: current source hash adopts current template (0.916625ms)
✔ PLM-002: known old stock hash adopts current template (0.166375ms)
✔ PLM-003: unknown hash preserves custom file (0.12ms)
✔ QS-001: fetchClaudeUsage distinguishes 401/403 from 5xx (10.988958ms)
✔ QS-002: fetchCodexUsage distinguishes 401/403 from 5xx (3.930583ms)
✔ QS-003: readClaudeCreds is macOS-only with explicit platform guard (0.178125ms)
✔ QS-004: readGeminiAccount has cross-platform documentation (0.118583ms)
✔ QS-005: /api/quota classify separates no-creds from API failure (0.160959ms)
✔ QS-006: settings.ts has 3-state dotClass (ok/warn/missing) (0.155ms)
✔ QS-007: settings.ts warn state triggers on authenticated === false (0.111334ms)
✔ QS-008: settings.ts error state keeps green (not warn) (0.255ms)
✔ QS-009: settings.ts auth hint shows for warn state too (0.128625ms)
✔ QS-010: QuotaEntry type includes authenticated and error fields (0.174333ms)
✔ QS-011: sidebar.css has .cli-dot.warn with yellow color (0.110708ms)
✔ QS-012: sidebar.css has all 3 dot states (0.081833ms)
▶ shieldMath — inline math
  ✔ should shield $E=mc^2$ (1.046ms)
  ✔ should shield multiple inline formulas (0.193583ms)
✔ shieldMath — inline math (2.050375ms)
▶ shieldMath — block math
  ✔ should shield $$...$$ (0.196375ms)
  ✔ should handle multiline block math (0.122041ms)
✔ shieldMath — block math (0.471667ms)
▶ shieldMath — code block exclusion
  ✔ should NOT shield $ inside inline code (0.174667ms)
  ✔ should NOT shield $ inside fenced code block (0.124791ms)
  ✔ should preserve code blocks verbatim (1.256958ms)
✔ shieldMath — code block exclusion (1.777667ms)
▶ shieldMath — currency exclusion
  ✔ should NOT shield currency $10 (0.201125ms)
  ✔ should NOT shield $$ used as currency without closing (0.100708ms)
✔ shieldMath — currency exclusion (0.447375ms)
▶ shieldMath — mixed content
  ✔ should handle code + math together (0.19225ms)
  ✔ should handle block math + code block (0.116125ms)
✔ shieldMath — mixed content (0.427458ms)
▶ shieldMath — GPT-style \[...\] block
  ✔ should shield \[...\] as block math (0.142792ms)
  ✔ should handle multiline \[...\] (0.073458ms)
✔ shieldMath — GPT-style \[...\] block (0.294833ms)
▶ shieldMath — GPT-style \(...\) inline
  ✔ should shield \(...\) as inline math (0.107625ms)
  ✔ should handle mixed $ and \( delimiters (0.067958ms)
✔ shieldMath — GPT-style \(...\) inline (0.248791ms)
▶ unshieldMath — fallback (no KaTeX)
  ✔ should restore inline math as <code> (0.214625ms)
  ✔ should restore block math as <pre><code> (0.061916ms)
  ✔ should handle multiple placeholders (0.075916ms)
  ✔ should handle missing block gracefully (0.062833ms)
✔ unshieldMath — fallback (no KaTeX) (0.529166ms)
▶ render sanitize (regex fallback)
  ✔ should strip <script> tags (1.431666ms)
  ✔ should strip multiline <script> blocks (0.179083ms)
  ✔ should neutralize inline event handlers (0.148834ms)
  ✔ should neutralize onclick handlers (0.1255ms)
  ✔ should neutralize onload handlers (0.10575ms)
  ✔ should neutralize javascript: URLs (0.159917ms)
  ✔ should handle case-insensitive javascript: URLs (0.109791ms)
  ✔ should preserve normal HTML content (0.124959ms)
  ✔ should preserve code blocks with angle brackets (0.119708ms)
  ✔ should preserve tables (0.152542ms)
  ✔ should handle multiple attack vectors in one string (0.103416ms)
✔ render sanitize (regex fallback) (3.98525ms)
✔ RH-001: package.json files array does not include skills_ref (1.166ms)
✔ RH-002: .npmignore excludes skills_ref (0.638167ms)
✔ RH-003: .npmignore excludes devlog (0.220708ms)
✔ RH-004: tests/phase-100 directory does not exist (0.123583ms)
✔ RH-005: employee-session-reuse.test.ts exists in tests/unit (0.116125ms)
✔ RH-006: .npmignore excludes dist/bin/cli-claw.js (0.149083ms)
✔ RH-007: package build runs clean:dist before tsc (0.213333ms)
✔ resume classifier invalidates explicit stale Claude session errors (0.885625ms)
✔ resume classifier invalidates generic invalid resume errors (0.24275ms)
✔ resume classifier preserves session for recoverable 429 errors (0.377458ms)
✔ resume classifier preserves session for auth errors (0.2655ms)
✔ resume classifier preserves session when no stale signal is present (0.09725ms)
✔ SAF-001: postinstall has JAW_SAFE safe mode guard (1.268209ms)
✔ SAF-002: postinstall has npm_config_jaw_safe guard (0.210917ms)
✔ SAF-003: safe mode returns early (no side effects) (0.158625ms)
✔ SAF-004: installCliTools is exported (0.107625ms)
✔ SAF-005: installMcpServers is exported (0.138333ms)
✔ SAF-006: installSkillDeps is exported (0.100666ms)
✔ SAF-007: InstallOpts type is exported (0.112208ms)
✔ SAF-008: all install functions support dryRun (0.1195ms)
✔ SAF-009: postinstall has isEntryPoint guard (0.120625ms)
✔ SAF-010: safe mode guard is before skills/uploads ensureDir (0.180167ms)
✔ INIT-001: init.ts has --dry-run option (38.578542ms)
✔ INIT-002: init.ts has --safe option for safe install mode (0.279583ms)
✔ INIT-003: init.ts uses dynamic import for postinstall (no static side-effect) (0.820084ms)
✔ INIT-004: init.ts imports and calls extracted install functions (0.895167ms)
✔ INIT-005: --dry-run guards settings/dir writes (0.171ms)
✔ validateTarget rejects null/undefined target (77.407166ms)
✔ validateTarget rejects empty targetId (2.307584ms)
✔ validateTarget rejects channel mismatch (0.862125ms)
✔ validateTarget accepts matching channel with valid targetId (1.129125ms)
✔ validateDiscordFileSize rejects 11 MiB (8.54425ms)
✔ validateDiscordFileSize accepts 5 MiB (3.324792ms)
✔ normalizeChannelSendRequest maps body fields correctly (2.904667ms)
✔ normalizeChannelSendRequest defaults channel to active (1.5145ms)
✔ chunkDiscordMessage handles empty string (6.594125ms)
✔ chunkDiscordMessage handles exactly 2000 chars (2.143708ms)
✔ chunkDiscordMessage splits 4000 chars into 2 chunks (1.753541ms)
✔ S-001: darwin platform → launchd backend (1.171916ms)
✔ S-002: /proc/1/comm exists on linux (0.142708ms)
✔ S-003: /.dockerenv detection (0.143542ms)
✔ S-004: default JAW_HOME produces "default" instance (0.13175ms)
✔ S-005: custom JAW_HOME produces hashed instance (0.503834ms)
✔ S-006: sanitizeUnitName strips invalid chars (0.156417ms)
✔ S-007: UNIT_NAME format is jaw-<sanitized-instance> (10.168667ms)
✔ S-008: UNIT_PATH follows /etc/systemd/system/ convention (0.149209ms)
✔ S-009: --port 3458 parsing (0.7465ms)
✔ S-010: --backend systemd parsing (0.225333ms)
✔ S-011: positional subcommand extraction (0.145542ms)
✔ S-012: no execSync string interpolation in production code (0.7135ms)
✔ S-013: instance.ts exports all required functions (0.212791ms)
✔ S-014: ExecStart uses --no-open to prevent browser auto-open (0.192209ms)
✔ S-015: service.ts validates --backend values (0.194833ms)
✔ S-016: macOS logs mapped to file paths (not delegated to launchd) (0.146167ms)
✔ S-017: service.ts uses mktemp for temp unit file (0.181291ms)
✔ S-018: cli-jaw.ts includes service in known commands (0.459667ms)
✔ S-019: doctor.ts includes headless detection (0.249125ms)
✔ S-020: launchd.ts imports shared functions from instance.ts (0.204625ms)
✔ S-021: port validation rejects non-numeric values (0.215958ms)
✔ S-022: port validation accepts valid port numbers (0.107583ms)
✔ S-023: status reads port from unit file content (0.135792ms)
✔ S-024: doctor uses npm root -g for playwright-core detection (0.177042ms)
✔ S-025: install.sh verifies Chromium binary after install (0.536625ms)
✔ S-026: install.sh uses npm root -g for playwright-core detection (0.159583ms)
✔ S-027: install.sh verifies chromium via --version not just command -v (0.140083ms)
✔ session persistence allows current owner to save successful non-fallback result (76.766916ms)
✔ session persistence blocks fallback runs from saving main session row (0.331583ms)
✔ session persistence blocks stale owner after generation bump (5.906041ms)
✔ session persistence blocks non-zero exits (0.137542ms)
✔ spawn.ts uses shared persistence and resume-classifier helpers (0.645042ms)
✔ resolveMainCli prefers explicit request over settings and session (1.08225ms)
✔ resolveMainCli prefers settings over stale session row (0.172125ms)
✔ buildSelectedSessionRow clears session id when selected CLI changed (0.483084ms)
✔ buildClearedSessionRow preserves settings intent rather than stale DB CLI (0.180541ms)
✔ shouldIncludeVisionClickHint follows intended CLI (0.126292ms)
✔ SM-001: perCli deep merge preserves existing effort (0.912584ms)
✔ SM-002: perCli adds new CLI without removing others (0.153375ms)
✔ SM-003: activeOverrides deep merge preserves sibling keys (0.122208ms)
✔ SM-004: top-level scalar fields are replaced (0.086042ms)
✔ SM-005: empty patch returns original (0.654875ms)
✔ SM-006: tui deep merge preserves sibling keys (0.12325ms)
[skills] symlink: /var/folders/l5/54gwcgf94ld_kt2gyry3q6p00000gn/T/jaw-wd-NazBCg/.agents/skills → /var/folders/l5/54gwcgf94ld_kt2gyry3q6p00000gn/T/jaw-home-JtRE8e/.cli-jaw/skills
[skills] symlink: /var/folders/l5/54gwcgf94ld_kt2gyry3q6p00000gn/T/jaw-wd-NazBCg/.claude/skills → /var/folders/l5/54gwcgf94ld_kt2gyry3q6p00000gn/T/jaw-home-JtRE8e/.cli-jaw/skills
[skills] conflict(skip): /var/folders/l5/54gwcgf94ld_kt2gyry3q6p00000gn/T/jaw-wd-MlsfXl/.agents/skills (existing path preserved)
[skills] symlink(updated): /var/folders/l5/54gwcgf94ld_kt2gyry3q6p00000gn/T/jaw-wd-eItZgs/.agents/skills → /var/folders/l5/54gwcgf94ld_kt2gyry3q6p00000gn/T/jaw-home-sxCZWG/.cli-jaw/skills
[skills] conflict(skip): /var/folders/l5/54gwcgf94ld_kt2gyry3q6p00000gn/T/jaw-wd-JWud0J/.agents/skills (unmanaged symlink preserved)
[skills] symlink: /var/folders/l5/54gwcgf94ld_kt2gyry3q6p00000gn/T/jaw-home-HMtI2q/.cli-jaw/.agents/skills → /var/folders/l5/54gwcgf94ld_kt2gyry3q6p00000gn/T/jaw-home-HMtI2q/.cli-jaw/skills
✔ SPI-001: postinstall has CLI_JAW_MIGRATE_SHARED_PATHS guard (1.346084ms)
✔ SPI-002: postinstall migration is opt-in via env (0.199834ms)
✔ SPI-003: postinstall logs shared path migration skipped message (0.105166ms)
✔ SPI-004: server.ts calls ensureWorkingDirSkillsLinks (not ensureSkillsSymlinks) (0.142542ms)
✔ SPI-005: command-context.ts uses runSkillReset, not inline repair flow (0.106417ms)
✔ SPI-005b: server startup uses onConflict skip by default (0.107959ms)
✔ SPI-006: doctor.ts checks shared path contamination (0.098042ms)
✔ SPI-007: doctor does not report symlinked as positive (0.174958ms)
✔ SPI-008: README does not instruct removing shared harness paths (0.212458ms)
✔ SPI-009: mcp-sync exports ensureWorkingDirSkillsLinks (not shared home) (0.270292ms)
✔ SPI-010: ensureSharedHomeSkillsLinks is exported as opt-in API (0.140084ms)
✔ SPI-009b: ensureWorkingDirSkillsLinks does not create home shared links (83.274792ms)
✔ SPI-011: ensureWorkingDirSkillsLinks skips when workingDir is homedir (2.318458ms)
✔ SPI-012: existing unmanaged repo-local .agents is not replaced (7.924125ms)
✔ SPI-013: allowReplaceManaged updates stale cli-jaw symlink (5.672708ms)
✔ SPI-013b: allowReplaceManaged skips unmanaged stale symlink (12.561792ms)
✔ SPI-015: reset repair backs up known legacy managed dir before relinking (15.27925ms)
✔ SPI-014: backup traces without active symlinks report resolved, not contaminated (22.753ms)
✔ CCD-001: shouldSkipClone() returns false when no meta file exists (1.42275ms)
✔ CCD-002: shouldSkipClone() returns false after a successful clone (1.206416ms)
✔ CCD-003: shouldSkipClone() returns true within cooldown after a failed clone (0.85425ms)
✔ CCD-004: shouldSkipClone() returns false after cooldown expires (0.824ms)
✔ CCD-005: writeCloneMeta() creates valid JSON at expected path (1.032667ms)
✔ CCD-006: corrupted meta file → shouldSkipClone() returns false (2.887458ms)
✔ CCD-007: JAW_FORCE_CLONE=1 bypasses cooldown even within window (1.051375ms)
✔ CCD-008: semantically invalid meta (wrong types) → shouldSkipClone() returns false (0.934167ms)
✔ CCD-009: partial meta (missing fields) → shouldSkipClone() returns false (0.95825ms)
✔ smoke: detects English spawn-subagent pattern with high confidence (0.91575ms)
✔ smoke: detects Korean continuation pattern (0.869083ms)
✔ smoke: thinking-only activity downgrades to medium confidence (0.236542ms)
✔ smoke: search activity means not smoke (0.085167ms)
✔ smoke: non-zero exit is not smoke (0.083334ms)
✔ smoke: codex smoke turn requires agent message without command execution (0.131709ms)
✔ smoke: continuation prompt embeds original task and direct-work guard (0.111125ms)
✔ spawn.ts imports smoke-detector helpers (0.919416ms)
✔ spawn.ts standard and ACP exit handlers both run smoke detection before retry handling (0.35225ms)
✔ spawn.ts smoke continuation keeps main-managed path and emits smoke event (1.576833ms)
✔ frontend and telegram consumers handle agent_smoke (0.59275ms)
✔ SOS-001: startup resets all active scoped orc_state rows (0.7085ms)
✔ SOS-002: snapshot endpoint includes scope (0.115834ms)
✔ SOS-003: WebSocket initial state includes scope (0.088834ms)
▶ PABCD state-machine
  ✔ 1. getState() = IDLE initially (3.025792ms)
  ✔ 2. setState P (0.789542ms)
  ✔ 3. setState with ctx (1.079667ms)
  ✔ 4. resetState → IDLE + null (1.630916ms)
  ✔ 5. IDLE→P valid (0.221417ms)
  ✔ 6. IDLE→B invalid (0.17075ms)
  ✔ 7. P→A valid (0.1545ms)
  ✔ 8. prefix P user = Pb2 (0.263542ms)
  ✔ 9. prefix B user = null (0.155166ms)
  ✔ 10. prefix B worker = Bb2 (0.152084ms)
  ✔ 11. statePrompt P not empty (0.104708ms)
  ✔ 12. statePrompt INVALID = empty (0.0925ms)
  ✔ 13. statePrompt D not empty (0.085334ms)
  ✔ 14. C→D and D→IDLE valid (0.080708ms)
  ✔ 15. D → reset → IDLE (0.274458ms)
  ✔ 16. P→D invalid (must go through C) (0.079208ms)
  ✔ 17. setState P with ctx preserves context (0.276125ms)
✔ PABCD state-machine (10.715166ms)
✔ STR-001: /steer registered in COMMANDS with web/telegram interfaces (not cli) (0.9395ms)
✔ STR-002: steerHandler kills agent and waits before re-orchestrate (0.208875ms)
✔ STR-003: steerHandler returns steer type for telegram, success for web/cli (0.119958ms)
✔ STR-004: steerHandler validates prompt is not empty (0.097ms)
✔ STR-005: steerHandler returns error when no agent running (0.097708ms)
✔ STR-006: bot.ts steer branch does not call steerAgent (no double orchestration) (0.133333ms)
✔ STR-007: steerHandler is imported in commands.ts (0.08625ms)
✔ STR-008: i18n keys exist for steer command in ko.json and en.json (11.571167ms)
✔ SF-001: steerAgent flow: kill → wait → insert → orchestrate (1.376042ms)
✔ SF-002: exit handler saves interrupted content to DB via insertMessageWithTrace (0.654833ms)
✔ SF-003: buildHistoryBlock uses trace for assistant messages, preserving interrupted tag (0.528875ms)
✔ SF-EDGE: processQueue is triggered after mainManaged exit in both paths (1.963833ms)
✔ SI-001: killActiveAgent sets killReason to the given reason (0.817125ms)
✔ SI-002: killActiveAgent defaults reason to "user" (0.1885ms)
✔ SI-003: ACP exit handler adds interrupted prefix to fullText when wasSteer (0.119625ms)
✔ SI-004: ACP exit handler adds interrupted prefix to traceText when wasSteer (0.094292ms)
✔ SI-005: ACP exit handler suppresses fallback on steer kill (0.103625ms)
✔ SI-006: Standard CLI close handler tags interrupted output (0.641375ms)
✔ SI-007: killReason is consumed (set to null) after mainManaged exit (1.300959ms)
✔ SI-STRUCT: ACP and CLI exit handlers have symmetric steer logic (0.577959ms)
✔ steerAgent calls killActiveAgent with "steer" reason (0.302917ms)
✔ steerAgent inserts user message and broadcasts before orchestrating (0.200333ms)
✔ buildHistoryBlock prefers trace over content for assistant messages (2.027125ms)
(node:62727) ExperimentalWarning: Module mocking is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
▶ streaming-render
  ▶ createStreamRenderer
    ✔ returns initial empty state with correct shape (4.646959ms)
  ✔ createStreamRenderer (5.335ms)
  ▶ appendChunk
    ✔ accumulates text chunks in fullText (0.887083ms)
    ✔ schedules exactly one rAF per batch (0.968375ms)
    ✔ renders accumulated text + cursor on rAF flush (1.486ms)
    ✔ clears pendingRAF after rAF fires (0.235166ms)
    ✔ schedules new rAF after previous one fires (0.172458ms)
    ✔ does not render after finalization (0.164833ms)
    ✔ handles empty chunks gracefully (0.150125ms)
  ✔ appendChunk (4.673541ms)
  ▶ finalizeStream
    ✔ returns accumulated text (0.196792ms)
    ✔ marks state as finalized (0.137625ms)
    ✔ cancels pending rAF via cancelAnimationFrame (0.121542ms)
    ✔ renders final HTML without cursor (0.11025ms)
    ✔ handles empty stream (0.094583ms)
    ✔ handles double finalization idempotently (0.112333ms)
    ✔ multi-chunk then finalize preserves full text (0.109041ms)
  ✔ finalizeStream (1.104667ms)
✔ streaming-render (11.695625ms)
[stt] prompt file not found: prompts/missing.md — continuing without STT prompt
✔ resolveSttPromptPath finds custom relative prompt path (1.765917ms)
✔ loadSttPrompt falls back to default prompt when custom path is missing (1.04225ms)
✔ loadSttPrompt returns empty string when no prompt file exists (0.854875ms)
✔ SM-001: empty text returns rejected/empty (1.240459ms)
✔ SM-002: idle + normal message calls insertMessage and orchestrate (0.211542ms)
✔ SM-003: busy path enqueues only, does NOT call insertMessage (0.122ms)
✔ SM-004: continue intent when idle → started + orchestrateContinue (0.1045ms)
✔ SM-005: continue intent when busy → rejected/busy (0.103084ms)
✔ SM-006: reset intent when idle → started + orchestrateReset (0.137916ms)
✔ SM-007: reset intent when busy still starts reset flow (0.484084ms)
✔ SM-008: displayText is used for insert and broadcast when provided (0.165416ms)
✔ SM-009: SubmitResult type includes pending field (0.106208ms)
✔ SM-010: origin from meta is used in broadcast and orchestrate (0.182917ms)
✔ SM-011: detached orchestration calls are rejection-safe (0.104791ms)
✔ system prompt references canonical /api/channel/send (0.8095ms)
✔ employee prompt references canonical /api/channel/send (0.103792ms)
✔ system prompt documents legacy telegram and discord endpoints (0.092041ms)
✔ employee prompt documents legacy endpoints (0.081625ms)
✔ system prompt documents Discord degraded mode (0.084875ms)
✔ system prompt mentions jaw doctor for Discord diagnosis (0.076917ms)
✔ employee prompt describes channel-generic delivery (0.099833ms)
✔ system prompt documents channel omission defaults to active (0.0865ms)
[telegram:retry] attempt 1/3 failed (429), retrying in 1000ms...
✔ validateFileSize: voice 49MB passes (under 50MB limit) (40.374125ms)
✔ validateFileSize: voice 51MB rejected (56.415875ms)
✔ validateFileSize: photo 11MB rejected (10MB limit) (16.19225ms)
✔ validateFileSize: document 1MB passes (4.694792ms)
✔ validateFileSize: text type is skipped (no limit) (1.2485ms)
✔ sendTelegramFile: succeeds on first attempt (2.032875ms)
[telegram:retry] attempt 2/3 failed (429), retrying in 2000ms...
[telegram:retry] attempt 1/3 failed (500), retrying in 1000ms...
✔ sendTelegramFile: retries on 429 then succeeds (3006.036959ms)
[telegram:retry] attempt 1/3 failed (network), retrying in 1000ms...
✔ sendTelegramFile: retries on 500 then succeeds (1003.087125ms)
[telegram:file] failed after 1 attempt(s): mock 400
[telegram:file] failed after 1 attempt(s): mock 403
[telegram:retry] attempt 1/3 failed (500), retrying in 1000ms...
✔ sendTelegramFile: retries on HttpError (network) (1003.896458ms)
✔ sendTelegramFile: does NOT retry on 400 (permanent) (0.810917ms)
✔ sendTelegramFile: does NOT retry on 403 (permanent) (1.039125ms)
[telegram:retry] attempt 2/3 failed (500), retrying in 2000ms...
✔ TQ-001: submitMessage metadata supports optional chatId (33.390334ms)
✔ TQ-002: busy path forwards target+chatId+requestId into enqueueMessage (0.135166ms)
✔ TQ-003: orchestrate paths forward target+chatId+requestId for continue/reset/normal (0.101208ms)
✔ TQ-004: processQueue isolates queue by groupQueueKey (0.085584ms)
✔ TQ-005: processQueue uses batch head source/chatId (no last-item leakage) (0.0935ms)
✔ TQ-006: processQueue no longer emits duplicate new_message broadcast (0.15075ms)
✔ TQ-006b: processQueue respects worker busy guards (0.1135ms)
✔ TQ-007: tgOrchestrate passes chatId to submitMessage (0.08675ms)
✔ TQ-008: queued telegram response filter uses requestId for isolation (0.097084ms)
(node:62859) ExperimentalWarning: Module mocking is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
▶ tool-ui
  ▶ buildToolGroupHtml
    ✔ returns empty string for empty array (0.700792ms)
    ✔ returns empty string for null/undefined (0.09ms)
    ✔ renders single tool entry with correct structure (0.443875ms)
    ✔ aria-controls matches details id (0.153292ms)
    ✔ renders multiple entries with same icon — count aggregated (0.132584ms)
    ✔ renders mixed icons with correct counts (0.110167ms)
    ✔ escapes HTML in icon and label (XSS prevention) (0.107958ms)
    ✔ renders collapsed details by default (0.097833ms)
    ✔ generates unique toolId using Date.now() (0.1765ms)
    ✔ chevron indicator present in summary (0.131541ms)
    ✔ done status dot present in summary (0.082875ms)
  ✔ buildToolGroupHtml (2.918917ms)
✔ tool-ui (16.573583ms)
✔ CS-001: filterSelectorItems returns all items when filter is empty (1.173542ms)
✔ CS-002: filterSelectorItems matches value substring (0.198375ms)
✔ CS-003: filterSelectorItems is case insensitive (0.152ms)
✔ CS-004: filterSelectorItems matches label (0.121417ms)
✔ CS-005: filterSelectorItems returns empty for no match (0.388333ms)
✔ CS-006: filterSelectorItems matches partial from middle of value (0.18425ms)
✔ CS-007: renderChoiceSelector returns positive height and writes output (0.437042ms)
✔ CS-008: renderChoiceSelector marks current item with bullet (1.587125ms)
✔ CS-009: renderChoiceSelector handles empty items list (1.604792ms)
✔ CS-010: renderChoiceSelector clamps to terminal rows (0.959166ms)
✔ CS-011: createSelectorState returns closed state with empty arrays (1.136333ms)
✔ CS-012: createOverlayState includes selector sub-state (0.178083ms)
✔ CS-013: modelHandler with explicit args still returns success (9.909541ms)
✔ CS-014: cliHandler with explicit args still returns success (6.936ms)
✔ CS-015: modelHandler no-arg returns readable text (Telegram/Discord contract) (2.83375ms)
✔ CS-016: cliHandler no-arg returns readable text (Telegram/Discord contract) (1.321667ms)
✔ CS-017: public/locales exists at the resolved PROJECT_ROOT (source tree) (17.870166ms)
✔ CS-018: modelHandler respects en locale when set (14.347167ms)
✔ CS-019: selector arrow-down on empty filteredItems does not produce negative index (0.16575ms)
✔ CS-020: selector enter on empty filteredItems is a no-op (0.071333ms)
✔ short single-line paste is absorbed into trailing text (0.864166ms)
✔ multiline paste becomes a collapsed paste segment (0.269542ms)
✔ backspace removes trailing paste block atomically (0.153667ms)
✔ plain command draft only exists for single text segment without newline (0.099541ms)
✔ consumePasteProtocol preserves normal escape sequences (0.72425ms)
✔ consumePasteProtocol handles fragmented bracketed paste markers (0.138625ms)
✔ classifyKeyAction detects navigation escape sequences (0.899167ms)
✔ classifyKeyAction detects enter family and control keys (0.163959ms)
✔ classifyKeyAction detects ctrl-k (0.0855ms)
✔ classifyKeyAction detects printable input and unknown keys (0.082125ms)
✔ renderHelpOverlay returns positive box height (1.514125ms)
✔ renderHelpOverlay with extra commands (0.19825ms)
✔ renderCommandPalette renders items with selection highlight (0.279625ms)
✔ renderCommandPalette handles empty items (0.116333ms)
✔ renderCommandPalette respects small terminal (0.160834ms)
✔ resolveAutocompleteState closes for non-command drafts (0.866ms)
✔ resolveAutocompleteState opens command completion list (0.715083ms)
✔ resolveAutocompleteState opens argument completion list with context header (0.302583ms)
✔ syncAutocompleteWindow clamps selection and window into visible range (0.161542ms)
✔ applyResolvedAutocompleteState and popupTotalRows keep overlay state coherent (0.152875ms)
✔ createPaneState starts closed with default side and width (0.678417ms)
✔ openPanel and closePanel manage explicit panel visibility (0.124291ms)
✔ togglePanel opens and closes the same panel (0.105667ms)
✔ each panel has an explicit empty state (0.081917ms)
✔ visualWidth ignores ANSI escape codes (6.312958ms)
✔ visualWidth counts Hangul as double-width (0.466709ms)
✔ clipTextToCols respects visual width for mixed-width text (18.4365ms)
✔ setupScrollRegion writes scroll region, divider, footer, and cursor restore in order (1.33ms)
✔ cleanupScrollRegion resets the scroll region and drops the cursor to the bottom row (0.153167ms)
✔ ensureSpaceBelow emits natural scroll newlines and restores cursor upward once (0.118166ms)
✔ ensureSpaceBelow is a no-op for non-positive counts (0.086167ms)
✔ resolveShellLayout keeps footer rows stable and allocates aux panel outside transcript (0.169791ms)
✔ appendUserItem adds user transcript entry (0.979125ms)
✔ assistant chunk flow: start → append → finalize (0.250458ms)
✔ appendToActiveAssistant returns false when no active assistant (0.108834ms)
✔ appendToActiveAssistant returns false after finalize (0.087042ms)
✔ agent_done with text but no prior chunks (0.102083ms)
✔ ephemeral status replaces previous status (0.152791ms)
✔ clearEphemeralStatus removes trailing status (0.115916ms)
✔ clearEphemeralStatus does nothing when last item is not status (0.084333ms)
✔ full conversation flow (0.726917ms)
✔ user item with paste (display differs from submit) (0.238666ms)
✔ UQ-001: shortLabel truncates "Premium" to "Prem" (0.991833ms)
✔ UQ-002: shortLabel truncates "plus monthly subscriber quota" to "plus" (0.133417ms)
✔ UQ-003: shortLabel preserves existing abbreviations (0.092583ms)
✔ UQ-004: shortLabel handles unknown labels gracefully (passthrough) (0.081333ms)
✔ UQ-005: source shortLabel chain matches test helper exactly (0.184709ms)
✔ UQ-006: label span uses overflow ellipsis, not fixed width (0.123208ms)
✔ UQ-007: label span has min-width for short labels (1-2 char) (0.083125ms)
✔ UQ-008: all known shortLabel outputs are ≤ 7 characters (0.116834ms)
✔ UQ-009: layout.css has .sidebar-hb-btn.w-auto override with width:auto (0.116292ms)
✔ UQ-010: .sidebar-hb-btn.w-auto has flex-shrink:0 to prevent collapse (0.167958ms)
✔ UQ-011: .sidebar-hb-btn.w-auto appears AFTER .sidebar-hb-btn in cascade (0.565208ms)
✔ UQ-012: appNameInput in index.html sits in a flex container with .input-agent-name (0.603375ms)
✔ UQ-013: .input-agent-name has flex:1 for space allocation (2.128958ms)
[upload] saved /var/folders/l5/54gwcgf94ld_kt2gyry3q6p00000gn/T/upload-test-1775827417106/1775827417106_notebook.ipynb (2 bytes)
[upload] saved /var/folders/l5/54gwcgf94ld_kt2gyry3q6p00000gn/T/upload-test-1775827417109/1775827417109_한글파일.xlsx (4 bytes)
[upload] saved /var/folders/l5/54gwcgf94ld_kt2gyry3q6p00000gn/T/upload-test-1775827417110/1775827417111_file.bin (1 bytes)
[upload] saved /var/folders/l5/54gwcgf94ld_kt2gyry3q6p00000gn/T/upload-test-1775827417118/1775827417119_noext.bin (1 bytes)
✔ UP-001: saveUpload preserves .ipynb extension (2.944958ms)
✔ UP-002: saveUpload preserves Korean filename stem (1.2865ms)
✔ UP-003: saveUpload falls back to "file" on empty stem (8.302291ms)
✔ UP-004: saveUpload defaults to .bin when no extension (1.087708ms)
✔ VR-001: pipeline imports getState from state-machine (1.119375ms)
✔ VR-002: pipeline uses PABCD state check before worker dispatch (0.166125ms)
✔ VR-003: pipeline feeds worker results back via recursive orchestrate (0.093875ms)
✔ VR-004: pipeline handles worker not found gracefully (0.084834ms)
✔ VR-005: state machine has PABCD transition guards (0.092208ms)
✔ VR-006: state machine exports canTransition (0.093ms)
✔ RV-001: state machine has PLANNING, AUDIT, and BUILD prefixes (0.073958ms)
✔ QP-001: queue policy is documented as "fair" (0.068791ms)
✔ QP-002: batch tail goes after remaining (fair ordering) (0.088375ms)
✔ RC-001: orchestrateContinue checks PABCD state before worklog (7.738792ms)
✔ RC-002: PABCD requires explicit entry (no auto-activation) (0.307417ms)
✔ RC-003: PABCD requires explicit phase advance (no auto-advance) (0.106541ms)
✔ PUT /api/settings handler is async (1.279375ms)
✔ applySettingsPatch calls applyRuntimeSettingsPatch (0.229708ms)
✔ applyRuntimeSettingsPatch rolls back on failure (0.158042ms)
✔ applyRuntimeSettingsPatch propagates error to caller (0.132958ms)
✔ web command context routes through applySettingsPatch (0.233167ms)
✔ P1-001: DEFAULT_SETTINGS.workingDir === JAW_HOME (1.54ms)
✔ P1-002: A2_DEFAULT prompt contains ~/.cli-jaw not bare ~/ (1.429667ms)
✔ WRH-001: finished worker result is injected exactly once (1.275292ms)
✔ WRH-002: failed reinjection releases replay claim and leaves replay pending (0.408209ms)
✔ WRH-003: replay drain picks up pending results from prior workers (0.186041ms)
✔ worker classification: pipeline marks non-done worker results as failures (0.725667ms)
✔ worker classification: replay contract only runs for done workers (0.112708ms)
✔ worker classification: pending replay list only includes done worker slots (0.082667ms)
✔ PHASES maps 5 phases correctly (0.964458ms)
✔ parseWorklogPending extracts pending agents from matrix (0.691875ms)
✔ parseWorklogPending returns empty array when no pending agents (0.784959ms)
✔ parseWorklogPending handles missing matrix section (0.132709ms)
✔ parseWorklogPending falls back to phase 1 when phase number is missing (0.153791ms)
✔ parseWorklogPending stops parsing at next section heading (0.128958ms)
ℹ tests 1002
ℹ suites 33
ℹ pass 998
ℹ fail 4
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 20729.2475

✖ failing tests:

test at tests/unit/fallback-retry.test.ts:1:9064
✖ timer pending blocks processQueue at runtime (2.865708ms)
  AssertionError [ERR_ASSERTION]: processQueue must guard on all three: activeProcess, retryPendingTimer, messageQueue
      at TestContext.<anonymous> (/Users/jun/Developer/new/700_projects/cli-jaw/tests/unit/fallback-retry.test.ts:237:16)
      at Test.runInAsyncScope (node:async_hooks:228:14)
      at Test.run (node:internal/test_runner/test:1118:25)
      at Test.start (node:internal/test_runner/test:1015:17)
      at node:internal/test_runner/test:1531:71
      at node:internal/per_context/primordials:466:82
      at new Promise (<anonymous>)
      at new SafePromise (node:internal/per_context/primordials:435:3)
      at node:internal/per_context/primordials:466:9
      at Array.map (<anonymous>) {
    generatedMessage: false,
    code: 'ERR_ASSERTION',
    actual: false,
    expected: true,
    operator: '==',
    diff: 'simple'
  }

test at tests/unit/orchestrator-research.test.ts:25:300
✖ RES-008: initial P request injects research before planning (123.946667ms)
  AssertionError [ERR_ASSERTION]: The input did not match the regular expression /Pre-Planning Research Report/i. Input:
  
  'compare auth and session approaches'
  
      at TestContext.<anonymous> (/Users/jun/Developer/new/700_projects/cli-jaw/tests/unit/orchestrator-research.test.ts:154:12)
      at async Test.run (node:internal/test_runner/test:1125:7)
      at async Test.processPendingSubtests (node:internal/test_runner/test:787:7) {
    generatedMessage: true,
    code: 'ERR_ASSERTION',
    actual: 'compare auth and session approaches',
    expected: /Pre-Planning Research Report/i,
    operator: 'match',
    diff: 'simple'
  }

test at tests/unit/orchestrator-research.test.ts:37:900
✖ RES-009: clear implementation request skips pre-planning research (26.444584ms)
  AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value:
  
    assert.ok(prompts[0]!.includes('[PABCD — P: PLANNING]'))
  
      at TestContext.<anonymous> (/Users/jun/Developer/new/700_projects/cli-jaw/tests/unit/orchestrator-research.test.ts:191:12)
      at async Test.run (node:internal/test_runner/test:1125:7)
      at async Test.processPendingSubtests (node:internal/test_runner/test:787:7) {
    generatedMessage: true,
    code: 'ERR_ASSERTION',
    actual: false,
    expected: true,
    operator: '==',
    diff: 'simple'
  }

test at tests/unit/orchestrator-worklog-contract.test.ts:1:781
✖ OWC-002: initial planning turn creates worklog before setState (20.741459ms)
  AssertionError [ERR_ASSERTION]: Expected "actual" to be strictly unequal to: -1
      at TestContext.<anonymous> (/Users/jun/Developer/new/700_projects/cli-jaw/tests/unit/orchestrator-worklog-contract.test.ts:24:12)
      at Test.runInAsyncScope (node:async_hooks:228:14)
      at Test.run (node:internal/test_runner/test:1118:25)
      at Test.processPendingSubtests (node:internal/test_runner/test:787:18)
      at Test.postRun (node:internal/test_runner/test:1247:19)
      at Test.run (node:internal/test_runner/test:1175:12)
      at async startSubtestAfterBootstrap (node:internal/test_runner/harness:358:3) {
    generatedMessage: true,
    code: 'ERR_ASSERTION',
    actual: -1,
    expected: -1,
    operator: 'notStrictEqual',
    diff: 'simple'
  }
