@winspan/claude-forge 8.50.6 → 8.51.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +7 -7
- package/dist/claudemd/claudemd-generator.d.ts.map +1 -1
- package/dist/claudemd/claudemd-generator.js +27 -237
- package/dist/claudemd/claudemd-generator.js.map +1 -1
- package/dist/claudemd/resume-manager.js +1 -1
- package/dist/claudemd/resume-manager.js.map +1 -1
- package/dist/claudemd/templates/swarm-protocol.md +222 -0
- package/dist/cli/commands/daemon.js +6 -6
- package/dist/cli/commands/daemon.js.map +1 -1
- package/dist/cli/commands/executions.d.ts.map +1 -1
- package/dist/cli/commands/executions.js +4 -3
- package/dist/cli/commands/executions.js.map +1 -1
- package/dist/cli/commands/init.js +2 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/logs.js.map +1 -1
- package/dist/cli/commands/mcp.d.ts.map +1 -1
- package/dist/cli/commands/mcp.js +3 -5
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/menu.d.ts.map +1 -1
- package/dist/cli/commands/menu.js +4 -3
- package/dist/cli/commands/menu.js.map +1 -1
- package/dist/cli/commands/stats.d.ts.map +1 -1
- package/dist/cli/commands/stats.js +2 -3
- package/dist/cli/commands/stats.js.map +1 -1
- package/dist/cli/commands/status.js +2 -2
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/trace.d.ts.map +1 -1
- package/dist/cli/commands/trace.js +11 -23
- package/dist/cli/commands/trace.js.map +1 -1
- package/dist/cli/init/hook-manager.d.ts.map +1 -1
- package/dist/cli/init/hook-manager.js +2 -2
- package/dist/cli/init/hook-manager.js.map +1 -1
- package/dist/core/ai/provider.js +2 -2
- package/dist/core/ai/provider.js.map +1 -1
- package/dist/core/constants.d.ts +12 -1
- package/dist/core/constants.d.ts.map +1 -1
- package/dist/core/constants.js +15 -1
- package/dist/core/constants.js.map +1 -1
- package/dist/core/event-fields.d.ts +16 -0
- package/dist/core/event-fields.d.ts.map +1 -0
- package/dist/core/event-fields.js +19 -0
- package/dist/core/event-fields.js.map +1 -0
- package/dist/core/queue/index.d.ts.map +1 -1
- package/dist/core/queue/index.js +3 -4
- package/dist/core/queue/index.js.map +1 -1
- package/dist/core/storage/base.d.ts +36 -3
- package/dist/core/storage/base.d.ts.map +1 -1
- package/dist/core/storage/base.js +101 -58
- package/dist/core/storage/base.js.map +1 -1
- package/dist/core/storage/events.d.ts +92 -3
- package/dist/core/storage/events.d.ts.map +1 -1
- package/dist/core/storage/events.js +147 -0
- package/dist/core/storage/events.js.map +1 -1
- package/dist/core/storage/routing.d.ts +54 -1
- package/dist/core/storage/routing.d.ts.map +1 -1
- package/dist/core/storage/routing.js +99 -1
- package/dist/core/storage/routing.js.map +1 -1
- package/dist/core/storage/schema.sql +12 -2
- package/dist/core/storage/sessions.d.ts +20 -0
- package/dist/core/storage/sessions.d.ts.map +1 -1
- package/dist/core/storage/sessions.js +59 -0
- package/dist/core/storage/sessions.js.map +1 -1
- package/dist/core/storage/skills.d.ts +23 -0
- package/dist/core/storage/skills.d.ts.map +1 -1
- package/dist/core/storage/skills.js +47 -0
- package/dist/core/storage/skills.js.map +1 -1
- package/dist/core/storage/sqlite.d.ts +35 -2
- package/dist/core/storage/sqlite.d.ts.map +1 -1
- package/dist/core/storage/sqlite.js +93 -4
- package/dist/core/storage/sqlite.js.map +1 -1
- package/dist/core/storage/tasks.d.ts +49 -0
- package/dist/core/storage/tasks.d.ts.map +1 -1
- package/dist/core/storage/tasks.js +143 -1
- package/dist/core/storage/tasks.js.map +1 -1
- package/dist/core/storage/token-usage.d.ts +1 -1
- package/dist/core/storage/token-usage.d.ts.map +1 -1
- package/dist/core/storage/token-usage.js +1 -1
- package/dist/core/storage/token-usage.js.map +1 -1
- package/dist/core/types.d.ts +24 -3
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/utils/error-handler.d.ts.map +1 -1
- package/dist/core/utils/error-handler.js +3 -2
- package/dist/core/utils/error-handler.js.map +1 -1
- package/dist/core/utils/git.d.ts +10 -0
- package/dist/core/utils/git.d.ts.map +1 -0
- package/dist/core/utils/git.js +24 -0
- package/dist/core/utils/git.js.map +1 -0
- package/dist/core/utils/logger.d.ts.map +1 -1
- package/dist/core/utils/logger.js +15 -1
- package/dist/core/utils/logger.js.map +1 -1
- package/dist/core/utils/lru-cache.d.ts +1 -0
- package/dist/core/utils/lru-cache.d.ts.map +1 -1
- package/dist/core/utils/lru-cache.js +3 -0
- package/dist/core/utils/lru-cache.js.map +1 -1
- package/dist/core/utils/token-tracker.js +1 -1
- package/dist/core/utils/token-tracker.js.map +1 -1
- package/dist/daemon/event-parser.d.ts.map +1 -1
- package/dist/daemon/event-parser.js +2 -1
- package/dist/daemon/event-parser.js.map +1 -1
- package/dist/daemon/handlers/history-exporter.js.map +1 -1
- package/dist/daemon/handlers/post-tool-use.d.ts.map +1 -1
- package/dist/daemon/handlers/post-tool-use.js +7 -3
- package/dist/daemon/handlers/post-tool-use.js.map +1 -1
- package/dist/daemon/handlers/stop.d.ts +4 -0
- package/dist/daemon/handlers/stop.d.ts.map +1 -1
- package/dist/daemon/handlers/stop.js +23 -35
- package/dist/daemon/handlers/stop.js.map +1 -1
- package/dist/daemon/handlers/user-prompt.d.ts +3 -3
- package/dist/daemon/handlers/user-prompt.d.ts.map +1 -1
- package/dist/daemon/handlers/user-prompt.js +12 -22
- package/dist/daemon/handlers/user-prompt.js.map +1 -1
- package/dist/daemon/hook-sync.d.ts +17 -0
- package/dist/daemon/hook-sync.d.ts.map +1 -0
- package/dist/daemon/hook-sync.js +74 -0
- package/dist/daemon/hook-sync.js.map +1 -0
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +33 -9
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/lifecycle.js +3 -4
- package/dist/daemon/lifecycle.js.map +1 -1
- package/dist/daemon/server.d.ts +6 -4
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +76 -85
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/services/task-segmenter.js +1 -1
- package/dist/daemon/services/task-segmenter.js.map +1 -1
- package/dist/hooks/hook-lib.sh +37 -0
- package/dist/hooks/notification.sh +2 -2
- package/dist/hooks/post-tool-use.sh +2 -2
- package/dist/hooks/pre-tool-use.sh +2 -2
- package/dist/hooks/stop.sh +9 -6
- package/dist/hooks/user-prompt-submit.sh +2 -2
- package/dist/{daemon/services → web/analytics}/anti-pattern-detector.d.ts +3 -4
- package/dist/web/analytics/anti-pattern-detector.d.ts.map +1 -0
- package/dist/{daemon/services → web/analytics}/anti-pattern-detector.js +7 -46
- package/dist/{daemon/services → web/analytics}/anti-pattern-detector.js.map +1 -1
- package/dist/web/analytics/drift-detector.d.ts.map +1 -0
- package/dist/{daemon/services → web/analytics}/drift-detector.js +10 -13
- package/dist/web/analytics/drift-detector.js.map +1 -0
- package/dist/web/analytics/weekly-report.d.ts.map +1 -0
- package/dist/{daemon/services → web/analytics}/weekly-report.js +51 -50
- package/dist/web/analytics/weekly-report.js.map +1 -0
- package/dist/web/auth-middleware.d.ts.map +1 -1
- package/dist/web/auth-middleware.js +1 -2
- package/dist/web/auth-middleware.js.map +1 -1
- package/dist/web/routes/_helpers.d.ts +16 -0
- package/dist/web/routes/_helpers.d.ts.map +1 -0
- package/dist/web/routes/_helpers.js +32 -0
- package/dist/web/routes/_helpers.js.map +1 -0
- package/dist/web/routes/drift.js +1 -1
- package/dist/web/routes/drift.js.map +1 -1
- package/dist/web/routes/insights.js +1 -1
- package/dist/web/routes/insights.js.map +1 -1
- package/dist/web/routes/reports.js +1 -1
- package/dist/web/routes/reports.js.map +1 -1
- package/dist/web/routes/rules.d.ts +3 -0
- package/dist/web/routes/rules.d.ts.map +1 -1
- package/dist/web/routes/rules.js +28 -52
- package/dist/web/routes/rules.js.map +1 -1
- package/dist/web/routes/sessions.d.ts.map +1 -1
- package/dist/web/routes/sessions.js +16 -30
- package/dist/web/routes/sessions.js.map +1 -1
- package/dist/web/routes/skill-stats.d.ts +2 -0
- package/dist/web/routes/skill-stats.d.ts.map +1 -1
- package/dist/web/routes/skill-stats.js +28 -64
- package/dist/web/routes/skill-stats.js.map +1 -1
- package/dist/web/routes/skills.d.ts.map +1 -1
- package/dist/web/routes/skills.js +5 -4
- package/dist/web/routes/skills.js.map +1 -1
- package/dist/web/routes/stats.d.ts +4 -0
- package/dist/web/routes/stats.d.ts.map +1 -1
- package/dist/web/routes/stats.js +19 -21
- package/dist/web/routes/stats.js.map +1 -1
- package/dist/web/routes/tasks.d.ts.map +1 -1
- package/dist/web/routes/tasks.js +17 -42
- package/dist/web/routes/tasks.js.map +1 -1
- package/dist/web/routes/trace.d.ts.map +1 -1
- package/dist/web/routes/trace.js +7 -17
- package/dist/web/routes/trace.js.map +1 -1
- package/dist/web/routes/types.d.ts.map +1 -1
- package/dist/web/routes/types.js +4 -3
- package/dist/web/routes/types.js.map +1 -1
- package/dist/web/static/assets/{AIConfig-BQCAQE9D.js → AIConfig-CdDWzJyO.js} +2 -2
- package/dist/web/static/assets/{AIConfig-BQCAQE9D.js.map → AIConfig-CdDWzJyO.js.map} +1 -1
- package/dist/web/static/assets/{Dashboard-D7Bo6Kan.js → Dashboard-CoEmmIDt.js} +2 -2
- package/dist/web/static/assets/{Dashboard-D7Bo6Kan.js.map → Dashboard-CoEmmIDt.js.map} +1 -1
- package/dist/web/static/assets/{Drawer-BeHRQxUS.js → Drawer-DdRTzlLB.js} +2 -2
- package/dist/web/static/assets/{Drawer-BeHRQxUS.js.map → Drawer-DdRTzlLB.js.map} +1 -1
- package/dist/web/static/assets/{Events-K_tCY2ti.js → Events-DrIq1SUS.js} +2 -2
- package/dist/web/static/assets/{Events-K_tCY2ti.js.map → Events-DrIq1SUS.js.map} +1 -1
- package/dist/web/static/assets/{Reports-BJCmBnc_.js → Reports-DFBM3MDK.js} +2 -2
- package/dist/web/static/assets/{Reports-BJCmBnc_.js.map → Reports-DFBM3MDK.js.map} +1 -1
- package/dist/web/static/assets/{SearchInput-BX2KhMkw.js → SearchInput-qCj_jAcf.js} +2 -2
- package/dist/web/static/assets/{SearchInput-BX2KhMkw.js.map → SearchInput-qCj_jAcf.js.map} +1 -1
- package/dist/web/static/assets/{SessionDetail-Bkr-kC7V.js → SessionDetail-CCzwdoT7.js} +2 -2
- package/dist/web/static/assets/{SessionDetail-Bkr-kC7V.js.map → SessionDetail-CCzwdoT7.js.map} +1 -1
- package/dist/web/static/assets/{Sessions-Chx9OCLH.js → Sessions-FfLYkAw9.js} +2 -2
- package/dist/web/static/assets/{Sessions-Chx9OCLH.js.map → Sessions-FfLYkAw9.js.map} +1 -1
- package/dist/web/static/assets/{Skills-O0GT1i7m.js → Skills-C8Gvs3Qa.js} +2 -2
- package/dist/web/static/assets/{Skills-O0GT1i7m.js.map → Skills-C8Gvs3Qa.js.map} +1 -1
- package/dist/web/static/assets/TaskDetail-BS8pYhaR.js +2 -0
- package/dist/web/static/assets/TaskDetail-BS8pYhaR.js.map +1 -0
- package/dist/web/static/assets/Tasks-CyuhizG8.js +2 -0
- package/dist/web/static/assets/Tasks-CyuhizG8.js.map +1 -0
- package/dist/web/static/assets/index-CBX47X8l.js +3 -0
- package/dist/web/static/assets/{index-DxIbmNmr.js.map → index-CBX47X8l.js.map} +1 -1
- package/dist/web/static/assets/index-DjIoMdoR.css +1 -0
- package/dist/web/static/assets/{lucide-fJlPI3H7.js → lucide-Bs_edTLa.js} +44 -39
- package/dist/web/static/assets/lucide-Bs_edTLa.js.map +1 -0
- package/dist/web/static/assets/react-router-r79dBVy4.js +20 -0
- package/dist/web/static/assets/{react-router-I-HqunH7.js.map → react-router-r79dBVy4.js.map} +1 -1
- package/dist/web/static/assets/task-title-BhOcemuR.js +2 -0
- package/dist/web/static/assets/task-title-BhOcemuR.js.map +1 -0
- package/dist/web/static/index.html +4 -4
- package/docs/design/h1-storage-aggregation-spec-20260518-1121.md +299 -0
- package/docs/design/h2-getdatabase-encapsulation-spec-20260518-1450.md +191 -0
- package/docs/design/h3-fallback-removal-spec-20260518-1245.md +76 -0
- package/docs/design/h4-index-dedup-spec-20260518-1230.md +109 -0
- package/docs/design/h6-services-migration-spec-20260518-1355.md +82 -0
- package/docs/design/l1-swarm-protocol-extract-spec-20260518-1605.md +106 -0
- package/docs/design/m10-forge-paths-spec-20260518-1320.md +121 -0
- package/docs/design/m2-m3-tool-input-spec-20260518-1425.md +131 -0
- package/docs/design/m7-routing-event-association-spec-20260518-1545.md +103 -0
- package/docs/design/project-path-gitroot-spec-20260518-1715.md +134 -0
- package/docs/design/task-active-gc-spec-20260518-1745.md +146 -0
- package/docs/implementation/h1-storage-aggregation-changelog-20260518-1121.md +82 -0
- package/docs/implementation/h2-final-changelog-20260518-1530.md +61 -0
- package/docs/implementation/h2-phase1-safety-net-changelog-20260518-1450.md +70 -0
- package/docs/implementation/h2-phase2-operations-changelog-20260518-1450.md +120 -0
- package/docs/implementation/h2-phase3-callsites-changelog-20260518-1450.md +71 -0
- package/docs/implementation/h3-fallback-removal-changelog-20260518-1245.md +71 -0
- package/docs/implementation/h4-index-dedup-changelog-20260518-1230.md +60 -0
- package/docs/implementation/h6-services-migration-changelog-20260518-1355.md +46 -0
- package/docs/implementation/h7-m9-defaults-changelog-20260518-1300.md +46 -0
- package/docs/implementation/l1-swarm-protocol-extract-changelog-20260518-1605.md +45 -0
- package/docs/implementation/l3-l4-daemon-perf-changelog-20260518-1410.md +63 -0
- package/docs/implementation/l6-l8-final-cleanup-changelog-20260518-1640.md +38 -0
- package/docs/implementation/m1-m4-m5-l7-cleanup-changelog-20260518-1310.md +58 -0
- package/docs/implementation/m10-forge-paths-changelog-20260518-1320.md +60 -0
- package/docs/implementation/m2-m3-tool-input-changelog-20260518-1425.md +43 -0
- package/docs/implementation/m6-m8-naming-shutdown-changelog-20260518-1340.md +56 -0
- package/docs/implementation/m7-routing-association-changelog-20260518-1545.md +69 -0
- package/docs/implementation/project-path-gitroot-changelog-20260518-1715.md +63 -0
- package/docs/implementation/task-active-gc-changelog-20260518-1745.md +35 -0
- package/docs/implementation/task-title-summary-changelog-20260518-1130.md +39 -0
- package/docs/implementation/tasks-detail-back-loses-filters-changelog-20260518-1100.md +22 -0
- package/docs/implementation/tasks-page-white-screen-hotfix-changelog-20260518-1015.md +56 -0
- package/docs/reviews/task-title-summary.md +92 -0
- package/docs/reviews/tasks-detail-back-loses-filters.md +58 -0
- package/docs/reviews/tasks-page-white-screen-hotfix.md +126 -0
- package/package.json +2 -2
- package/src/claudemd/claudemd-generator.ts +29 -238
- package/src/claudemd/resume-manager.ts +1 -1
- package/src/claudemd/templates/swarm-protocol.md +222 -0
- package/src/cli/commands/daemon.ts +6 -6
- package/src/cli/commands/executions.ts +4 -3
- package/src/cli/commands/init.ts +2 -2
- package/src/cli/commands/logs.ts +1 -1
- package/src/cli/commands/mcp.ts +3 -5
- package/src/cli/commands/menu.ts +4 -3
- package/src/cli/commands/stats.ts +2 -3
- package/src/cli/commands/status.ts +2 -2
- package/src/cli/commands/trace.ts +10 -26
- package/src/cli/init/hook-manager.ts +2 -2
- package/src/core/ai/provider.ts +2 -2
- package/src/core/constants.ts +18 -1
- package/src/core/event-fields.ts +32 -0
- package/src/core/queue/index.ts +3 -4
- package/src/core/storage/base.ts +132 -56
- package/src/core/storage/events.ts +183 -4
- package/src/core/storage/routing.ts +129 -1
- package/src/core/storage/schema.sql +12 -2
- package/src/core/storage/sessions.ts +64 -0
- package/src/core/storage/skills.ts +69 -0
- package/src/core/storage/sqlite.ts +103 -4
- package/src/core/storage/tasks.ts +149 -1
- package/src/core/storage/token-usage.ts +1 -1
- package/src/core/types.ts +30 -3
- package/src/core/utils/error-handler.ts +3 -2
- package/src/core/utils/git.ts +23 -0
- package/src/core/utils/logger.ts +16 -1
- package/src/core/utils/lru-cache.ts +4 -0
- package/src/core/utils/token-tracker.ts +1 -1
- package/src/daemon/event-parser.ts +4 -3
- package/src/daemon/handlers/history-exporter.ts +1 -1
- package/src/daemon/handlers/post-tool-use.ts +7 -3
- package/src/daemon/handlers/stop.ts +32 -39
- package/src/daemon/handlers/user-prompt.ts +12 -22
- package/src/daemon/hook-sync.ts +91 -0
- package/src/daemon/index.ts +34 -10
- package/src/daemon/lifecycle.ts +3 -3
- package/src/daemon/server.ts +76 -89
- package/src/daemon/services/task-segmenter.ts +1 -1
- package/src/hooks/hook-lib.sh +37 -0
- package/src/hooks/notification.sh +2 -2
- package/src/hooks/post-tool-use.sh +2 -2
- package/src/hooks/pre-tool-use.sh +2 -2
- package/src/hooks/stop.sh +9 -6
- package/src/hooks/user-prompt-submit.sh +2 -2
- package/src/{daemon/services → web/analytics}/anti-pattern-detector.ts +9 -54
- package/src/{daemon/services → web/analytics}/drift-detector.ts +10 -23
- package/src/{daemon/services → web/analytics}/weekly-report.ts +52 -75
- package/src/web/auth-middleware.ts +1 -2
- package/src/web/routes/_helpers.ts +34 -0
- package/src/web/routes/drift.ts +1 -1
- package/src/web/routes/insights.ts +1 -1
- package/src/web/routes/reports.ts +1 -1
- package/src/web/routes/rules.ts +31 -56
- package/src/web/routes/sessions.ts +18 -30
- package/src/web/routes/skill-stats.ts +29 -69
- package/src/web/routes/skills.ts +5 -4
- package/src/web/routes/stats.ts +19 -29
- package/src/web/routes/tasks.ts +17 -42
- package/src/web/routes/trace.ts +7 -19
- package/src/web/routes/types.ts +4 -3
- package/tests/integration/claudemd-generator.test.ts +90 -0
- package/tests/integration/web-analytics.integration.test.ts +133 -0
- package/tests/integration/web-stats.integration.test.ts +135 -0
- package/tests/integration/web-trace.integration.test.ts +175 -0
- package/tests/unit/core/forge-paths.test.ts +99 -0
- package/tests/unit/daemon/hook-sync.test.ts +71 -0
- package/tests/unit/daemon/post-tool-use.test.ts +121 -0
- package/tests/unit/daemon/stop-handler-behavior-summary.test.ts +202 -0
- package/tests/unit/daemon/task-segmenter-recover.test.ts +84 -0
- package/tests/unit/event-fields.test.ts +88 -0
- package/tests/unit/event-parser.test.ts +55 -0
- package/tests/unit/hooks/resolve-project-path.test.ts +122 -0
- package/tests/unit/socket-server.test.ts +183 -0
- package/tests/unit/storage/event-operations-aggregates.test.ts +342 -0
- package/tests/unit/storage/migration-idempotent.test.ts +304 -0
- package/tests/unit/storage/routing-aggregates.test.ts +276 -0
- package/tests/unit/storage/routing.test.ts +117 -0
- package/tests/unit/storage/schema-missing.test.ts +81 -0
- package/tests/unit/storage/session-operations-aggregates.test.ts +120 -0
- package/tests/unit/storage/skill-operations-counts.test.ts +106 -0
- package/tests/unit/storage/skills-aggregates.test.ts +104 -0
- package/tests/unit/storage/sqlite-refactor-harness.test.ts +3 -3
- package/tests/unit/storage/task-operations-counts.test.ts +46 -0
- package/tests/unit/storage/tasks-getById.test.ts +343 -0
- package/tests/unit/storage/tasks-stale-gc.test.ts +86 -0
- package/tests/unit/token-usage.test.ts +6 -6
- package/tests/unit/web/navigation-back-contract.test.ts +134 -0
- package/tests/unit/web/routes-rules.test.ts +182 -0
- package/tests/unit/web/routes-tasks.test.ts +34 -0
- package/tests/unit/web/task-title-contract.test.ts +210 -0
- package/tests/unit/web/tasks-component-contract.test.ts +179 -0
- package/vitest.config.ts +1 -1
- package/web/src/pages/TaskDetail.tsx +9 -5
- package/web/src/pages/Tasks.tsx +315 -50
- package/web/src/utils/navigation.ts +25 -0
- package/web/src/utils/task-title.ts +49 -0
- package/dist/daemon/services/anti-pattern-detector.d.ts.map +0 -1
- package/dist/daemon/services/drift-detector.d.ts.map +0 -1
- package/dist/daemon/services/drift-detector.js.map +0 -1
- package/dist/daemon/services/weekly-report.d.ts.map +0 -1
- package/dist/daemon/services/weekly-report.js.map +0 -1
- package/dist/web/static/assets/TaskDetail-5SR8zGzv.js +0 -2
- package/dist/web/static/assets/TaskDetail-5SR8zGzv.js.map +0 -1
- package/dist/web/static/assets/Tasks-DCgDqvOZ.js +0 -2
- package/dist/web/static/assets/Tasks-DCgDqvOZ.js.map +0 -1
- package/dist/web/static/assets/index-D8AKj26b.css +0 -1
- package/dist/web/static/assets/index-DxIbmNmr.js +0 -3
- package/dist/web/static/assets/lucide-fJlPI3H7.js.map +0 -1
- package/dist/web/static/assets/react-router-I-HqunH7.js +0 -20
- /package/dist/{daemon/services → web/analytics}/drift-detector.d.ts +0 -0
- /package/dist/{daemon/services → web/analytics}/weekly-report.d.ts +0 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test for ClaudeMdGenerator — safety-net for L1 SWARM_PROTOCOL extraction.
|
|
3
|
+
*
|
|
4
|
+
* Anchors the SWARM section content (open → projectContent boundary) via SHA256
|
|
5
|
+
* so that the .md file extraction must produce byte-for-byte identical output.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
9
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { createHash } from 'node:crypto';
|
|
13
|
+
import { ClaudeMdGenerator } from '../../src/claudemd/claudemd-generator.js';
|
|
14
|
+
|
|
15
|
+
// SWARM section SHA256, captured against the literal-string implementation
|
|
16
|
+
// before extracting to swarm-protocol.md. After extraction, this MUST stay equal.
|
|
17
|
+
const SWARM_SECTION_SHA256 =
|
|
18
|
+
'260dfe1e48fdecc324419a6330428eaa3597e3fd6e6262cf66cdcf301ca32e18';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Minimal stub for ClaudeProvider — only `complete` is exercised by generate().
|
|
22
|
+
*/
|
|
23
|
+
function makeFailingAi(): any {
|
|
24
|
+
return {
|
|
25
|
+
complete: async () => {
|
|
26
|
+
throw new Error('forced AI failure (safety-net)');
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('ClaudeMdGenerator (safety-net)', () => {
|
|
32
|
+
let tmpDir: string;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'claudemd-gen-'));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('generates fallback CLAUDE.md containing all SWARM_PROTOCOL anchors', async () => {
|
|
43
|
+
const gen = new ClaudeMdGenerator(makeFailingAi());
|
|
44
|
+
const output = await gen.generate(tmpDir);
|
|
45
|
+
|
|
46
|
+
// ── Key field anchors (SWARM section) ──────────────────────────────
|
|
47
|
+
expect(output).toContain('# claude-forge 工作区规范');
|
|
48
|
+
expect(output).toContain('## 自检');
|
|
49
|
+
expect(output).toContain('Two-Phase Workflow');
|
|
50
|
+
expect(output).toContain('harness-hotfix');
|
|
51
|
+
expect(output).toContain('refactor-safe');
|
|
52
|
+
expect(output).toContain('hybrid-feature-with-safety');
|
|
53
|
+
|
|
54
|
+
// ── Total line count ≥ 200 (SWARM ~223 lines + fallback) ───────────
|
|
55
|
+
const lineCount = output.split('\n').length;
|
|
56
|
+
expect(lineCount).toBeGreaterThanOrEqual(200);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('SWARM section hash is stable across literal/file-loaded implementations', async () => {
|
|
60
|
+
const gen = new ClaudeMdGenerator(makeFailingAi());
|
|
61
|
+
const output = await gen.generate(tmpDir);
|
|
62
|
+
|
|
63
|
+
// Split SWARM from projectContent: fallback always starts with sectionOverview ("## 项目概述").
|
|
64
|
+
// SWARM_PROTOCOL ends with "\n" + "\n\n" separator, so the boundary is the first
|
|
65
|
+
// "\n\n## 项目概述" occurrence — everything before that is the SWARM section.
|
|
66
|
+
const boundary = output.indexOf('\n\n## 项目概述');
|
|
67
|
+
expect(boundary).toBeGreaterThan(0);
|
|
68
|
+
|
|
69
|
+
// Include the trailing newline of SWARM_PROTOCOL (it ends with `\n`), exclude the `\n\n` separator.
|
|
70
|
+
const swarmSection = output.slice(0, boundary + 1);
|
|
71
|
+
|
|
72
|
+
const hash = createHash('sha256').update(swarmSection, 'utf-8').digest('hex');
|
|
73
|
+
|
|
74
|
+
// Print for first-run baseline capture (visible with --reporter=verbose).
|
|
75
|
+
// eslint-disable-next-line no-console
|
|
76
|
+
console.log('[safety-net] SWARM_SECTION_SHA256 =', hash);
|
|
77
|
+
// eslint-disable-next-line no-console
|
|
78
|
+
console.log('[safety-net] SWARM section length =', swarmSection.length, 'bytes');
|
|
79
|
+
|
|
80
|
+
// Sanity: SWARM section must be substantial.
|
|
81
|
+
expect(swarmSection.length).toBeGreaterThan(4000);
|
|
82
|
+
expect(swarmSection.startsWith('# claude-forge 工作区规范')).toBe(true);
|
|
83
|
+
expect(swarmSection.endsWith('(由 daemon 自动更新,记录最近任务上下文)\n\n')).toBe(true);
|
|
84
|
+
|
|
85
|
+
// If a known hash is set (non-placeholder), assert equality.
|
|
86
|
+
// The placeholder below is the all-zero-ish string used on first run; replaced
|
|
87
|
+
// automatically by the orchestration script once baseline is captured.
|
|
88
|
+
expect(hash).toBe(SWARM_SECTION_SHA256);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration safety-net for web analytics endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Purpose: lock the wiring of the 3 analytics routes (drift / weekly report /
|
|
5
|
+
* insights) before migrating their backing classes from `daemon/services/` to
|
|
6
|
+
* `web/analytics/`. We assert HTTP 200 + presence of key response fields only;
|
|
7
|
+
* business-correctness coverage is out of scope.
|
|
8
|
+
*
|
|
9
|
+
* If any endpoint regresses to a 4xx/5xx response, this test fails and blocks
|
|
10
|
+
* the H6 migration commit.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
14
|
+
import express, { type Application } from 'express';
|
|
15
|
+
import request from 'supertest';
|
|
16
|
+
import { SQLiteStorage } from '../../src/core/storage/sqlite.js';
|
|
17
|
+
import { registerDriftRoutes } from '../../src/web/routes/drift.js';
|
|
18
|
+
import { registerReportsRoutes } from '../../src/web/routes/reports.js';
|
|
19
|
+
import { registerInsightsRoutes } from '../../src/web/routes/insights.js';
|
|
20
|
+
|
|
21
|
+
const FIXTURE_SESSION = 'sess-analytics-fixture';
|
|
22
|
+
const FIXTURE_PROJECT = '/tmp/proj-analytics-fixture';
|
|
23
|
+
|
|
24
|
+
function nowIso(): string {
|
|
25
|
+
return new Date().toISOString();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Seed a small but deterministic dataset so the analytics endpoints have
|
|
30
|
+
* something to aggregate. H2 Phase 1 enhancement: in addition to shape checks
|
|
31
|
+
* we assert key aggregated values to lock the current behaviour before the
|
|
32
|
+
* weekly-report / anti-pattern / drift inline SQL is migrated to Operations.
|
|
33
|
+
*/
|
|
34
|
+
function seedFixtureData(storage: SQLiteStorage): void {
|
|
35
|
+
// 4 events on the current (active) day so weekly/insights see them.
|
|
36
|
+
storage.writeEvent({
|
|
37
|
+
session_id: FIXTURE_SESSION, project_path: FIXTURE_PROJECT, timestamp: nowIso(),
|
|
38
|
+
hook_type: 'UserPromptSubmit', user_prompt: 'do work',
|
|
39
|
+
});
|
|
40
|
+
storage.writeEvent({
|
|
41
|
+
session_id: FIXTURE_SESSION, project_path: FIXTURE_PROJECT, timestamp: nowIso(),
|
|
42
|
+
hook_type: 'PreToolUse', tool_name: 'Bash', tool_input: { command: 'ls' },
|
|
43
|
+
});
|
|
44
|
+
storage.writeEvent({
|
|
45
|
+
session_id: FIXTURE_SESSION, project_path: FIXTURE_PROJECT, timestamp: nowIso(),
|
|
46
|
+
hook_type: 'PreToolUse', tool_name: 'Edit', tool_input: { file_path: '/a.ts' },
|
|
47
|
+
});
|
|
48
|
+
storage.writeEvent({
|
|
49
|
+
session_id: FIXTURE_SESSION, project_path: FIXTURE_PROJECT, timestamp: nowIso(),
|
|
50
|
+
hook_type: 'PostToolUse', tool_name: 'Edit', tool_output: 'ok',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('Web analytics endpoints (safety-net)', () => {
|
|
55
|
+
let app: Application;
|
|
56
|
+
let storage: SQLiteStorage;
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
storage = new SQLiteStorage(':memory:');
|
|
60
|
+
app = express();
|
|
61
|
+
app.use(express.json());
|
|
62
|
+
registerDriftRoutes(app, { storage });
|
|
63
|
+
registerReportsRoutes(app, { storage });
|
|
64
|
+
registerInsightsRoutes(app, { storage });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
storage.close();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('GET /api/drift/report returns a DriftReport shape', async () => {
|
|
72
|
+
const res = await request(app).get('/api/drift/report');
|
|
73
|
+
expect(res.status).toBe(200);
|
|
74
|
+
expect(res.body).toHaveProperty('generatedAt');
|
|
75
|
+
expect(res.body).toHaveProperty('period');
|
|
76
|
+
expect(res.body).toHaveProperty('checks');
|
|
77
|
+
expect(res.body).toHaveProperty('overallScore');
|
|
78
|
+
expect(Array.isArray(res.body.checks)).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('GET /api/reports/weekly returns a WeeklyReport shape', async () => {
|
|
82
|
+
const res = await request(app).get('/api/reports/weekly');
|
|
83
|
+
expect(res.status).toBe(200);
|
|
84
|
+
expect(res.body).toHaveProperty('generatedAt');
|
|
85
|
+
expect(res.body).toHaveProperty('week');
|
|
86
|
+
expect(res.body).toHaveProperty('overview');
|
|
87
|
+
// overview should at least exist as an object with numeric fields
|
|
88
|
+
expect(typeof res.body.overview).toBe('object');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('GET /api/insights returns insights summary + patterns array', async () => {
|
|
92
|
+
const res = await request(app).get('/api/insights');
|
|
93
|
+
expect(res.status).toBe(200);
|
|
94
|
+
expect(res.body).toHaveProperty('generatedAt');
|
|
95
|
+
expect(res.body).toHaveProperty('windowDays');
|
|
96
|
+
expect(res.body).toHaveProperty('summary');
|
|
97
|
+
expect(res.body).toHaveProperty('patterns');
|
|
98
|
+
expect(Array.isArray(res.body.patterns)).toBe(true);
|
|
99
|
+
expect(res.body.summary).toHaveProperty('total');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ── H2 Phase 1 value-level safety-net ────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
it('GET /api/reports/weekly reflects seeded events in overview.totalEvents', async () => {
|
|
105
|
+
seedFixtureData(storage);
|
|
106
|
+
const res = await request(app).get('/api/reports/weekly').query({ weekOffset: 0 });
|
|
107
|
+
expect(res.status).toBe(200);
|
|
108
|
+
// overview is aggregated from events within the active ISO week. The
|
|
109
|
+
// seed inserts 4 events on the current timestamp → they MUST be inside
|
|
110
|
+
// the current week range. activeProjects MUST include the fixture path.
|
|
111
|
+
expect(res.body.overview.totalEvents).toBe(4);
|
|
112
|
+
expect(res.body.overview.totalSessions).toBe(1);
|
|
113
|
+
expect(res.body.overview.activeProjects).toContain(FIXTURE_PROJECT);
|
|
114
|
+
// Tools section: 1x Bash + 1x Edit (PreToolUse aggregation)
|
|
115
|
+
expect(res.body.tools.totalCalls).toBeGreaterThanOrEqual(2);
|
|
116
|
+
const toolNames = res.body.tools.topTools.map((t: { name: string }) => t.name);
|
|
117
|
+
expect(toolNames).toContain('Bash');
|
|
118
|
+
expect(toolNames).toContain('Edit');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('GET /api/insights summary.total equals patterns.length', async () => {
|
|
122
|
+
seedFixtureData(storage);
|
|
123
|
+
const res = await request(app).get('/api/insights').query({ days: 7 });
|
|
124
|
+
expect(res.status).toBe(200);
|
|
125
|
+
// Whatever patterns the detector returns, summary.total MUST match the
|
|
126
|
+
// patterns array length (locks the route's summarisation logic).
|
|
127
|
+
expect(res.body.summary.total).toBe(res.body.patterns.length);
|
|
128
|
+
// Severity buckets MUST exist as numeric fields.
|
|
129
|
+
expect(typeof res.body.summary.critical).toBe('number');
|
|
130
|
+
expect(typeof res.body.summary.warn).toBe('number');
|
|
131
|
+
expect(typeof res.body.summary.info).toBe('number');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration safety-net for /api/stats.
|
|
3
|
+
*
|
|
4
|
+
* H2 Phase 1: lock the shape and baseline aggregation behaviour of the
|
|
5
|
+
* dashboard stats endpoint before migrating its inline SQL into the
|
|
6
|
+
* EventOperations / SessionOperations / SkillOperations layers.
|
|
7
|
+
*
|
|
8
|
+
* Coverage:
|
|
9
|
+
* - Status 200 + presence of totalEvents / totalSessions / toolUsage /
|
|
10
|
+
* dailyActivity / skillInvocations fields
|
|
11
|
+
* - dailyActivity entry shape (date / eventCount / sessionCount)
|
|
12
|
+
* - Tool usage aggregation with fixture data
|
|
13
|
+
* - Skill invocation count with fixture data
|
|
14
|
+
*
|
|
15
|
+
* If this test fails, the H2 refactor commit must be reverted.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
19
|
+
import express, { type Application } from 'express';
|
|
20
|
+
import request from 'supertest';
|
|
21
|
+
import { SQLiteStorage } from '../../src/core/storage/sqlite.js';
|
|
22
|
+
import { registerStatsRoutes } from '../../src/web/routes/stats.js';
|
|
23
|
+
|
|
24
|
+
const SESSION = 'sess-stats-int';
|
|
25
|
+
const PROJECT = '/tmp/proj-stats-int';
|
|
26
|
+
|
|
27
|
+
function nowIso(): string {
|
|
28
|
+
return new Date().toISOString();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('GET /api/stats (safety-net)', () => {
|
|
32
|
+
let app: Application;
|
|
33
|
+
let storage: SQLiteStorage;
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
storage = new SQLiteStorage(':memory:');
|
|
37
|
+
app = express();
|
|
38
|
+
app.use(express.json());
|
|
39
|
+
registerStatsRoutes(app, { storage });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
storage.close();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('returns the expected response shape with empty database', async () => {
|
|
47
|
+
const res = await request(app).get('/api/stats');
|
|
48
|
+
expect(res.status).toBe(200);
|
|
49
|
+
|
|
50
|
+
expect(res.body).toHaveProperty('totalEvents');
|
|
51
|
+
expect(res.body).toHaveProperty('totalSessions');
|
|
52
|
+
expect(res.body).toHaveProperty('toolUsage');
|
|
53
|
+
expect(res.body).toHaveProperty('dailyActivity');
|
|
54
|
+
expect(res.body).toHaveProperty('skillInvocations');
|
|
55
|
+
|
|
56
|
+
expect(typeof res.body.totalEvents).toBe('number');
|
|
57
|
+
expect(typeof res.body.totalSessions).toBe('number');
|
|
58
|
+
expect(typeof res.body.toolUsage).toBe('object');
|
|
59
|
+
expect(Array.isArray(res.body.dailyActivity)).toBe(true);
|
|
60
|
+
expect(typeof res.body.skillInvocations).toBe('number');
|
|
61
|
+
|
|
62
|
+
expect(res.body.totalEvents).toBe(0);
|
|
63
|
+
expect(res.body.totalSessions).toBe(0);
|
|
64
|
+
expect(res.body.skillInvocations).toBe(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('aggregates totalEvents, totalSessions and toolUsage from events', async () => {
|
|
68
|
+
storage.writeEvent({
|
|
69
|
+
session_id: SESSION, project_path: PROJECT, timestamp: nowIso(),
|
|
70
|
+
hook_type: 'UserPromptSubmit', user_prompt: 'hi',
|
|
71
|
+
});
|
|
72
|
+
storage.writeEvent({
|
|
73
|
+
session_id: SESSION, project_path: PROJECT, timestamp: nowIso(),
|
|
74
|
+
hook_type: 'PreToolUse', tool_name: 'Bash', tool_input: { command: 'ls' },
|
|
75
|
+
});
|
|
76
|
+
storage.writeEvent({
|
|
77
|
+
session_id: SESSION, project_path: PROJECT, timestamp: nowIso(),
|
|
78
|
+
hook_type: 'PreToolUse', tool_name: 'Bash', tool_input: { command: 'pwd' },
|
|
79
|
+
});
|
|
80
|
+
storage.writeEvent({
|
|
81
|
+
session_id: SESSION, project_path: PROJECT, timestamp: nowIso(),
|
|
82
|
+
hook_type: 'PreToolUse', tool_name: 'Edit', tool_input: { file_path: '/x' },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const res = await request(app).get('/api/stats');
|
|
86
|
+
expect(res.status).toBe(200);
|
|
87
|
+
|
|
88
|
+
expect(res.body.totalEvents).toBe(4);
|
|
89
|
+
expect(res.body.totalSessions).toBe(1);
|
|
90
|
+
expect(res.body.toolUsage.Bash).toBe(2);
|
|
91
|
+
expect(res.body.toolUsage.Edit).toBe(1);
|
|
92
|
+
expect(res.body.toolUsage).not.toHaveProperty('UserPromptSubmit');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('dailyActivity entries have date / eventCount / sessionCount fields', async () => {
|
|
96
|
+
storage.writeEvent({
|
|
97
|
+
session_id: SESSION, project_path: PROJECT, timestamp: nowIso(),
|
|
98
|
+
hook_type: 'PreToolUse', tool_name: 'Read',
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const res = await request(app).get('/api/stats');
|
|
102
|
+
expect(res.status).toBe(200);
|
|
103
|
+
expect(res.body.dailyActivity.length).toBeGreaterThanOrEqual(1);
|
|
104
|
+
|
|
105
|
+
const entry = res.body.dailyActivity[0];
|
|
106
|
+
expect(entry).toHaveProperty('date');
|
|
107
|
+
expect(entry).toHaveProperty('eventCount');
|
|
108
|
+
expect(entry).toHaveProperty('sessionCount');
|
|
109
|
+
expect(typeof entry.date).toBe('string');
|
|
110
|
+
expect(typeof entry.eventCount).toBe('number');
|
|
111
|
+
expect(typeof entry.sessionCount).toBe('number');
|
|
112
|
+
expect(entry.eventCount).toBeGreaterThanOrEqual(1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('counts skill invocations independently of events', async () => {
|
|
116
|
+
storage.writeSkillInvocation({
|
|
117
|
+
id: 'si-int-1', route_request_id: null, session_id: SESSION,
|
|
118
|
+
agent_id: null, skill_id: 'official-tdd',
|
|
119
|
+
invocation_type: 'dynamic', reason: null,
|
|
120
|
+
workflow: null, phase: null, feature_slug: null, artifact_path: null,
|
|
121
|
+
depth: 0, success: 1, error: null, timestamp: Date.now(),
|
|
122
|
+
});
|
|
123
|
+
storage.writeSkillInvocation({
|
|
124
|
+
id: 'si-int-2', route_request_id: null, session_id: SESSION,
|
|
125
|
+
agent_id: null, skill_id: 'official-debug',
|
|
126
|
+
invocation_type: 'dynamic', reason: null,
|
|
127
|
+
workflow: null, phase: null, feature_slug: null, artifact_path: null,
|
|
128
|
+
depth: 0, success: 1, error: null, timestamp: Date.now(),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const res = await request(app).get('/api/stats');
|
|
132
|
+
expect(res.status).toBe(200);
|
|
133
|
+
expect(res.body.skillInvocations).toBe(2);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration safety-net for /api/trace/:commit.
|
|
3
|
+
*
|
|
4
|
+
* H2 Phase 1: lock the response shape + error branches before migrating
|
|
5
|
+
* trace's inline SQL (event_breakdown / agent_calls / skills aggregation)
|
|
6
|
+
* into EventOperations / SkillOperations.
|
|
7
|
+
*
|
|
8
|
+
* Approach: spin up a real tmp git repository, create a commit, optionally
|
|
9
|
+
* attach a git note containing `forge-session: <id>`, then assert each
|
|
10
|
+
* branch of the route handler.
|
|
11
|
+
*
|
|
12
|
+
* Coverage:
|
|
13
|
+
* - 400 when ?project query is missing / non-absolute / not a directory /
|
|
14
|
+
* not a git repo
|
|
15
|
+
* - 404 when commit ref cannot be resolved
|
|
16
|
+
* - 200 with empty sessions[] when HEAD has no git note
|
|
17
|
+
* - 200 with non-empty sessions[] (shape validated) when a note links to
|
|
18
|
+
* a session row in storage
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
|
|
22
|
+
import express, { type Application } from 'express';
|
|
23
|
+
import request from 'supertest';
|
|
24
|
+
import { execFileSync } from 'node:child_process';
|
|
25
|
+
import fs from 'node:fs';
|
|
26
|
+
import os from 'node:os';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import { SQLiteStorage } from '../../src/core/storage/sqlite.js';
|
|
29
|
+
import { registerTraceRoutes } from '../../src/web/routes/trace.js';
|
|
30
|
+
|
|
31
|
+
const SESSION = 'sess-trace-int-abcdef01';
|
|
32
|
+
|
|
33
|
+
function git(args: string[], cwd: string): string {
|
|
34
|
+
return execFileSync('git', args, {
|
|
35
|
+
cwd, stdio: 'pipe', timeout: 10000,
|
|
36
|
+
env: {
|
|
37
|
+
...process.env,
|
|
38
|
+
GIT_AUTHOR_NAME: 'forge-test',
|
|
39
|
+
GIT_AUTHOR_EMAIL: 'forge@test.local',
|
|
40
|
+
GIT_COMMITTER_NAME: 'forge-test',
|
|
41
|
+
GIT_COMMITTER_EMAIL: 'forge@test.local',
|
|
42
|
+
},
|
|
43
|
+
}).toString().trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('GET /api/trace/:commit (safety-net)', () => {
|
|
47
|
+
let app: Application;
|
|
48
|
+
let storage: SQLiteStorage;
|
|
49
|
+
let repoDir: string;
|
|
50
|
+
let nonGitDir: string;
|
|
51
|
+
let commitWithNote: string;
|
|
52
|
+
let commitWithoutNote: string;
|
|
53
|
+
|
|
54
|
+
beforeAll(() => {
|
|
55
|
+
// 1) tmp git repo with two commits
|
|
56
|
+
repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'forge-trace-repo-'));
|
|
57
|
+
git(['init', '-q', '-b', 'main'], repoDir);
|
|
58
|
+
fs.writeFileSync(path.join(repoDir, 'a.txt'), 'one');
|
|
59
|
+
git(['add', '.'], repoDir);
|
|
60
|
+
git(['commit', '-q', '-m', 'first commit'], repoDir);
|
|
61
|
+
commitWithoutNote = git(['rev-parse', 'HEAD'], repoDir);
|
|
62
|
+
|
|
63
|
+
fs.writeFileSync(path.join(repoDir, 'b.txt'), 'two');
|
|
64
|
+
git(['add', '.'], repoDir);
|
|
65
|
+
git(['commit', '-q', '-m', 'second commit'], repoDir);
|
|
66
|
+
commitWithNote = git(['rev-parse', 'HEAD'], repoDir);
|
|
67
|
+
|
|
68
|
+
// Attach a git note to the second commit so the route can resolve a session
|
|
69
|
+
git(['notes', 'add', '-m', `forge-session: ${SESSION}`, commitWithNote], repoDir);
|
|
70
|
+
|
|
71
|
+
// 2) a sibling tmp directory that is *not* a git repo
|
|
72
|
+
nonGitDir = fs.mkdtempSync(path.join(os.tmpdir(), 'forge-trace-nogit-'));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterAll(() => {
|
|
76
|
+
try { fs.rmSync(repoDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
77
|
+
try { fs.rmSync(nonGitDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
storage = new SQLiteStorage(':memory:');
|
|
82
|
+
app = express();
|
|
83
|
+
app.use(express.json());
|
|
84
|
+
registerTraceRoutes(app, { storage });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
storage.close();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('returns 400 when ?project query parameter is missing', async () => {
|
|
92
|
+
const res = await request(app).get(`/api/trace/${commitWithNote}`);
|
|
93
|
+
expect(res.status).toBe(400);
|
|
94
|
+
expect(res.body.error).toMatch(/project/i);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('returns 400 when project path is relative', async () => {
|
|
98
|
+
const res = await request(app)
|
|
99
|
+
.get(`/api/trace/${commitWithNote}`)
|
|
100
|
+
.query({ project: 'relative/path' });
|
|
101
|
+
expect(res.status).toBe(400);
|
|
102
|
+
expect(res.body.error).toMatch(/absolute/i);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('returns 400 when project path is not a git repo', async () => {
|
|
106
|
+
const res = await request(app)
|
|
107
|
+
.get(`/api/trace/${commitWithNote}`)
|
|
108
|
+
.query({ project: nonGitDir });
|
|
109
|
+
expect(res.status).toBe(400);
|
|
110
|
+
expect(res.body.error).toMatch(/git repository/i);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('returns 404 when commit cannot be resolved', async () => {
|
|
114
|
+
const res = await request(app)
|
|
115
|
+
.get('/api/trace/nonexistent-commit-ref-zzz')
|
|
116
|
+
.query({ project: repoDir });
|
|
117
|
+
expect(res.status).toBe(404);
|
|
118
|
+
expect(res.body.error).toMatch(/resolve commit/i);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('returns 200 with empty sessions[] when HEAD has no git note', async () => {
|
|
122
|
+
const res = await request(app)
|
|
123
|
+
.get(`/api/trace/${commitWithoutNote}`)
|
|
124
|
+
.query({ project: repoDir });
|
|
125
|
+
expect(res.status).toBe(200);
|
|
126
|
+
expect(res.body.commit).toBe(commitWithoutNote);
|
|
127
|
+
expect(typeof res.body.message).toBe('string');
|
|
128
|
+
expect(Array.isArray(res.body.sessions)).toBe(true);
|
|
129
|
+
expect(res.body.sessions).toEqual([]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('returns 200 with sessions[] entries when commit has a forge-session note', async () => {
|
|
133
|
+
// Seed storage so the session resolves; route looks up by session_id /
|
|
134
|
+
// session_id startsWith
|
|
135
|
+
storage.writeEvent({
|
|
136
|
+
session_id: SESSION,
|
|
137
|
+
project_path: repoDir,
|
|
138
|
+
timestamp: new Date().toISOString(),
|
|
139
|
+
hook_type: 'UserPromptSubmit',
|
|
140
|
+
user_prompt: 'hello',
|
|
141
|
+
});
|
|
142
|
+
storage.writeEvent({
|
|
143
|
+
session_id: SESSION,
|
|
144
|
+
project_path: repoDir,
|
|
145
|
+
timestamp: new Date().toISOString(),
|
|
146
|
+
hook_type: 'PreToolUse',
|
|
147
|
+
tool_name: 'Bash',
|
|
148
|
+
tool_input: { command: 'ls' },
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const res = await request(app)
|
|
152
|
+
.get(`/api/trace/${commitWithNote}`)
|
|
153
|
+
.query({ project: repoDir });
|
|
154
|
+
|
|
155
|
+
expect(res.status).toBe(200);
|
|
156
|
+
expect(res.body.commit).toBe(commitWithNote);
|
|
157
|
+
expect(Array.isArray(res.body.sessions)).toBe(true);
|
|
158
|
+
expect(res.body.sessions.length).toBe(1);
|
|
159
|
+
|
|
160
|
+
const session = res.body.sessions[0];
|
|
161
|
+
// Session should have been found and hydrated with detail fields
|
|
162
|
+
expect(session.session_id).toBe(SESSION);
|
|
163
|
+
expect(session.found).toBe(true);
|
|
164
|
+
expect(session).toHaveProperty('event_count');
|
|
165
|
+
expect(session).toHaveProperty('event_breakdown');
|
|
166
|
+
expect(session).toHaveProperty('agent_calls');
|
|
167
|
+
expect(session).toHaveProperty('skills');
|
|
168
|
+
expect(typeof session.event_breakdown).toBe('object');
|
|
169
|
+
expect(Array.isArray(session.agent_calls)).toBe(true);
|
|
170
|
+
expect(Array.isArray(session.skills)).toBe(true);
|
|
171
|
+
// We wrote one PreToolUse + one UserPromptSubmit
|
|
172
|
+
expect(session.event_breakdown.PreToolUse).toBe(1);
|
|
173
|
+
expect(session.event_breakdown.UserPromptSubmit).toBe(1);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for FORGE_PATHS (src/core/constants.ts)
|
|
3
|
+
*
|
|
4
|
+
* Verifies that every path method returns a value prefixed by FORGE_HOME
|
|
5
|
+
* and matches the expected sub-path.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from 'vitest';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { FORGE_HOME, FORGE_PATHS } from '../../../src/core/constants.js';
|
|
11
|
+
|
|
12
|
+
describe('FORGE_PATHS', () => {
|
|
13
|
+
describe('existing methods', () => {
|
|
14
|
+
it('home() returns FORGE_HOME', () => {
|
|
15
|
+
expect(FORGE_PATHS.home()).toBe(FORGE_HOME);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('config() returns FORGE_HOME/config.yaml', () => {
|
|
19
|
+
expect(FORGE_PATHS.config()).toBe(join(FORGE_HOME, 'config.yaml'));
|
|
20
|
+
expect(FORGE_PATHS.config().startsWith(FORGE_HOME)).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('database() returns FORGE_HOME/data.db', () => {
|
|
24
|
+
expect(FORGE_PATHS.database()).toBe(join(FORGE_HOME, 'data.db'));
|
|
25
|
+
expect(FORGE_PATHS.database().startsWith(FORGE_HOME)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('logs() returns FORGE_HOME/logs', () => {
|
|
29
|
+
expect(FORGE_PATHS.logs()).toBe(join(FORGE_HOME, 'logs'));
|
|
30
|
+
expect(FORGE_PATHS.logs().startsWith(FORGE_HOME)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('daemon runtime files', () => {
|
|
35
|
+
it('daemonSocket() returns FORGE_HOME/daemon.sock', () => {
|
|
36
|
+
expect(FORGE_PATHS.daemonSocket()).toBe(join(FORGE_HOME, 'daemon.sock'));
|
|
37
|
+
expect(FORGE_PATHS.daemonSocket().startsWith(FORGE_HOME)).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('daemonPid() returns FORGE_HOME/daemon.pid', () => {
|
|
41
|
+
expect(FORGE_PATHS.daemonPid()).toBe(join(FORGE_HOME, 'daemon.pid'));
|
|
42
|
+
expect(FORGE_PATHS.daemonPid().startsWith(FORGE_HOME)).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('daemonToken() returns FORGE_HOME/daemon.token', () => {
|
|
46
|
+
expect(FORGE_PATHS.daemonToken()).toBe(join(FORGE_HOME, 'daemon.token'));
|
|
47
|
+
expect(FORGE_PATHS.daemonToken().startsWith(FORGE_HOME)).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('daemonLog() returns FORGE_HOME/daemon.log', () => {
|
|
51
|
+
expect(FORGE_PATHS.daemonLog()).toBe(join(FORGE_HOME, 'daemon.log'));
|
|
52
|
+
expect(FORGE_PATHS.daemonLog().startsWith(FORGE_HOME)).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('daemonStdout() returns FORGE_HOME/daemon-stdout.log', () => {
|
|
56
|
+
expect(FORGE_PATHS.daemonStdout()).toBe(join(FORGE_HOME, 'daemon-stdout.log'));
|
|
57
|
+
expect(FORGE_PATHS.daemonStdout().startsWith(FORGE_HOME)).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('daemonStderr() returns FORGE_HOME/daemon-stderr.log', () => {
|
|
61
|
+
expect(FORGE_PATHS.daemonStderr()).toBe(join(FORGE_HOME, 'daemon-stderr.log'));
|
|
62
|
+
expect(FORGE_PATHS.daemonStderr().startsWith(FORGE_HOME)).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('subdirs', () => {
|
|
67
|
+
it('hooks() returns FORGE_HOME/hooks', () => {
|
|
68
|
+
expect(FORGE_PATHS.hooks()).toBe(join(FORGE_HOME, 'hooks'));
|
|
69
|
+
expect(FORGE_PATHS.hooks().startsWith(FORGE_HOME)).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('queue() returns FORGE_HOME/queue', () => {
|
|
73
|
+
expect(FORGE_PATHS.queue()).toBe(join(FORGE_HOME, 'queue'));
|
|
74
|
+
expect(FORGE_PATHS.queue().startsWith(FORGE_HOME)).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('queueDead() returns FORGE_HOME/queue/dead', () => {
|
|
78
|
+
expect(FORGE_PATHS.queueDead()).toBe(join(FORGE_HOME, 'queue', 'dead'));
|
|
79
|
+
expect(FORGE_PATHS.queueDead().startsWith(FORGE_HOME)).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('patchable targets', () => {
|
|
84
|
+
it('routingYaml() returns FORGE_HOME/routing.yaml', () => {
|
|
85
|
+
expect(FORGE_PATHS.routingYaml()).toBe(join(FORGE_HOME, 'routing.yaml'));
|
|
86
|
+
expect(FORGE_PATHS.routingYaml().startsWith(FORGE_HOME)).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("backups('skills') returns FORGE_HOME/backups/skills", () => {
|
|
90
|
+
expect(FORGE_PATHS.backups('skills')).toBe(join(FORGE_HOME, 'backups', 'skills'));
|
|
91
|
+
expect(FORGE_PATHS.backups('skills').startsWith(FORGE_HOME)).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("backups('routing') returns FORGE_HOME/backups/routing", () => {
|
|
95
|
+
expect(FORGE_PATHS.backups('routing')).toBe(join(FORGE_HOME, 'backups', 'routing'));
|
|
96
|
+
expect(FORGE_PATHS.backups('routing').startsWith(FORGE_HOME)).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|