@winspan/claude-forge 8.51.1 → 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 +121 -2
- 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/constants.d.ts +2 -0
- package/dist/core/constants.d.ts.map +1 -1
- package/dist/core/constants.js +4 -0
- package/dist/core/constants.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 +30 -5
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/skill-sync.d.ts +21 -0
- package/dist/daemon/skill-sync.d.ts.map +1 -0
- package/dist/daemon/skill-sync.js +75 -0
- package/dist/daemon/skill-sync.js.map +1 -0
- package/dist/hooks/notification.sh +1 -1
- package/dist/hooks/post-tool-use.sh +1 -1
- package/dist/hooks/pre-tool-use.sh +1 -1
- package/dist/hooks/stop.sh +1 -1
- package/dist/hooks/user-prompt-submit.sh +1 -1
- package/dist/skills/official/code-simplifier.md +37 -1
- package/dist/skills/official/find-skills.md +120 -1
- package/dist/skills/official/official-api-design.md +14 -1
- package/dist/skills/official/official-architecture-decision.md +22 -1
- package/dist/skills/official/official-db-schema-design.md +19 -1
- package/dist/skills/official/official-debug.md +9 -1
- package/dist/skills/official/official-pr-review.md +1 -1
- package/dist/skills/official/official-security-hardening.md +7 -1
- package/dist/skills/official/planning-with-files.md +206 -2
- package/dist/skills/official/ui-ux-pro-max.md +88 -1
- package/dist/skills/official/webapp-testing.md +85 -1
- package/dist/skills/registry.d.ts +1 -1
- package/dist/skills/registry.d.ts.map +1 -1
- package/dist/skills/registry.js +15 -4
- package/dist/skills/registry.js.map +1 -1
- package/dist/skills/semantic-matcher.d.ts +4 -3
- package/dist/skills/semantic-matcher.d.ts.map +1 -1
- package/dist/skills/semantic-matcher.js +20 -22
- package/dist/skills/semantic-matcher.js.map +1 -1
- package/dist/skills/upgrade-engine.d.ts +93 -0
- package/dist/skills/upgrade-engine.d.ts.map +1 -0
- package/dist/skills/upgrade-engine.js +447 -0
- package/dist/skills/upgrade-engine.js.map +1 -0
- package/dist/skills/upgrade-prompt.d.ts +20 -0
- package/dist/skills/upgrade-prompt.d.ts.map +1 -0
- package/dist/skills/upgrade-prompt.js +75 -0
- package/dist/skills/upgrade-prompt.js.map +1 -0
- 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/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/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/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 -185
- 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 -62
- 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 -302
- 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/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 -16
- package/src/skills/official/find-skills.md +0 -23
- package/src/skills/official/official-api-design.md +0 -17
- package/src/skills/official/official-architecture-decision.md +0 -20
- package/src/skills/official/official-bmad.md +0 -118
- package/src/skills/official/official-db-schema-design.md +0 -16
- package/src/skills/official/official-debug.md +0 -17
- 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 -26
- package/src/skills/official/official-spec-driven-design.md +0 -31
- package/src/skills/official/planning-with-files.md +0 -37
- package/src/skills/official/ui-ux-pro-max.md +0 -18
- package/src/skills/official/webapp-testing.md +0 -12
- package/src/skills/official-skills.ts +0 -89
- package/src/skills/registry.ts +0 -355
- package/src/skills/semantic-matcher.ts +0 -231
- 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/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/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/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,211 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import { InvocationGuard } from '../../../src/skills/invocation-guard.js';
|
|
3
|
-
|
|
4
|
-
describe('InvocationGuard - TTL Mechanism', () => {
|
|
5
|
-
let guard: InvocationGuard;
|
|
6
|
-
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
guard = new InvocationGuard();
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
afterEach(() => {
|
|
12
|
-
guard.stopCleanupTimer();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
describe('TTL expiration', () => {
|
|
16
|
-
it('should allow invocation after session expires', () => {
|
|
17
|
-
const sessionId = 'test-session-1';
|
|
18
|
-
const skillId = 'test-skill';
|
|
19
|
-
|
|
20
|
-
// Record initial invocation
|
|
21
|
-
guard.record(sessionId, skillId);
|
|
22
|
-
|
|
23
|
-
// Verify skill is blocked (idempotent guard)
|
|
24
|
-
let result = guard.check(sessionId, skillId);
|
|
25
|
-
expect(result.allowed).toBe(false);
|
|
26
|
-
expect(result.reason).toContain('already invoked');
|
|
27
|
-
|
|
28
|
-
// Fast-forward time by 31 minutes (beyond TTL)
|
|
29
|
-
const stats = guard.getStats(sessionId);
|
|
30
|
-
if (stats) {
|
|
31
|
-
stats.lastAccessTime = Date.now() - 31 * 60 * 1000;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Should allow invocation after expiration
|
|
35
|
-
result = guard.check(sessionId, skillId);
|
|
36
|
-
expect(result.allowed).toBe(true);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('should update lastAccessTime on record', () => {
|
|
40
|
-
const sessionId = 'test-session-2';
|
|
41
|
-
const skillId = 'test-skill';
|
|
42
|
-
|
|
43
|
-
const before = Date.now();
|
|
44
|
-
guard.record(sessionId, skillId);
|
|
45
|
-
const after = Date.now();
|
|
46
|
-
|
|
47
|
-
const stats = guard.getStats(sessionId);
|
|
48
|
-
expect(stats).not.toBeNull();
|
|
49
|
-
expect(stats!.lastAccessTime).toBeGreaterThanOrEqual(before);
|
|
50
|
-
expect(stats!.lastAccessTime).toBeLessThanOrEqual(after);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('should update lastAccessTime on complete', () => {
|
|
54
|
-
const sessionId = 'test-session-3';
|
|
55
|
-
const skillId = 'test-skill';
|
|
56
|
-
|
|
57
|
-
guard.record(sessionId, skillId);
|
|
58
|
-
const initialTime = guard.getStats(sessionId)!.lastAccessTime;
|
|
59
|
-
|
|
60
|
-
// Wait a bit
|
|
61
|
-
vi.useFakeTimers();
|
|
62
|
-
vi.advanceTimersByTime(1000);
|
|
63
|
-
|
|
64
|
-
guard.complete(sessionId);
|
|
65
|
-
const updatedTime = guard.getStats(sessionId)!.lastAccessTime;
|
|
66
|
-
|
|
67
|
-
expect(updatedTime).toBeGreaterThan(initialTime);
|
|
68
|
-
|
|
69
|
-
vi.useRealTimers();
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe('automatic cleanup', () => {
|
|
74
|
-
it('should clean up expired sessions', () => {
|
|
75
|
-
const sessionId1 = 'expired-session-1';
|
|
76
|
-
const sessionId2 = 'active-session-2';
|
|
77
|
-
|
|
78
|
-
// Record two sessions
|
|
79
|
-
guard.record(sessionId1, 'skill-1');
|
|
80
|
-
guard.record(sessionId2, 'skill-2');
|
|
81
|
-
|
|
82
|
-
expect(guard.getSessionCount()).toBe(2);
|
|
83
|
-
|
|
84
|
-
// Expire first session
|
|
85
|
-
const stats1 = guard.getStats(sessionId1);
|
|
86
|
-
if (stats1) {
|
|
87
|
-
stats1.lastAccessTime = Date.now() - 31 * 60 * 1000;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Trigger cleanup manually
|
|
91
|
-
(guard as any).cleanupExpiredSessions();
|
|
92
|
-
|
|
93
|
-
// Only active session should remain
|
|
94
|
-
expect(guard.getSessionCount()).toBe(1);
|
|
95
|
-
expect(guard.getStats(sessionId1)).toBeNull();
|
|
96
|
-
expect(guard.getStats(sessionId2)).not.toBeNull();
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('should not clean up active sessions', () => {
|
|
100
|
-
const sessionId = 'active-session';
|
|
101
|
-
|
|
102
|
-
guard.record(sessionId, 'skill-1');
|
|
103
|
-
expect(guard.getSessionCount()).toBe(1);
|
|
104
|
-
|
|
105
|
-
// Trigger cleanup
|
|
106
|
-
(guard as any).cleanupExpiredSessions();
|
|
107
|
-
|
|
108
|
-
// Session should still exist
|
|
109
|
-
expect(guard.getSessionCount()).toBe(1);
|
|
110
|
-
expect(guard.getStats(sessionId)).not.toBeNull();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('should handle empty sessions map', () => {
|
|
114
|
-
expect(guard.getSessionCount()).toBe(0);
|
|
115
|
-
|
|
116
|
-
// Should not throw
|
|
117
|
-
expect(() => {
|
|
118
|
-
(guard as any).cleanupExpiredSessions();
|
|
119
|
-
}).not.toThrow();
|
|
120
|
-
|
|
121
|
-
expect(guard.getSessionCount()).toBe(0);
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
describe('cleanup timer', () => {
|
|
126
|
-
it('should start cleanup timer on construction', () => {
|
|
127
|
-
const newGuard = new InvocationGuard();
|
|
128
|
-
expect((newGuard as any).cleanupTimer).not.toBeNull();
|
|
129
|
-
newGuard.stopCleanupTimer();
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('should stop cleanup timer', () => {
|
|
133
|
-
guard.stopCleanupTimer();
|
|
134
|
-
expect((guard as any).cleanupTimer).toBeNull();
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('should not start multiple timers', () => {
|
|
138
|
-
const timer1 = (guard as any).cleanupTimer;
|
|
139
|
-
(guard as any).startCleanupTimer();
|
|
140
|
-
const timer2 = (guard as any).cleanupTimer;
|
|
141
|
-
|
|
142
|
-
expect(timer1).toBe(timer2);
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
describe('getSessionCount', () => {
|
|
147
|
-
it('should return 0 for empty guard', () => {
|
|
148
|
-
expect(guard.getSessionCount()).toBe(0);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('should return correct count after recording', () => {
|
|
152
|
-
guard.record('session-1', 'skill-1');
|
|
153
|
-
expect(guard.getSessionCount()).toBe(1);
|
|
154
|
-
|
|
155
|
-
guard.record('session-2', 'skill-2');
|
|
156
|
-
expect(guard.getSessionCount()).toBe(2);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('should decrease count after clear', () => {
|
|
160
|
-
guard.record('session-1', 'skill-1');
|
|
161
|
-
guard.record('session-2', 'skill-2');
|
|
162
|
-
expect(guard.getSessionCount()).toBe(2);
|
|
163
|
-
|
|
164
|
-
guard.clear('session-1');
|
|
165
|
-
expect(guard.getSessionCount()).toBe(1);
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
describe('integration with existing functionality', () => {
|
|
170
|
-
it('should maintain depth tracking with TTL', () => {
|
|
171
|
-
const sessionId = 'test-session';
|
|
172
|
-
|
|
173
|
-
guard.record(sessionId, 'skill-1');
|
|
174
|
-
expect(guard.getStats(sessionId)!.depth).toBe(1);
|
|
175
|
-
|
|
176
|
-
guard.record(sessionId, 'skill-2');
|
|
177
|
-
expect(guard.getStats(sessionId)!.depth).toBe(2);
|
|
178
|
-
|
|
179
|
-
guard.complete(sessionId);
|
|
180
|
-
expect(guard.getStats(sessionId)!.depth).toBe(1);
|
|
181
|
-
|
|
182
|
-
// lastAccessTime should be updated
|
|
183
|
-
expect(guard.getStats(sessionId)!.lastAccessTime).toBeLessThanOrEqual(Date.now());
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('should maintain total count with TTL', () => {
|
|
187
|
-
const sessionId = 'test-session';
|
|
188
|
-
|
|
189
|
-
guard.record(sessionId, 'skill-1');
|
|
190
|
-
expect(guard.getStats(sessionId)!.total).toBe(1);
|
|
191
|
-
|
|
192
|
-
guard.record(sessionId, 'skill-2');
|
|
193
|
-
expect(guard.getStats(sessionId)!.total).toBe(2);
|
|
194
|
-
|
|
195
|
-
// lastAccessTime should be updated
|
|
196
|
-
expect(guard.getStats(sessionId)!.lastAccessTime).toBeLessThanOrEqual(Date.now());
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('should maintain calledSkills set with TTL', () => {
|
|
200
|
-
const sessionId = 'test-session';
|
|
201
|
-
|
|
202
|
-
guard.record(sessionId, 'skill-1');
|
|
203
|
-
guard.record(sessionId, 'skill-2');
|
|
204
|
-
|
|
205
|
-
const stats = guard.getStats(sessionId);
|
|
206
|
-
expect(stats!.calledSkills.has('skill-1')).toBe(true);
|
|
207
|
-
expect(stats!.calledSkills.has('skill-2')).toBe(true);
|
|
208
|
-
expect(stats!.lastAccessTime).toBeLessThanOrEqual(Date.now());
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
});
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
-
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { loadOfficialSkills } from '../../../src/skills/official-skills.js';
|
|
6
|
-
|
|
7
|
-
describe('loadOfficialSkills', () => {
|
|
8
|
-
let tempDir: string;
|
|
9
|
-
|
|
10
|
-
beforeAll(() => {
|
|
11
|
-
tempDir = mkdtempSync(join(tmpdir(), 'forge-official-skills-test-'));
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
afterAll(() => {
|
|
15
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('loads all .md files from the given directory', () => {
|
|
19
|
-
writeFileSync(
|
|
20
|
-
join(tempDir, 'test-skill-a.md'),
|
|
21
|
-
`---\nname: test-skill-a\nversion: 1.0.0\ndescription: "Test skill A"\ntags: [tag1, tag2]\n---\n\n# Test Skill A\n\nBody content here.`
|
|
22
|
-
);
|
|
23
|
-
writeFileSync(
|
|
24
|
-
join(tempDir, 'test-skill-b.md'),
|
|
25
|
-
`---\nname: test-skill-b\nversion: 2.0.0\ndescription: "Test skill B"\nkeywords: [kw1, kw2]\n---\n\n# Test Skill B\n\nMore content.`
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
const skills = loadOfficialSkills(tempDir);
|
|
29
|
-
const names = skills.map(s => s.name);
|
|
30
|
-
expect(names).toContain('test-skill-a');
|
|
31
|
-
expect(names).toContain('test-skill-b');
|
|
32
|
-
expect(skills.length).toBeGreaterThanOrEqual(2);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('parses frontmatter correctly (name, version, description, keywords from tags)', () => {
|
|
36
|
-
const skillDir = mkdtempSync(join(tmpdir(), 'forge-frontmatter-test-'));
|
|
37
|
-
try {
|
|
38
|
-
writeFileSync(
|
|
39
|
-
join(skillDir, 'my-skill.md'),
|
|
40
|
-
`---\nname: my-skill\nversion: 3.1.0\ndescription: "My test skill"\ntags: [alpha, beta, gamma]\n---\n\n# My Skill\n\nContent.`
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
const skills = loadOfficialSkills(skillDir);
|
|
44
|
-
expect(skills).toHaveLength(1);
|
|
45
|
-
const skill = skills[0];
|
|
46
|
-
expect(skill.name).toBe('my-skill');
|
|
47
|
-
expect(skill.version).toBe('3.1.0');
|
|
48
|
-
expect(skill.description).toBe('My test skill');
|
|
49
|
-
expect(skill.keywords).toEqual(['alpha', 'beta', 'gamma']);
|
|
50
|
-
} finally {
|
|
51
|
-
rmSync(skillDir, { recursive: true, force: true });
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('parses frontmatter correctly (keywords field takes precedence over tags)', () => {
|
|
56
|
-
const skillDir = mkdtempSync(join(tmpdir(), 'forge-kw-test-'));
|
|
57
|
-
try {
|
|
58
|
-
writeFileSync(
|
|
59
|
-
join(skillDir, 'kw-skill.md'),
|
|
60
|
-
`---\nname: kw-skill\nversion: 1.0.0\ndescription: "KW skill"\nkeywords: [kw-a, kw-b]\ntags: [tag-a]\n---\n\n# KW Skill`
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
const skills = loadOfficialSkills(skillDir);
|
|
64
|
-
expect(skills).toHaveLength(1);
|
|
65
|
-
// keywords takes precedence (data.keywords ?? data.tags => keywords wins)
|
|
66
|
-
expect(skills[0].keywords).toEqual(['kw-a', 'kw-b']);
|
|
67
|
-
} finally {
|
|
68
|
-
rmSync(skillDir, { recursive: true, force: true });
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('falls back to filename as name when frontmatter name is missing', () => {
|
|
73
|
-
const skillDir = mkdtempSync(join(tmpdir(), 'forge-fallback-test-'));
|
|
74
|
-
try {
|
|
75
|
-
writeFileSync(
|
|
76
|
-
join(skillDir, 'unnamed-skill.md'),
|
|
77
|
-
`---\nversion: 1.0.0\n---\n\n# Unnamed Skill`
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
const skills = loadOfficialSkills(skillDir);
|
|
81
|
-
expect(skills).toHaveLength(1);
|
|
82
|
-
expect(skills[0].name).toBe('unnamed-skill');
|
|
83
|
-
} finally {
|
|
84
|
-
rmSync(skillDir, { recursive: true, force: true });
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('ignores non-.md files in the directory', () => {
|
|
89
|
-
const skillDir = mkdtempSync(join(tmpdir(), 'forge-nonmd-test-'));
|
|
90
|
-
try {
|
|
91
|
-
writeFileSync(join(skillDir, 'skill.md'), `---\nname: skill\n---\n# Skill`);
|
|
92
|
-
writeFileSync(join(skillDir, 'README.txt'), 'not a skill');
|
|
93
|
-
writeFileSync(join(skillDir, 'index.json'), '{}');
|
|
94
|
-
|
|
95
|
-
const skills = loadOfficialSkills(skillDir);
|
|
96
|
-
expect(skills).toHaveLength(1);
|
|
97
|
-
expect(skills[0].name).toBe('skill');
|
|
98
|
-
} finally {
|
|
99
|
-
rmSync(skillDir, { recursive: true, force: true });
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('throws a meaningful error when directory does not exist', () => {
|
|
104
|
-
const nonExistentDir = join(tmpdir(), 'forge-does-not-exist-' + Date.now());
|
|
105
|
-
expect(() => loadOfficialSkills(nonExistentDir)).toThrow(/Official skills directory not found/);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('loads all official skills from the actual built-in directory', async () => {
|
|
109
|
-
// Verify the real built-in directory works end-to-end
|
|
110
|
-
const { fileURLToPath } = await import('node:url');
|
|
111
|
-
const { dirname, join: pathJoin } = await import('node:path');
|
|
112
|
-
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
113
|
-
const builtinDir = pathJoin(thisDir, '../../../src/skills/official');
|
|
114
|
-
|
|
115
|
-
const skills = loadOfficialSkills(builtinDir);
|
|
116
|
-
// Should have 17 official skills
|
|
117
|
-
expect(skills.length).toBeGreaterThanOrEqual(17);
|
|
118
|
-
// Spot-check a known skill
|
|
119
|
-
const harness = skills.find(s => s.name === 'official-harness-engineering');
|
|
120
|
-
expect(harness).toBeDefined();
|
|
121
|
-
expect(harness!.version).toBe('2.0.1');
|
|
122
|
-
expect(harness!.description).toContain('Harness Engineering');
|
|
123
|
-
// path should NOT be '<built-in>'
|
|
124
|
-
expect(harness!.name).toBe('official-harness-engineering');
|
|
125
|
-
});
|
|
126
|
-
});
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for SkillRegistry multi-format support
|
|
3
|
-
* Tests both flat .md and directory-based SKILL.md formats
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, expect } from 'vitest';
|
|
7
|
-
import matter from 'gray-matter';
|
|
8
|
-
|
|
9
|
-
describe('SkillRegistry - Multi-format Support', () => {
|
|
10
|
-
it('should parse flat .md format with description', () => {
|
|
11
|
-
const flatSkillContent = `---
|
|
12
|
-
name: flat-skill
|
|
13
|
-
description: A flat format skill for testing
|
|
14
|
-
keywords: [test, flat, format]
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
# Flat Skill
|
|
18
|
-
|
|
19
|
-
This is a flat format skill.`;
|
|
20
|
-
|
|
21
|
-
const parsed = matter(flatSkillContent);
|
|
22
|
-
expect(parsed.data.name).toBe('flat-skill');
|
|
23
|
-
expect(parsed.data.description).toBe('A flat format skill for testing');
|
|
24
|
-
expect(parsed.data.keywords).toEqual(['test', 'flat', 'format']);
|
|
25
|
-
expect(parsed.content.trim()).toContain('# Flat Skill');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('should parse directory format SKILL.md with description', () => {
|
|
29
|
-
const dirSkillContent = `---
|
|
30
|
-
name: dir-skill
|
|
31
|
-
description: A directory format skill from agent-skills
|
|
32
|
-
keywords: [agent, directory, skill]
|
|
33
|
-
---
|
|
34
|
-
|
|
35
|
-
# Directory Skill
|
|
36
|
-
|
|
37
|
-
This is a directory format skill (agent-skills format).`;
|
|
38
|
-
|
|
39
|
-
const parsed = matter(dirSkillContent);
|
|
40
|
-
expect(parsed.data.name).toBe('dir-skill');
|
|
41
|
-
expect(parsed.data.description).toBe('A directory format skill from agent-skills');
|
|
42
|
-
expect(parsed.data.keywords).toEqual(['agent', 'directory', 'skill']);
|
|
43
|
-
expect(parsed.content.trim()).toContain('# Directory Skill');
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('should handle skills without keywords', () => {
|
|
47
|
-
const noKeywordsContent = `---
|
|
48
|
-
name: no-keywords-skill
|
|
49
|
-
description: A skill without keywords for fallback testing
|
|
50
|
-
---
|
|
51
|
-
|
|
52
|
-
# No Keywords Skill
|
|
53
|
-
|
|
54
|
-
This skill has no keywords, should fallback to description matching.`;
|
|
55
|
-
|
|
56
|
-
const parsed = matter(noKeywordsContent);
|
|
57
|
-
expect(parsed.data.name).toBe('no-keywords-skill');
|
|
58
|
-
expect(parsed.data.description).toBe('A skill without keywords for fallback testing');
|
|
59
|
-
expect(parsed.data.keywords).toBeUndefined();
|
|
60
|
-
expect(parsed.content.trim()).toContain('# No Keywords Skill');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('should handle skills without description', () => {
|
|
64
|
-
const noDescContent = `---
|
|
65
|
-
name: no-desc-skill
|
|
66
|
-
keywords: [test, nodesc]
|
|
67
|
-
---
|
|
68
|
-
|
|
69
|
-
# No Description Skill
|
|
70
|
-
|
|
71
|
-
This skill has no description field.`;
|
|
72
|
-
|
|
73
|
-
const parsed = matter(noDescContent);
|
|
74
|
-
expect(parsed.data.name).toBe('no-desc-skill');
|
|
75
|
-
expect(parsed.data.description).toBeUndefined();
|
|
76
|
-
expect(parsed.data.keywords).toEqual(['test', 'nodesc']);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('should handle empty frontmatter gracefully', () => {
|
|
80
|
-
const emptyFrontmatterContent = `---
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
# Minimal Skill
|
|
84
|
-
|
|
85
|
-
Just content, no metadata.`;
|
|
86
|
-
|
|
87
|
-
const parsed = matter(emptyFrontmatterContent);
|
|
88
|
-
expect(parsed.data).toEqual({});
|
|
89
|
-
expect(parsed.content.trim()).toContain('# Minimal Skill');
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SocketServer behavior tests
|
|
3
|
-
*
|
|
4
|
-
* 重点验证 L3 性能修复:buffer 分片到达时,仅在遇到换行符后才解析,
|
|
5
|
-
* 避免大事件每个 chunk 都跑一次完整 JSON.parse 的 N² 行为。
|
|
6
|
-
*/
|
|
7
|
-
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
8
|
-
import net from 'node:net';
|
|
9
|
-
import os from 'node:os';
|
|
10
|
-
import path from 'node:path';
|
|
11
|
-
import fs from 'node:fs';
|
|
12
|
-
import { SocketServer } from '../../src/daemon/server.js';
|
|
13
|
-
import { EventParser } from '../../src/daemon/event-parser.js';
|
|
14
|
-
|
|
15
|
-
function makeSockPath(): string {
|
|
16
|
-
const name = `forge-test-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.sock`;
|
|
17
|
-
return path.join(os.tmpdir(), name);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function makeEvent(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
21
|
-
return {
|
|
22
|
-
hook_type: 'PostToolUse',
|
|
23
|
-
timestamp: new Date().toISOString(),
|
|
24
|
-
session_id: 'test-session',
|
|
25
|
-
project_path: '/tmp/test',
|
|
26
|
-
tool_name: 'Read',
|
|
27
|
-
...overrides,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function sendOnSocket(sockPath: string, chunks: string[], delayMs = 5): Promise<string> {
|
|
32
|
-
return new Promise((resolve, reject) => {
|
|
33
|
-
const client = net.createConnection(sockPath);
|
|
34
|
-
let response = '';
|
|
35
|
-
client.on('connect', async () => {
|
|
36
|
-
for (const chunk of chunks) {
|
|
37
|
-
client.write(chunk);
|
|
38
|
-
if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
client.on('data', (data) => {
|
|
42
|
-
response += data.toString();
|
|
43
|
-
});
|
|
44
|
-
client.on('end', () => resolve(response));
|
|
45
|
-
client.on('close', () => resolve(response));
|
|
46
|
-
client.on('error', reject);
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
describe('SocketServer — L3 chunked-parse behavior', () => {
|
|
51
|
-
const servers: SocketServer[] = [];
|
|
52
|
-
const sockPaths: string[] = [];
|
|
53
|
-
|
|
54
|
-
afterEach(async () => {
|
|
55
|
-
for (const s of servers) await s.close();
|
|
56
|
-
servers.length = 0;
|
|
57
|
-
for (const p of sockPaths) {
|
|
58
|
-
try { fs.unlinkSync(p); } catch { /* ignore */ }
|
|
59
|
-
}
|
|
60
|
-
sockPaths.length = 0;
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('parses a complete single-chunk message (with trailing newline)', async () => {
|
|
64
|
-
const sockPath = makeSockPath();
|
|
65
|
-
sockPaths.push(sockPath);
|
|
66
|
-
|
|
67
|
-
const handler = vi.fn().mockReturnValue({ allow: true });
|
|
68
|
-
const server = new SocketServer(sockPath, handler);
|
|
69
|
-
servers.push(server);
|
|
70
|
-
await new Promise((r) => setTimeout(r, 30));
|
|
71
|
-
|
|
72
|
-
const event = makeEvent();
|
|
73
|
-
const payload = JSON.stringify(event) + '\n';
|
|
74
|
-
await sendOnSocket(sockPath, [payload]);
|
|
75
|
-
|
|
76
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
77
|
-
const passed = handler.mock.calls[0][0];
|
|
78
|
-
expect(passed.hook_type).toBe('PostToolUse');
|
|
79
|
-
expect(passed.session_id).toBe('test-session');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('parses a large message split across many chunks WITHOUT per-chunk JSON.parse', async () => {
|
|
83
|
-
const sockPath = makeSockPath();
|
|
84
|
-
sockPaths.push(sockPath);
|
|
85
|
-
|
|
86
|
-
// 监视 JSON.parse —— 用于断言不再 N²(不能为每个 chunk 都 parse 一次)
|
|
87
|
-
const parseSpy = vi.spyOn(JSON, 'parse');
|
|
88
|
-
|
|
89
|
-
const handler = vi.fn().mockReturnValue({ allow: true });
|
|
90
|
-
const server = new SocketServer(sockPath, handler);
|
|
91
|
-
servers.push(server);
|
|
92
|
-
await new Promise((r) => setTimeout(r, 30));
|
|
93
|
-
|
|
94
|
-
// 构造大事件(约 50KB tool_output),分成 ~20 个 chunk 喂入
|
|
95
|
-
const bigOutput = 'x'.repeat(50_000);
|
|
96
|
-
const event = makeEvent({ tool_output: { data: bigOutput } });
|
|
97
|
-
const payload = JSON.stringify(event) + '\n';
|
|
98
|
-
const chunkSize = Math.ceil(payload.length / 20);
|
|
99
|
-
const chunks: string[] = [];
|
|
100
|
-
for (let i = 0; i < payload.length; i += chunkSize) {
|
|
101
|
-
chunks.push(payload.slice(i, i + chunkSize));
|
|
102
|
-
}
|
|
103
|
-
expect(chunks.length).toBeGreaterThan(10);
|
|
104
|
-
|
|
105
|
-
const callsBefore = parseSpy.mock.calls.length;
|
|
106
|
-
await sendOnSocket(sockPath, chunks, 2);
|
|
107
|
-
const callsAfter = parseSpy.mock.calls.length;
|
|
108
|
-
|
|
109
|
-
// 关键断言:handler 必须收到完整事件
|
|
110
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
111
|
-
|
|
112
|
-
// 关键断言:JSON.parse 调用次数远少于 chunk 数(旧代码会 ≈ chunks.length 次)
|
|
113
|
-
// 现在应该只有:可能 1 次(无 auth 时)或 2 次(有 auth 时——auth 检查 + EventParser)
|
|
114
|
-
const parseDelta = callsAfter - callsBefore;
|
|
115
|
-
expect(parseDelta).toBeLessThanOrEqual(3);
|
|
116
|
-
expect(parseDelta).toBeLessThan(chunks.length); // 强对比旧 N² 行为
|
|
117
|
-
|
|
118
|
-
parseSpy.mockRestore();
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('rejects oversized buffer that never sees a newline', async () => {
|
|
122
|
-
const sockPath = makeSockPath();
|
|
123
|
-
sockPaths.push(sockPath);
|
|
124
|
-
|
|
125
|
-
const handler = vi.fn();
|
|
126
|
-
const server = new SocketServer(sockPath, handler);
|
|
127
|
-
servers.push(server);
|
|
128
|
-
await new Promise((r) => setTimeout(r, 30));
|
|
129
|
-
|
|
130
|
-
// 600KB 无换行 —— 必须超过 MAX_BUFFER_SIZE (512KB) 并被丢弃
|
|
131
|
-
// 注意:服务端 destroy 后客户端继续 write 会触发 EPIPE,要容错
|
|
132
|
-
const oversized = 'a'.repeat(600 * 1024);
|
|
133
|
-
await new Promise<void>((resolve) => {
|
|
134
|
-
const client = net.createConnection(sockPath);
|
|
135
|
-
client.on('error', () => { /* EPIPE 预期 */ });
|
|
136
|
-
client.on('close', () => resolve());
|
|
137
|
-
client.on('connect', () => {
|
|
138
|
-
client.write(oversized, () => {
|
|
139
|
-
// 给服务端处理时间
|
|
140
|
-
setTimeout(() => client.destroy(), 80);
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
expect(handler).not.toHaveBeenCalled();
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('uses fallback parse on socket end() for messages without trailing newline', async () => {
|
|
149
|
-
const sockPath = makeSockPath();
|
|
150
|
-
sockPaths.push(sockPath);
|
|
151
|
-
|
|
152
|
-
const handler = vi.fn().mockReturnValue({ allow: true });
|
|
153
|
-
const server = new SocketServer(sockPath, handler);
|
|
154
|
-
servers.push(server);
|
|
155
|
-
await new Promise((r) => setTimeout(r, 30));
|
|
156
|
-
|
|
157
|
-
// 无换行结尾 —— 走 socket.end() 兜底
|
|
158
|
-
const event = makeEvent();
|
|
159
|
-
const payload = JSON.stringify(event);
|
|
160
|
-
await new Promise<void>((resolve, reject) => {
|
|
161
|
-
const client = net.createConnection(sockPath);
|
|
162
|
-
client.on('connect', () => {
|
|
163
|
-
client.end(payload); // 写完即关闭:触发 server 的 'end' 事件
|
|
164
|
-
});
|
|
165
|
-
client.on('close', () => resolve());
|
|
166
|
-
client.on('error', reject);
|
|
167
|
-
});
|
|
168
|
-
// 等 server 端处理完
|
|
169
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
170
|
-
|
|
171
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
describe('EventParser — sanity', () => {
|
|
176
|
-
it('parses a valid event JSON', () => {
|
|
177
|
-
const parser = new EventParser();
|
|
178
|
-
const raw = JSON.stringify(makeEvent());
|
|
179
|
-
const out = parser.parse(raw);
|
|
180
|
-
expect(out.session_id).toBe('test-session');
|
|
181
|
-
expect(out.hook_type).toBe('PostToolUse');
|
|
182
|
-
});
|
|
183
|
-
});
|