Add bd exec bridge and mutation API routes with tests

This commit is contained in:
zenchantlive 2026-02-11 19:46:02 -08:00
parent 0e3815ac3c
commit 2c80265258
15 changed files with 904 additions and 5 deletions

View file

@ -3,12 +3,13 @@
{"id":"bb-29x.2","title":"Implement API integration tests for read, mutate, and SSE routes","description":"Validate route contracts and interaction boundaries across read/write/realtime layers.","acceptance_criteria":"Integration suite verifies route behavior and error semantics.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:17.4912736-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:17.4912736-08:00","labels":["integration","tests"],"dependencies":[{"issue_id":"bb-29x.2","depends_on_id":"bb-29x","type":"parent-child","created_at":"2026-02-11T17:12:17.4923012-08:00","created_by":"zenchantlive"},{"issue_id":"bb-29x.2","depends_on_id":"bb-29x.1","type":"blocks","created_at":"2026-02-11T17:12:38.9423299-08:00","created_by":"zenchantlive"}]}
{"id":"bb-29x.3","title":"Record parser and realtime performance baseline against PRD targets","description":"Measure parse latency and update propagation using realistic sample sizes and document outcomes.","acceptance_criteria":"Performance report exists with methodology and observed timings.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:18.3210495-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:18.3210495-08:00","labels":["benchmark","perf"],"dependencies":[{"issue_id":"bb-29x.3","depends_on_id":"bb-29x","type":"parent-child","created_at":"2026-02-11T17:12:18.3220949-08:00","created_by":"zenchantlive"},{"issue_id":"bb-29x.3","depends_on_id":"bb-29x.2","type":"blocks","created_at":"2026-02-11T17:12:39.4534943-08:00","created_by":"zenchantlive"}]}
{"id":"bb-29x.4","title":"Document operational runbook and boundary rationale","description":"Write architecture docs covering scanner policy, bd bridge behavior, and consistency guardrails for future maintainers.","acceptance_criteria":"Runbook documents startup, troubleshooting, and boundary rules.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:19.1385778-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:19.1385778-08:00","labels":["docs","runbook"],"dependencies":[{"issue_id":"bb-29x.4","depends_on_id":"bb-29x","type":"parent-child","created_at":"2026-02-11T17:12:19.1402086-08:00","created_by":"zenchantlive"},{"issue_id":"bb-29x.4","depends_on_id":"bb-29x.2","type":"blocks","created_at":"2026-02-11T17:12:39.9591458-08:00","created_by":"zenchantlive"}]}
{"id":"bb-3pr","title":"Smoke test mutation lifecycle 2","description":"Temporary issue for API mutation smoke test","status":"closed","priority":1,"issue_type":"task","assignee":"zenchantlive","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T19:44:10.9737485-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T19:44:16.4912473-08:00","closed_at":"2026-02-11T19:44:16.4912473-08:00","close_reason":"Cleanup after API smoke test","labels":["api","smoke"],"comments":[{"id":1,"issue_id":"bb-3pr","author":"zenchantlive","text":"Smoke test comment via API route","created_at":"2026-02-12T03:44:13Z"},{"id":2,"issue_id":"bb-3pr","author":"zenchantlive","text":"Smoke test reopen","created_at":"2026-02-12T03:44:15Z"}]}
{"id":"bb-6aj","title":"Project Registry and Multi-Project Scanner","description":"Support multiple Windows project roots using profile-scoped registry storage and safe discovery scanning tuned for developer machines.","acceptance_criteria":"Projects can be added/removed/listed and discovered via scanner with deterministic normalization.","status":"open","priority":0,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:47.7205517-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:11:47.7205517-08:00","labels":["multi-project","scanner"],"dependencies":[{"issue_id":"bb-6aj","depends_on_id":"bb-92d","type":"blocks","created_at":"2026-02-11T17:12:19.6374139-08:00","created_by":"zenchantlive"}]}
{"id":"bb-6aj.1","title":"Persist project registry in %USERPROFILE%\\\\.beadboard\\\\projects.json","description":"Implement read/write management for registry file in user profile path, isolated from repository files and safe for local machine usage.","acceptance_criteria":"Registry file is created lazily and survives app restarts.","status":"closed","priority":0,"issue_type":"task","assignee":"agent-a","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:48.5403111-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:53:17.2085722-08:00","closed_at":"2026-02-11T17:53:17.2085722-08:00","close_reason":"Implemented %USERPROFILE%/.beadboard/projects.json registry persistence with Windows-safe normalization and dedupe.","labels":["config","registry"],"dependencies":[{"issue_id":"bb-6aj.1","depends_on_id":"bb-6aj","type":"parent-child","created_at":"2026-02-11T17:11:48.5419102-08:00","created_by":"zenchantlive"}]}
{"id":"bb-6aj.2","title":"Implement registry API for add/remove/list operations","description":"Expose robust API endpoints with path validation and normalized identity checks to prevent duplicates.","acceptance_criteria":"API supports add, remove, list and returns clear validation errors.","status":"closed","priority":0,"issue_type":"task","assignee":"agent-a","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:49.3542564-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:53:23.9298353-08:00","closed_at":"2026-02-11T17:53:23.9298353-08:00","close_reason":"Implemented /api/projects GET/POST/DELETE with validation, normalization, and registry integration.","labels":["api","registry"],"dependencies":[{"issue_id":"bb-6aj.2","depends_on_id":"bb-6aj","type":"parent-child","created_at":"2026-02-11T17:11:49.3558158-08:00","created_by":"zenchantlive"},{"issue_id":"bb-6aj.2","depends_on_id":"bb-6aj.1","type":"blocks","created_at":"2026-02-11T17:12:26.7117348-08:00","created_by":"zenchantlive"}]}
{"id":"bb-6aj.3","title":"Build scanner with profile-root default and depth/ignore controls","description":"Scan %USERPROFILE% and user-defined roots for .beads directories with bounded recursion and ignore patterns to protect performance.","acceptance_criteria":"Scanner discovers projects without traversing entire drives by default.","status":"open","priority":0,"issue_type":"task","assignee":"agent-c","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:50.1925005-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:43:32.4095636-08:00","labels":["performance","scanner"],"dependencies":[{"issue_id":"bb-6aj.3","depends_on_id":"bb-6aj","type":"parent-child","created_at":"2026-02-11T17:11:50.1940841-08:00","created_by":"zenchantlive"},{"issue_id":"bb-6aj.3","depends_on_id":"bb-6aj.1","type":"blocks","created_at":"2026-02-11T17:12:27.2225981-08:00","created_by":"zenchantlive"}]}
{"id":"bb-6aj.3.1","title":"Add explicit full-drive scan mode for C:/D: by user action","description":"Provide an opt-in scan mode for full drive enumeration while retaining safe defaults and progress reporting expectations.","acceptance_criteria":"Full-drive scan is only activated explicitly, never by default startup logic.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:51.0244174-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:11:51.0244174-08:00","labels":["optional","scanner"],"dependencies":[{"issue_id":"bb-6aj.3.1","depends_on_id":"bb-6aj.3","type":"parent-child","created_at":"2026-02-11T17:11:51.0259617-08:00","created_by":"zenchantlive"}]}
{"id":"bb-6aj.4","title":"Implement aggregate project issue context model","description":"Define normalized project identity payload attached to every issue for cross-project Kanban, timeline, and session views.","acceptance_criteria":"Aggregated read output always includes stable project metadata.","status":"in_progress","priority":1,"issue_type":"task","assignee":"zenchantlive","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:51.8518922-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T19:31:12.4614879-08:00","labels":["aggregation","data-model"],"dependencies":[{"issue_id":"bb-6aj.4","depends_on_id":"bb-6aj","type":"parent-child","created_at":"2026-02-11T17:11:51.8534893-08:00","created_by":"zenchantlive"},{"issue_id":"bb-6aj.4","depends_on_id":"bb-6aj.2","type":"blocks","created_at":"2026-02-11T17:12:27.7270195-08:00","created_by":"zenchantlive"}]}
{"id":"bb-6aj.4","title":"Implement aggregate project issue context model","description":"Define normalized project identity payload attached to every issue for cross-project Kanban, timeline, and session views.","acceptance_criteria":"Aggregated read output always includes stable project metadata.","status":"closed","priority":1,"issue_type":"task","assignee":"zenchantlive","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:51.8518922-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T19:45:21.5826669-08:00","closed_at":"2026-02-11T19:45:21.5826669-08:00","close_reason":"Added project context model and attached to read issues.","labels":["aggregation","data-model"],"dependencies":[{"issue_id":"bb-6aj.4","depends_on_id":"bb-6aj","type":"parent-child","created_at":"2026-02-11T17:11:51.8534893-08:00","created_by":"zenchantlive"},{"issue_id":"bb-6aj.4","depends_on_id":"bb-6aj.2","type":"blocks","created_at":"2026-02-11T17:12:27.7270195-08:00","created_by":"zenchantlive"}]}
{"id":"bb-92d","title":"Foundation and Read/Write Boundary","description":"Establish the Windows-native Next.js foundation, canonical Beads schema handling, and strict data boundaries: read from JSONL, write only via bd.exe. This epic defines the non-negotiable invariants that all later work must preserve.","acceptance_criteria":"App boots on Windows, schema/parser contracts exist, and no direct issues.jsonl write path exists in code.","status":"closed","priority":0,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:41.0756295-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:28:27.8108066-08:00","closed_at":"2026-02-11T17:28:27.8108066-08:00","close_reason":"Completed foundation milestone: bootstrap, licensing/docs, schema contracts, parser, windows path normalization, and write-boundary guardrails.","labels":["beadboard","foundation","windows"]}
{"id":"bb-92d.1","title":"Bootstrap Next.js 15 + React 19 + TypeScript strict","description":"Initialize project scaffold with strict TypeScript, App Router baseline, and repeatable scripts for lint/typecheck/test in PowerShell.","acceptance_criteria":"npm install and dev startup work on Windows; strict type checking enabled.","status":"closed","priority":0,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:41.9363647-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:23:14.0089901-08:00","closed_at":"2026-02-11T17:23:14.0089901-08:00","close_reason":"Bootstrapped Next.js 15 + React 19 + strict TypeScript; install/typecheck/dev startup verified on Windows.","labels":["foundation","nextjs"],"dependencies":[{"issue_id":"bb-92d.1","depends_on_id":"bb-92d","type":"parent-child","created_at":"2026-02-11T17:11:41.9379355-08:00","created_by":"zenchantlive"}]}
{"id":"bb-92d.2","title":"Add MIT license and baseline repository docs","description":"Add LICENSE and baseline docs that state Windows-native support, read/write boundaries, and required runtime dependencies.","acceptance_criteria":"MIT license present and docs describe core architecture constraints.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:42.7699961-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:23:50.7519159-08:00","closed_at":"2026-02-11T17:23:50.7519159-08:00","close_reason":"Added MIT license and baseline repository documentation with architecture boundary rules.","labels":["docs","license"],"dependencies":[{"issue_id":"bb-92d.2","depends_on_id":"bb-92d","type":"parent-child","created_at":"2026-02-11T17:11:42.7715653-08:00","created_by":"zenchantlive"}]}
@ -24,6 +25,7 @@
{"id":"bb-bc4.3","title":"Redesign tokenized theme and visual hierarchy","description":"Upgrade visual system quality using semantic tokens for surface/text/status/priority states, stronger typography hierarchy, and improved contrast. Move away from flat/basic palette while preserving clarity and performance.","acceptance_criteria":"UI theme shows clear hierarchy and contrast, aligns with premium demo quality expectations, and remains consistent across components.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T18:50:44.8548956-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T18:59:19.0348391-08:00","closed_at":"2026-02-11T18:59:19.0348391-08:00","close_reason":"Redesigned semantic tokens/theme contrast and hierarchy to improve production visual quality.","labels":["design-system","theme","tokens"],"dependencies":[{"issue_id":"bb-bc4.3","depends_on_id":"bb-bc4","type":"parent-child","created_at":"2026-02-11T18:50:44.8564376-08:00","created_by":"zenchantlive"},{"issue_id":"bb-bc4.3","depends_on_id":"bb-bc4.1","type":"blocks","created_at":"2026-02-11T18:50:44.8606805-08:00","created_by":"zenchantlive"}]}
{"id":"bb-bc4.4","title":"Implement mobile/tablet detail panel interaction model","description":"Adapt detail panel behavior for small screens (overlay or drawer model) with safe viewport sizing, accessible dismissal, and non-destructive navigation. Desktop retains efficient side-panel behavior.","acceptance_criteria":"Detail view is usable on mobile/tablet and does not trap or obscure board interaction irrecoverably.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T18:50:45.8342573-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T18:59:19.8911935-08:00","closed_at":"2026-02-11T18:59:19.8911935-08:00","close_reason":"Implemented mobile detail overlay flow while preserving desktop sticky side-detail behavior.","labels":["detail-panel","mobile","ux"],"dependencies":[{"issue_id":"bb-bc4.4","depends_on_id":"bb-bc4","type":"parent-child","created_at":"2026-02-11T18:50:45.8360334-08:00","created_by":"zenchantlive"},{"issue_id":"bb-bc4.4","depends_on_id":"bb-bc4.2","type":"blocks","created_at":"2026-02-11T18:51:10.0929812-08:00","created_by":"zenchantlive"},{"issue_id":"bb-bc4.4","depends_on_id":"bb-bc4.3","type":"blocks","created_at":"2026-02-11T18:51:10.9352149-08:00","created_by":"zenchantlive"}]}
{"id":"bb-bc4.5","title":"Playwright multi-breakpoint visual verification","description":"Capture and review before/after screenshots at 390x844, 768x1024, and 1440x900 to validate reachability, clipping, control usability, and detail-panel behavior. Store artifacts under artifacts/ with explicit naming conventions.","acceptance_criteria":"Required six screenshots exist (before/after x 3 breakpoints) and observations confirm responsive/visual acceptance criteria.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T18:50:47.0018379-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T18:59:20.7427588-08:00","closed_at":"2026-02-11T18:59:20.7427588-08:00","close_reason":"Captured required Playwright before/after screenshots at mobile/tablet/desktop and validated layout usability.","labels":["playwright","verification","visual"],"dependencies":[{"issue_id":"bb-bc4.5","depends_on_id":"bb-bc4","type":"parent-child","created_at":"2026-02-11T18:50:47.0034039-08:00","created_by":"zenchantlive"},{"issue_id":"bb-bc4.5","depends_on_id":"bb-bc4.4","type":"blocks","created_at":"2026-02-11T18:51:11.7817934-08:00","created_by":"zenchantlive"},{"issue_id":"bb-bc4.5","depends_on_id":"bb-bc4.3","type":"blocks","created_at":"2026-02-11T18:51:12.6236762-08:00","created_by":"zenchantlive"}]}
{"id":"bb-bq6","title":"Smoke test mutation lifecycle","description":"Temporary issue for API mutation smoke test","status":"open","priority":3,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T19:43:52.1686473-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T19:43:52.1686473-08:00","labels":["api","smoke"]}
{"id":"bb-bvn","title":"Dependency Graph (React Flow)","description":"Visualize issue relationships and blocked chains through an interactive graph backed by parsed dependency edges.","acceptance_criteria":"Graph renders dependencies correctly and supports navigation to issue details.","status":"open","priority":2,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:09.2057278-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:09.2057278-08:00","labels":["graph","react-flow"],"dependencies":[{"issue_id":"bb-bvn","depends_on_id":"bb-trz","type":"blocks","created_at":"2026-02-11T17:12:22.6642419-08:00","created_by":"zenchantlive"}]}
{"id":"bb-bvn.1","title":"Parse dependency edges and build adjacency structures","description":"Extract edges for blocks, parent, relates_to, duplicates, and supersedes to support graph rendering and analysis.","acceptance_criteria":"Adjacency output is complete and consistent for all supported edge types.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:10.0434044-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:10.0434044-08:00","labels":["graph","parser"],"dependencies":[{"issue_id":"bb-bvn.1","depends_on_id":"bb-bvn","type":"parent-child","created_at":"2026-02-11T17:12:10.0449367-08:00","created_by":"zenchantlive"}]}
{"id":"bb-bvn.2","title":"Implement React Flow graph view with pan/zoom/select interactions","description":"Render nodes and edges with interactive navigation and issue selection integration.","acceptance_criteria":"Users can pan, zoom, and select nodes to inspect linked issue context.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:10.8683725-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:10.8683725-08:00","labels":["graph","ui"],"dependencies":[{"issue_id":"bb-bvn.2","depends_on_id":"bb-bvn","type":"parent-child","created_at":"2026-02-11T17:12:10.8694189-08:00","created_by":"zenchantlive"},{"issue_id":"bb-bvn.2","depends_on_id":"bb-bvn.1","type":"blocks","created_at":"2026-02-11T17:12:36.8736785-08:00","created_by":"zenchantlive"}]}
@ -47,8 +49,8 @@
{"id":"bb-xhm.2","title":"Implement snapshot diffing for derived timeline events","description":"Compare periodic snapshots and watcher updates to infer meaningful change events without requiring write interception.","acceptance_criteria":"Diff engine emits deterministic event records for relevant field changes.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:07.5007059-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:07.5007059-08:00","labels":["diff","timeline"],"dependencies":[{"issue_id":"bb-xhm.2","depends_on_id":"bb-xhm","type":"parent-child","created_at":"2026-02-11T17:12:07.501756-08:00","created_by":"zenchantlive"},{"issue_id":"bb-xhm.2","depends_on_id":"bb-xhm.1","type":"blocks","created_at":"2026-02-11T17:12:35.3430513-08:00","created_by":"zenchantlive"},{"issue_id":"bb-xhm.2","depends_on_id":"bb-tpc.2","type":"blocks","created_at":"2026-02-11T17:12:35.8495336-08:00","created_by":"zenchantlive"}]}
{"id":"bb-xhm.3","title":"Build timeline UI with date grouping and project/assignee/event filters","description":"Render reverse-chronological feed suitable for morning review workflows with practical filter controls.","acceptance_criteria":"Timeline view supports grouping and filter combinations with acceptable performance.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:08.3834905-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:08.3834905-08:00","labels":["timeline","ui"],"dependencies":[{"issue_id":"bb-xhm.3","depends_on_id":"bb-xhm","type":"parent-child","created_at":"2026-02-11T17:12:08.3851144-08:00","created_by":"zenchantlive"},{"issue_id":"bb-xhm.3","depends_on_id":"bb-xhm.2","type":"blocks","created_at":"2026-02-11T17:12:36.3627477-08:00","created_by":"zenchantlive"}]}
{"id":"bb-ymg","title":"CLI Write-Back via bd.exe","description":"Enable safe issue mutations from UI by routing all write operations through bd.exe and reflecting results through realtime reconciliation.","acceptance_criteria":"No direct JSONL writes exist; all mutations use bd commands and recover cleanly from failures.","status":"open","priority":1,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:00.9164956-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:00.9164956-08:00","labels":["bd-cli","mutation"],"dependencies":[{"issue_id":"bb-ymg","depends_on_id":"bb-trz","type":"blocks","created_at":"2026-02-11T17:12:21.1512868-08:00","created_by":"zenchantlive"},{"issue_id":"bb-ymg","depends_on_id":"bb-tpc","type":"blocks","created_at":"2026-02-11T17:12:21.6536312-08:00","created_by":"zenchantlive"}]}
{"id":"bb-ymg.1","title":"Implement bd bridge using child_process.execFile with project-scoped cwd","description":"Wrap bd execution with command argument safety, Windows path compatibility, stdout/stderr parsing, and project-specific current working directory.","acceptance_criteria":"Bridge executes supported bd commands and returns structured result/error payloads.","status":"in_progress","priority":0,"issue_type":"task","assignee":"zenchantlive","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:01.7327732-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T19:35:15.8003769-08:00","labels":["bridge","execfile"],"dependencies":[{"issue_id":"bb-ymg.1","depends_on_id":"bb-ymg","type":"parent-child","created_at":"2026-02-11T17:12:01.7343468-08:00","created_by":"zenchantlive"},{"issue_id":"bb-ymg.1","depends_on_id":"bb-6aj.2","type":"blocks","created_at":"2026-02-11T17:12:32.3039711-08:00","created_by":"zenchantlive"}]}
{"id":"bb-ymg.1.1","title":"Resolve bd.exe location from PATH and configuration fallback","description":"Add detection logic for bd executable and actionable errors when not found, including setup guidance.","acceptance_criteria":"Missing bd path returns clear setup instructions and diagnostics.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:02.5593205-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:02.5593205-08:00","labels":["bridge","setup"],"dependencies":[{"issue_id":"bb-ymg.1.1","depends_on_id":"bb-ymg.1","type":"parent-child","created_at":"2026-02-11T17:12:02.5603636-08:00","created_by":"zenchantlive"}]}
{"id":"bb-ymg.2","title":"Implement mutation API for create/update/close/reopen/comment operations","description":"Expose strict server-side mutation endpoints translating UI actions to corresponding bd commands with validated arguments.","acceptance_criteria":"All required mutation operations execute via bd and return normalized responses.","status":"open","priority":0,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:03.3757503-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:03.3757503-08:00","labels":["api","mutation"],"dependencies":[{"issue_id":"bb-ymg.2","depends_on_id":"bb-ymg","type":"parent-child","created_at":"2026-02-11T17:12:03.377343-08:00","created_by":"zenchantlive"},{"issue_id":"bb-ymg.2","depends_on_id":"bb-ymg.1","type":"blocks","created_at":"2026-02-11T17:12:32.810993-08:00","created_by":"zenchantlive"},{"issue_id":"bb-ymg.2","depends_on_id":"bb-ymg.1.1","type":"blocks","created_at":"2026-02-11T17:12:33.313807-08:00","created_by":"zenchantlive"}]}
{"id":"bb-ymg.1","title":"Implement bd bridge using child_process.execFile with project-scoped cwd","description":"Wrap bd execution with command argument safety, Windows path compatibility, stdout/stderr parsing, and project-specific current working directory.","acceptance_criteria":"Bridge executes supported bd commands and returns structured result/error payloads.","notes":"Implemented src/lib/bridge.ts with execFile-based bd runner, project-scoped cwd, timeout support, structured command result payload, and failure classification (not_found, timeout, bad_args, non_zero_exit, unknown). Added RED-\u003eGREEN tests in tests/lib/bridge.test.ts.","status":"closed","priority":0,"issue_type":"task","assignee":"zenchantlive","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:01.7327732-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T19:45:16.7478549-08:00","closed_at":"2026-02-11T19:45:16.7478549-08:00","close_reason":"Bridge implemented with structured result/error classification and project-scoped execFile command execution; tests added.","labels":["bridge","execfile"],"dependencies":[{"issue_id":"bb-ymg.1","depends_on_id":"bb-ymg","type":"parent-child","created_at":"2026-02-11T17:12:01.7343468-08:00","created_by":"zenchantlive"},{"issue_id":"bb-ymg.1","depends_on_id":"bb-6aj.2","type":"blocks","created_at":"2026-02-11T17:12:32.3039711-08:00","created_by":"zenchantlive"}]}
{"id":"bb-ymg.1.1","title":"Resolve bd.exe location from PATH and configuration fallback","description":"Add detection logic for bd executable and actionable errors when not found, including setup guidance.","acceptance_criteria":"Missing bd path returns clear setup instructions and diagnostics.","notes":"Implemented src/lib/bd-path.ts executable resolution with config-first then PATH lookup (bd.exe/bd.cmd/bd.bat/bd), plus actionable setup guidance when missing. Added tests/lib/bd-path.test.ts for success/failure cases.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:02.5593205-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T19:44:57.3720854-08:00","closed_at":"2026-02-11T19:44:57.3720854-08:00","close_reason":"Executable resolution implemented with config/PATH fallback and actionable missing-bd guidance; tests added.","labels":["bridge","setup"],"dependencies":[{"issue_id":"bb-ymg.1.1","depends_on_id":"bb-ymg.1","type":"parent-child","created_at":"2026-02-11T17:12:02.5603636-08:00","created_by":"zenchantlive"}]}
{"id":"bb-ymg.2","title":"Implement mutation API for create/update/close/reopen/comment operations","description":"Expose strict server-side mutation endpoints translating UI actions to corresponding bd commands with validated arguments.","acceptance_criteria":"All required mutation operations execute via bd and return normalized responses.","notes":"Implemented mutation validation/mapping/execution layer in src/lib/mutations.ts and App Router endpoints: /api/beads/create|update|close|reopen|comment. Added payload validation tests, route validation tests, and smoke-tested create/update/comment/close/reopen lifecycle via API.","status":"closed","priority":0,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:03.3757503-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T19:45:26.3234246-08:00","closed_at":"2026-02-11T19:45:26.3234246-08:00","close_reason":"Mutation API implemented for create/update/close/reopen/comment with payload validation, command mapping, normalized error shape, and verified smoke lifecycle via API.","labels":["api","mutation"],"dependencies":[{"issue_id":"bb-ymg.2","depends_on_id":"bb-ymg","type":"parent-child","created_at":"2026-02-11T17:12:03.377343-08:00","created_by":"zenchantlive"},{"issue_id":"bb-ymg.2","depends_on_id":"bb-ymg.1","type":"blocks","created_at":"2026-02-11T17:12:32.810993-08:00","created_by":"zenchantlive"},{"issue_id":"bb-ymg.2","depends_on_id":"bb-ymg.1.1","type":"blocks","created_at":"2026-02-11T17:12:33.313807-08:00","created_by":"zenchantlive"}]}
{"id":"bb-ymg.3","title":"Add optimistic updates with rollback and SSE reconciliation","description":"Apply immediate UI updates for responsiveness, rollback on command failure, and reconcile with watcher-triggered authoritative state updates.","acceptance_criteria":"Failed mutations restore previous UI state and emit meaningful error feedback.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:04.1956393-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:04.1956393-08:00","labels":["optimistic","state"],"dependencies":[{"issue_id":"bb-ymg.3","depends_on_id":"bb-ymg","type":"parent-child","created_at":"2026-02-11T17:12:04.1966728-08:00","created_by":"zenchantlive"},{"issue_id":"bb-ymg.3","depends_on_id":"bb-ymg.2","type":"blocks","created_at":"2026-02-11T17:12:33.8246167-08:00","created_by":"zenchantlive"}]}
{"id":"bb-ymg.4","title":"Implement drag-and-drop status transitions mapped to bd commands","description":"Map card moves to valid status transitions and use close/reopen semantics where applicable instead of direct file manipulation.","acceptance_criteria":"DnD transitions call proper bd commands and reject invalid transitions safely.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:05.0129676-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:05.0129676-08:00","labels":["dnd","kanban"],"dependencies":[{"issue_id":"bb-ymg.4","depends_on_id":"bb-ymg","type":"parent-child","created_at":"2026-02-11T17:12:05.014527-08:00","created_by":"zenchantlive"},{"issue_id":"bb-ymg.4","depends_on_id":"bb-ymg.2","type":"blocks","created_at":"2026-02-11T17:12:34.329788-08:00","created_by":"zenchantlive"},{"issue_id":"bb-ymg.4","depends_on_id":"bb-trz.1","type":"blocks","created_at":"2026-02-11T17:12:34.8422542-08:00","created_by":"zenchantlive"}]}

View file

@ -9,7 +9,7 @@
"start": "next start",
"lint": "next lint",
"typecheck": "tsc --noEmit",
"test": "node --test tests/bootstrap.test.mjs && node --import tsx --test tests/lib/parser.test.ts && node --import tsx --test tests/lib/pathing.test.ts && node --import tsx --test tests/lib/kanban.test.ts && node --import tsx --test tests/lib/read-issues.test.ts && node --test tests/guards/no-direct-jsonl-write.test.mjs && node --test tests/guards/no-inline-style-in-kanban.test.mjs && node --test tests/guards/kanban-responsive-contract.test.mjs"
"test": "node --test tests/bootstrap.test.mjs && node --import tsx --test tests/lib/parser.test.ts && node --import tsx --test tests/lib/pathing.test.ts && node --import tsx --test tests/lib/kanban.test.ts && node --import tsx --test tests/lib/read-issues.test.ts && node --import tsx --test tests/lib/bd-path.test.ts && node --import tsx --test tests/lib/bridge.test.ts && node --import tsx --test tests/lib/mutations.test.ts && node --import tsx --test tests/api/mutations-routes.test.ts && node --test tests/guards/no-direct-jsonl-write.test.mjs && node --test tests/guards/no-inline-style-in-kanban.test.mjs && node --test tests/guards/kanban-responsive-contract.test.mjs"
},
"dependencies": {
"framer-motion": "^11.18.2",

View file

@ -0,0 +1,51 @@
import { NextResponse } from 'next/server';
import { executeMutation, MutationValidationError, validateMutationPayload, type MutationOperation } from '../../../lib/mutations';
function badRequest(message: string, operation: MutationOperation) {
return NextResponse.json(
{
ok: false,
operation,
error: {
classification: 'bad_args',
message,
},
},
{ status: 400 },
);
}
export async function handleMutationRequest(request: Request, operation: MutationOperation): Promise<Response> {
let body: unknown;
try {
body = await request.json();
} catch {
return badRequest('Invalid JSON body.', operation);
}
try {
const payload = validateMutationPayload(operation, body);
const result = await executeMutation(operation, payload);
const status = result.ok ? 200 : result.error?.classification === 'not_found' ? 404 : 400;
return NextResponse.json(result, { status });
} catch (error) {
if (error instanceof MutationValidationError) {
return badRequest(error.message, operation);
}
return NextResponse.json(
{
ok: false,
operation,
error: {
classification: 'unknown',
message: error instanceof Error ? error.message : 'Unknown mutation error.',
},
},
{ status: 500 },
);
}
}

View file

@ -0,0 +1,5 @@
import { handleMutationRequest } from '../_shared';
export async function POST(request: Request): Promise<Response> {
return handleMutationRequest(request, 'close');
}

View file

@ -0,0 +1,5 @@
import { handleMutationRequest } from '../_shared';
export async function POST(request: Request): Promise<Response> {
return handleMutationRequest(request, 'comment');
}

View file

@ -0,0 +1,5 @@
import { handleMutationRequest } from '../_shared';
export async function POST(request: Request): Promise<Response> {
return handleMutationRequest(request, 'create');
}

View file

@ -0,0 +1,5 @@
import { handleMutationRequest } from '../_shared';
export async function POST(request: Request): Promise<Response> {
return handleMutationRequest(request, 'reopen');
}

View file

@ -0,0 +1,5 @@
import { handleMutationRequest } from '../_shared';
export async function POST(request: Request): Promise<Response> {
return handleMutationRequest(request, 'update');
}

78
src/lib/bd-path.ts Normal file
View file

@ -0,0 +1,78 @@
import fs from 'node:fs/promises';
import path from 'node:path';
export interface ResolveBdExecutableOptions {
explicitPath?: string | null;
env?: NodeJS.ProcessEnv;
}
export interface BdExecutableResolution {
executable: string;
source: 'config' | 'path';
}
export class BdExecutableNotFoundError extends Error {
readonly code = 'BD_NOT_FOUND';
constructor(message: string) {
super(message);
this.name = 'BdExecutableNotFoundError';
}
}
async function fileExists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
function splitEnvPath(env: NodeJS.ProcessEnv = process.env): string[] {
const value = env.Path ?? env.PATH ?? '';
if (!value.trim()) {
return [];
}
return value.split(';').map((segment) => segment.trim()).filter(Boolean);
}
function executableCandidates(directory: string): string[] {
return ['bd.exe', 'bd.cmd', 'bd.bat', 'bd'].map((name) => path.join(directory, name));
}
function buildNotFoundMessage(explicitPath?: string | null): string {
const lines = [
'bd.exe was not found.',
'Install it with: npm install -g @beads/bd',
'Or configure an explicit executable path in request payload/config.',
];
if (explicitPath) {
lines.push(`Configured path was not found: ${explicitPath}`);
}
return lines.join(' ');
}
export async function resolveBdExecutable(options: ResolveBdExecutableOptions = {}): Promise<BdExecutableResolution> {
if (options.explicitPath && options.explicitPath.trim()) {
const explicit = path.resolve(options.explicitPath);
if (await fileExists(explicit)) {
return { executable: explicit, source: 'config' };
}
throw new BdExecutableNotFoundError(buildNotFoundMessage(options.explicitPath));
}
for (const dir of splitEnvPath(options.env)) {
for (const candidate of executableCandidates(dir)) {
if (await fileExists(candidate)) {
return { executable: candidate, source: 'path' };
}
}
}
throw new BdExecutableNotFoundError(buildNotFoundMessage());
}

163
src/lib/bridge.ts Normal file
View file

@ -0,0 +1,163 @@
import { execFile as nodeExecFile } from 'node:child_process';
import { promisify } from 'node:util';
import { BdExecutableNotFoundError, resolveBdExecutable } from './bd-path';
const execFileAsync = promisify(nodeExecFile);
export type BdFailureClassification = 'not_found' | 'timeout' | 'non_zero_exit' | 'bad_args' | 'unknown';
export interface RunBdCommandOptions {
projectRoot: string;
args: string[];
timeoutMs?: number;
explicitBdPath?: string | null;
}
export interface RunBdCommandResult {
success: boolean;
classification: BdFailureClassification | null;
command: string;
args: string[];
cwd: string;
stdout: string;
stderr: string;
code: number | null;
durationMs: number;
error: string | null;
}
type ExecFileOptions = {
cwd: string;
timeout: number;
windowsHide: boolean;
env: NodeJS.ProcessEnv;
};
type ExecFileLike = (
command: string,
args: string[],
options: ExecFileOptions,
) => Promise<{ stdout: string; stderr: string }>;
interface RunBdCommandDeps {
resolveBdExecutable: typeof resolveBdExecutable;
execFile: ExecFileLike;
env: NodeJS.ProcessEnv;
}
function normalizeOutput(text: unknown): string {
if (typeof text !== 'string') {
return '';
}
return text.replaceAll('\r\n', '\n').trim();
}
function toErrorMessage(value: unknown): string {
if (value instanceof Error) {
return value.message;
}
return String(value ?? 'Unknown error');
}
function classifyFailure(error: NodeJS.ErrnoException & { stderr?: string; killed?: boolean; signal?: string }): BdFailureClassification {
if (error.code === 'ENOENT') {
return 'not_found';
}
if (error.code === 'ETIMEDOUT' || error.killed || error.signal === 'SIGTERM') {
return 'timeout';
}
const stderr = normalizeOutput(error.stderr);
if (typeof error.code === 'number') {
if (/(unknown|invalid|required|usage)/i.test(stderr)) {
return 'bad_args';
}
return 'non_zero_exit';
}
return 'unknown';
}
export async function runBdCommand(
options: RunBdCommandOptions,
injectedDeps?: Partial<RunBdCommandDeps>,
): Promise<RunBdCommandResult> {
const startedAt = Date.now();
const timeoutMs = options.timeoutMs ?? 30_000;
const cwd = options.projectRoot;
const args = [...options.args];
const deps: RunBdCommandDeps = {
resolveBdExecutable: injectedDeps?.resolveBdExecutable ?? resolveBdExecutable,
execFile: injectedDeps?.execFile ?? execFileAsync,
env: injectedDeps?.env ?? process.env,
};
let command = options.explicitBdPath ?? 'bd.exe';
try {
const resolved = await deps.resolveBdExecutable({
explicitPath: options.explicitBdPath,
env: deps.env,
});
command = resolved.executable;
const { stdout, stderr } = await deps.execFile(command, args, {
cwd,
timeout: timeoutMs,
windowsHide: true,
env: deps.env,
});
return {
success: true,
classification: null,
command,
args,
cwd,
stdout: normalizeOutput(stdout),
stderr: normalizeOutput(stderr),
code: 0,
durationMs: Date.now() - startedAt,
error: null,
};
} catch (rawError) {
if (rawError instanceof BdExecutableNotFoundError) {
return {
success: false,
classification: 'not_found',
command,
args,
cwd,
stdout: '',
stderr: '',
code: null,
durationMs: Date.now() - startedAt,
error: rawError.message,
};
}
const error = rawError as NodeJS.ErrnoException & {
stderr?: string;
stdout?: string;
killed?: boolean;
signal?: string;
};
return {
success: false,
classification: classifyFailure(error),
command,
args,
cwd,
stdout: normalizeOutput(error.stdout),
stderr: normalizeOutput(error.stderr),
code: typeof error.code === 'number' ? error.code : null,
durationMs: Date.now() - startedAt,
error: toErrorMessage(error),
};
}
}

295
src/lib/mutations.ts Normal file
View file

@ -0,0 +1,295 @@
import { runBdCommand, type RunBdCommandResult } from './bridge';
export type MutationOperation = 'create' | 'update' | 'close' | 'reopen' | 'comment';
export type MutationStatus = 'open' | 'in_progress' | 'blocked' | 'deferred' | 'closed';
interface MutationBasePayload {
projectRoot: string;
bdPath?: string;
}
export interface CreateMutationPayload extends MutationBasePayload {
title: string;
description?: string;
priority?: number;
issueType?: string;
assignee?: string;
labels?: string[];
}
export interface UpdateMutationPayload extends MutationBasePayload {
id: string;
title?: string;
description?: string;
status?: MutationStatus;
priority?: number;
assignee?: string;
labels?: string[];
}
export interface CloseMutationPayload extends MutationBasePayload {
id: string;
reason?: string;
}
export interface ReopenMutationPayload extends MutationBasePayload {
id: string;
reason?: string;
}
export interface CommentMutationPayload extends MutationBasePayload {
id: string;
text: string;
}
export type MutationPayload =
| CreateMutationPayload
| UpdateMutationPayload
| CloseMutationPayload
| ReopenMutationPayload
| CommentMutationPayload;
export interface MutationErrorShape {
classification: 'bad_args' | 'not_found' | 'timeout' | 'non_zero_exit' | 'unknown';
message: string;
}
export interface MutationResponse {
ok: boolean;
operation: MutationOperation;
command: RunBdCommandResult;
error?: MutationErrorShape;
}
export class MutationValidationError extends Error {
readonly code = 'MUTATION_VALIDATION_ERROR';
constructor(message: string) {
super(message);
this.name = 'MutationValidationError';
}
}
function asNonEmptyString(value: unknown, field: string): string {
if (typeof value !== 'string' || !value.trim()) {
throw new MutationValidationError(`"${field}" is required.`);
}
return value.trim();
}
function asOptionalString(value: unknown): string | undefined {
if (value === undefined || value === null) {
return undefined;
}
if (typeof value !== 'string') {
throw new MutationValidationError('Expected a string value.');
}
const trimmed = value.trim();
return trimmed ? trimmed : undefined;
}
function asOptionalPriority(value: unknown): number | undefined {
if (value === undefined || value === null) {
return undefined;
}
if (typeof value !== 'number' || Number.isNaN(value) || value < 0 || value > 4) {
throw new MutationValidationError('"priority" must be a number between 0 and 4.');
}
return value;
}
function asOptionalLabels(value: unknown): string[] | undefined {
if (value === undefined || value === null) {
return undefined;
}
if (!Array.isArray(value)) {
throw new MutationValidationError('"labels" must be an array of strings.');
}
const labels = value.map((label) => {
if (typeof label !== 'string' || !label.trim()) {
throw new MutationValidationError('"labels" must be an array of non-empty strings.');
}
return label.trim();
});
return labels.length ? labels : undefined;
}
function asOptionalStatus(value: unknown): MutationStatus | undefined {
if (value === undefined || value === null) {
return undefined;
}
const status = asNonEmptyString(value, 'status');
if (!['open', 'in_progress', 'blocked', 'deferred', 'closed'].includes(status)) {
throw new MutationValidationError('"status" is invalid.');
}
return status as MutationStatus;
}
function parseBasePayload(raw: unknown): MutationBasePayload {
if (!raw || typeof raw !== 'object') {
throw new MutationValidationError('Payload must be a JSON object.');
}
const data = raw as Record<string, unknown>;
return {
projectRoot: asNonEmptyString(data.projectRoot, 'projectRoot'),
bdPath: asOptionalString(data.bdPath),
};
}
export function validateMutationPayload(operation: MutationOperation, payload: unknown): MutationPayload {
const base = parseBasePayload(payload);
const data = payload as Record<string, unknown>;
if (operation === 'create') {
return {
...base,
title: asNonEmptyString(data.title, 'title'),
description: asOptionalString(data.description),
priority: asOptionalPriority(data.priority),
issueType: asOptionalString(data.issueType),
assignee: asOptionalString(data.assignee),
labels: asOptionalLabels(data.labels),
};
}
if (operation === 'update') {
const mapped: UpdateMutationPayload = {
...base,
id: asNonEmptyString(data.id, 'id'),
title: asOptionalString(data.title),
description: asOptionalString(data.description),
status: asOptionalStatus(data.status),
priority: asOptionalPriority(data.priority),
assignee: asOptionalString(data.assignee),
labels: asOptionalLabels(data.labels),
};
if (!mapped.title && !mapped.description && !mapped.status && mapped.priority === undefined && !mapped.assignee && !mapped.labels) {
throw new MutationValidationError('At least one update field is required.');
}
return mapped;
}
if (operation === 'close') {
return {
...base,
id: asNonEmptyString(data.id, 'id'),
reason: asOptionalString(data.reason),
};
}
if (operation === 'reopen') {
return {
...base,
id: asNonEmptyString(data.id, 'id'),
reason: asOptionalString(data.reason),
};
}
return {
...base,
id: asNonEmptyString(data.id, 'id'),
text: asNonEmptyString(data.text, 'text'),
};
}
function pushOptionalArg(args: string[], flag: string, value: string | undefined): void {
if (value) {
args.push(flag, value);
}
}
function pushOptionalLabels(args: string[], labels: string[] | undefined): void {
if (labels && labels.length > 0) {
args.push('-l', labels.join(','));
}
}
export function buildBdMutationArgs(operation: MutationOperation, payload: MutationPayload): string[] {
if (operation === 'create') {
const data = payload as CreateMutationPayload;
const args = ['create', data.title];
pushOptionalArg(args, '-d', data.description);
if (data.priority !== undefined) {
args.push('-p', String(data.priority));
}
pushOptionalArg(args, '-t', data.issueType);
pushOptionalArg(args, '-a', data.assignee);
pushOptionalLabels(args, data.labels);
args.push('--json');
return args;
}
if (operation === 'update') {
const data = payload as UpdateMutationPayload;
const args = ['update', data.id];
pushOptionalArg(args, '--title', data.title);
pushOptionalArg(args, '-d', data.description);
pushOptionalArg(args, '-s', data.status);
if (data.priority !== undefined) {
args.push('-p', String(data.priority));
}
pushOptionalArg(args, '-a', data.assignee);
pushOptionalLabels(args, data.labels);
args.push('--json');
return args;
}
if (operation === 'close') {
const data = payload as CloseMutationPayload;
const args = ['close', data.id];
pushOptionalArg(args, '-r', data.reason);
args.push('--json');
return args;
}
if (operation === 'reopen') {
const data = payload as ReopenMutationPayload;
const args = ['reopen', data.id];
pushOptionalArg(args, '-r', data.reason);
args.push('--json');
return args;
}
const data = payload as CommentMutationPayload;
return ['comments', 'add', data.id, data.text, '--json'];
}
interface ExecuteMutationDeps {
runBdCommand: typeof runBdCommand;
}
export async function executeMutation(
operation: MutationOperation,
payload: MutationPayload,
deps: Partial<ExecuteMutationDeps> = {},
): Promise<MutationResponse> {
const runner = deps.runBdCommand ?? runBdCommand;
const args = buildBdMutationArgs(operation, payload);
const command = await runner({
projectRoot: payload.projectRoot,
args,
explicitBdPath: payload.bdPath,
});
if (!command.success) {
return {
ok: false,
operation,
command,
error: {
classification: command.classification ?? 'unknown',
message: command.error ?? (command.stderr || 'Mutation command failed.'),
},
};
}
return {
ok: true,
operation,
command,
};
}

View file

@ -0,0 +1,55 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { POST as createPost } from '../../src/app/api/beads/create/route';
import { POST as reopenPost } from '../../src/app/api/beads/reopen/route';
import { POST as commentPost } from '../../src/app/api/beads/comment/route';
async function readJson(response: Response): Promise<any> {
const text = await response.text();
return text ? JSON.parse(text) : {};
}
test('create route returns 400 for invalid payload', async () => {
const response = await createPost(
new Request('http://localhost/api/beads/create', {
method: 'POST',
body: JSON.stringify({ projectRoot: '', title: '' }),
headers: { 'content-type': 'application/json' },
}),
);
assert.equal(response.status, 400);
const data = await readJson(response);
assert.equal(data.ok, false);
assert.equal(data.error.classification, 'bad_args');
});
test('reopen route returns 400 for missing id', async () => {
const response = await reopenPost(
new Request('http://localhost/api/beads/reopen', {
method: 'POST',
body: JSON.stringify({ projectRoot: 'C:/repo' }),
headers: { 'content-type': 'application/json' },
}),
);
assert.equal(response.status, 400);
const data = await readJson(response);
assert.equal(data.ok, false);
});
test('comment route returns 400 for missing comment text', async () => {
const response = await commentPost(
new Request('http://localhost/api/beads/comment', {
method: 'POST',
body: JSON.stringify({ projectRoot: 'C:/repo', id: 'bb-1' }),
headers: { 'content-type': 'application/json' },
}),
);
assert.equal(response.status, 400);
const data = await readJson(response);
assert.equal(data.ok, false);
assert.equal(typeof data.error.message, 'string');
});

43
tests/lib/bd-path.test.ts Normal file
View file

@ -0,0 +1,43 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { BdExecutableNotFoundError, resolveBdExecutable } from '../../src/lib/bd-path';
test('resolveBdExecutable prefers explicit configured path when provided', async () => {
const temp = await fs.mkdtemp(path.join(os.tmpdir(), 'beadboard-bd-path-'));
const explicit = path.join(temp, 'tools', 'bd.exe');
await fs.mkdir(path.dirname(explicit), { recursive: true });
await fs.writeFile(explicit, '');
const resolved = await resolveBdExecutable({ explicitPath: explicit, env: { Path: '', NODE_ENV: 'test' } });
assert.equal(resolved.executable, explicit);
assert.equal(resolved.source, 'config');
});
test('resolveBdExecutable finds bd.exe on PATH when explicit path is not set', async () => {
const temp = await fs.mkdtemp(path.join(os.tmpdir(), 'beadboard-bd-path-env-'));
const candidate = path.join(temp, 'bd.exe');
await fs.writeFile(candidate, '');
const resolved = await resolveBdExecutable({ env: { Path: temp, NODE_ENV: 'test' } });
assert.equal(resolved.executable, candidate);
assert.equal(resolved.source, 'path');
});
test('resolveBdExecutable throws actionable setup guidance when executable is missing', async () => {
await assert.rejects(
() => resolveBdExecutable({ env: { Path: '', NODE_ENV: 'test' } }),
(error: unknown) => {
assert.equal(error instanceof BdExecutableNotFoundError, true);
const message = String((error as Error).message).toLowerCase();
assert.equal(message.includes('npm install -g @beads/bd'), true);
assert.equal(message.includes('bd.exe'), true);
return true;
},
);
});

86
tests/lib/bridge.test.ts Normal file
View file

@ -0,0 +1,86 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { runBdCommand } from '../../src/lib/bridge';
test('runBdCommand returns structured success payload from execFile output', async () => {
const result = await runBdCommand(
{
projectRoot: 'C:/repo/project',
args: ['list', '--json'],
timeoutMs: 2000,
explicitBdPath: 'C:/tools/bd.exe',
},
{
resolveBdExecutable: async () => ({ executable: 'C:/tools/bd.exe', source: 'config' }),
execFile: async (command, args, options) => {
assert.equal(command, 'C:/tools/bd.exe');
assert.deepEqual(args, ['list', '--json']);
assert.equal(options.cwd, 'C:/repo/project');
return { stdout: '[{"id":"bb-1"}]\r\n', stderr: '' };
},
},
);
assert.equal(result.success, true);
assert.equal(result.classification, null);
assert.equal(result.stdout, '[{"id":"bb-1"}]');
});
test('runBdCommand classifies missing executable as not_found', async () => {
const result = await runBdCommand(
{ projectRoot: 'C:/repo/project', args: ['list'] },
{
resolveBdExecutable: async () => ({ executable: 'C:/tools/bd.exe', source: 'config' }),
execFile: async () => {
const error = new Error('spawn ENOENT') as NodeJS.ErrnoException;
error.code = 'ENOENT';
throw error;
},
},
);
assert.equal(result.success, false);
assert.equal(result.classification, 'not_found');
});
test('runBdCommand classifies timeout failures', async () => {
const result = await runBdCommand(
{ projectRoot: 'C:/repo/project', args: ['list'], timeoutMs: 5 },
{
resolveBdExecutable: async () => ({ executable: 'C:/tools/bd.exe', source: 'config' }),
execFile: async () => {
const error = new Error('timed out') as NodeJS.ErrnoException & { killed?: boolean; signal?: string };
error.code = 'ETIMEDOUT';
error.killed = true;
error.signal = 'SIGTERM';
throw error;
},
},
);
assert.equal(result.success, false);
assert.equal(result.classification, 'timeout');
});
test('runBdCommand classifies non-zero bad-argument exits', async () => {
const result = await runBdCommand(
{ projectRoot: 'C:/repo/project', args: ['update', '--bad-flag'] },
{
resolveBdExecutable: async () => ({ executable: 'C:/tools/bd.exe', source: 'config' }),
execFile: async () => {
const error = new Error('exit code 1') as NodeJS.ErrnoException & {
stdout?: string;
stderr?: string;
};
(error as any).code = 1;
error.stderr = 'unknown flag: --bad-flag';
error.stdout = '';
throw error;
},
},
);
assert.equal(result.success, false);
assert.equal(result.classification, 'bad_args');
});

101
tests/lib/mutations.test.ts Normal file
View file

@ -0,0 +1,101 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
MutationValidationError,
buildBdMutationArgs,
validateMutationPayload,
executeMutation,
type MutationOperation,
} from '../../src/lib/mutations';
const root = 'C:/Users/Zenchant/codex/beadboard';
test('validateMutationPayload rejects invalid payloads', () => {
assert.throws(
() => validateMutationPayload('create', { projectRoot: '', title: '' }),
(error: unknown) => error instanceof MutationValidationError,
);
});
test('buildBdMutationArgs maps reopen correctly', () => {
const payload = validateMutationPayload('reopen', {
projectRoot: root,
id: 'bb-123',
reason: 'retry work',
});
const args = buildBdMutationArgs('reopen', payload);
assert.deepEqual(args, ['reopen', 'bb-123', '-r', 'retry work', '--json']);
});
test('buildBdMutationArgs maps comment correctly', () => {
const payload = validateMutationPayload('comment', {
projectRoot: root,
id: 'bb-123',
text: 'Added notes',
});
const args = buildBdMutationArgs('comment', payload);
assert.deepEqual(args, ['comments', 'add', 'bb-123', 'Added notes', '--json']);
});
test('executeMutation surfaces bridge failures in normalized response', async () => {
const payload = validateMutationPayload('close', {
projectRoot: root,
id: 'bb-123',
reason: 'completed',
});
const result = await executeMutation('close', payload, {
runBdCommand: async ({ args }) => {
assert.deepEqual(args, ['close', 'bb-123', '-r', 'completed', '--json']);
return {
success: false,
classification: 'non_zero_exit',
command: 'bd.exe',
args,
cwd: root,
stdout: '',
stderr: 'cannot close',
code: 1,
durationMs: 3,
error: 'cannot close',
};
},
});
assert.equal(result.ok, false);
assert.equal(result.error?.classification, 'non_zero_exit');
});
test('executeMutation returns successful normalized response', async () => {
const payload = validateMutationPayload('update', {
projectRoot: root,
id: 'bb-123',
status: 'in_progress',
priority: 1,
});
const result = await executeMutation('update', payload, {
runBdCommand: async ({ args }) => {
assert.deepEqual(args, ['update', 'bb-123', '-s', 'in_progress', '-p', '1', '--json']);
return {
success: true,
classification: null,
command: 'bd.exe',
args,
cwd: root,
stdout: '{"id":"bb-123"}',
stderr: '',
code: 0,
durationMs: 2,
error: null,
};
},
});
assert.equal(result.ok, true);
assert.equal(result.operation, 'update');
assert.equal(result.command.success, true);
});