@winspan/claude-forge 8.53.2 → 8.54.3
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/DEVELOPMENT.md +290 -221
- package/README.md +50 -8
- package/dist/cli/commands/skills.d.ts.map +1 -1
- package/dist/cli/commands/skills.js +7 -3
- package/dist/cli/commands/skills.js.map +1 -1
- package/dist/cli/init/hook-manager.d.ts +1 -1
- package/dist/cli/init/hook-manager.d.ts.map +1 -1
- package/dist/cli/init/hook-manager.js +1 -0
- package/dist/cli/init/hook-manager.js.map +1 -1
- package/dist/core/storage/events.d.ts.map +1 -1
- package/dist/core/storage/events.js +0 -1
- package/dist/core/storage/events.js.map +1 -1
- package/dist/core/storage/maintenance.d.ts +25 -3
- package/dist/core/storage/maintenance.d.ts.map +1 -1
- package/dist/core/storage/maintenance.js +33 -4
- package/dist/core/storage/maintenance.js.map +1 -1
- package/dist/core/storage/routing.d.ts +4 -0
- package/dist/core/storage/routing.d.ts.map +1 -1
- package/dist/core/storage/routing.js +10 -4
- package/dist/core/storage/routing.js.map +1 -1
- package/dist/core/storage/sessions.d.ts +17 -0
- package/dist/core/storage/sessions.d.ts.map +1 -1
- package/dist/core/storage/sessions.js +64 -0
- package/dist/core/storage/sessions.js.map +1 -1
- package/dist/core/storage/skills.d.ts +4 -0
- package/dist/core/storage/skills.d.ts.map +1 -1
- package/dist/core/storage/skills.js +10 -2
- package/dist/core/storage/skills.js.map +1 -1
- package/dist/core/storage/sqlite.d.ts +5 -0
- package/dist/core/storage/sqlite.d.ts.map +1 -1
- package/dist/core/storage/sqlite.js +6 -0
- package/dist/core/storage/sqlite.js.map +1 -1
- package/dist/core/storage/tasks.d.ts.map +1 -1
- package/dist/core/storage/tasks.js +2 -0
- package/dist/core/storage/tasks.js.map +1 -1
- package/dist/core/types.d.ts +7 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +19 -4
- package/dist/daemon/index.js.map +1 -1
- package/dist/skills/registry.d.ts.map +1 -1
- package/dist/skills/registry.js +13 -2
- package/dist/skills/registry.js.map +1 -1
- package/dist/skills/semantic-matcher.d.ts +2 -2
- package/dist/skills/semantic-matcher.d.ts.map +1 -1
- package/dist/skills/semantic-matcher.js +14 -19
- package/dist/skills/semantic-matcher.js.map +1 -1
- package/dist/skills/upgrade-engine.d.ts +3 -1
- package/dist/skills/upgrade-engine.d.ts.map +1 -1
- package/dist/skills/upgrade-engine.js +25 -14
- package/dist/skills/upgrade-engine.js.map +1 -1
- package/dist/web/analytics/weekly-report.d.ts.map +1 -1
- package/dist/web/analytics/weekly-report.js +21 -29
- package/dist/web/analytics/weekly-report.js.map +1 -1
- package/dist/web/routes/patch.d.ts.map +1 -1
- package/dist/web/routes/patch.js +32 -2
- package/dist/web/routes/patch.js.map +1 -1
- package/dist/web/routes/sessions.d.ts.map +1 -1
- package/dist/web/routes/sessions.js +9 -7
- package/dist/web/routes/sessions.js.map +1 -1
- package/dist/web/routes/trace.d.ts.map +1 -1
- package/dist/web/routes/trace.js +2 -3
- package/dist/web/routes/trace.js.map +1 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +3 -2
- package/dist/web/server.js.map +1 -1
- package/package.json +12 -2
- package/scripts/postinstall.cjs +21 -0
- package/.claude/CLAUDE.md +0 -17
- package/.eslintrc.js +0 -23
- package/.prettierrc +0 -8
- package/ARCHITECTURE_ISSUES.md +0 -249
- package/CLAUDE.md +0 -265
- package/CLAUDE.md.backup +0 -488
- package/docs/concurrent-agents.md +0 -129
- package/docs/design/architecture-review-20260516.md +0 -232
- package/docs/design/fix-skills-data-and-set-leak-spec-20260516-1300.md +0 -219
- package/docs/design/h1-storage-aggregation-spec-20260518-1121.md +0 -299
- package/docs/design/h2-getdatabase-encapsulation-spec-20260518-1450.md +0 -191
- package/docs/design/h3-fallback-removal-spec-20260518-1245.md +0 -76
- package/docs/design/h4-index-dedup-spec-20260518-1230.md +0 -109
- package/docs/design/h6-services-migration-spec-20260518-1355.md +0 -82
- package/docs/design/hook-failure-queue-spec-20260516-1530.md +0 -204
- package/docs/design/l1-swarm-protocol-extract-spec-20260518-1605.md +0 -106
- package/docs/design/m10-forge-paths-spec-20260518-1320.md +0 -121
- package/docs/design/m2-m3-tool-input-spec-20260518-1425.md +0 -131
- package/docs/design/m7-routing-event-association-spec-20260518-1545.md +0 -103
- package/docs/design/project-path-gitroot-spec-20260518-1715.md +0 -134
- package/docs/design/refactor-phase1-spec-20260515-1600.md +0 -543
- package/docs/design/refactor-phase2-spec-20260515-1700.md +0 -424
- package/docs/design/skill-ai-upgrade-spec-20260518-1930.md +0 -297
- package/docs/design/task-active-gc-spec-20260518-1745.md +0 -146
- package/docs/design/tasks-list-filter-pagination-spec-20260518-0930.md +0 -208
- package/docs/implementation/daemon-skill-sync-changelog-20260518-2000.md +0 -22
- package/docs/implementation/fix-skills-data-and-set-leak-changelog-20260516-1300.md +0 -104
- package/docs/implementation/h1-storage-aggregation-changelog-20260518-1121.md +0 -82
- package/docs/implementation/h2-final-changelog-20260518-1530.md +0 -61
- package/docs/implementation/h2-phase1-safety-net-changelog-20260518-1450.md +0 -70
- package/docs/implementation/h2-phase2-operations-changelog-20260518-1450.md +0 -120
- package/docs/implementation/h2-phase3-callsites-changelog-20260518-1450.md +0 -71
- package/docs/implementation/h3-fallback-removal-changelog-20260518-1245.md +0 -71
- package/docs/implementation/h4-index-dedup-changelog-20260518-1230.md +0 -60
- package/docs/implementation/h6-services-migration-changelog-20260518-1355.md +0 -46
- package/docs/implementation/h7-m9-defaults-changelog-20260518-1300.md +0 -46
- package/docs/implementation/hook-failure-queue-changelog-20260516-1530.md +0 -196
- package/docs/implementation/hotfix-daemon-event-reject-20260516-1430.md +0 -56
- package/docs/implementation/l1-swarm-protocol-extract-changelog-20260518-1605.md +0 -45
- package/docs/implementation/l3-l4-daemon-perf-changelog-20260518-1410.md +0 -63
- package/docs/implementation/l6-l8-final-cleanup-changelog-20260518-1640.md +0 -38
- package/docs/implementation/m1-m4-m5-l7-cleanup-changelog-20260518-1310.md +0 -58
- package/docs/implementation/m10-forge-paths-changelog-20260518-1320.md +0 -60
- package/docs/implementation/m2-m3-tool-input-changelog-20260518-1425.md +0 -43
- package/docs/implementation/m6-m8-naming-shutdown-changelog-20260518-1340.md +0 -56
- package/docs/implementation/m7-routing-association-changelog-20260518-1545.md +0 -69
- package/docs/implementation/project-path-gitroot-changelog-20260518-1715.md +0 -63
- package/docs/implementation/refactor-phase1-changelog-20260515-1630.md +0 -354
- package/docs/implementation/refactor-phase2-changelog-20260515-1705.md +0 -421
- package/docs/implementation/skill-ai-upgrade-changelog-20260518-1930.md +0 -49
- package/docs/implementation/task-active-gc-changelog-20260518-1745.md +0 -35
- package/docs/implementation/task-title-summary-changelog-20260518-1130.md +0 -39
- package/docs/implementation/tasks-detail-back-loses-filters-changelog-20260518-1100.md +0 -22
- package/docs/implementation/tasks-list-filter-pagination-changelog-20260518-0930.md +0 -72
- package/docs/implementation/tasks-page-white-screen-hotfix-changelog-20260518-1015.md +0 -56
- package/docs/reviews/claudemd-template-sync.md +0 -54
- package/docs/reviews/task-title-summary.md +0 -92
- package/docs/reviews/tasks-detail-back-loses-filters.md +0 -58
- package/docs/reviews/tasks-filter-pagination.md +0 -80
- package/docs/reviews/tasks-page-white-screen-hotfix.md +0 -126
- package/docs/ruflo-learning-strategy.md +0 -322
- package/docs/skills-deduplication-analysis.md +0 -83
- package/docs/skills-multiformat-support.md +0 -177
- package/docs/skills-third-party.md +0 -183
- package/docs/testing/tasks-filter-pagination-test-report.md +0 -86
- package/forge +0 -321
- package/playwright.config.ts +0 -40
- package/scripts/demo-v2.ts +0 -91
- package/scripts/dev-daemon.sh +0 -232
- package/scripts/dev-web.ts +0 -109
- package/scripts/e2e-mcp-link.ts +0 -423
- package/scripts/e2e-methodology-quality.ts +0 -253
- package/scripts/e2e-routing.ts +0 -456
- package/scripts/e2e-user-methodology.ts +0 -326
- package/scripts/e2e-web-workflows.ts +0 -299
- package/scripts/migrate-legacy-to-dynamic.sql +0 -108
- package/scripts/regenerate-execution-docs.ts +0 -116
- package/scripts/sync-agent-skills.ts +0 -193
- package/scripts/test-hook.sh +0 -71
- package/scripts/verify-skill-loading.ts +0 -62
- package/src/claudemd/claudemd-generator.ts +0 -568
- package/src/claudemd/convention-extractor.ts +0 -69
- package/src/claudemd/index.ts +0 -35
- package/src/claudemd/persona-manager.ts +0 -88
- package/src/claudemd/resume-manager.ts +0 -236
- package/src/claudemd/tech-detector.ts +0 -220
- package/src/claudemd/templates/swarm-protocol.md +0 -222
- package/src/cli/commands/claudemd.ts +0 -84
- package/src/cli/commands/config.ts +0 -46
- package/src/cli/commands/daemon.ts +0 -310
- package/src/cli/commands/executions.ts +0 -115
- package/src/cli/commands/init.ts +0 -204
- package/src/cli/commands/logs.ts +0 -181
- package/src/cli/commands/mcp.ts +0 -242
- package/src/cli/commands/menu.ts +0 -357
- package/src/cli/commands/skills.ts +0 -328
- package/src/cli/commands/stats.ts +0 -73
- package/src/cli/commands/status.ts +0 -69
- package/src/cli/commands/template.ts +0 -77
- package/src/cli/commands/trace.ts +0 -148
- package/src/cli/index.ts +0 -42
- package/src/cli/init/hook-manager.ts +0 -132
- package/src/core/ai/provider.ts +0 -308
- package/src/core/ai/types.ts +0 -51
- package/src/core/config.ts +0 -124
- package/src/core/constants.ts +0 -67
- package/src/core/event-fields.ts +0 -32
- package/src/core/queue/index.ts +0 -192
- package/src/core/storage/base.ts +0 -302
- package/src/core/storage/events.ts +0 -434
- package/src/core/storage/injections.ts +0 -78
- package/src/core/storage/maintenance.ts +0 -59
- package/src/core/storage/migrations/002_add_skill_tracking.sql +0 -6
- package/src/core/storage/migrations/003_add_skill_invocations.sql +0 -23
- package/src/core/storage/performance-indexes.sql +0 -23
- package/src/core/storage/routing.ts +0 -322
- package/src/core/storage/rows.ts +0 -112
- package/src/core/storage/schema.sql +0 -224
- package/src/core/storage/sessions.ts +0 -168
- package/src/core/storage/skills.ts +0 -233
- package/src/core/storage/sqlite.ts +0 -293
- package/src/core/storage/tasks.ts +0 -318
- package/src/core/storage/token-usage.ts +0 -93
- package/src/core/types.ts +0 -181
- package/src/core/utils/error-handler.ts +0 -257
- package/src/core/utils/forge-resume-block.ts +0 -74
- package/src/core/utils/format.ts +0 -69
- package/src/core/utils/git.ts +0 -23
- package/src/core/utils/logger.ts +0 -134
- package/src/core/utils/lru-cache.ts +0 -54
- package/src/core/utils/path.ts +0 -19
- package/src/core/utils/session.ts +0 -26
- package/src/core/utils/time.ts +0 -37
- package/src/core/utils/token-tracker.ts +0 -97
- package/src/daemon/event-parser.ts +0 -36
- package/src/daemon/handlers/history-exporter.ts +0 -117
- package/src/daemon/handlers/post-tool-use.ts +0 -54
- package/src/daemon/handlers/stop.ts +0 -208
- package/src/daemon/handlers/user-prompt.ts +0 -178
- package/src/daemon/hook-sync.ts +0 -91
- package/src/daemon/index.ts +0 -312
- package/src/daemon/launchd/com.claude-forge.daemon.plist.template +0 -47
- package/src/daemon/launchd-installer.ts +0 -260
- package/src/daemon/lifecycle.ts +0 -128
- package/src/daemon/router.ts +0 -40
- package/src/daemon/server.ts +0 -196
- package/src/daemon/services/task-segmenter.ts +0 -112
- package/src/daemon/skill-sync.ts +0 -88
- package/src/hooks/hook-lib.sh +0 -118
- package/src/hooks/notification.sh +0 -35
- package/src/hooks/post-tool-use.sh +0 -61
- package/src/hooks/pre-tool-use.sh +0 -63
- package/src/hooks/stop.sh +0 -43
- package/src/hooks/user-prompt-submit.sh +0 -69
- package/src/mcp/server.ts +0 -322
- package/src/skills/index.ts +0 -2
- package/src/skills/invocation-guard.ts +0 -177
- package/src/skills/matcher.ts +0 -148
- package/src/skills/official/code-simplifier.md +0 -52
- package/src/skills/official/find-skills.md +0 -142
- package/src/skills/official/official-api-design.md +0 -30
- package/src/skills/official/official-architecture-decision.md +0 -41
- package/src/skills/official/official-bmad.md +0 -118
- package/src/skills/official/official-db-schema-design.md +0 -34
- package/src/skills/official/official-debug.md +0 -25
- package/src/skills/official/official-doc-driven.md +0 -31
- package/src/skills/official/official-harness-engineering.md +0 -108
- package/src/skills/official/official-performance-optimization.md +0 -30
- package/src/skills/official/official-pr-review.md +0 -35
- package/src/skills/official/official-release-checklist.md +0 -30
- package/src/skills/official/official-security-hardening.md +0 -32
- package/src/skills/official/official-spec-driven-design.md +0 -31
- package/src/skills/official/planning-with-files.md +0 -241
- package/src/skills/official/ui-ux-pro-max.md +0 -105
- package/src/skills/official/webapp-testing.md +0 -96
- package/src/skills/official-skills.ts +0 -89
- package/src/skills/registry.ts +0 -355
- package/src/skills/semantic-matcher.ts +0 -234
- package/src/skills/tools/pipeline-suggest.ts +0 -226
- package/src/skills/tools/skill-invoke.ts +0 -168
- package/src/skills/tools/skill-list.ts +0 -59
- package/src/skills/upgrade-engine.ts +0 -541
- package/src/skills/upgrade-prompt.ts +0 -84
- package/src/templates/go.yaml +0 -53
- package/src/templates/python.yaml +0 -59
- package/src/templates/react.yaml +0 -55
- package/src/templates/template-manager.ts +0 -170
- package/src/web/analytics/anti-pattern-detector.ts +0 -367
- package/src/web/analytics/drift-detector.ts +0 -219
- package/src/web/analytics/weekly-report.ts +0 -431
- package/src/web/auth-middleware.ts +0 -54
- package/src/web/routes/_helpers.ts +0 -34
- package/src/web/routes/ai.ts +0 -204
- package/src/web/routes/auth.ts +0 -22
- package/src/web/routes/drift.ts +0 -25
- package/src/web/routes/error-handler.ts +0 -120
- package/src/web/routes/events.ts +0 -47
- package/src/web/routes/insights.ts +0 -43
- package/src/web/routes/patch.ts +0 -117
- package/src/web/routes/reports.ts +0 -34
- package/src/web/routes/rules.ts +0 -76
- package/src/web/routes/sessions.ts +0 -250
- package/src/web/routes/skill-stats.ts +0 -92
- package/src/web/routes/skills.ts +0 -350
- package/src/web/routes/static.ts +0 -67
- package/src/web/routes/stats.ts +0 -50
- package/src/web/routes/status.ts +0 -30
- package/src/web/routes/tasks.ts +0 -193
- package/src/web/routes/token-usage.ts +0 -20
- package/src/web/routes/trace.ts +0 -126
- package/src/web/routes/types.ts +0 -57
- package/src/web/server.ts +0 -134
- package/src/web/ssrf-guard.ts +0 -112
- package/src/web/static/index.html +0 -3251
- package/src/web/static/vendor/chart.umd.min.js +0 -20
- package/tests/e2e/dashboard.spec.ts +0 -205
- package/tests/e2e/routing-skill-e2e.test.ts +0 -39
- package/tests/helpers/mock-ai.ts +0 -92
- package/tests/helpers/mock-storage.ts +0 -159
- package/tests/integration/claudemd-generator.test.ts +0 -90
- package/tests/integration/queue-replay.integration.test.ts +0 -193
- package/tests/integration/tasks-filter.integration.test.ts +0 -154
- package/tests/integration/web-analytics.integration.test.ts +0 -133
- package/tests/integration/web-stats.integration.test.ts +0 -135
- package/tests/integration/web-trace.integration.test.ts +0 -175
- package/tests/performance/database.benchmark.ts +0 -161
- package/tests/semantic-matcher.test.ts +0 -99
- package/tests/skill-matcher.test.ts +0 -110
- package/tests/unit/ai-provider-retry.test.ts +0 -194
- package/tests/unit/ai-provider-vision.test.ts +0 -224
- package/tests/unit/claudemd-generator.test.ts +0 -68
- package/tests/unit/cli-mcp.test.ts +0 -141
- package/tests/unit/core/forge-paths.test.ts +0 -99
- package/tests/unit/daemon/hook-sync.test.ts +0 -71
- package/tests/unit/daemon/post-tool-use.test.ts +0 -121
- package/tests/unit/daemon/skill-sync.test.ts +0 -75
- package/tests/unit/daemon/stop-handler-behavior-summary.test.ts +0 -202
- package/tests/unit/daemon/task-segmenter-recover.test.ts +0 -84
- package/tests/unit/event-fields.test.ts +0 -88
- package/tests/unit/event-parser.test.ts +0 -55
- package/tests/unit/handlers.test.ts +0 -171
- package/tests/unit/hooks/resolve-project-path.test.ts +0 -122
- package/tests/unit/invocation-guard.test.ts +0 -125
- package/tests/unit/queue.test.ts +0 -272
- package/tests/unit/router.test.ts +0 -138
- package/tests/unit/security.test.ts +0 -128
- package/tests/unit/skill-invocations-workflow.test.ts +0 -495
- package/tests/unit/skill-registry.test.ts +0 -94
- package/tests/unit/skills/invocation-guard-ttl.test.ts +0 -211
- package/tests/unit/skills/official-skills-loader.test.ts +0 -126
- package/tests/unit/skills/registry-multiformat.test.ts +0 -92
- package/tests/unit/skills/upgrade-engine-parse.test.ts +0 -138
- package/tests/unit/skills/upgrade-engine.test.ts +0 -401
- package/tests/unit/skills/upgrade-prompt.test.ts +0 -89
- package/tests/unit/socket-server.test.ts +0 -183
- package/tests/unit/storage/event-operations-aggregates.test.ts +0 -342
- package/tests/unit/storage/migration-idempotent.test.ts +0 -304
- package/tests/unit/storage/routing-aggregates.test.ts +0 -276
- package/tests/unit/storage/routing.test.ts +0 -117
- package/tests/unit/storage/schema-missing.test.ts +0 -81
- package/tests/unit/storage/session-operations-aggregates.test.ts +0 -120
- package/tests/unit/storage/sessions-aggregate.test.ts +0 -435
- package/tests/unit/storage/skill-operations-counts.test.ts +0 -106
- package/tests/unit/storage/skills-aggregates.test.ts +0 -104
- package/tests/unit/storage/sqlite-refactor-harness.test.ts +0 -314
- package/tests/unit/storage/task-operations-counts.test.ts +0 -46
- package/tests/unit/storage/tasks-getById.test.ts +0 -343
- package/tests/unit/storage/tasks-stale-gc.test.ts +0 -86
- package/tests/unit/storage.test.ts +0 -172
- package/tests/unit/token-usage.test.ts +0 -144
- package/tests/unit/type-guards.test.ts +0 -201
- package/tests/unit/utils/format.test.ts +0 -189
- package/tests/unit/utils/session.test.ts +0 -89
- package/tests/unit/utils/time.test.ts +0 -112
- package/tests/unit/web/navigation-back-contract.test.ts +0 -134
- package/tests/unit/web/routes-auth.test.ts +0 -93
- package/tests/unit/web/routes-events.test.ts +0 -101
- package/tests/unit/web/routes-rules.test.ts +0 -182
- package/tests/unit/web/routes-sessions.test.ts +0 -181
- package/tests/unit/web/routes-skill-stats.test.ts +0 -179
- package/tests/unit/web/routes-stats.test.ts +0 -92
- package/tests/unit/web/routes-tasks.test.ts +0 -385
- package/tests/unit/web/task-title-contract.test.ts +0 -210
- package/tests/unit/web/tasks-component-contract.test.ts +0 -179
- package/tsconfig.json +0 -22
- package/vitest.config.ts +0 -21
- package/vitest.integration.config.ts +0 -16
- package/web/CLAUDE.md +0 -20
- package/web/index.html +0 -13
- package/web/package-lock.json +0 -4854
- package/web/package.json +0 -35
- package/web/postcss.config.js +0 -6
- package/web/src/App.tsx +0 -110
- package/web/src/components/CodeBlock.tsx +0 -31
- package/web/src/components/Confirm.tsx +0 -96
- package/web/src/components/Drawer.tsx +0 -60
- package/web/src/components/Layout.tsx +0 -145
- package/web/src/components/MarkdownRenderer.tsx +0 -77
- package/web/src/components/SearchInput.tsx +0 -31
- package/web/src/components/SessionDetailContent.tsx +0 -157
- package/web/src/components/Toast.tsx +0 -92
- package/web/src/index.css +0 -19
- package/web/src/main.tsx +0 -31
- package/web/src/pages/AIConfig.tsx +0 -233
- package/web/src/pages/Dashboard.tsx +0 -572
- package/web/src/pages/Events.tsx +0 -271
- package/web/src/pages/Reports.tsx +0 -428
- package/web/src/pages/SessionDetail.tsx +0 -162
- package/web/src/pages/Sessions.tsx +0 -205
- package/web/src/pages/Skills.tsx +0 -180
- package/web/src/pages/TaskDetail.tsx +0 -515
- package/web/src/pages/Tasks.tsx +0 -415
- package/web/src/utils/auth.ts +0 -59
- package/web/src/utils/export.ts +0 -54
- package/web/src/utils/navigation.ts +0 -25
- package/web/src/utils/task-title.ts +0 -49
- package/web/src/utils/time.ts +0 -13
- package/web/tailwind.config.js +0 -11
- package/web/tsconfig.json +0 -21
- package/web/tsconfig.node.json +0 -10
- package/web/vite.config.ts +0 -76
- package/winspan-claude-forge-8.43.0.tgz +0 -0
|
@@ -1,342 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* H2: EventOperations 新增 aggregate / count / query 方法测试
|
|
3
|
-
*
|
|
4
|
-
* 覆盖 11 个方法:
|
|
5
|
-
* countAllEvents
|
|
6
|
-
* aggregateToolUsage(含 hook_type 过滤)
|
|
7
|
-
* aggregateDailyEventCounts
|
|
8
|
-
* aggregateHookTypeBySession
|
|
9
|
-
* aggregateAgentTypeBySession
|
|
10
|
-
* aggregateToolUsageBySession
|
|
11
|
-
* countActiveDays
|
|
12
|
-
* aggregateOverviewByRange
|
|
13
|
-
* queryDistinctProjects
|
|
14
|
-
* aggregateToolFailureRate
|
|
15
|
-
* queryFileEditInputs
|
|
16
|
-
* queryEventsByTimeRange
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
20
|
-
import { mkdtempSync, rmSync } from 'node:fs';
|
|
21
|
-
import { tmpdir } from 'node:os';
|
|
22
|
-
import { join } from 'node:path';
|
|
23
|
-
import { SQLiteStorage } from '../../../src/core/storage/sqlite.js';
|
|
24
|
-
import type { ForgeEvent } from '../../../src/core/types.js';
|
|
25
|
-
|
|
26
|
-
describe('EventOperations H2 aggregates', () => {
|
|
27
|
-
let tmp: string;
|
|
28
|
-
let storage: SQLiteStorage;
|
|
29
|
-
|
|
30
|
-
beforeEach(() => {
|
|
31
|
-
tmp = mkdtempSync(join(tmpdir(), 'forge-h2-events-'));
|
|
32
|
-
storage = new SQLiteStorage(join(tmp, 'data.db'));
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
afterEach(() => {
|
|
36
|
-
try { storage.close(); } catch { /* ignore */ }
|
|
37
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
function makeEvent(overrides: Partial<ForgeEvent>): ForgeEvent {
|
|
41
|
-
return {
|
|
42
|
-
session_id: 's1',
|
|
43
|
-
project_path: '/tmp/proj',
|
|
44
|
-
timestamp: '2026-05-10T10:00:00.000Z',
|
|
45
|
-
hook_type: 'PreToolUse',
|
|
46
|
-
...overrides,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ── countAllEvents ─────────────────────────────────────────────────
|
|
51
|
-
describe('countAllEvents', () => {
|
|
52
|
-
it('空表返回 0', () => {
|
|
53
|
-
expect(storage.countAllEvents()).toBe(0);
|
|
54
|
-
});
|
|
55
|
-
it('多条事件返回总数', () => {
|
|
56
|
-
storage.writeEvent(makeEvent({ session_id: 'a' }));
|
|
57
|
-
storage.writeEvent(makeEvent({ session_id: 'b' }));
|
|
58
|
-
storage.writeEvent(makeEvent({ session_id: 'c' }));
|
|
59
|
-
expect(storage.countAllEvents()).toBe(3);
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// ── aggregateToolUsage ─────────────────────────────────────────────
|
|
64
|
-
describe('aggregateToolUsage', () => {
|
|
65
|
-
it('空表返回空数组', () => {
|
|
66
|
-
expect(storage.aggregateToolUsage()).toEqual([]);
|
|
67
|
-
});
|
|
68
|
-
it('按 count 降序,跳过空 tool_name', () => {
|
|
69
|
-
storage.writeEvent(makeEvent({ tool_name: 'Bash' }));
|
|
70
|
-
storage.writeEvent(makeEvent({ tool_name: 'Bash' }));
|
|
71
|
-
storage.writeEvent(makeEvent({ tool_name: 'Read' }));
|
|
72
|
-
storage.writeEvent(makeEvent({ hook_type: 'UserPromptSubmit' })); // null tool_name
|
|
73
|
-
const r = storage.aggregateToolUsage();
|
|
74
|
-
expect(r).toEqual([
|
|
75
|
-
{ tool_name: 'Bash', count: 2 },
|
|
76
|
-
{ tool_name: 'Read', count: 1 },
|
|
77
|
-
]);
|
|
78
|
-
});
|
|
79
|
-
it('hook_type 过滤只统计指定类型', () => {
|
|
80
|
-
storage.writeEvent(makeEvent({ tool_name: 'Bash', hook_type: 'PreToolUse' }));
|
|
81
|
-
storage.writeEvent(makeEvent({ tool_name: 'Bash', hook_type: 'PostToolUse' }));
|
|
82
|
-
storage.writeEvent(makeEvent({ tool_name: 'Read', hook_type: 'PreToolUse' }));
|
|
83
|
-
const r = storage.aggregateToolUsage({ hook_type: 'PreToolUse' });
|
|
84
|
-
expect(r.find(x => x.tool_name === 'Bash')?.count).toBe(1);
|
|
85
|
-
expect(r.find(x => x.tool_name === 'Read')?.count).toBe(1);
|
|
86
|
-
});
|
|
87
|
-
it('since 过滤排除早期事件', () => {
|
|
88
|
-
storage.writeEvent(makeEvent({ tool_name: 'Bash', timestamp: '2026-01-01T00:00:00.000Z' }));
|
|
89
|
-
storage.writeEvent(makeEvent({ tool_name: 'Bash', timestamp: '2026-05-10T00:00:00.000Z', session_id: 's2' }));
|
|
90
|
-
const r = storage.aggregateToolUsage({ since: '2026-03-01T00:00:00.000Z' });
|
|
91
|
-
expect(r).toEqual([{ tool_name: 'Bash', count: 1 }]);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
// ── aggregateDailyEventCounts ──────────────────────────────────────
|
|
96
|
-
describe('aggregateDailyEventCounts', () => {
|
|
97
|
-
it('按 date 分组并升序', () => {
|
|
98
|
-
storage.writeEvent(makeEvent({ timestamp: '2026-05-10T10:00:00.000Z' }));
|
|
99
|
-
storage.writeEvent(makeEvent({ timestamp: '2026-05-10T15:00:00.000Z', session_id: 's2' }));
|
|
100
|
-
storage.writeEvent(makeEvent({ timestamp: '2026-05-11T10:00:00.000Z', session_id: 's3' }));
|
|
101
|
-
const r = storage.aggregateDailyEventCounts({ since: '2026-05-01T00:00:00.000Z' });
|
|
102
|
-
expect(r).toEqual([
|
|
103
|
-
{ date: '2026-05-10', count: 2 },
|
|
104
|
-
{ date: '2026-05-11', count: 1 },
|
|
105
|
-
]);
|
|
106
|
-
});
|
|
107
|
-
it('until 过滤排除上界', () => {
|
|
108
|
-
storage.writeEvent(makeEvent({ timestamp: '2026-05-10T10:00:00.000Z' }));
|
|
109
|
-
storage.writeEvent(makeEvent({ timestamp: '2026-05-15T10:00:00.000Z', session_id: 's2' }));
|
|
110
|
-
const r = storage.aggregateDailyEventCounts({
|
|
111
|
-
since: '2026-05-01T00:00:00.000Z',
|
|
112
|
-
until: '2026-05-12T00:00:00.000Z',
|
|
113
|
-
});
|
|
114
|
-
expect(r).toEqual([{ date: '2026-05-10', count: 1 }]);
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// ── aggregateHookTypeBySession ─────────────────────────────────────
|
|
119
|
-
describe('aggregateHookTypeBySession', () => {
|
|
120
|
-
it('空表返回空', () => {
|
|
121
|
-
expect(storage.aggregateHookTypeBySession('nope')).toEqual([]);
|
|
122
|
-
});
|
|
123
|
-
it('只统计指定 session 的 hook_type', () => {
|
|
124
|
-
storage.writeEvent(makeEvent({ session_id: 'a', hook_type: 'PreToolUse' }));
|
|
125
|
-
storage.writeEvent(makeEvent({ session_id: 'a', hook_type: 'PostToolUse' }));
|
|
126
|
-
storage.writeEvent(makeEvent({ session_id: 'a', hook_type: 'PreToolUse' }));
|
|
127
|
-
storage.writeEvent(makeEvent({ session_id: 'b', hook_type: 'PreToolUse' }));
|
|
128
|
-
const r = storage.aggregateHookTypeBySession('a');
|
|
129
|
-
const map = Object.fromEntries(r.map(x => [x.hook_type, x.count]));
|
|
130
|
-
expect(map).toEqual({ PreToolUse: 2, PostToolUse: 1 });
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
// ── aggregateAgentTypeBySession ────────────────────────────────────
|
|
135
|
-
describe('aggregateAgentTypeBySession', () => {
|
|
136
|
-
it('json_extract subagent_type 并分组', () => {
|
|
137
|
-
storage.writeEvent(makeEvent({
|
|
138
|
-
session_id: 'a',
|
|
139
|
-
tool_name: 'Task',
|
|
140
|
-
tool_input: { subagent_type: 'researcher' },
|
|
141
|
-
}));
|
|
142
|
-
storage.writeEvent(makeEvent({
|
|
143
|
-
session_id: 'a',
|
|
144
|
-
tool_name: 'Task',
|
|
145
|
-
tool_input: { subagent_type: 'researcher' },
|
|
146
|
-
}));
|
|
147
|
-
storage.writeEvent(makeEvent({
|
|
148
|
-
session_id: 'a',
|
|
149
|
-
tool_name: 'Agent',
|
|
150
|
-
tool_input: { subagent_type: 'coder' },
|
|
151
|
-
}));
|
|
152
|
-
// 非 Agent/Task 不应被纳入
|
|
153
|
-
storage.writeEvent(makeEvent({
|
|
154
|
-
session_id: 'a',
|
|
155
|
-
tool_name: 'Bash',
|
|
156
|
-
tool_input: { foo: 'bar' },
|
|
157
|
-
}));
|
|
158
|
-
const r = storage.aggregateAgentTypeBySession('a');
|
|
159
|
-
const map = Object.fromEntries(r.map(x => [x.agent_type, x.count]));
|
|
160
|
-
expect(map.researcher).toBe(2);
|
|
161
|
-
expect(map.coder).toBe(1);
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// ── aggregateToolUsageBySession ────────────────────────────────────
|
|
166
|
-
describe('aggregateToolUsageBySession', () => {
|
|
167
|
-
it('按 session_id 统计 tool_name', () => {
|
|
168
|
-
storage.writeEvent(makeEvent({ session_id: 'a', tool_name: 'Bash' }));
|
|
169
|
-
storage.writeEvent(makeEvent({ session_id: 'a', tool_name: 'Bash' }));
|
|
170
|
-
storage.writeEvent(makeEvent({ session_id: 'a', tool_name: 'Read' }));
|
|
171
|
-
storage.writeEvent(makeEvent({ session_id: 'b', tool_name: 'Bash' }));
|
|
172
|
-
const r = storage.aggregateToolUsageBySession('a');
|
|
173
|
-
expect(r).toEqual([
|
|
174
|
-
{ tool_name: 'Bash', count: 2 },
|
|
175
|
-
{ tool_name: 'Read', count: 1 },
|
|
176
|
-
]);
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
// ── countActiveDays ────────────────────────────────────────────────
|
|
181
|
-
describe('countActiveDays', () => {
|
|
182
|
-
it('distinct date 计数', () => {
|
|
183
|
-
storage.writeEvent(makeEvent({ timestamp: '2026-05-10T10:00:00.000Z' }));
|
|
184
|
-
storage.writeEvent(makeEvent({ timestamp: '2026-05-10T11:00:00.000Z', session_id: 's2' }));
|
|
185
|
-
storage.writeEvent(makeEvent({ timestamp: '2026-05-11T10:00:00.000Z', session_id: 's3' }));
|
|
186
|
-
storage.writeEvent(makeEvent({ timestamp: '2026-05-12T10:00:00.000Z', session_id: 's4' }));
|
|
187
|
-
expect(storage.countActiveDays({ since: '2026-05-01T00:00:00.000Z' })).toBe(3);
|
|
188
|
-
});
|
|
189
|
-
it('空范围返回 0', () => {
|
|
190
|
-
expect(storage.countActiveDays({ since: '2026-05-01T00:00:00.000Z' })).toBe(0);
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
// ── aggregateOverviewByRange ───────────────────────────────────────
|
|
195
|
-
describe('aggregateOverviewByRange', () => {
|
|
196
|
-
it('返回 event/session/day 复合计数', () => {
|
|
197
|
-
storage.writeEvent(makeEvent({ session_id: 'a', timestamp: '2026-05-10T10:00:00.000Z' }));
|
|
198
|
-
storage.writeEvent(makeEvent({ session_id: 'a', timestamp: '2026-05-10T11:00:00.000Z' }));
|
|
199
|
-
storage.writeEvent(makeEvent({ session_id: 'b', timestamp: '2026-05-11T10:00:00.000Z' }));
|
|
200
|
-
const r = storage.aggregateOverviewByRange({
|
|
201
|
-
since: '2026-05-01T00:00:00.000Z',
|
|
202
|
-
until: '2026-06-01T00:00:00.000Z',
|
|
203
|
-
});
|
|
204
|
-
expect(r.event_count).toBe(3);
|
|
205
|
-
expect(r.session_count).toBe(2);
|
|
206
|
-
expect(r.day_count).toBe(2);
|
|
207
|
-
});
|
|
208
|
-
it('空表返回全 0', () => {
|
|
209
|
-
const r = storage.aggregateOverviewByRange({
|
|
210
|
-
since: '2026-05-01T00:00:00.000Z',
|
|
211
|
-
until: '2026-06-01T00:00:00.000Z',
|
|
212
|
-
});
|
|
213
|
-
expect(r).toEqual({ event_count: 0, session_count: 0, day_count: 0 });
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
// ── queryDistinctProjects ──────────────────────────────────────────
|
|
218
|
-
describe('queryDistinctProjects', () => {
|
|
219
|
-
it('返回 distinct project_path', () => {
|
|
220
|
-
storage.writeEvent(makeEvent({ project_path: '/a' }));
|
|
221
|
-
storage.writeEvent(makeEvent({ project_path: '/a', session_id: 's2' }));
|
|
222
|
-
storage.writeEvent(makeEvent({ project_path: '/b', session_id: 's3' }));
|
|
223
|
-
const r = storage.queryDistinctProjects({
|
|
224
|
-
since: '2026-05-01T00:00:00.000Z',
|
|
225
|
-
until: '2026-06-01T00:00:00.000Z',
|
|
226
|
-
});
|
|
227
|
-
expect(r.sort()).toEqual(['/a', '/b']);
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
// ── aggregateToolFailureRate ───────────────────────────────────────
|
|
232
|
-
describe('aggregateToolFailureRate', () => {
|
|
233
|
-
it('LIKE %error% 三个分支正确识别失败', () => {
|
|
234
|
-
// PostToolUse with error
|
|
235
|
-
storage.writeEvent(makeEvent({
|
|
236
|
-
hook_type: 'PostToolUse',
|
|
237
|
-
tool_name: 'Bash',
|
|
238
|
-
tool_output: { error: 'oops' }, // becomes '"error":"oops"' in JSON
|
|
239
|
-
}));
|
|
240
|
-
storage.writeEvent(makeEvent({
|
|
241
|
-
hook_type: 'PostToolUse',
|
|
242
|
-
tool_name: 'Bash',
|
|
243
|
-
tool_output: { is_error: true },
|
|
244
|
-
session_id: 's2',
|
|
245
|
-
}));
|
|
246
|
-
storage.writeEvent(makeEvent({
|
|
247
|
-
hook_type: 'PostToolUse',
|
|
248
|
-
tool_name: 'Bash',
|
|
249
|
-
tool_output: { isError: true },
|
|
250
|
-
session_id: 's3',
|
|
251
|
-
}));
|
|
252
|
-
// PostToolUse OK
|
|
253
|
-
storage.writeEvent(makeEvent({
|
|
254
|
-
hook_type: 'PostToolUse',
|
|
255
|
-
tool_name: 'Bash',
|
|
256
|
-
tool_output: { result: 'ok' },
|
|
257
|
-
session_id: 's4',
|
|
258
|
-
}));
|
|
259
|
-
// PreToolUse should not count
|
|
260
|
-
storage.writeEvent(makeEvent({
|
|
261
|
-
hook_type: 'PreToolUse',
|
|
262
|
-
tool_name: 'Bash',
|
|
263
|
-
tool_output: { error: 'should not count' },
|
|
264
|
-
session_id: 's5',
|
|
265
|
-
}));
|
|
266
|
-
|
|
267
|
-
const r = storage.aggregateToolFailureRate({
|
|
268
|
-
since: '2026-05-01T00:00:00.000Z',
|
|
269
|
-
until: '2026-06-01T00:00:00.000Z',
|
|
270
|
-
});
|
|
271
|
-
expect(r.post_total).toBe(4);
|
|
272
|
-
expect(r.failed).toBe(3);
|
|
273
|
-
});
|
|
274
|
-
it('空范围返回 0/0', () => {
|
|
275
|
-
const r = storage.aggregateToolFailureRate({
|
|
276
|
-
since: '2026-05-01T00:00:00.000Z',
|
|
277
|
-
until: '2026-06-01T00:00:00.000Z',
|
|
278
|
-
});
|
|
279
|
-
expect(r).toEqual({ post_total: 0, failed: 0 });
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
// ── queryFileEditInputs ────────────────────────────────────────────
|
|
284
|
-
describe('queryFileEditInputs', () => {
|
|
285
|
-
it('IN 展开多个 tool_name', () => {
|
|
286
|
-
storage.writeEvent(makeEvent({
|
|
287
|
-
hook_type: 'PreToolUse',
|
|
288
|
-
tool_name: 'Edit',
|
|
289
|
-
tool_input: { file_path: '/a.ts' },
|
|
290
|
-
}));
|
|
291
|
-
storage.writeEvent(makeEvent({
|
|
292
|
-
hook_type: 'PreToolUse',
|
|
293
|
-
tool_name: 'Write',
|
|
294
|
-
tool_input: { file_path: '/b.ts' },
|
|
295
|
-
session_id: 's2',
|
|
296
|
-
}));
|
|
297
|
-
storage.writeEvent(makeEvent({
|
|
298
|
-
hook_type: 'PreToolUse',
|
|
299
|
-
tool_name: 'Bash', // not file edit
|
|
300
|
-
tool_input: { command: 'ls' },
|
|
301
|
-
session_id: 's3',
|
|
302
|
-
}));
|
|
303
|
-
const r = storage.queryFileEditInputs({
|
|
304
|
-
since: '2026-05-01T00:00:00.000Z',
|
|
305
|
-
until: '2026-06-01T00:00:00.000Z',
|
|
306
|
-
tool_names: ['Edit', 'Write', 'MultiEdit'],
|
|
307
|
-
});
|
|
308
|
-
expect(r.length).toBe(2);
|
|
309
|
-
expect(r.map(x => JSON.parse(x.tool_input).file_path).sort()).toEqual(['/a.ts', '/b.ts']);
|
|
310
|
-
});
|
|
311
|
-
it('空 tool_names 返回空', () => {
|
|
312
|
-
storage.writeEvent(makeEvent({ tool_name: 'Edit', tool_input: { file_path: '/a.ts' } }));
|
|
313
|
-
expect(storage.queryFileEditInputs({
|
|
314
|
-
since: '2026-05-01T00:00:00.000Z',
|
|
315
|
-
until: '2026-06-01T00:00:00.000Z',
|
|
316
|
-
tool_names: [],
|
|
317
|
-
})).toEqual([]);
|
|
318
|
-
});
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
// ── queryEventsByTimeRange ─────────────────────────────────────────
|
|
322
|
-
describe('queryEventsByTimeRange', () => {
|
|
323
|
-
it('返回 [since, until) 区间事件,升序', () => {
|
|
324
|
-
storage.writeEvent(makeEvent({ timestamp: '2026-05-10T10:00:00.000Z', tool_name: 'A' }));
|
|
325
|
-
storage.writeEvent(makeEvent({ timestamp: '2026-05-11T10:00:00.000Z', tool_name: 'B', session_id: 's2' }));
|
|
326
|
-
storage.writeEvent(makeEvent({ timestamp: '2026-05-12T10:00:00.000Z', tool_name: 'C', session_id: 's3' }));
|
|
327
|
-
const r = storage.queryEventsByTimeRange({
|
|
328
|
-
since: '2026-05-10T00:00:00.000Z',
|
|
329
|
-
until: '2026-05-12T00:00:00.000Z',
|
|
330
|
-
});
|
|
331
|
-
expect(r.length).toBe(2);
|
|
332
|
-
expect(r[0].tool_name).toBe('A');
|
|
333
|
-
expect(r[1].tool_name).toBe('B');
|
|
334
|
-
});
|
|
335
|
-
it('未指定 until 返回 since 起始的全部', () => {
|
|
336
|
-
storage.writeEvent(makeEvent({ timestamp: '2026-05-10T10:00:00.000Z' }));
|
|
337
|
-
storage.writeEvent(makeEvent({ timestamp: '2026-05-20T10:00:00.000Z', session_id: 's2' }));
|
|
338
|
-
const r = storage.queryEventsByTimeRange({ since: '2026-05-15T00:00:00.000Z' });
|
|
339
|
-
expect(r.length).toBe(1);
|
|
340
|
-
});
|
|
341
|
-
});
|
|
342
|
-
});
|
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* H4: migration idempotent
|
|
3
|
-
*
|
|
4
|
-
* 覆盖 schema.sql 与 base.ts::runMigrations 的去重边界:
|
|
5
|
-
* 1. baseline — 老库缺索引/列,SQLiteStorage 启动后必须补齐
|
|
6
|
-
* 2. 新库无害 — 走完整 schema.sql 后,再 new SQLiteStorage 不抛错,不重复创建
|
|
7
|
-
* 3. idempotent — 连续 new 多次,索引/列数量不变
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
11
|
-
import Database from 'better-sqlite3';
|
|
12
|
-
import { mkdtempSync, rmSync, readFileSync, existsSync } from 'node:fs';
|
|
13
|
-
import { tmpdir } from 'node:os';
|
|
14
|
-
import { dirname, join } from 'node:path';
|
|
15
|
-
import { fileURLToPath } from 'node:url';
|
|
16
|
-
import { SQLiteStorage } from '../../../src/core/storage/sqlite.js';
|
|
17
|
-
import { logger } from '../../../src/core/utils/logger.js';
|
|
18
|
-
|
|
19
|
-
// 与 base.ts::initSchema 解析方式一致
|
|
20
|
-
const THIS_DIR = dirname(fileURLToPath(import.meta.url));
|
|
21
|
-
const SCHEMA_PATH = join(THIS_DIR, '../../../src/core/storage/schema.sql');
|
|
22
|
-
|
|
23
|
-
function listIndexes(db: Database.Database): Set<string> {
|
|
24
|
-
const rows = db
|
|
25
|
-
.prepare(`SELECT name FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%'`)
|
|
26
|
-
.all() as Array<{ name: string }>;
|
|
27
|
-
return new Set(rows.map(r => r.name));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function listColumns(db: Database.Database, table: string): Set<string> {
|
|
31
|
-
const rows = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
|
|
32
|
-
return new Set(rows.map(r => r.name));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* 写入一份"老库"——故意删掉若干索引,模拟存量库。
|
|
37
|
-
* 列定义保持与最新 schema.sql 等价(不缺列),因为 schema.sql 的
|
|
38
|
-
* CREATE TABLE IF NOT EXISTS 在表已存在时不会补列;同时 schema.sql 中
|
|
39
|
-
* 多个索引(如 idx_skill_invocations_workflow)依赖 workflow 列存在。
|
|
40
|
-
* 故"老库缺列"测试见单独 case,与"老库缺索引"分开覆盖,避免初始化期失败。
|
|
41
|
-
*/
|
|
42
|
-
function execLegacySchemaMissingIndexes(db: Database.Database): void {
|
|
43
|
-
db.exec(`
|
|
44
|
-
CREATE TABLE events (
|
|
45
|
-
event_id TEXT PRIMARY KEY,
|
|
46
|
-
session_id TEXT NOT NULL,
|
|
47
|
-
project_path TEXT NOT NULL,
|
|
48
|
-
timestamp TEXT NOT NULL,
|
|
49
|
-
hook_type TEXT NOT NULL,
|
|
50
|
-
tool_name TEXT,
|
|
51
|
-
tool_input TEXT,
|
|
52
|
-
tool_output TEXT,
|
|
53
|
-
user_prompt TEXT,
|
|
54
|
-
ai_response TEXT,
|
|
55
|
-
distilled INTEGER DEFAULT 0,
|
|
56
|
-
created_at TEXT DEFAULT (datetime('now'))
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
CREATE TABLE sessions (
|
|
60
|
-
session_id TEXT PRIMARY KEY,
|
|
61
|
-
project_path TEXT NOT NULL,
|
|
62
|
-
status TEXT NOT NULL DEFAULT 'active',
|
|
63
|
-
first_prompt TEXT,
|
|
64
|
-
start_time TEXT NOT NULL,
|
|
65
|
-
end_time TEXT,
|
|
66
|
-
last_event_time TEXT,
|
|
67
|
-
event_count INTEGER DEFAULT 0,
|
|
68
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
69
|
-
updated_at TEXT DEFAULT (datetime('now'))
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
CREATE TABLE injections (
|
|
73
|
-
id TEXT PRIMARY KEY,
|
|
74
|
-
event_id TEXT,
|
|
75
|
-
session_id TEXT NOT NULL,
|
|
76
|
-
timestamp TEXT NOT NULL,
|
|
77
|
-
source_handler TEXT NOT NULL,
|
|
78
|
-
injection_type TEXT NOT NULL,
|
|
79
|
-
content TEXT NOT NULL,
|
|
80
|
-
created_at TEXT DEFAULT (datetime('now'))
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
CREATE TABLE tasks (
|
|
84
|
-
id TEXT PRIMARY KEY,
|
|
85
|
-
session_id TEXT NOT NULL,
|
|
86
|
-
title TEXT NOT NULL,
|
|
87
|
-
description TEXT,
|
|
88
|
-
start_time TEXT NOT NULL,
|
|
89
|
-
end_time TEXT,
|
|
90
|
-
status TEXT DEFAULT 'active',
|
|
91
|
-
event_count INTEGER DEFAULT 0,
|
|
92
|
-
created_at TEXT DEFAULT (datetime('now'))
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
CREATE TABLE task_events (
|
|
96
|
-
task_id TEXT NOT NULL,
|
|
97
|
-
event_id TEXT NOT NULL,
|
|
98
|
-
PRIMARY KEY (task_id, event_id)
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
CREATE TABLE routing_events (
|
|
102
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
103
|
-
session_id TEXT NOT NULL,
|
|
104
|
-
route_request_id TEXT,
|
|
105
|
-
project_path TEXT NOT NULL,
|
|
106
|
-
ts INTEGER NOT NULL,
|
|
107
|
-
prompt TEXT NOT NULL,
|
|
108
|
-
intent_json TEXT NOT NULL,
|
|
109
|
-
routed_to_type TEXT,
|
|
110
|
-
routed_to_name TEXT,
|
|
111
|
-
is_forced INTEGER DEFAULT 0,
|
|
112
|
-
obeyed INTEGER,
|
|
113
|
-
classification_ms INTEGER,
|
|
114
|
-
fallback_used INTEGER DEFAULT 0,
|
|
115
|
-
refusal_reason TEXT,
|
|
116
|
-
first_tool_name TEXT,
|
|
117
|
-
first_tool_ts INTEGER,
|
|
118
|
-
completed_ts INTEGER,
|
|
119
|
-
total_execution_ms INTEGER,
|
|
120
|
-
completion_reason TEXT,
|
|
121
|
-
downstream_task_chain TEXT,
|
|
122
|
-
injection_version TEXT,
|
|
123
|
-
experiment_id TEXT,
|
|
124
|
-
experiment_group TEXT,
|
|
125
|
-
skill_confidence REAL,
|
|
126
|
-
skill_source TEXT,
|
|
127
|
-
created_at TEXT DEFAULT (datetime('now'))
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
CREATE TABLE token_usage (
|
|
131
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
132
|
-
session_id TEXT NOT NULL,
|
|
133
|
-
timestamp INTEGER NOT NULL,
|
|
134
|
-
input_tokens INTEGER NOT NULL,
|
|
135
|
-
output_tokens INTEGER NOT NULL,
|
|
136
|
-
total_tokens INTEGER NOT NULL,
|
|
137
|
-
model TEXT,
|
|
138
|
-
tool_name TEXT,
|
|
139
|
-
created_at TEXT DEFAULT (datetime('now'))
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
CREATE TABLE skill_invocations (
|
|
143
|
-
id TEXT PRIMARY KEY,
|
|
144
|
-
route_request_id TEXT,
|
|
145
|
-
session_id TEXT NOT NULL,
|
|
146
|
-
agent_id TEXT,
|
|
147
|
-
skill_id TEXT NOT NULL,
|
|
148
|
-
invocation_type TEXT NOT NULL,
|
|
149
|
-
reason TEXT,
|
|
150
|
-
workflow TEXT,
|
|
151
|
-
phase TEXT,
|
|
152
|
-
feature_slug TEXT,
|
|
153
|
-
artifact_path TEXT,
|
|
154
|
-
depth INTEGER DEFAULT 0,
|
|
155
|
-
success INTEGER DEFAULT 1,
|
|
156
|
-
error TEXT,
|
|
157
|
-
timestamp INTEGER NOT NULL,
|
|
158
|
-
created_at TEXT DEFAULT (datetime('now'))
|
|
159
|
-
);
|
|
160
|
-
-- 故意不创建以下任何索引(migration 必须补齐)
|
|
161
|
-
`);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* 老库——只缺 sessions.first_prompt 列(其他列齐全 + 完整索引)。
|
|
166
|
-
* 单独 case,验证 addColumnIfMissing 补齐。
|
|
167
|
-
*/
|
|
168
|
-
function execLegacySchemaMissingFirstPrompt(db: Database.Database): void {
|
|
169
|
-
db.exec(`
|
|
170
|
-
CREATE TABLE sessions (
|
|
171
|
-
session_id TEXT PRIMARY KEY,
|
|
172
|
-
project_path TEXT NOT NULL,
|
|
173
|
-
status TEXT NOT NULL DEFAULT 'active',
|
|
174
|
-
start_time TEXT NOT NULL,
|
|
175
|
-
end_time TEXT,
|
|
176
|
-
last_event_time TEXT,
|
|
177
|
-
event_count INTEGER DEFAULT 0,
|
|
178
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
179
|
-
updated_at TEXT DEFAULT (datetime('now'))
|
|
180
|
-
);
|
|
181
|
-
`);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
describe('H4: storage migration idempotent', () => {
|
|
185
|
-
let tmp: string;
|
|
186
|
-
let dbPath: string;
|
|
187
|
-
|
|
188
|
-
beforeEach(() => {
|
|
189
|
-
tmp = mkdtempSync(join(tmpdir(), 'forge-h4-migration-'));
|
|
190
|
-
dbPath = join(tmp, 'data.db');
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
afterEach(() => {
|
|
194
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
195
|
-
vi.restoreAllMocks();
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('baseline (缺索引): 老库索引齐缺,SQLiteStorage 启动后补齐全部 migration 索引', () => {
|
|
199
|
-
// 1) 手工写入只缺索引的旧 schema
|
|
200
|
-
const raw = new Database(dbPath);
|
|
201
|
-
execLegacySchemaMissingIndexes(raw);
|
|
202
|
-
const beforeIdx = listIndexes(raw);
|
|
203
|
-
expect(beforeIdx.has('idx_routing_events_type_ts')).toBe(false);
|
|
204
|
-
expect(beforeIdx.has('idx_skill_invocations_workflow')).toBe(false);
|
|
205
|
-
expect(beforeIdx.has('idx_skill_invocations_feature')).toBe(false);
|
|
206
|
-
expect(beforeIdx.has('idx_sessions_start_time')).toBe(false);
|
|
207
|
-
expect(beforeIdx.has('idx_events_session_ts')).toBe(false);
|
|
208
|
-
raw.close();
|
|
209
|
-
|
|
210
|
-
// 2) 用 SQLiteStorage 打开 → 触发 schema.sql + runMigrations
|
|
211
|
-
const storage = new SQLiteStorage(dbPath);
|
|
212
|
-
const db = storage.getDatabase();
|
|
213
|
-
|
|
214
|
-
// 3) 关键索引全部补齐(涵盖 spec 重复清单中所有 migration-only / 双写索引)
|
|
215
|
-
const idx = listIndexes(db);
|
|
216
|
-
const expected = [
|
|
217
|
-
'idx_sessions_start_time',
|
|
218
|
-
'idx_events_session_ts',
|
|
219
|
-
'idx_routing_events_session_ts',
|
|
220
|
-
'idx_skill_invocations_session_ts',
|
|
221
|
-
'idx_routing_events_obeyed_ts',
|
|
222
|
-
'idx_events_session_hook',
|
|
223
|
-
'idx_injections_session_handler',
|
|
224
|
-
'idx_routing_events_type_ts',
|
|
225
|
-
'idx_skill_invocations_workflow',
|
|
226
|
-
'idx_skill_invocations_feature',
|
|
227
|
-
];
|
|
228
|
-
for (const name of expected) {
|
|
229
|
-
expect(idx.has(name), `expected index ${name} to exist after migration`).toBe(true);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
storage.close();
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it('baseline (缺列): 老库缺 sessions.first_prompt,migration ALTER 补齐', () => {
|
|
236
|
-
const raw = new Database(dbPath);
|
|
237
|
-
execLegacySchemaMissingFirstPrompt(raw);
|
|
238
|
-
expect(listColumns(raw, 'sessions').has('first_prompt')).toBe(false);
|
|
239
|
-
raw.close();
|
|
240
|
-
|
|
241
|
-
const storage = new SQLiteStorage(dbPath);
|
|
242
|
-
const db = storage.getDatabase();
|
|
243
|
-
|
|
244
|
-
// first_prompt 列由 addColumnIfMissing 补齐
|
|
245
|
-
expect(listColumns(db, 'sessions').has('first_prompt')).toBe(true);
|
|
246
|
-
|
|
247
|
-
storage.close();
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it('新库(完整 schema.sql 初始化)再启动 SQLiteStorage:migration 不会重复创建索引', () => {
|
|
251
|
-
// 1) 用完整 schema.sql 初始化(模拟 base.initSchema 已跑完,sqlite-base 同样这么干)
|
|
252
|
-
expect(existsSync(SCHEMA_PATH)).toBe(true);
|
|
253
|
-
const schemaSql = readFileSync(SCHEMA_PATH, 'utf-8');
|
|
254
|
-
const raw = new Database(dbPath);
|
|
255
|
-
raw.exec(schemaSql);
|
|
256
|
-
const indexesAfterSchema = listIndexes(raw);
|
|
257
|
-
raw.close();
|
|
258
|
-
|
|
259
|
-
// 2) 监听 logger.debug,统计"migration created idx_xxx"次数
|
|
260
|
-
const debugSpy = vi.spyOn(logger, 'debug').mockImplementation(() => {});
|
|
261
|
-
const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {});
|
|
262
|
-
const errorSpy = vi.spyOn(logger, 'error').mockImplementation(() => {});
|
|
263
|
-
|
|
264
|
-
// 3) new SQLiteStorage → runMigrations 应当跳过所有 createIndexIfMissing
|
|
265
|
-
const storage = new SQLiteStorage(dbPath);
|
|
266
|
-
|
|
267
|
-
const createdCalls = debugSpy.mock.calls.filter(
|
|
268
|
-
args => typeof args[0] === 'string' && args[0].includes('migration created'),
|
|
269
|
-
);
|
|
270
|
-
expect(createdCalls.length).toBe(0);
|
|
271
|
-
|
|
272
|
-
// 不应有 warn/error
|
|
273
|
-
expect(warnSpy).not.toHaveBeenCalled();
|
|
274
|
-
expect(errorSpy).not.toHaveBeenCalled();
|
|
275
|
-
|
|
276
|
-
// 索引集合相比 schema.sql 初始化后无变化
|
|
277
|
-
const indexesAfterMigration = listIndexes(storage.getDatabase());
|
|
278
|
-
expect(indexesAfterMigration).toEqual(indexesAfterSchema);
|
|
279
|
-
|
|
280
|
-
storage.close();
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it('idempotent: 连续多次 new SQLiteStorage 不改变索引/列数量', () => {
|
|
284
|
-
const snapshots: Array<{ idx: Set<string>; cols: Set<string> }> = [];
|
|
285
|
-
|
|
286
|
-
for (let i = 0; i < 3; i++) {
|
|
287
|
-
const storage = new SQLiteStorage(dbPath);
|
|
288
|
-
const db = storage.getDatabase();
|
|
289
|
-
snapshots.push({
|
|
290
|
-
idx: listIndexes(db),
|
|
291
|
-
cols: listColumns(db, 'skill_invocations'),
|
|
292
|
-
});
|
|
293
|
-
storage.close();
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// 索引集合在三次之间完全一致
|
|
297
|
-
expect(snapshots[0].idx).toEqual(snapshots[1].idx);
|
|
298
|
-
expect(snapshots[1].idx).toEqual(snapshots[2].idx);
|
|
299
|
-
|
|
300
|
-
// skill_invocations 列集合在三次之间一致
|
|
301
|
-
expect(snapshots[0].cols).toEqual(snapshots[1].cols);
|
|
302
|
-
expect(snapshots[1].cols).toEqual(snapshots[2].cols);
|
|
303
|
-
});
|
|
304
|
-
});
|