@planu/cli 0.88.1 → 0.90.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/activate.d.ts +14 -0
- package/dist/cli/commands/activate.d.ts.map +1 -0
- package/dist/cli/commands/activate.js +174 -0
- package/dist/cli/commands/activate.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +16 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +162 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/install.d.ts +48 -0
- package/dist/cli/commands/install.d.ts.map +1 -0
- package/dist/cli/commands/install.js +348 -0
- package/dist/cli/commands/install.js.map +1 -0
- package/dist/cli/commands/uninstall.d.ts +10 -0
- package/dist/cli/commands/uninstall.d.ts.map +1 -0
- package/dist/cli/commands/uninstall.js +133 -0
- package/dist/cli/commands/uninstall.js.map +1 -0
- package/dist/cli/router.d.ts.map +1 -1
- package/dist/cli/router.js +9 -1
- package/dist/cli/router.js.map +1 -1
- package/dist/config/license-plans.json +5 -2
- package/dist/engine/agent-generator.test.d.ts +2 -0
- package/dist/engine/agent-generator.test.d.ts.map +1 -0
- package/dist/engine/agent-generator.test.js +556 -0
- package/dist/engine/agent-generator.test.js.map +1 -0
- package/dist/engine/analyzer.test.d.ts +2 -0
- package/dist/engine/analyzer.test.d.ts.map +1 -0
- package/dist/engine/analyzer.test.js +1461 -0
- package/dist/engine/analyzer.test.js.map +1 -0
- package/dist/engine/auditor.test.d.ts +2 -0
- package/dist/engine/auditor.test.d.ts.map +1 -0
- package/dist/engine/auditor.test.js +2075 -0
- package/dist/engine/auditor.test.js.map +1 -0
- package/dist/engine/convention-scanner/codebase-scanner.js +2 -2
- package/dist/engine/convention-scanner/codebase-scanner.js.map +1 -1
- package/dist/engine/conventions-cache.d.ts +6 -0
- package/dist/engine/conventions-cache.d.ts.map +1 -0
- package/dist/engine/conventions-cache.js +20 -0
- package/dist/engine/conventions-cache.js.map +1 -0
- package/dist/engine/doc-generator.test.d.ts +2 -0
- package/dist/engine/doc-generator.test.d.ts.map +1 -0
- package/dist/engine/doc-generator.test.js +961 -0
- package/dist/engine/doc-generator.test.js.map +1 -0
- package/dist/engine/estimator.test.d.ts +2 -0
- package/dist/engine/estimator.test.d.ts.map +1 -0
- package/dist/engine/estimator.test.js +334 -0
- package/dist/engine/estimator.test.js.map +1 -0
- package/dist/engine/skill-generator.test.d.ts +2 -0
- package/dist/engine/skill-generator.test.d.ts.map +1 -0
- package/dist/engine/skill-generator.test.js +742 -0
- package/dist/engine/skill-generator.test.js.map +1 -0
- package/dist/engine/spec-migrator/filesystem-import.d.ts +14 -0
- package/dist/engine/spec-migrator/filesystem-import.d.ts.map +1 -0
- package/dist/engine/spec-migrator/filesystem-import.js +96 -0
- package/dist/engine/spec-migrator/filesystem-import.js.map +1 -0
- package/dist/engine/spec-migrator/flatten-specs.d.ts +12 -0
- package/dist/engine/spec-migrator/flatten-specs.d.ts.map +1 -0
- package/dist/engine/spec-migrator/flatten-specs.js +111 -0
- package/dist/engine/spec-migrator/flatten-specs.js.map +1 -0
- package/dist/engine/spec-migrator/folder-operations.d.ts +9 -0
- package/dist/engine/spec-migrator/folder-operations.d.ts.map +1 -0
- package/dist/engine/spec-migrator/folder-operations.js +109 -0
- package/dist/engine/spec-migrator/folder-operations.js.map +1 -0
- package/dist/engine/spec-migrator/frontmatter-parser.d.ts +11 -0
- package/dist/engine/spec-migrator/frontmatter-parser.d.ts.map +1 -0
- package/dist/engine/spec-migrator/frontmatter-parser.js +92 -0
- package/dist/engine/spec-migrator/frontmatter-parser.js.map +1 -0
- package/dist/engine/spec-migrator/index.d.ts +9 -0
- package/dist/engine/spec-migrator/index.d.ts.map +1 -0
- package/dist/engine/spec-migrator/index.js +18 -0
- package/dist/engine/spec-migrator/index.js.map +1 -0
- package/dist/engine/spec-migrator/legacy-migration.d.ts +13 -0
- package/dist/engine/spec-migrator/legacy-migration.d.ts.map +1 -0
- package/dist/engine/spec-migrator/legacy-migration.js +75 -0
- package/dist/engine/spec-migrator/legacy-migration.js.map +1 -0
- package/dist/engine/spec-migrator/migration-validator.d.ts +20 -0
- package/dist/engine/spec-migrator/migration-validator.d.ts.map +1 -0
- package/dist/engine/spec-migrator/migration-validator.js +35 -0
- package/dist/engine/spec-migrator/migration-validator.js.map +1 -0
- package/dist/engine/spec-migrator/path-utils.d.ts +13 -0
- package/dist/engine/spec-migrator/path-utils.d.ts.map +1 -0
- package/dist/engine/spec-migrator/path-utils.js +40 -0
- package/dist/engine/spec-migrator/path-utils.js.map +1 -0
- package/dist/engine/spec-migrator/prefix-migration.d.ts +11 -0
- package/dist/engine/spec-migrator/prefix-migration.d.ts.map +1 -0
- package/dist/engine/spec-migrator/prefix-migration.js +73 -0
- package/dist/engine/spec-migrator/prefix-migration.js.map +1 -0
- package/dist/engine/spec-migrator/reconcile-paths.d.ts +12 -0
- package/dist/engine/spec-migrator/reconcile-paths.d.ts.map +1 -0
- package/dist/engine/spec-migrator/reconcile-paths.js +77 -0
- package/dist/engine/spec-migrator/reconcile-paths.js.map +1 -0
- package/dist/engine/spec-migrator/version-detection.d.ts +5 -0
- package/dist/engine/spec-migrator/version-detection.d.ts.map +1 -0
- package/dist/engine/spec-migrator/version-detection.js +19 -0
- package/dist/engine/spec-migrator/version-detection.js.map +1 -0
- package/dist/engine/spec-migrator.d.ts +1 -58
- package/dist/engine/spec-migrator.d.ts.map +1 -1
- package/dist/engine/spec-migrator.js +2 -658
- package/dist/engine/spec-migrator.js.map +1 -1
- package/dist/engine/spec-summary-html/dashboard-renderer.d.ts +6 -0
- package/dist/engine/spec-summary-html/dashboard-renderer.d.ts.map +1 -0
- package/dist/engine/spec-summary-html/dashboard-renderer.js +333 -0
- package/dist/engine/spec-summary-html/dashboard-renderer.js.map +1 -0
- package/dist/engine/spec-summary-html/hash-utils.d.ts +11 -0
- package/dist/engine/spec-summary-html/hash-utils.d.ts.map +1 -0
- package/dist/engine/spec-summary-html/hash-utils.js +39 -0
- package/dist/engine/spec-summary-html/hash-utils.js.map +1 -0
- package/dist/engine/spec-summary-html/index.d.ts +4 -0
- package/dist/engine/spec-summary-html/index.d.ts.map +1 -0
- package/dist/engine/spec-summary-html/index.js +6 -0
- package/dist/engine/spec-summary-html/index.js.map +1 -0
- package/dist/engine/spec-summary-html/report-renderer.d.ts +9 -0
- package/dist/engine/spec-summary-html/report-renderer.d.ts.map +1 -0
- package/dist/engine/spec-summary-html/report-renderer.js +139 -0
- package/dist/engine/spec-summary-html/report-renderer.js.map +1 -0
- package/dist/engine/spec-summary-html.d.ts +1 -0
- package/dist/engine/spec-summary-html.d.ts.map +1 -1
- package/dist/engine/spec-summary-html.js +19 -473
- package/dist/engine/spec-summary-html.js.map +1 -1
- package/dist/engine/update-notifier.d.ts +8 -0
- package/dist/engine/update-notifier.d.ts.map +1 -0
- package/dist/engine/update-notifier.js +130 -0
- package/dist/engine/update-notifier.js.map +1 -0
- package/dist/engine/validator/dor-dod.d.ts.map +1 -1
- package/dist/engine/validator/dor-dod.js +8 -5
- package/dist/engine/validator/dor-dod.js.map +1 -1
- package/dist/engine/validator.d.ts.map +1 -1
- package/dist/engine/validator.js +4 -3
- package/dist/engine/validator.js.map +1 -1
- package/dist/engine/validator.test.d.ts +2 -0
- package/dist/engine/validator.test.d.ts.map +1 -0
- package/dist/engine/validator.test.js +2371 -0
- package/dist/engine/validator.test.js.map +1 -0
- package/dist/engine/web-fetcher.test.d.ts +2 -0
- package/dist/engine/web-fetcher.test.d.ts.map +1 -0
- package/dist/engine/web-fetcher.test.js +360 -0
- package/dist/engine/web-fetcher.test.js.map +1 -0
- package/dist/i18n/index.test.d.ts +2 -0
- package/dist/i18n/index.test.d.ts.map +1 -0
- package/dist/i18n/index.test.js +375 -0
- package/dist/i18n/index.test.js.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +124 -0
- package/dist/index.test.js.map +1 -0
- package/dist/resources/patterns.test.d.ts +2 -0
- package/dist/resources/patterns.test.d.ts.map +1 -0
- package/dist/resources/patterns.test.js +142 -0
- package/dist/resources/patterns.test.js.map +1 -0
- package/dist/resources/process.test.d.ts +2 -0
- package/dist/resources/process.test.d.ts.map +1 -0
- package/dist/resources/process.test.js +48 -0
- package/dist/resources/process.test.js.map +1 -0
- package/dist/resources/registry.test.d.ts +2 -0
- package/dist/resources/registry.test.d.ts.map +1 -0
- package/dist/resources/registry.test.js +138 -0
- package/dist/resources/registry.test.js.map +1 -0
- package/dist/resources/specs.test.d.ts +2 -0
- package/dist/resources/specs.test.d.ts.map +1 -0
- package/dist/resources/specs.test.js +130 -0
- package/dist/resources/specs.test.js.map +1 -0
- package/dist/resources/templates.test.d.ts +2 -0
- package/dist/resources/templates.test.d.ts.map +1 -0
- package/dist/resources/templates.test.js +119 -0
- package/dist/resources/templates.test.js.map +1 -0
- package/dist/smoke.test.d.ts +2 -0
- package/dist/smoke.test.d.ts.map +1 -0
- package/dist/smoke.test.js +229 -0
- package/dist/smoke.test.js.map +1 -0
- package/dist/storage/base-store.test.d.ts +2 -0
- package/dist/storage/base-store.test.d.ts.map +1 -0
- package/dist/storage/base-store.test.js +180 -0
- package/dist/storage/base-store.test.js.map +1 -0
- package/dist/storage/global-store.test.d.ts +2 -0
- package/dist/storage/global-store.test.d.ts.map +1 -0
- package/dist/storage/global-store.test.js +327 -0
- package/dist/storage/global-store.test.js.map +1 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +1 -0
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/index.test.d.ts +2 -0
- package/dist/storage/index.test.d.ts.map +1 -0
- package/dist/storage/index.test.js +56 -0
- package/dist/storage/index.test.js.map +1 -0
- package/dist/storage/knowledge-store.test.d.ts +2 -0
- package/dist/storage/knowledge-store.test.d.ts.map +1 -0
- package/dist/storage/knowledge-store.test.js +368 -0
- package/dist/storage/knowledge-store.test.js.map +1 -0
- package/dist/storage/lessons-store.d.ts +10 -0
- package/dist/storage/lessons-store.d.ts.map +1 -0
- package/dist/storage/lessons-store.js +67 -0
- package/dist/storage/lessons-store.js.map +1 -0
- package/dist/storage/metrics-store.test.d.ts +2 -0
- package/dist/storage/metrics-store.test.d.ts.map +1 -0
- package/dist/storage/metrics-store.test.js +212 -0
- package/dist/storage/metrics-store.test.js.map +1 -0
- package/dist/storage/pattern-store.test.d.ts +2 -0
- package/dist/storage/pattern-store.test.d.ts.map +1 -0
- package/dist/storage/pattern-store.test.js +224 -0
- package/dist/storage/pattern-store.test.js.map +1 -0
- package/dist/storage/spec-store.test.d.ts +2 -0
- package/dist/storage/spec-store.test.d.ts.map +1 -0
- package/dist/storage/spec-store.test.js +227 -0
- package/dist/storage/spec-store.test.js.map +1 -0
- package/dist/tools/audit.test.d.ts +2 -0
- package/dist/tools/audit.test.d.ts.map +1 -0
- package/dist/tools/audit.test.js +169 -0
- package/dist/tools/audit.test.js.map +1 -0
- package/dist/tools/challenge-spec.test.d.ts +2 -0
- package/dist/tools/challenge-spec.test.d.ts.map +1 -0
- package/dist/tools/challenge-spec.test.js +782 -0
- package/dist/tools/challenge-spec.test.js.map +1 -0
- package/dist/tools/check-versions.test.d.ts +2 -0
- package/dist/tools/check-versions.test.d.ts.map +1 -0
- package/dist/tools/check-versions.test.js +214 -0
- package/dist/tools/check-versions.test.js.map +1 -0
- package/dist/tools/clarify-requirements.test.d.ts +2 -0
- package/dist/tools/clarify-requirements.test.d.ts.map +1 -0
- package/dist/tools/clarify-requirements.test.js +161 -0
- package/dist/tools/clarify-requirements.test.js.map +1 -0
- package/dist/tools/consult-docs.test.d.ts +2 -0
- package/dist/tools/consult-docs.test.d.ts.map +1 -0
- package/dist/tools/consult-docs.test.js +140 -0
- package/dist/tools/consult-docs.test.js.map +1 -0
- package/dist/tools/create-spec/lessons-injector.d.ts +6 -0
- package/dist/tools/create-spec/lessons-injector.d.ts.map +1 -0
- package/dist/tools/create-spec/lessons-injector.js +53 -0
- package/dist/tools/create-spec/lessons-injector.js.map +1 -0
- package/dist/tools/create-spec.d.ts.map +1 -1
- package/dist/tools/create-spec.js +6 -1
- package/dist/tools/create-spec.js.map +1 -1
- package/dist/tools/create-spec.test.d.ts +2 -0
- package/dist/tools/create-spec.test.d.ts.map +1 -0
- package/dist/tools/create-spec.test.js +233 -0
- package/dist/tools/create-spec.test.js.map +1 -0
- package/dist/tools/define-ui-contract.test.d.ts +2 -0
- package/dist/tools/define-ui-contract.test.d.ts.map +1 -0
- package/dist/tools/define-ui-contract.test.js +479 -0
- package/dist/tools/define-ui-contract.test.js.map +1 -0
- package/dist/tools/design-schema.test.d.ts +2 -0
- package/dist/tools/design-schema.test.d.ts.map +1 -0
- package/dist/tools/design-schema.test.js +301 -0
- package/dist/tools/design-schema.test.js.map +1 -0
- package/dist/tools/detect-agent.test.d.ts +2 -0
- package/dist/tools/detect-agent.test.d.ts.map +1 -0
- package/dist/tools/detect-agent.test.js +133 -0
- package/dist/tools/detect-agent.test.js.map +1 -0
- package/dist/tools/detect-drift.test.d.ts +2 -0
- package/dist/tools/detect-drift.test.d.ts.map +1 -0
- package/dist/tools/detect-drift.test.js +312 -0
- package/dist/tools/detect-drift.test.js.map +1 -0
- package/dist/tools/discover-mcps.test.d.ts +2 -0
- package/dist/tools/discover-mcps.test.d.ts.map +1 -0
- package/dist/tools/discover-mcps.test.js +345 -0
- package/dist/tools/discover-mcps.test.js.map +1 -0
- package/dist/tools/estimate.test.d.ts +2 -0
- package/dist/tools/estimate.test.d.ts.map +1 -0
- package/dist/tools/estimate.test.js +137 -0
- package/dist/tools/estimate.test.js.map +1 -0
- package/dist/tools/generate-adr.test.d.ts +2 -0
- package/dist/tools/generate-adr.test.d.ts.map +1 -0
- package/dist/tools/generate-adr.test.js +206 -0
- package/dist/tools/generate-adr.test.js.map +1 -0
- package/dist/tools/generate-checklist.test.d.ts +2 -0
- package/dist/tools/generate-checklist.test.d.ts.map +1 -0
- package/dist/tools/generate-checklist.test.js +201 -0
- package/dist/tools/generate-checklist.test.js.map +1 -0
- package/dist/tools/generate-docs.test.d.ts +2 -0
- package/dist/tools/generate-docs.test.d.ts.map +1 -0
- package/dist/tools/generate-docs.test.js +183 -0
- package/dist/tools/generate-docs.test.js.map +1 -0
- package/dist/tools/generate-execution-plan.test.d.ts +2 -0
- package/dist/tools/generate-execution-plan.test.d.ts.map +1 -0
- package/dist/tools/generate-execution-plan.test.js +643 -0
- package/dist/tools/generate-execution-plan.test.js.map +1 -0
- package/dist/tools/generate-rules.test.d.ts +2 -0
- package/dist/tools/generate-rules.test.d.ts.map +1 -0
- package/dist/tools/generate-rules.test.js +148 -0
- package/dist/tools/generate-rules.test.js.map +1 -0
- package/dist/tools/generate-skill.test.d.ts +2 -0
- package/dist/tools/generate-skill.test.d.ts.map +1 -0
- package/dist/tools/generate-skill.test.js +138 -0
- package/dist/tools/generate-skill.test.js.map +1 -0
- package/dist/tools/generate-sub-agent.test.d.ts +2 -0
- package/dist/tools/generate-sub-agent.test.d.ts.map +1 -0
- package/dist/tools/generate-sub-agent.test.js +162 -0
- package/dist/tools/generate-sub-agent.test.js.map +1 -0
- package/dist/tools/generate-tests.test.d.ts +2 -0
- package/dist/tools/generate-tests.test.d.ts.map +1 -0
- package/dist/tools/generate-tests.test.js +222 -0
- package/dist/tools/generate-tests.test.js.map +1 -0
- package/dist/tools/init-constitution.test.d.ts +2 -0
- package/dist/tools/init-constitution.test.d.ts.map +1 -0
- package/dist/tools/init-constitution.test.js +398 -0
- package/dist/tools/init-constitution.test.js.map +1 -0
- package/dist/tools/init-project/config-builder.d.ts +12 -0
- package/dist/tools/init-project/config-builder.d.ts.map +1 -0
- package/dist/tools/init-project/config-builder.js +31 -0
- package/dist/tools/init-project/config-builder.js.map +1 -0
- package/dist/tools/init-project/git-setup.d.ts +8 -0
- package/dist/tools/init-project/git-setup.d.ts.map +1 -0
- package/dist/tools/init-project/git-setup.js +70 -0
- package/dist/tools/init-project/git-setup.js.map +1 -0
- package/dist/tools/init-project/handler.d.ts.map +1 -1
- package/dist/tools/init-project/handler.js +27 -364
- package/dist/tools/init-project/handler.js.map +1 -1
- package/dist/tools/init-project/lifecycle-helpers.d.ts +32 -0
- package/dist/tools/init-project/lifecycle-helpers.d.ts.map +1 -0
- package/dist/tools/init-project/lifecycle-helpers.js +153 -0
- package/dist/tools/init-project/lifecycle-helpers.js.map +1 -0
- package/dist/tools/init-project/migration-runner.d.ts +28 -0
- package/dist/tools/init-project/migration-runner.d.ts.map +1 -0
- package/dist/tools/init-project/migration-runner.js +57 -0
- package/dist/tools/init-project/migration-runner.js.map +1 -0
- package/dist/tools/init-project/result-builder.d.ts.map +1 -1
- package/dist/tools/init-project/result-builder.js +1 -0
- package/dist/tools/init-project/result-builder.js.map +1 -1
- package/dist/tools/init-project/rules-writer.d.ts +14 -0
- package/dist/tools/init-project/rules-writer.d.ts.map +1 -0
- package/dist/tools/init-project/rules-writer.js +43 -0
- package/dist/tools/init-project/rules-writer.js.map +1 -0
- package/dist/tools/init-project/scaffold-writer.d.ts +29 -0
- package/dist/tools/init-project/scaffold-writer.d.ts.map +1 -0
- package/dist/tools/init-project/scaffold-writer.js +76 -0
- package/dist/tools/init-project/scaffold-writer.js.map +1 -0
- package/dist/tools/init-project/stack-detector.d.ts +16 -0
- package/dist/tools/init-project/stack-detector.d.ts.map +1 -0
- package/dist/tools/init-project/stack-detector.js +19 -0
- package/dist/tools/init-project/stack-detector.js.map +1 -0
- package/dist/tools/init-project.test.d.ts +2 -0
- package/dist/tools/init-project.test.d.ts.map +1 -0
- package/dist/tools/init-project.test.js +158 -0
- package/dist/tools/init-project.test.js.map +1 -0
- package/dist/tools/integrate-pm.test.d.ts +2 -0
- package/dist/tools/integrate-pm.test.d.ts.map +1 -0
- package/dist/tools/integrate-pm.test.js +558 -0
- package/dist/tools/integrate-pm.test.js.map +1 -0
- package/dist/tools/learn.test.d.ts +2 -0
- package/dist/tools/learn.test.d.ts.map +1 -0
- package/dist/tools/learn.test.js +123 -0
- package/dist/tools/learn.test.js.map +1 -0
- package/dist/tools/lessons-handler.d.ts +6 -0
- package/dist/tools/lessons-handler.d.ts.map +1 -0
- package/dist/tools/lessons-handler.js +64 -0
- package/dist/tools/lessons-handler.js.map +1 -0
- package/dist/tools/list-specs.js +1 -1
- package/dist/tools/list-specs.js.map +1 -1
- package/dist/tools/list-specs.test.d.ts +2 -0
- package/dist/tools/list-specs.test.d.ts.map +1 -0
- package/dist/tools/list-specs.test.js +110 -0
- package/dist/tools/list-specs.test.js.map +1 -0
- package/dist/tools/manage-context.test.d.ts +2 -0
- package/dist/tools/manage-context.test.d.ts.map +1 -0
- package/dist/tools/manage-context.test.js +359 -0
- package/dist/tools/manage-context.test.js.map +1 -0
- package/dist/tools/manage-git.test.d.ts +2 -0
- package/dist/tools/manage-git.test.d.ts.map +1 -0
- package/dist/tools/manage-git.test.js +882 -0
- package/dist/tools/manage-git.test.js.map +1 -0
- package/dist/tools/orchestrate.test.d.ts +2 -0
- package/dist/tools/orchestrate.test.d.ts.map +1 -0
- package/dist/tools/orchestrate.test.js +1117 -0
- package/dist/tools/orchestrate.test.js.map +1 -0
- package/dist/tools/reconcile-spec.test.d.ts +2 -0
- package/dist/tools/reconcile-spec.test.d.ts.map +1 -0
- package/dist/tools/reconcile-spec.test.js +259 -0
- package/dist/tools/reconcile-spec.test.js.map +1 -0
- package/dist/tools/red-team.d.ts +3 -0
- package/dist/tools/red-team.d.ts.map +1 -0
- package/dist/tools/red-team.js +302 -0
- package/dist/tools/red-team.js.map +1 -0
- package/dist/tools/register-lessons-tools.d.ts +3 -0
- package/dist/tools/register-lessons-tools.d.ts.map +1 -0
- package/dist/tools/register-lessons-tools.js +62 -0
- package/dist/tools/register-lessons-tools.js.map +1 -0
- package/dist/tools/register-platform-tools/design-stack-tools.d.ts.map +1 -1
- package/dist/tools/register-platform-tools/design-stack-tools.js +14 -0
- package/dist/tools/register-platform-tools/design-stack-tools.js.map +1 -1
- package/dist/tools/register-platform-tools.test.d.ts +2 -0
- package/dist/tools/register-platform-tools.test.d.ts.map +1 -0
- package/dist/tools/register-platform-tools.test.js +404 -0
- package/dist/tools/register-platform-tools.test.js.map +1 -0
- package/dist/tools/register-spec-tools.test.d.ts +2 -0
- package/dist/tools/register-spec-tools.test.d.ts.map +1 -0
- package/dist/tools/register-spec-tools.test.js +407 -0
- package/dist/tools/register-spec-tools.test.js.map +1 -0
- package/dist/tools/reverse-engineer.test.d.ts +2 -0
- package/dist/tools/reverse-engineer.test.d.ts.map +1 -0
- package/dist/tools/reverse-engineer.test.js +206 -0
- package/dist/tools/reverse-engineer.test.js.map +1 -0
- package/dist/tools/schemas.d.ts +20 -0
- package/dist/tools/schemas.d.ts.map +1 -0
- package/dist/tools/schemas.js +133 -0
- package/dist/tools/schemas.js.map +1 -0
- package/dist/tools/schemas.test.d.ts +2 -0
- package/dist/tools/schemas.test.d.ts.map +1 -0
- package/dist/tools/schemas.test.js +245 -0
- package/dist/tools/schemas.test.js.map +1 -0
- package/dist/tools/set-locale.test.d.ts +2 -0
- package/dist/tools/set-locale.test.d.ts.map +1 -0
- package/dist/tools/set-locale.test.js +74 -0
- package/dist/tools/set-locale.test.js.map +1 -0
- package/dist/tools/suggest-mcps.test.d.ts +2 -0
- package/dist/tools/suggest-mcps.test.d.ts.map +1 -0
- package/dist/tools/suggest-mcps.test.js +198 -0
- package/dist/tools/suggest-mcps.test.js.map +1 -0
- package/dist/tools/suggest-stack.test.d.ts +2 -0
- package/dist/tools/suggest-stack.test.d.ts.map +1 -0
- package/dist/tools/suggest-stack.test.js +181 -0
- package/dist/tools/suggest-stack.test.js.map +1 -0
- package/dist/tools/suggest-tooling.test.d.ts +2 -0
- package/dist/tools/suggest-tooling.test.d.ts.map +1 -0
- package/dist/tools/suggest-tooling.test.js +213 -0
- package/dist/tools/suggest-tooling.test.js.map +1 -0
- package/dist/tools/summarize-spec.test.d.ts +2 -0
- package/dist/tools/summarize-spec.test.d.ts.map +1 -0
- package/dist/tools/summarize-spec.test.js +180 -0
- package/dist/tools/summarize-spec.test.js.map +1 -0
- package/dist/tools/update-status/dod-gates.d.ts +16 -0
- package/dist/tools/update-status/dod-gates.d.ts.map +1 -0
- package/dist/tools/update-status/dod-gates.js +117 -0
- package/dist/tools/update-status/dod-gates.js.map +1 -0
- package/dist/tools/update-status/file-sync.d.ts +6 -0
- package/dist/tools/update-status/file-sync.d.ts.map +1 -0
- package/dist/tools/update-status/file-sync.js +112 -0
- package/dist/tools/update-status/file-sync.js.map +1 -0
- package/dist/tools/update-status/index.d.ts +3 -0
- package/dist/tools/update-status/index.d.ts.map +1 -0
- package/dist/tools/update-status/index.js +181 -0
- package/dist/tools/update-status/index.js.map +1 -0
- package/dist/tools/update-status/response-builder.d.ts +4 -0
- package/dist/tools/update-status/response-builder.d.ts.map +1 -0
- package/dist/tools/update-status/response-builder.js +69 -0
- package/dist/tools/update-status/response-builder.js.map +1 -0
- package/dist/tools/update-status/side-effects.d.ts +15 -0
- package/dist/tools/update-status/side-effects.d.ts.map +1 -0
- package/dist/tools/update-status/side-effects.js +64 -0
- package/dist/tools/update-status/side-effects.js.map +1 -0
- package/dist/tools/update-status/transition-guard.d.ts +20 -0
- package/dist/tools/update-status/transition-guard.d.ts.map +1 -0
- package/dist/tools/update-status/transition-guard.js +75 -0
- package/dist/tools/update-status/transition-guard.js.map +1 -0
- package/dist/tools/update-status.d.ts +1 -2
- package/dist/tools/update-status.d.ts.map +1 -1
- package/dist/tools/update-status.js +2 -461
- package/dist/tools/update-status.js.map +1 -1
- package/dist/tools/update-status.test.d.ts +2 -0
- package/dist/tools/update-status.test.d.ts.map +1 -0
- package/dist/tools/update-status.test.js +142 -0
- package/dist/tools/update-status.test.js.map +1 -0
- package/dist/tools/validate.d.ts.map +1 -1
- package/dist/tools/validate.js +18 -4
- package/dist/tools/validate.js.map +1 -1
- package/dist/tools/validate.test.d.ts +2 -0
- package/dist/tools/validate.test.d.ts.map +1 -0
- package/dist/tools/validate.test.js +137 -0
- package/dist/tools/validate.test.js.map +1 -0
- package/dist/types/analysis.d.ts +2 -1
- package/dist/types/analysis.d.ts.map +1 -1
- package/dist/types/conventions.d.ts +5 -0
- package/dist/types/conventions.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/lessons.d.ts +50 -0
- package/dist/types/lessons.d.ts.map +1 -0
- package/dist/types/lessons.js +3 -0
- package/dist/types/lessons.js.map +1 -0
- package/dist/types/project/planu-config.d.ts +2 -0
- package/dist/types/project/planu-config.d.ts.map +1 -1
- package/dist/types/red-team.d.ts +29 -0
- package/dist/types/red-team.d.ts.map +1 -0
- package/dist/types/red-team.js +3 -0
- package/dist/types/red-team.js.map +1 -0
- package/dist/types/update-notifier.d.ts +5 -0
- package/dist/types/update-notifier.d.ts.map +1 -0
- package/dist/types/update-notifier.js +3 -0
- package/dist/types/update-notifier.js.map +1 -0
- package/package.json +9 -2
- package/src/config/license-plans.json +5 -2
- package/src/i18n/messages/en.json +5 -0
- package/src/i18n/messages/es.json +5 -0
- package/src/i18n/messages/pt.json +5 -0
|
@@ -0,0 +1,2371 @@
|
|
|
1
|
+
// SpecForge — Validator Engine Tests (comprehensive, 98%+ coverage)
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
// === Mock modules ===
|
|
4
|
+
vi.mock('node:fs/promises', () => ({
|
|
5
|
+
readFile: vi.fn(),
|
|
6
|
+
stat: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
vi.mock('glob', () => ({
|
|
9
|
+
glob: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
// Import mocked modules so we can control them
|
|
12
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
13
|
+
import { glob } from 'glob';
|
|
14
|
+
// Import the functions under test AFTER mocking
|
|
15
|
+
import { validateSpec, detectDrift, generateChecklist, generateDoR, generateDoD, } from './validator.js';
|
|
16
|
+
// Cast mocked functions for type-safe usage
|
|
17
|
+
const mockReadFile = vi.mocked(readFile);
|
|
18
|
+
const mockStat = vi.mocked(stat);
|
|
19
|
+
const mockGlob = vi.mocked(glob);
|
|
20
|
+
// === Helpers ===
|
|
21
|
+
function makeEstimation(overrides = {}) {
|
|
22
|
+
return {
|
|
23
|
+
devHours: 8,
|
|
24
|
+
reviewHours: 1.6,
|
|
25
|
+
recommendedModel: 'mixed',
|
|
26
|
+
tokensOpus: 10000,
|
|
27
|
+
tokensSonnet: 20000,
|
|
28
|
+
apiCostUsd: 0.21,
|
|
29
|
+
hourlyRate: 65,
|
|
30
|
+
humanCostUsd: 624,
|
|
31
|
+
totalCostUsd: 624.21,
|
|
32
|
+
tokenOptimization: {
|
|
33
|
+
mode: 'local',
|
|
34
|
+
reasoning: 'test',
|
|
35
|
+
estimatedTokens: 15000,
|
|
36
|
+
savings: '~50%',
|
|
37
|
+
},
|
|
38
|
+
...overrides,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function makeSpec(overrides = {}) {
|
|
42
|
+
return {
|
|
43
|
+
id: 'SPEC-001',
|
|
44
|
+
title: 'Test Spec Title',
|
|
45
|
+
slug: 'test-spec-title',
|
|
46
|
+
type: 'feature',
|
|
47
|
+
scope: 'feature',
|
|
48
|
+
status: 'draft',
|
|
49
|
+
difficulty: 3,
|
|
50
|
+
risk: 'medium',
|
|
51
|
+
projectId: 'proj-1',
|
|
52
|
+
createdAt: '2025-01-01T00:00:00Z',
|
|
53
|
+
updatedAt: '2025-01-01T00:00:00Z',
|
|
54
|
+
huPath: '/tmp/HU.md',
|
|
55
|
+
fichaTecnicaPath: '/tmp/FICHA-TECNICA.md',
|
|
56
|
+
estimation: makeEstimation(),
|
|
57
|
+
actuals: null,
|
|
58
|
+
target: 'backend',
|
|
59
|
+
tags: [],
|
|
60
|
+
dependencies: [],
|
|
61
|
+
blockedBy: [],
|
|
62
|
+
gitBranch: '',
|
|
63
|
+
impactAnalysis: null,
|
|
64
|
+
...overrides,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// === Setup ===
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
vi.clearAllMocks();
|
|
70
|
+
// Default: readFile throws (file not found)
|
|
71
|
+
mockReadFile.mockRejectedValue(new Error('ENOENT'));
|
|
72
|
+
// Default: stat throws (file not found)
|
|
73
|
+
mockStat.mockRejectedValue(new Error('ENOENT'));
|
|
74
|
+
// Default: glob returns no files
|
|
75
|
+
mockGlob.mockResolvedValue([]);
|
|
76
|
+
});
|
|
77
|
+
// ============================================================
|
|
78
|
+
// generateDoR
|
|
79
|
+
// ============================================================
|
|
80
|
+
describe('generateDoR', () => {
|
|
81
|
+
it('should generate all 10 DoR items with the correct specId', () => {
|
|
82
|
+
const spec = makeSpec();
|
|
83
|
+
const result = generateDoR(spec);
|
|
84
|
+
expect(result.items).toHaveLength(10);
|
|
85
|
+
expect(result.specId).toBe('SPEC-001');
|
|
86
|
+
});
|
|
87
|
+
it('should pass title check when title > 3 chars', () => {
|
|
88
|
+
const result = generateDoR(makeSpec({ title: 'Valid Title' }));
|
|
89
|
+
expect(result.items.find((i) => i.id === 'dor-1')?.status).toBe('passed');
|
|
90
|
+
});
|
|
91
|
+
it('should fail title check when title <= 3 chars', () => {
|
|
92
|
+
const result = generateDoR(makeSpec({ title: 'AB' }));
|
|
93
|
+
expect(result.items.find((i) => i.id === 'dor-1')?.status).toBe('failed');
|
|
94
|
+
});
|
|
95
|
+
it('should fail title check for exactly 3 chars', () => {
|
|
96
|
+
const result = generateDoR(makeSpec({ title: 'ABC' }));
|
|
97
|
+
expect(result.items.find((i) => i.id === 'dor-1')?.status).toBe('failed');
|
|
98
|
+
});
|
|
99
|
+
it('should pass type+scope check when both are non-empty', () => {
|
|
100
|
+
const result = generateDoR(makeSpec({ type: 'feature', scope: 'feature' }));
|
|
101
|
+
expect(result.items.find((i) => i.id === 'dor-2')?.status).toBe('passed');
|
|
102
|
+
});
|
|
103
|
+
it('should fail type+scope check when type is empty', () => {
|
|
104
|
+
const result = generateDoR(makeSpec({ type: '' }));
|
|
105
|
+
expect(result.items.find((i) => i.id === 'dor-2')?.status).toBe('failed');
|
|
106
|
+
});
|
|
107
|
+
it('should fail type+scope check when scope is empty', () => {
|
|
108
|
+
const result = generateDoR(makeSpec({ scope: '' }));
|
|
109
|
+
expect(result.items.find((i) => i.id === 'dor-2')?.status).toBe('failed');
|
|
110
|
+
});
|
|
111
|
+
it('should pass difficulty check for values 1-5', () => {
|
|
112
|
+
for (const d of [1, 2, 3, 4, 5]) {
|
|
113
|
+
const result = generateDoR(makeSpec({ difficulty: d }));
|
|
114
|
+
expect(result.items.find((i) => i.id === 'dor-3')?.status).toBe('passed');
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
it('should fail difficulty check for out-of-range values', () => {
|
|
118
|
+
const result = generateDoR(makeSpec({ difficulty: 0 }));
|
|
119
|
+
expect(result.items.find((i) => i.id === 'dor-3')?.status).toBe('failed');
|
|
120
|
+
});
|
|
121
|
+
it('should fail difficulty check for value above 5', () => {
|
|
122
|
+
const result = generateDoR(makeSpec({ difficulty: 6 }));
|
|
123
|
+
expect(result.items.find((i) => i.id === 'dor-3')?.status).toBe('failed');
|
|
124
|
+
});
|
|
125
|
+
it('should pass estimation check when devHours > 0', () => {
|
|
126
|
+
const result = generateDoR(makeSpec({ estimation: makeEstimation({ devHours: 5 }) }));
|
|
127
|
+
expect(result.items.find((i) => i.id === 'dor-4')?.status).toBe('passed');
|
|
128
|
+
});
|
|
129
|
+
it('should fail estimation check when devHours is 0', () => {
|
|
130
|
+
const result = generateDoR(makeSpec({ estimation: makeEstimation({ devHours: 0 }) }));
|
|
131
|
+
expect(result.items.find((i) => i.id === 'dor-4')?.status).toBe('failed');
|
|
132
|
+
});
|
|
133
|
+
it('should pass HU check when huPath is set', () => {
|
|
134
|
+
const result = generateDoR(makeSpec({ huPath: '/some/HU.md' }));
|
|
135
|
+
expect(result.items.find((i) => i.id === 'dor-5')?.status).toBe('passed');
|
|
136
|
+
});
|
|
137
|
+
it('should fail HU check when huPath is empty', () => {
|
|
138
|
+
const result = generateDoR(makeSpec({ huPath: '' }));
|
|
139
|
+
expect(result.items.find((i) => i.id === 'dor-5')?.status).toBe('failed');
|
|
140
|
+
});
|
|
141
|
+
it('should pass ficha check when fichaTecnicaPath is set', () => {
|
|
142
|
+
const result = generateDoR(makeSpec({ fichaTecnicaPath: '/some/FICHA.md' }));
|
|
143
|
+
expect(result.items.find((i) => i.id === 'dor-6')?.status).toBe('passed');
|
|
144
|
+
});
|
|
145
|
+
it('should fail ficha check when fichaTecnicaPath is empty', () => {
|
|
146
|
+
const result = generateDoR(makeSpec({ fichaTecnicaPath: '' }));
|
|
147
|
+
expect(result.items.find((i) => i.id === 'dor-6')?.status).toBe('failed');
|
|
148
|
+
});
|
|
149
|
+
it('should always pass dor-7 (dependencies identified)', () => {
|
|
150
|
+
const result = generateDoR(makeSpec());
|
|
151
|
+
expect(result.items.find((i) => i.id === 'dor-7')?.status).toBe('passed');
|
|
152
|
+
});
|
|
153
|
+
it('should pass blockedBy check when empty', () => {
|
|
154
|
+
const result = generateDoR(makeSpec({ blockedBy: [] }));
|
|
155
|
+
expect(result.items.find((i) => i.id === 'dor-8')?.status).toBe('passed');
|
|
156
|
+
});
|
|
157
|
+
it('should fail blockedBy check when non-empty', () => {
|
|
158
|
+
const result = generateDoR(makeSpec({ blockedBy: ['SPEC-002'] }));
|
|
159
|
+
expect(result.items.find((i) => i.id === 'dor-8')?.status).toBe('failed');
|
|
160
|
+
});
|
|
161
|
+
it('should pass risk check when risk is non-empty', () => {
|
|
162
|
+
const result = generateDoR(makeSpec({ risk: 'high' }));
|
|
163
|
+
expect(result.items.find((i) => i.id === 'dor-9')?.status).toBe('passed');
|
|
164
|
+
});
|
|
165
|
+
it('should fail risk check when risk is empty', () => {
|
|
166
|
+
const result = generateDoR(makeSpec({ risk: '' }));
|
|
167
|
+
expect(result.items.find((i) => i.id === 'dor-9')?.status).toBe('failed');
|
|
168
|
+
});
|
|
169
|
+
it('should pass target check when target is non-empty', () => {
|
|
170
|
+
const result = generateDoR(makeSpec({ target: 'frontend' }));
|
|
171
|
+
expect(result.items.find((i) => i.id === 'dor-10')?.status).toBe('passed');
|
|
172
|
+
});
|
|
173
|
+
it('should fail target check when target is empty', () => {
|
|
174
|
+
const result = generateDoR(makeSpec({ target: '' }));
|
|
175
|
+
expect(result.items.find((i) => i.id === 'dor-10')?.status).toBe('failed');
|
|
176
|
+
});
|
|
177
|
+
it('should mark isReady=true when all required items pass', () => {
|
|
178
|
+
const result = generateDoR(makeSpec());
|
|
179
|
+
expect(result.isReady).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
it('should mark isReady=false when any required item fails', () => {
|
|
182
|
+
const result = generateDoR(makeSpec({ huPath: '' }));
|
|
183
|
+
expect(result.isReady).toBe(false);
|
|
184
|
+
});
|
|
185
|
+
it('should calculate passedCount from items', () => {
|
|
186
|
+
const result = generateDoR(makeSpec());
|
|
187
|
+
const expected = result.items.filter((i) => i.status === 'passed').length;
|
|
188
|
+
expect(result.passedCount).toBe(expected);
|
|
189
|
+
});
|
|
190
|
+
it('should calculate requiredCount from required items', () => {
|
|
191
|
+
const result = generateDoR(makeSpec());
|
|
192
|
+
const expected = result.items.filter((i) => i.required).length;
|
|
193
|
+
expect(result.requiredCount).toBe(expected);
|
|
194
|
+
});
|
|
195
|
+
it('should include a valid generatedAt ISO timestamp', () => {
|
|
196
|
+
const result = generateDoR(makeSpec());
|
|
197
|
+
expect(result.generatedAt).toBeDefined();
|
|
198
|
+
expect(new Date(result.generatedAt).toISOString()).toBe(result.generatedAt);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
// ============================================================
|
|
202
|
+
// generateDoD
|
|
203
|
+
// ============================================================
|
|
204
|
+
describe('generateDoD', () => {
|
|
205
|
+
it('should generate all 8 DoD items', () => {
|
|
206
|
+
const result = generateDoD(makeSpec());
|
|
207
|
+
expect(result.items).toHaveLength(8);
|
|
208
|
+
expect(result.specId).toBe('SPEC-001');
|
|
209
|
+
});
|
|
210
|
+
it('should mark acceptance criteria passed when score is 100', () => {
|
|
211
|
+
const validation = {
|
|
212
|
+
matches: ['a'],
|
|
213
|
+
missing: [],
|
|
214
|
+
extra: [],
|
|
215
|
+
fieldsImplemented: 1,
|
|
216
|
+
fieldsTotal: 1,
|
|
217
|
+
score: 100,
|
|
218
|
+
qualityIssues: [],
|
|
219
|
+
};
|
|
220
|
+
const result = generateDoD(makeSpec(), validation);
|
|
221
|
+
expect(result.items.find((i) => i.id === 'dod-1')?.status).toBe('passed');
|
|
222
|
+
});
|
|
223
|
+
it('should mark acceptance criteria failed when score > 0 but < 100', () => {
|
|
224
|
+
const validation = {
|
|
225
|
+
matches: ['a'],
|
|
226
|
+
missing: ['b'],
|
|
227
|
+
extra: [],
|
|
228
|
+
fieldsImplemented: 1,
|
|
229
|
+
fieldsTotal: 2,
|
|
230
|
+
score: 50,
|
|
231
|
+
qualityIssues: [],
|
|
232
|
+
};
|
|
233
|
+
const result = generateDoD(makeSpec(), validation);
|
|
234
|
+
expect(result.items.find((i) => i.id === 'dod-1')?.status).toBe('failed');
|
|
235
|
+
});
|
|
236
|
+
it('should mark acceptance criteria pending when score is 0', () => {
|
|
237
|
+
const validation = {
|
|
238
|
+
matches: [],
|
|
239
|
+
missing: ['a'],
|
|
240
|
+
extra: [],
|
|
241
|
+
fieldsImplemented: 0,
|
|
242
|
+
fieldsTotal: 1,
|
|
243
|
+
score: 0,
|
|
244
|
+
qualityIssues: [],
|
|
245
|
+
};
|
|
246
|
+
const result = generateDoD(makeSpec(), validation);
|
|
247
|
+
expect(result.items.find((i) => i.id === 'dod-1')?.status).toBe('pending');
|
|
248
|
+
});
|
|
249
|
+
it('should mark acceptance criteria pending when no validation provided', () => {
|
|
250
|
+
const result = generateDoD(makeSpec());
|
|
251
|
+
expect(result.items.find((i) => i.id === 'dod-1')?.status).toBe('pending');
|
|
252
|
+
});
|
|
253
|
+
it('should pass quality check when no quality issues', () => {
|
|
254
|
+
const validation = {
|
|
255
|
+
matches: [],
|
|
256
|
+
missing: [],
|
|
257
|
+
extra: [],
|
|
258
|
+
fieldsImplemented: 0,
|
|
259
|
+
fieldsTotal: 0,
|
|
260
|
+
score: 0,
|
|
261
|
+
qualityIssues: [],
|
|
262
|
+
};
|
|
263
|
+
const result = generateDoD(makeSpec(), validation);
|
|
264
|
+
expect(result.items.find((i) => i.id === 'dod-2')?.status).toBe('passed');
|
|
265
|
+
});
|
|
266
|
+
it('should fail quality check when quality issues exist', () => {
|
|
267
|
+
const validation = {
|
|
268
|
+
matches: [],
|
|
269
|
+
missing: [],
|
|
270
|
+
extra: [],
|
|
271
|
+
fieldsImplemented: 0,
|
|
272
|
+
fieldsTotal: 0,
|
|
273
|
+
score: 0,
|
|
274
|
+
qualityIssues: [
|
|
275
|
+
{
|
|
276
|
+
file: 'x.ts',
|
|
277
|
+
category: 'clean-code',
|
|
278
|
+
severity: 'warning',
|
|
279
|
+
rule: 'test',
|
|
280
|
+
message: 'msg',
|
|
281
|
+
suggestion: 'fix',
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
};
|
|
285
|
+
const result = generateDoD(makeSpec(), validation);
|
|
286
|
+
expect(result.items.find((i) => i.id === 'dod-2')?.status).toBe('failed');
|
|
287
|
+
});
|
|
288
|
+
it('should pass quality check when no validation (defaults to 0 issues)', () => {
|
|
289
|
+
const result = generateDoD(makeSpec());
|
|
290
|
+
expect(result.items.find((i) => i.id === 'dod-2')?.status).toBe('passed');
|
|
291
|
+
});
|
|
292
|
+
it('should mark dod-3 (unit tests) as pending always', () => {
|
|
293
|
+
const result = generateDoD(makeSpec());
|
|
294
|
+
expect(result.items.find((i) => i.id === 'dod-3')?.status).toBe('pending');
|
|
295
|
+
});
|
|
296
|
+
it('should mark dod-4 (code reviewed) as pending always', () => {
|
|
297
|
+
const result = generateDoD(makeSpec());
|
|
298
|
+
expect(result.items.find((i) => i.id === 'dod-4')?.status).toBe('pending');
|
|
299
|
+
});
|
|
300
|
+
it('should mark dod-5 (documentation) as pending always', () => {
|
|
301
|
+
const result = generateDoD(makeSpec());
|
|
302
|
+
expect(result.items.find((i) => i.id === 'dod-5')?.status).toBe('pending');
|
|
303
|
+
});
|
|
304
|
+
it('should mark dod-6 (no regressions) as pending always', () => {
|
|
305
|
+
const result = generateDoD(makeSpec());
|
|
306
|
+
expect(result.items.find((i) => i.id === 'dod-6')?.status).toBe('pending');
|
|
307
|
+
});
|
|
308
|
+
it('should pass spec status check when status is done', () => {
|
|
309
|
+
const result = generateDoD(makeSpec({ status: 'done' }));
|
|
310
|
+
expect(result.items.find((i) => i.id === 'dod-7')?.status).toBe('passed');
|
|
311
|
+
});
|
|
312
|
+
it('should mark spec status as pending when status is not done', () => {
|
|
313
|
+
const result = generateDoD(makeSpec({ status: 'implementing' }));
|
|
314
|
+
expect(result.items.find((i) => i.id === 'dod-7')?.status).toBe('pending');
|
|
315
|
+
});
|
|
316
|
+
it('should pass actuals check when actuals exist', () => {
|
|
317
|
+
const result = generateDoD(makeSpec({
|
|
318
|
+
actuals: {
|
|
319
|
+
devHours: 8,
|
|
320
|
+
reviewHours: 2,
|
|
321
|
+
tokensOpus: 10000,
|
|
322
|
+
tokensSonnet: 20000,
|
|
323
|
+
apiCostUsd: 0.21,
|
|
324
|
+
humanCostUsd: 650,
|
|
325
|
+
totalCostUsd: 650.21,
|
|
326
|
+
completedAt: '2025-01-01T00:00:00Z',
|
|
327
|
+
notes: 'done',
|
|
328
|
+
},
|
|
329
|
+
}));
|
|
330
|
+
expect(result.items.find((i) => i.id === 'dod-8')?.status).toBe('passed');
|
|
331
|
+
});
|
|
332
|
+
it('should mark actuals as pending when actuals is null', () => {
|
|
333
|
+
const result = generateDoD(makeSpec({ actuals: null }));
|
|
334
|
+
expect(result.items.find((i) => i.id === 'dod-8')?.status).toBe('pending');
|
|
335
|
+
});
|
|
336
|
+
it('should set isDone=false when not all required items are passed', () => {
|
|
337
|
+
const result = generateDoD(makeSpec({ status: 'draft' }));
|
|
338
|
+
expect(result.isDone).toBe(false);
|
|
339
|
+
});
|
|
340
|
+
it('should set completedAt=null when isDone is false', () => {
|
|
341
|
+
const result = generateDoD(makeSpec({ status: 'draft' }));
|
|
342
|
+
expect(result.completedAt).toBeNull();
|
|
343
|
+
});
|
|
344
|
+
it('should set isDone=true and completedAt when all required items pass', () => {
|
|
345
|
+
const validation = {
|
|
346
|
+
matches: ['a'],
|
|
347
|
+
missing: [],
|
|
348
|
+
extra: [],
|
|
349
|
+
fieldsImplemented: 1,
|
|
350
|
+
fieldsTotal: 1,
|
|
351
|
+
score: 100,
|
|
352
|
+
qualityIssues: [],
|
|
353
|
+
};
|
|
354
|
+
const spec = makeSpec({
|
|
355
|
+
status: 'done',
|
|
356
|
+
actuals: {
|
|
357
|
+
devHours: 8,
|
|
358
|
+
reviewHours: 2,
|
|
359
|
+
tokensOpus: 10000,
|
|
360
|
+
tokensSonnet: 20000,
|
|
361
|
+
apiCostUsd: 0.21,
|
|
362
|
+
humanCostUsd: 650,
|
|
363
|
+
totalCostUsd: 650.21,
|
|
364
|
+
completedAt: '2025-01-01T00:00:00Z',
|
|
365
|
+
notes: 'done',
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
const result = generateDoD(spec, validation);
|
|
369
|
+
// dod-3 (unit tests), dod-4 (code reviewed), dod-6 (no regressions) are still pending
|
|
370
|
+
// so isDone will be false unless they are passed
|
|
371
|
+
// Actually those are required + pending => isDone = false
|
|
372
|
+
expect(result.isDone).toBe(false);
|
|
373
|
+
expect(result.completedAt).toBeNull();
|
|
374
|
+
});
|
|
375
|
+
it('should calculate passedCount correctly', () => {
|
|
376
|
+
const result = generateDoD(makeSpec());
|
|
377
|
+
const expected = result.items.filter((i) => i.status === 'passed').length;
|
|
378
|
+
expect(result.passedCount).toBe(expected);
|
|
379
|
+
});
|
|
380
|
+
it('should calculate requiredCount correctly', () => {
|
|
381
|
+
const result = generateDoD(makeSpec());
|
|
382
|
+
const expected = result.items.filter((i) => i.required).length;
|
|
383
|
+
expect(result.requiredCount).toBe(expected);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
// ============================================================
|
|
387
|
+
// generateChecklist
|
|
388
|
+
// ============================================================
|
|
389
|
+
describe('generateChecklist', () => {
|
|
390
|
+
it('should generate checklist items for a backend spec', () => {
|
|
391
|
+
const result = generateChecklist(makeSpec({ target: 'backend' }));
|
|
392
|
+
expect(result.items.length).toBeGreaterThan(0);
|
|
393
|
+
expect(result.specId).toBe('SPEC-001');
|
|
394
|
+
});
|
|
395
|
+
it('should include UX items for frontend specs', () => {
|
|
396
|
+
const result = generateChecklist(makeSpec({ target: 'frontend' }));
|
|
397
|
+
const uxItems = result.items.filter((i) => i.category === 'ux');
|
|
398
|
+
expect(uxItems.length).toBe(2);
|
|
399
|
+
});
|
|
400
|
+
it('should include UX items for fullstack specs', () => {
|
|
401
|
+
const result = generateChecklist(makeSpec({ target: 'fullstack' }));
|
|
402
|
+
const uxItems = result.items.filter((i) => i.category === 'ux');
|
|
403
|
+
expect(uxItems.length).toBe(2);
|
|
404
|
+
});
|
|
405
|
+
it('should NOT include UX items for backend-only specs', () => {
|
|
406
|
+
const result = generateChecklist(makeSpec({ target: 'backend' }));
|
|
407
|
+
const uxItems = result.items.filter((i) => i.category === 'ux');
|
|
408
|
+
expect(uxItems).toHaveLength(0);
|
|
409
|
+
});
|
|
410
|
+
it('should NOT include UX items for infrastructure specs', () => {
|
|
411
|
+
const result = generateChecklist(makeSpec({ target: 'infrastructure' }));
|
|
412
|
+
const uxItems = result.items.filter((i) => i.category === 'ux');
|
|
413
|
+
expect(uxItems).toHaveLength(0);
|
|
414
|
+
});
|
|
415
|
+
it('should filter by focus categories when provided', () => {
|
|
416
|
+
const result = generateChecklist(makeSpec({ target: 'fullstack' }), ['security']);
|
|
417
|
+
expect(result.items.every((i) => i.category === 'security')).toBe(true);
|
|
418
|
+
expect(result.items.length).toBeGreaterThan(0);
|
|
419
|
+
});
|
|
420
|
+
it('should filter by multiple focus categories', () => {
|
|
421
|
+
const result = generateChecklist(makeSpec({ target: 'frontend' }), ['security', 'ux']);
|
|
422
|
+
const categories = new Set(result.items.map((i) => i.category));
|
|
423
|
+
for (const cat of categories) {
|
|
424
|
+
expect(['security', 'ux']).toContain(cat);
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
it('should return all categories when focus is empty array', () => {
|
|
428
|
+
const result = generateChecklist(makeSpec({ target: 'frontend' }), []);
|
|
429
|
+
const categories = new Set(result.items.map((i) => i.category));
|
|
430
|
+
expect(categories.size).toBeGreaterThan(1);
|
|
431
|
+
});
|
|
432
|
+
it('should return all categories when no focus provided', () => {
|
|
433
|
+
const result = generateChecklist(makeSpec({ target: 'frontend' }));
|
|
434
|
+
const categories = new Set(result.items.map((i) => i.category));
|
|
435
|
+
expect(categories.size).toBeGreaterThan(1);
|
|
436
|
+
});
|
|
437
|
+
it('should return empty items if focus matches nothing', () => {
|
|
438
|
+
// 'ux' items only exist for frontend/fullstack; backend spec + ux filter => empty
|
|
439
|
+
// Actually 'ux' won't exist for backend. Let's use a nonexistent category approach:
|
|
440
|
+
// focus requires items that don't exist
|
|
441
|
+
const result = generateChecklist(makeSpec({ target: 'backend' }), ['ux']);
|
|
442
|
+
expect(result.items).toHaveLength(0);
|
|
443
|
+
expect(result.score).toBe(0);
|
|
444
|
+
});
|
|
445
|
+
// validateScopeDifficulty coverage
|
|
446
|
+
it('should validate trivial scope with difficulty 1 (yes)', () => {
|
|
447
|
+
const result = generateChecklist(makeSpec({ scope: 'trivial', difficulty: 1 }));
|
|
448
|
+
expect(result.items.find((i) => i.id === 'cl-cons-1')?.answer).toBe('yes');
|
|
449
|
+
});
|
|
450
|
+
it('should validate trivial scope with difficulty 2 (yes)', () => {
|
|
451
|
+
const result = generateChecklist(makeSpec({ scope: 'trivial', difficulty: 2 }));
|
|
452
|
+
expect(result.items.find((i) => i.id === 'cl-cons-1')?.answer).toBe('yes');
|
|
453
|
+
});
|
|
454
|
+
it('should flag trivial scope with difficulty 3 (no)', () => {
|
|
455
|
+
const result = generateChecklist(makeSpec({ scope: 'trivial', difficulty: 3 }));
|
|
456
|
+
expect(result.items.find((i) => i.id === 'cl-cons-1')?.answer).toBe('no');
|
|
457
|
+
});
|
|
458
|
+
it('should validate feature scope with difficulty 1-3 (yes)', () => {
|
|
459
|
+
for (const d of [1, 2, 3]) {
|
|
460
|
+
const result = generateChecklist(makeSpec({ scope: 'feature', difficulty: d }));
|
|
461
|
+
expect(result.items.find((i) => i.id === 'cl-cons-1')?.answer).toBe('yes');
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
it('should flag feature scope with difficulty 4 (no)', () => {
|
|
465
|
+
const result = generateChecklist(makeSpec({ scope: 'feature', difficulty: 4 }));
|
|
466
|
+
expect(result.items.find((i) => i.id === 'cl-cons-1')?.answer).toBe('no');
|
|
467
|
+
});
|
|
468
|
+
it('should validate cross-module scope with difficulty 2-4 (yes)', () => {
|
|
469
|
+
for (const d of [2, 3, 4]) {
|
|
470
|
+
const result = generateChecklist(makeSpec({ scope: 'cross-module', difficulty: d }));
|
|
471
|
+
expect(result.items.find((i) => i.id === 'cl-cons-1')?.answer).toBe('yes');
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
it('should flag cross-module scope with difficulty 1 (no)', () => {
|
|
475
|
+
const result = generateChecklist(makeSpec({ scope: 'cross-module', difficulty: 1 }));
|
|
476
|
+
expect(result.items.find((i) => i.id === 'cl-cons-1')?.answer).toBe('no');
|
|
477
|
+
});
|
|
478
|
+
it('should flag cross-module scope with difficulty 5 (no)', () => {
|
|
479
|
+
const result = generateChecklist(makeSpec({ scope: 'cross-module', difficulty: 5 }));
|
|
480
|
+
expect(result.items.find((i) => i.id === 'cl-cons-1')?.answer).toBe('no');
|
|
481
|
+
});
|
|
482
|
+
it('should validate architectural scope with difficulty 3-5 (yes)', () => {
|
|
483
|
+
for (const d of [3, 4, 5]) {
|
|
484
|
+
const result = generateChecklist(makeSpec({ scope: 'architectural', difficulty: d }));
|
|
485
|
+
expect(result.items.find((i) => i.id === 'cl-cons-1')?.answer).toBe('yes');
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
it('should flag architectural scope with difficulty 1 (no)', () => {
|
|
489
|
+
const result = generateChecklist(makeSpec({ scope: 'architectural', difficulty: 1 }));
|
|
490
|
+
expect(result.items.find((i) => i.id === 'cl-cons-1')?.answer).toBe('no');
|
|
491
|
+
});
|
|
492
|
+
it('should return yes for unknown scope (always valid)', () => {
|
|
493
|
+
const result = generateChecklist(makeSpec({ scope: 'unknown-scope' }));
|
|
494
|
+
expect(result.items.find((i) => i.id === 'cl-cons-1')?.answer).toBe('yes');
|
|
495
|
+
});
|
|
496
|
+
// Completeness checks for huPath / fichaTecnicaPath
|
|
497
|
+
it('should mark cl-comp-1 as pending when huPath exists', () => {
|
|
498
|
+
const result = generateChecklist(makeSpec({ huPath: '/some/HU.md' }));
|
|
499
|
+
expect(result.items.find((i) => i.id === 'cl-comp-1')?.answer).toBe('pending');
|
|
500
|
+
});
|
|
501
|
+
it('should mark cl-comp-1 as no when huPath is empty', () => {
|
|
502
|
+
const result = generateChecklist(makeSpec({ huPath: '' }));
|
|
503
|
+
expect(result.items.find((i) => i.id === 'cl-comp-1')?.answer).toBe('no');
|
|
504
|
+
});
|
|
505
|
+
it('should mark cl-comp-2 as pending when fichaTecnicaPath exists', () => {
|
|
506
|
+
const result = generateChecklist(makeSpec({ fichaTecnicaPath: '/some/FICHA.md' }));
|
|
507
|
+
expect(result.items.find((i) => i.id === 'cl-comp-2')?.answer).toBe('pending');
|
|
508
|
+
});
|
|
509
|
+
it('should mark cl-comp-2 as no when fichaTecnicaPath is empty', () => {
|
|
510
|
+
const result = generateChecklist(makeSpec({ fichaTecnicaPath: '' }));
|
|
511
|
+
expect(result.items.find((i) => i.id === 'cl-comp-2')?.answer).toBe('no');
|
|
512
|
+
});
|
|
513
|
+
// Feasibility blocker check
|
|
514
|
+
it('should mark cl-feas-2 as yes when no blockers', () => {
|
|
515
|
+
const result = generateChecklist(makeSpec({ blockedBy: [] }));
|
|
516
|
+
expect(result.items.find((i) => i.id === 'cl-feas-2')?.answer).toBe('yes');
|
|
517
|
+
});
|
|
518
|
+
it('should mark cl-feas-2 as no when blockers exist', () => {
|
|
519
|
+
const result = generateChecklist(makeSpec({ blockedBy: ['SPEC-X'] }));
|
|
520
|
+
expect(result.items.find((i) => i.id === 'cl-feas-2')?.answer).toBe('no');
|
|
521
|
+
});
|
|
522
|
+
// Score calculation
|
|
523
|
+
it('should calculate score correctly', () => {
|
|
524
|
+
const result = generateChecklist(makeSpec({ scope: 'feature', difficulty: 2, blockedBy: [] }));
|
|
525
|
+
const passedCount = result.items.filter((i) => i.answer === 'yes').length;
|
|
526
|
+
const expectedScore = Math.round((passedCount / result.items.length) * 100);
|
|
527
|
+
expect(result.passedCount).toBe(passedCount);
|
|
528
|
+
expect(result.score).toBe(expectedScore);
|
|
529
|
+
});
|
|
530
|
+
it('should return score 0 when no yes answers', () => {
|
|
531
|
+
const result = generateChecklist(makeSpec({
|
|
532
|
+
huPath: '',
|
|
533
|
+
fichaTecnicaPath: '',
|
|
534
|
+
blockedBy: ['x'],
|
|
535
|
+
scope: 'architectural',
|
|
536
|
+
difficulty: 1,
|
|
537
|
+
target: 'backend',
|
|
538
|
+
}));
|
|
539
|
+
const yesCount = result.items.filter((i) => i.answer === 'yes').length;
|
|
540
|
+
expect(yesCount).toBe(0);
|
|
541
|
+
expect(result.score).toBe(0);
|
|
542
|
+
});
|
|
543
|
+
it('should include generatedAt ISO timestamp', () => {
|
|
544
|
+
const result = generateChecklist(makeSpec());
|
|
545
|
+
expect(result.generatedAt).toBeDefined();
|
|
546
|
+
expect(new Date(result.generatedAt).toISOString()).toBe(result.generatedAt);
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
// ============================================================
|
|
550
|
+
// validateSpec
|
|
551
|
+
// ============================================================
|
|
552
|
+
describe('validateSpec', () => {
|
|
553
|
+
const projectPath = '/test/project';
|
|
554
|
+
it('should return score 0 when no criteria match (no files, HU+ficha unreadable)', async () => {
|
|
555
|
+
// Both readFile calls will fail (default mock), glob returns nothing
|
|
556
|
+
const spec = makeSpec({ target: 'backend' });
|
|
557
|
+
const result = await validateSpec(spec, projectPath);
|
|
558
|
+
// extractCriteria falls back to metadata-based criteria
|
|
559
|
+
// checkCriterion won't find anything because no files, no fileContents
|
|
560
|
+
expect(result.score).toBe(0);
|
|
561
|
+
expect(result.fieldsTotal).toBeGreaterThan(0);
|
|
562
|
+
expect(result.missing.length).toBeGreaterThan(0);
|
|
563
|
+
});
|
|
564
|
+
it('should return fieldsTotal 0 and score 0 for empty criteria dedup edge', async () => {
|
|
565
|
+
// Prove that when criteria are generated, we get something
|
|
566
|
+
const spec = makeSpec({ target: 'backend' });
|
|
567
|
+
const result = await validateSpec(spec, projectPath);
|
|
568
|
+
// Default criteria: '"Feature ..." is implemented', 'API endpoints respond correctly', 'No regression in existing tests'
|
|
569
|
+
expect(result.fieldsTotal).toBe(3);
|
|
570
|
+
});
|
|
571
|
+
it('should generate frontend criteria when target is frontend', async () => {
|
|
572
|
+
const spec = makeSpec({ target: 'frontend' });
|
|
573
|
+
const result = await validateSpec(spec, projectPath);
|
|
574
|
+
// criteria: Feature implemented, UI components render correctly, No regression
|
|
575
|
+
expect(result.fieldsTotal).toBe(3);
|
|
576
|
+
expect(result.missing).toContain('UI components render correctly');
|
|
577
|
+
});
|
|
578
|
+
it('should generate fullstack criteria for both UI and API', async () => {
|
|
579
|
+
const spec = makeSpec({ target: 'fullstack' });
|
|
580
|
+
const result = await validateSpec(spec, projectPath);
|
|
581
|
+
// Feature implemented, UI components render correctly, API endpoints respond correctly, No regression
|
|
582
|
+
expect(result.fieldsTotal).toBe(4);
|
|
583
|
+
});
|
|
584
|
+
it('should generate shared target criteria (only base + regression)', async () => {
|
|
585
|
+
const spec = makeSpec({ target: 'shared' });
|
|
586
|
+
const result = await validateSpec(spec, projectPath);
|
|
587
|
+
// Feature implemented, No regression
|
|
588
|
+
expect(result.fieldsTotal).toBe(2);
|
|
589
|
+
});
|
|
590
|
+
// extractCriteria: extract from HU.md acceptance criteria section
|
|
591
|
+
it('should extract acceptance criteria from HU.md', async () => {
|
|
592
|
+
const huContent = [
|
|
593
|
+
'# User Story',
|
|
594
|
+
'## Acceptance Criteria',
|
|
595
|
+
'- The user can log in successfully',
|
|
596
|
+
'- The user sees a dashboard after login',
|
|
597
|
+
'## Other Section',
|
|
598
|
+
'Some text',
|
|
599
|
+
].join('\n');
|
|
600
|
+
mockReadFile.mockImplementation(((path) => {
|
|
601
|
+
if (String(path).includes('HU')) {
|
|
602
|
+
return huContent;
|
|
603
|
+
}
|
|
604
|
+
throw new Error('ENOENT');
|
|
605
|
+
}));
|
|
606
|
+
const spec = makeSpec();
|
|
607
|
+
const result = await validateSpec(spec, projectPath);
|
|
608
|
+
// Extracted: "The user can log in successfully", "The user sees a dashboard after login"
|
|
609
|
+
expect(result.fieldsTotal).toBe(2);
|
|
610
|
+
});
|
|
611
|
+
it('should extract acceptance criteria using Spanish heading', async () => {
|
|
612
|
+
const huContent = [
|
|
613
|
+
'# Historia de Usuario',
|
|
614
|
+
'## Criterios de Aceptaci\u00f3n',
|
|
615
|
+
'- El usuario puede iniciar sesion correctamente',
|
|
616
|
+
'## Otra Seccion',
|
|
617
|
+
].join('\n');
|
|
618
|
+
mockReadFile.mockImplementation(((path) => {
|
|
619
|
+
if (String(path).includes('HU')) {
|
|
620
|
+
return huContent;
|
|
621
|
+
}
|
|
622
|
+
throw new Error('ENOENT');
|
|
623
|
+
}));
|
|
624
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
625
|
+
expect(result.fieldsTotal).toBe(1);
|
|
626
|
+
expect(result.missing[0] ?? result.matches[0]).toContain('El usuario puede iniciar sesion correctamente');
|
|
627
|
+
});
|
|
628
|
+
it('should extract Given/When/Then patterns from HU.md', async () => {
|
|
629
|
+
const huContent = [
|
|
630
|
+
'# User Story',
|
|
631
|
+
'Given the user is logged in',
|
|
632
|
+
'When they click the button',
|
|
633
|
+
'Then they see a confirmation',
|
|
634
|
+
].join('\n');
|
|
635
|
+
mockReadFile.mockImplementation(((path) => {
|
|
636
|
+
if (String(path).includes('HU')) {
|
|
637
|
+
return huContent;
|
|
638
|
+
}
|
|
639
|
+
throw new Error('ENOENT');
|
|
640
|
+
}));
|
|
641
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
642
|
+
// 3 GWT matches
|
|
643
|
+
expect(result.fieldsTotal).toBe(3);
|
|
644
|
+
});
|
|
645
|
+
it('should extract Spanish GWT patterns (Dado/Cuando/Entonces)', async () => {
|
|
646
|
+
const huContent = [
|
|
647
|
+
'Dado que el usuario tiene permisos de admin',
|
|
648
|
+
'Cuando accede al panel de control exitosamente',
|
|
649
|
+
'Entonces ve las opciones de administracion completas',
|
|
650
|
+
].join('\n');
|
|
651
|
+
mockReadFile.mockImplementation(((path) => {
|
|
652
|
+
if (String(path).includes('HU')) {
|
|
653
|
+
return huContent;
|
|
654
|
+
}
|
|
655
|
+
throw new Error('ENOENT');
|
|
656
|
+
}));
|
|
657
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
658
|
+
expect(result.fieldsTotal).toBe(3);
|
|
659
|
+
});
|
|
660
|
+
it('should extract technical requirements from FICHA-TECNICA.md', async () => {
|
|
661
|
+
const fichaContent = [
|
|
662
|
+
'# Technical Design',
|
|
663
|
+
'## Requirements',
|
|
664
|
+
'- Database schema must support multi-tenancy',
|
|
665
|
+
'- API must handle 1000 requests per second',
|
|
666
|
+
'## Implementation',
|
|
667
|
+
'Details here',
|
|
668
|
+
].join('\n');
|
|
669
|
+
mockReadFile.mockImplementation(((path) => {
|
|
670
|
+
if (String(path).includes('FICHA')) {
|
|
671
|
+
return fichaContent;
|
|
672
|
+
}
|
|
673
|
+
throw new Error('ENOENT');
|
|
674
|
+
}));
|
|
675
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
676
|
+
expect(result.fieldsTotal).toBe(2);
|
|
677
|
+
});
|
|
678
|
+
it('should extract from FICHA-TECNICA with "requisitos" heading', async () => {
|
|
679
|
+
const fichaContent = [
|
|
680
|
+
'# Ficha Tecnica',
|
|
681
|
+
'## Requisitos',
|
|
682
|
+
'- Soporte para multiples idiomas nativo',
|
|
683
|
+
'## Otro',
|
|
684
|
+
].join('\n');
|
|
685
|
+
mockReadFile.mockImplementation(((path) => {
|
|
686
|
+
if (String(path).includes('FICHA')) {
|
|
687
|
+
return fichaContent;
|
|
688
|
+
}
|
|
689
|
+
throw new Error('ENOENT');
|
|
690
|
+
}));
|
|
691
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
692
|
+
expect(result.fieldsTotal).toBe(1);
|
|
693
|
+
});
|
|
694
|
+
it('should extract from FICHA-TECNICA with "technical" heading', async () => {
|
|
695
|
+
const fichaContent = [
|
|
696
|
+
'# Spec',
|
|
697
|
+
'## Technical Details',
|
|
698
|
+
'- Must use PostgreSQL for persistence layer',
|
|
699
|
+
'## End',
|
|
700
|
+
].join('\n');
|
|
701
|
+
mockReadFile.mockImplementation(((path) => {
|
|
702
|
+
if (String(path).includes('FICHA')) {
|
|
703
|
+
return fichaContent;
|
|
704
|
+
}
|
|
705
|
+
throw new Error('ENOENT');
|
|
706
|
+
}));
|
|
707
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
708
|
+
expect(result.fieldsTotal).toBe(1);
|
|
709
|
+
});
|
|
710
|
+
it('should extract from FICHA-TECNICA with "implementation" heading', async () => {
|
|
711
|
+
const fichaContent = [
|
|
712
|
+
'# Spec',
|
|
713
|
+
'## Implementation Notes',
|
|
714
|
+
'1. Create the service layer for authentication',
|
|
715
|
+
'## End',
|
|
716
|
+
].join('\n');
|
|
717
|
+
mockReadFile.mockImplementation(((path) => {
|
|
718
|
+
if (String(path).includes('FICHA')) {
|
|
719
|
+
return fichaContent;
|
|
720
|
+
}
|
|
721
|
+
throw new Error('ENOENT');
|
|
722
|
+
}));
|
|
723
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
724
|
+
expect(result.fieldsTotal).toBe(1);
|
|
725
|
+
});
|
|
726
|
+
it('should deduplicate criteria from both files', async () => {
|
|
727
|
+
const huContent = [
|
|
728
|
+
'# Story',
|
|
729
|
+
'## Acceptance Criteria',
|
|
730
|
+
'- Must implement logging service correctly',
|
|
731
|
+
].join('\n');
|
|
732
|
+
const fichaContent = [
|
|
733
|
+
'# Tech',
|
|
734
|
+
'## Requirements',
|
|
735
|
+
'- Must implement logging service correctly',
|
|
736
|
+
].join('\n');
|
|
737
|
+
mockReadFile.mockImplementation(((path) => {
|
|
738
|
+
const p = String(path);
|
|
739
|
+
if (p.includes('HU')) {
|
|
740
|
+
return huContent;
|
|
741
|
+
}
|
|
742
|
+
if (p.includes('FICHA')) {
|
|
743
|
+
return fichaContent;
|
|
744
|
+
}
|
|
745
|
+
throw new Error('ENOENT');
|
|
746
|
+
}));
|
|
747
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
748
|
+
// Deduplicated: only 1 criterion
|
|
749
|
+
expect(result.fieldsTotal).toBe(1);
|
|
750
|
+
});
|
|
751
|
+
it('should merge criteria from both HU and FICHA', async () => {
|
|
752
|
+
const huContent = [
|
|
753
|
+
'# Story',
|
|
754
|
+
'## Acceptance Criteria',
|
|
755
|
+
'- Feature A must work correctly in production',
|
|
756
|
+
].join('\n');
|
|
757
|
+
const fichaContent = [
|
|
758
|
+
'# Tech',
|
|
759
|
+
'## Requirements',
|
|
760
|
+
'- Feature B must be implemented with tests',
|
|
761
|
+
].join('\n');
|
|
762
|
+
mockReadFile.mockImplementation(((path) => {
|
|
763
|
+
const p = String(path);
|
|
764
|
+
if (p.includes('HU')) {
|
|
765
|
+
return huContent;
|
|
766
|
+
}
|
|
767
|
+
if (p.includes('FICHA')) {
|
|
768
|
+
return fichaContent;
|
|
769
|
+
}
|
|
770
|
+
throw new Error('ENOENT');
|
|
771
|
+
}));
|
|
772
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
773
|
+
expect(result.fieldsTotal).toBe(2);
|
|
774
|
+
});
|
|
775
|
+
// extractSection: sub-heading within captured section (deeper level does NOT break)
|
|
776
|
+
it('should include sub-heading content within captured section', async () => {
|
|
777
|
+
const huContent = [
|
|
778
|
+
'## Acceptance Criteria',
|
|
779
|
+
'- Top level criterion is long enough',
|
|
780
|
+
'### Sub-section Details',
|
|
781
|
+
'- Sub-section criterion is long enough',
|
|
782
|
+
'## Next Section At Same Level',
|
|
783
|
+
'- Should not be captured at all here',
|
|
784
|
+
].join('\n');
|
|
785
|
+
mockReadFile.mockImplementation(((path) => {
|
|
786
|
+
if (String(path).includes('HU')) {
|
|
787
|
+
return huContent;
|
|
788
|
+
}
|
|
789
|
+
throw new Error('ENOENT');
|
|
790
|
+
}));
|
|
791
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
792
|
+
// Both top-level and sub-section criteria should be captured (sub-heading is deeper)
|
|
793
|
+
expect(result.fieldsTotal).toBe(2);
|
|
794
|
+
});
|
|
795
|
+
// extractSection: heading match that is already capturing but hits deeper heading
|
|
796
|
+
it('should not break on a deeper heading when already capturing', async () => {
|
|
797
|
+
const huContent = [
|
|
798
|
+
'## Acceptance Criteria',
|
|
799
|
+
'- First criterion is definitely long enough',
|
|
800
|
+
'#### Very Deep Sub-heading',
|
|
801
|
+
'- Deep criterion is also long enough here',
|
|
802
|
+
'# Top Level Heading Stops Capture',
|
|
803
|
+
'- This should not be captured anymore',
|
|
804
|
+
].join('\n');
|
|
805
|
+
mockReadFile.mockImplementation(((path) => {
|
|
806
|
+
if (String(path).includes('HU')) {
|
|
807
|
+
return huContent;
|
|
808
|
+
}
|
|
809
|
+
throw new Error('ENOENT');
|
|
810
|
+
}));
|
|
811
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
812
|
+
expect(result.fieldsTotal).toBe(2);
|
|
813
|
+
});
|
|
814
|
+
// extractListItems: numbered lists
|
|
815
|
+
it('should extract numbered list items from spec files', async () => {
|
|
816
|
+
const huContent = [
|
|
817
|
+
'# Story',
|
|
818
|
+
'## Acceptance Criteria',
|
|
819
|
+
'1. The system must validate user input data',
|
|
820
|
+
'2) The system must log all access attempts',
|
|
821
|
+
].join('\n');
|
|
822
|
+
mockReadFile.mockImplementation(((path) => {
|
|
823
|
+
if (String(path).includes('HU')) {
|
|
824
|
+
return huContent;
|
|
825
|
+
}
|
|
826
|
+
throw new Error('ENOENT');
|
|
827
|
+
}));
|
|
828
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
829
|
+
expect(result.fieldsTotal).toBe(2);
|
|
830
|
+
});
|
|
831
|
+
// extractListItems: checkbox lists
|
|
832
|
+
it('should extract checkbox list items from spec files', async () => {
|
|
833
|
+
const huContent = [
|
|
834
|
+
'# Story',
|
|
835
|
+
'## Acceptance Criteria',
|
|
836
|
+
'- [x] Completed criterion that already works',
|
|
837
|
+
'- [ ] Pending criterion that needs implementation',
|
|
838
|
+
'- [X] Another completed criterion that works',
|
|
839
|
+
].join('\n');
|
|
840
|
+
mockReadFile.mockImplementation(((path) => {
|
|
841
|
+
if (String(path).includes('HU')) {
|
|
842
|
+
return huContent;
|
|
843
|
+
}
|
|
844
|
+
throw new Error('ENOENT');
|
|
845
|
+
}));
|
|
846
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
847
|
+
expect(result.fieldsTotal).toBe(3);
|
|
848
|
+
});
|
|
849
|
+
// extractListItems: skip short items (<=5 chars)
|
|
850
|
+
it('should skip trivially short list items (<=5 chars)', async () => {
|
|
851
|
+
const huContent = [
|
|
852
|
+
'# Story',
|
|
853
|
+
'## Acceptance Criteria',
|
|
854
|
+
'- Yes',
|
|
855
|
+
'- No',
|
|
856
|
+
'- A valid criterion that is long enough',
|
|
857
|
+
'1. Abc',
|
|
858
|
+
'2. Another valid numbered criterion here',
|
|
859
|
+
].join('\n');
|
|
860
|
+
mockReadFile.mockImplementation(((path) => {
|
|
861
|
+
if (String(path).includes('HU')) {
|
|
862
|
+
return huContent;
|
|
863
|
+
}
|
|
864
|
+
throw new Error('ENOENT');
|
|
865
|
+
}));
|
|
866
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
867
|
+
// "Yes" (3 chars), "No" (2 chars), "Abc" (3 chars) are skipped
|
|
868
|
+
expect(result.fieldsTotal).toBe(2);
|
|
869
|
+
});
|
|
870
|
+
// extractListItems: different bullet styles
|
|
871
|
+
it('should extract items with * and + bullet styles', async () => {
|
|
872
|
+
const huContent = [
|
|
873
|
+
'# Story',
|
|
874
|
+
'## Acceptance Criteria',
|
|
875
|
+
'* Star bullet criterion that is long',
|
|
876
|
+
'+ Plus bullet criterion that is long',
|
|
877
|
+
].join('\n');
|
|
878
|
+
mockReadFile.mockImplementation(((path) => {
|
|
879
|
+
if (String(path).includes('HU')) {
|
|
880
|
+
return huContent;
|
|
881
|
+
}
|
|
882
|
+
throw new Error('ENOENT');
|
|
883
|
+
}));
|
|
884
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
885
|
+
expect(result.fieldsTotal).toBe(2);
|
|
886
|
+
});
|
|
887
|
+
// extractSection: heading level boundary
|
|
888
|
+
it('should stop section extraction when a same-level heading is encountered', async () => {
|
|
889
|
+
const huContent = [
|
|
890
|
+
'## Acceptance Criteria',
|
|
891
|
+
'- Criterion one is valid here',
|
|
892
|
+
'## Another Section At Same Level',
|
|
893
|
+
'- This should NOT be extracted',
|
|
894
|
+
].join('\n');
|
|
895
|
+
mockReadFile.mockImplementation(((path) => {
|
|
896
|
+
if (String(path).includes('HU')) {
|
|
897
|
+
return huContent;
|
|
898
|
+
}
|
|
899
|
+
throw new Error('ENOENT');
|
|
900
|
+
}));
|
|
901
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
902
|
+
expect(result.fieldsTotal).toBe(1);
|
|
903
|
+
expect(result.missing[0] ?? result.matches[0]).toContain('Criterion one is valid here');
|
|
904
|
+
});
|
|
905
|
+
// extractSection: higher-level heading stops extraction
|
|
906
|
+
it('should stop section extraction when a higher-level heading is encountered', async () => {
|
|
907
|
+
const huContent = [
|
|
908
|
+
'### Acceptance Criteria',
|
|
909
|
+
'- Criterion in subsection is valid',
|
|
910
|
+
'## Higher Level Heading Here',
|
|
911
|
+
'- Should not be captured here',
|
|
912
|
+
].join('\n');
|
|
913
|
+
mockReadFile.mockImplementation(((path) => {
|
|
914
|
+
if (String(path).includes('HU')) {
|
|
915
|
+
return huContent;
|
|
916
|
+
}
|
|
917
|
+
throw new Error('ENOENT');
|
|
918
|
+
}));
|
|
919
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
920
|
+
expect(result.fieldsTotal).toBe(1);
|
|
921
|
+
});
|
|
922
|
+
// extractSection: returns null when no matching heading
|
|
923
|
+
it('should fallback to metadata criteria when no matching section heading', async () => {
|
|
924
|
+
const huContent = ['# Some Heading', '## Not Related', '- Some item here'].join('\n');
|
|
925
|
+
mockReadFile.mockImplementation(((path) => {
|
|
926
|
+
if (String(path).includes('HU')) {
|
|
927
|
+
return huContent;
|
|
928
|
+
}
|
|
929
|
+
throw new Error('ENOENT');
|
|
930
|
+
}));
|
|
931
|
+
const result = await validateSpec(makeSpec({ target: 'backend' }), projectPath);
|
|
932
|
+
// No section match, no GWT => fallback to metadata criteria
|
|
933
|
+
expect(result.fieldsTotal).toBe(3); // Feature + API + No regression
|
|
934
|
+
});
|
|
935
|
+
// extractSection: empty section (heading exists but no content before next heading)
|
|
936
|
+
it('should return null for section with no content lines', async () => {
|
|
937
|
+
const huContent = [
|
|
938
|
+
'## Acceptance Criteria',
|
|
939
|
+
'## Next Section Immediately',
|
|
940
|
+
'- Content in next section here',
|
|
941
|
+
].join('\n');
|
|
942
|
+
mockReadFile.mockImplementation(((path) => {
|
|
943
|
+
if (String(path).includes('HU')) {
|
|
944
|
+
return huContent;
|
|
945
|
+
}
|
|
946
|
+
throw new Error('ENOENT');
|
|
947
|
+
}));
|
|
948
|
+
const result = await validateSpec(makeSpec({ target: 'backend' }), projectPath);
|
|
949
|
+
// Empty AC section => no criteria from it, fallback to metadata
|
|
950
|
+
expect(result.fieldsTotal).toBe(3);
|
|
951
|
+
});
|
|
952
|
+
// scanCodeForSpec: impactAnalysis with affectedFiles
|
|
953
|
+
it('should scan affected files from impactAnalysis', async () => {
|
|
954
|
+
const spec = makeSpec({
|
|
955
|
+
impactAnalysis: {
|
|
956
|
+
affectedModules: ['engine'],
|
|
957
|
+
affectedFiles: ['src/engine/validator.ts'],
|
|
958
|
+
breakingChanges: false,
|
|
959
|
+
requiresMigration: false,
|
|
960
|
+
migrationReversible: false,
|
|
961
|
+
requiresFeatureFlag: false,
|
|
962
|
+
rollbackPlan: '',
|
|
963
|
+
testingStrategy: {
|
|
964
|
+
unitTests: [],
|
|
965
|
+
integrationTests: [],
|
|
966
|
+
e2eTests: [],
|
|
967
|
+
manualTests: [],
|
|
968
|
+
},
|
|
969
|
+
environments: [],
|
|
970
|
+
},
|
|
971
|
+
});
|
|
972
|
+
mockReadFile.mockImplementation(((path) => {
|
|
973
|
+
const p = String(path);
|
|
974
|
+
if (p.includes('validator.ts')) {
|
|
975
|
+
return 'export function validateSpec() {}';
|
|
976
|
+
}
|
|
977
|
+
throw new Error('ENOENT');
|
|
978
|
+
}));
|
|
979
|
+
const result = await validateSpec(spec, projectPath);
|
|
980
|
+
// With affected files and content available, optimistic check passes
|
|
981
|
+
expect(result.fieldsTotal).toBeGreaterThan(0);
|
|
982
|
+
});
|
|
983
|
+
it('should handle impactAnalysis file that doesnt exist', async () => {
|
|
984
|
+
const spec = makeSpec({
|
|
985
|
+
impactAnalysis: {
|
|
986
|
+
affectedModules: ['engine'],
|
|
987
|
+
affectedFiles: ['src/nonexistent.ts'],
|
|
988
|
+
breakingChanges: false,
|
|
989
|
+
requiresMigration: false,
|
|
990
|
+
migrationReversible: false,
|
|
991
|
+
requiresFeatureFlag: false,
|
|
992
|
+
rollbackPlan: '',
|
|
993
|
+
testingStrategy: {
|
|
994
|
+
unitTests: [],
|
|
995
|
+
integrationTests: [],
|
|
996
|
+
e2eTests: [],
|
|
997
|
+
manualTests: [],
|
|
998
|
+
},
|
|
999
|
+
environments: [],
|
|
1000
|
+
},
|
|
1001
|
+
});
|
|
1002
|
+
// readFile always throws (default)
|
|
1003
|
+
const result = await validateSpec(spec, projectPath);
|
|
1004
|
+
// File doesn't exist, so affectedFiles is empty
|
|
1005
|
+
expect(result.fieldsTotal).toBeGreaterThan(0);
|
|
1006
|
+
});
|
|
1007
|
+
// scanCodeForSpec: gitBranch set (no-op but covers the branch)
|
|
1008
|
+
it('should cover the gitBranch code path', async () => {
|
|
1009
|
+
const spec = makeSpec({ gitBranch: 'feature/test-branch' });
|
|
1010
|
+
const result = await validateSpec(spec, projectPath);
|
|
1011
|
+
expect(result).toBeDefined();
|
|
1012
|
+
});
|
|
1013
|
+
// scanCodeForSpec: glob finds matching files
|
|
1014
|
+
it('should detect unexpected files matching the slug', async () => {
|
|
1015
|
+
mockGlob.mockResolvedValue(['src/test-spec-title.ts']);
|
|
1016
|
+
const spec = makeSpec({ slug: 'test-spec-title' });
|
|
1017
|
+
const result = await validateSpec(spec, projectPath);
|
|
1018
|
+
// The file is unexpected (not in affectedFiles)
|
|
1019
|
+
expect(result.extra).toContain('Unexpected file: src/test-spec-title.ts');
|
|
1020
|
+
});
|
|
1021
|
+
it('should not mark file as unexpected if it is in affectedFiles', async () => {
|
|
1022
|
+
const spec = makeSpec({
|
|
1023
|
+
slug: 'test-spec-title',
|
|
1024
|
+
impactAnalysis: {
|
|
1025
|
+
affectedModules: [],
|
|
1026
|
+
affectedFiles: ['src/test-spec-title.ts'],
|
|
1027
|
+
breakingChanges: false,
|
|
1028
|
+
requiresMigration: false,
|
|
1029
|
+
migrationReversible: false,
|
|
1030
|
+
requiresFeatureFlag: false,
|
|
1031
|
+
rollbackPlan: '',
|
|
1032
|
+
testingStrategy: {
|
|
1033
|
+
unitTests: [],
|
|
1034
|
+
integrationTests: [],
|
|
1035
|
+
e2eTests: [],
|
|
1036
|
+
manualTests: [],
|
|
1037
|
+
},
|
|
1038
|
+
environments: [],
|
|
1039
|
+
},
|
|
1040
|
+
});
|
|
1041
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1042
|
+
const p = String(path);
|
|
1043
|
+
if (p.includes('test-spec-title')) {
|
|
1044
|
+
return 'export const x = 1;';
|
|
1045
|
+
}
|
|
1046
|
+
throw new Error('ENOENT');
|
|
1047
|
+
}));
|
|
1048
|
+
mockGlob.mockResolvedValue(['src/test-spec-title.ts']);
|
|
1049
|
+
const result = await validateSpec(spec, projectPath);
|
|
1050
|
+
expect(result.extra).not.toContain('Unexpected file: src/test-spec-title.ts');
|
|
1051
|
+
});
|
|
1052
|
+
// scanCodeForSpec: glob error
|
|
1053
|
+
it('should handle glob errors gracefully in scanCodeForSpec', async () => {
|
|
1054
|
+
mockGlob.mockRejectedValue(new Error('glob failure'));
|
|
1055
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
1056
|
+
expect(result).toBeDefined();
|
|
1057
|
+
expect(result.extra).toHaveLength(0);
|
|
1058
|
+
});
|
|
1059
|
+
// checkCriterion: file existence criteria
|
|
1060
|
+
it('should check file existence for "create file" criterion', async () => {
|
|
1061
|
+
const huContent = [
|
|
1062
|
+
'## Acceptance Criteria',
|
|
1063
|
+
'- Create file "src/utils.ts" for utilities',
|
|
1064
|
+
].join('\n');
|
|
1065
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1066
|
+
if (String(path).includes('HU')) {
|
|
1067
|
+
return huContent;
|
|
1068
|
+
}
|
|
1069
|
+
throw new Error('ENOENT');
|
|
1070
|
+
}));
|
|
1071
|
+
mockStat.mockResolvedValue({});
|
|
1072
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
1073
|
+
expect(result.matches).toContain('Create file "src/utils.ts" for utilities');
|
|
1074
|
+
});
|
|
1075
|
+
it('should mark file criterion as missing when file does not exist', async () => {
|
|
1076
|
+
const huContent = [
|
|
1077
|
+
'## Acceptance Criteria',
|
|
1078
|
+
'- Add file src/missing.ts for new module',
|
|
1079
|
+
].join('\n');
|
|
1080
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1081
|
+
if (String(path).includes('HU')) {
|
|
1082
|
+
return huContent;
|
|
1083
|
+
}
|
|
1084
|
+
throw new Error('ENOENT');
|
|
1085
|
+
}));
|
|
1086
|
+
// stat throws by default
|
|
1087
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
1088
|
+
expect(result.missing).toContain('Add file src/missing.ts for new module');
|
|
1089
|
+
});
|
|
1090
|
+
it('should detect "implement file" pattern for file check', async () => {
|
|
1091
|
+
const huContent = [
|
|
1092
|
+
'## Acceptance Criteria',
|
|
1093
|
+
'- Implement file config.json for settings',
|
|
1094
|
+
].join('\n');
|
|
1095
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1096
|
+
if (String(path).includes('HU')) {
|
|
1097
|
+
return huContent;
|
|
1098
|
+
}
|
|
1099
|
+
throw new Error('ENOENT');
|
|
1100
|
+
}));
|
|
1101
|
+
mockStat.mockResolvedValue({});
|
|
1102
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
1103
|
+
expect(result.matches.length).toBe(1);
|
|
1104
|
+
});
|
|
1105
|
+
// checkCriterion: component/function search in fileContents
|
|
1106
|
+
it('should find component name in affected file contents', async () => {
|
|
1107
|
+
const huContent = [
|
|
1108
|
+
'## Acceptance Criteria',
|
|
1109
|
+
'- Component UserDashboard must render correctly',
|
|
1110
|
+
].join('\n');
|
|
1111
|
+
const spec = makeSpec({
|
|
1112
|
+
impactAnalysis: {
|
|
1113
|
+
affectedModules: [],
|
|
1114
|
+
affectedFiles: ['src/components/dashboard.tsx'],
|
|
1115
|
+
breakingChanges: false,
|
|
1116
|
+
requiresMigration: false,
|
|
1117
|
+
migrationReversible: false,
|
|
1118
|
+
requiresFeatureFlag: false,
|
|
1119
|
+
rollbackPlan: '',
|
|
1120
|
+
testingStrategy: {
|
|
1121
|
+
unitTests: [],
|
|
1122
|
+
integrationTests: [],
|
|
1123
|
+
e2eTests: [],
|
|
1124
|
+
manualTests: [],
|
|
1125
|
+
},
|
|
1126
|
+
environments: [],
|
|
1127
|
+
},
|
|
1128
|
+
});
|
|
1129
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1130
|
+
const p = String(path);
|
|
1131
|
+
if (p.includes('HU')) {
|
|
1132
|
+
return huContent;
|
|
1133
|
+
}
|
|
1134
|
+
if (p.includes('dashboard.tsx')) {
|
|
1135
|
+
return 'export function UserDashboard() { return <div/>; }';
|
|
1136
|
+
}
|
|
1137
|
+
throw new Error('ENOENT');
|
|
1138
|
+
}));
|
|
1139
|
+
const result = await validateSpec(spec, projectPath);
|
|
1140
|
+
expect(result.matches).toContain('Component UserDashboard must render correctly');
|
|
1141
|
+
});
|
|
1142
|
+
// checkCriterion: component/function search via glob broader search
|
|
1143
|
+
it('should find component via broader glob search when not in fileContents', async () => {
|
|
1144
|
+
const huContent = [
|
|
1145
|
+
'## Acceptance Criteria',
|
|
1146
|
+
'- Function calculateTotal must work correctly',
|
|
1147
|
+
].join('\n');
|
|
1148
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1149
|
+
const p = String(path);
|
|
1150
|
+
if (p.includes('HU')) {
|
|
1151
|
+
return huContent;
|
|
1152
|
+
}
|
|
1153
|
+
if (p.includes('math.ts')) {
|
|
1154
|
+
return 'export function calculateTotal() { return 42; }';
|
|
1155
|
+
}
|
|
1156
|
+
throw new Error('ENOENT');
|
|
1157
|
+
}));
|
|
1158
|
+
// First glob call for scanCodeForSpec slug match, second for component search
|
|
1159
|
+
let callCount = 0;
|
|
1160
|
+
mockGlob.mockImplementation((() => {
|
|
1161
|
+
callCount++;
|
|
1162
|
+
if (callCount === 1) {
|
|
1163
|
+
return [];
|
|
1164
|
+
} // slug match
|
|
1165
|
+
return ['src/math.ts']; // component search
|
|
1166
|
+
}));
|
|
1167
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
1168
|
+
expect(result.matches).toContain('Function calculateTotal must work correctly');
|
|
1169
|
+
});
|
|
1170
|
+
// checkCriterion: component found via broader search but readFile fails
|
|
1171
|
+
it('should handle unreadable files during broader component search', async () => {
|
|
1172
|
+
const huContent = [
|
|
1173
|
+
'## Acceptance Criteria',
|
|
1174
|
+
'- Class AuthService must be implemented fully',
|
|
1175
|
+
].join('\n');
|
|
1176
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1177
|
+
const p = String(path);
|
|
1178
|
+
if (p.includes('HU')) {
|
|
1179
|
+
return huContent;
|
|
1180
|
+
}
|
|
1181
|
+
throw new Error('ENOENT');
|
|
1182
|
+
}));
|
|
1183
|
+
let callCount = 0;
|
|
1184
|
+
mockGlob.mockImplementation((() => {
|
|
1185
|
+
callCount++;
|
|
1186
|
+
if (callCount === 1) {
|
|
1187
|
+
return [];
|
|
1188
|
+
}
|
|
1189
|
+
return ['src/auth.ts'];
|
|
1190
|
+
}));
|
|
1191
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
1192
|
+
// auth.ts can't be read, so AuthService not found
|
|
1193
|
+
expect(result.missing).toContain('Class AuthService must be implemented fully');
|
|
1194
|
+
});
|
|
1195
|
+
// checkCriterion: glob error during broader search
|
|
1196
|
+
it('should handle glob error during broader component search', async () => {
|
|
1197
|
+
const huContent = [
|
|
1198
|
+
'## Acceptance Criteria',
|
|
1199
|
+
'- Module PaymentService must handle transactions',
|
|
1200
|
+
].join('\n');
|
|
1201
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1202
|
+
if (String(path).includes('HU')) {
|
|
1203
|
+
return huContent;
|
|
1204
|
+
}
|
|
1205
|
+
throw new Error('ENOENT');
|
|
1206
|
+
}));
|
|
1207
|
+
let callCount = 0;
|
|
1208
|
+
mockGlob.mockImplementation((() => {
|
|
1209
|
+
callCount++;
|
|
1210
|
+
if (callCount === 1) {
|
|
1211
|
+
return [];
|
|
1212
|
+
}
|
|
1213
|
+
throw new Error('glob error');
|
|
1214
|
+
}));
|
|
1215
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
1216
|
+
expect(result.missing).toContain('Module PaymentService must handle transactions');
|
|
1217
|
+
});
|
|
1218
|
+
// checkCriterion: component search limits to 50 files
|
|
1219
|
+
it('should limit broader search to first 50 files', async () => {
|
|
1220
|
+
const huContent = [
|
|
1221
|
+
'## Acceptance Criteria',
|
|
1222
|
+
'- Service TargetService must exist in codebase',
|
|
1223
|
+
].join('\n');
|
|
1224
|
+
const files = Array.from({ length: 60 }, (_, i) => `src/file${i}.ts`);
|
|
1225
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1226
|
+
const p = String(path);
|
|
1227
|
+
if (p.includes('HU')) {
|
|
1228
|
+
return huContent;
|
|
1229
|
+
}
|
|
1230
|
+
// Only file59.ts contains "TargetService" (index 59, which is beyond 50 limit)
|
|
1231
|
+
if (p.includes('file59.ts')) {
|
|
1232
|
+
return 'export class TargetService {}';
|
|
1233
|
+
}
|
|
1234
|
+
return 'export const noop = true;';
|
|
1235
|
+
}));
|
|
1236
|
+
let callCount = 0;
|
|
1237
|
+
mockGlob.mockImplementation((() => {
|
|
1238
|
+
callCount++;
|
|
1239
|
+
if (callCount === 1) {
|
|
1240
|
+
return [];
|
|
1241
|
+
}
|
|
1242
|
+
return files;
|
|
1243
|
+
}));
|
|
1244
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
1245
|
+
// file59 is at index 59, slice(0,50) excludes it
|
|
1246
|
+
expect(result.missing).toContain('Service TargetService must exist in codebase');
|
|
1247
|
+
});
|
|
1248
|
+
// checkCriterion: service name patterns
|
|
1249
|
+
it('should detect "service" keyword in criterion', async () => {
|
|
1250
|
+
const huContent = [
|
|
1251
|
+
'## Acceptance Criteria',
|
|
1252
|
+
'- Service EmailHandler must send notifications',
|
|
1253
|
+
].join('\n');
|
|
1254
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1255
|
+
const p = String(path);
|
|
1256
|
+
if (p.includes('HU')) {
|
|
1257
|
+
return huContent;
|
|
1258
|
+
}
|
|
1259
|
+
if (p.includes('email.ts')) {
|
|
1260
|
+
return 'class EmailHandler {}';
|
|
1261
|
+
}
|
|
1262
|
+
throw new Error('ENOENT');
|
|
1263
|
+
}));
|
|
1264
|
+
let callCount = 0;
|
|
1265
|
+
mockGlob.mockImplementation((() => {
|
|
1266
|
+
callCount++;
|
|
1267
|
+
if (callCount === 1) {
|
|
1268
|
+
return [];
|
|
1269
|
+
}
|
|
1270
|
+
return ['src/email.ts'];
|
|
1271
|
+
}));
|
|
1272
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
1273
|
+
expect(result.matches).toContain('Service EmailHandler must send notifications');
|
|
1274
|
+
});
|
|
1275
|
+
// checkCriterion: API endpoint criteria
|
|
1276
|
+
it('should find API endpoint criterion in file contents', async () => {
|
|
1277
|
+
const huContent = [
|
|
1278
|
+
'## Acceptance Criteria',
|
|
1279
|
+
'- Endpoint for users must return user list',
|
|
1280
|
+
].join('\n');
|
|
1281
|
+
const spec = makeSpec({
|
|
1282
|
+
impactAnalysis: {
|
|
1283
|
+
affectedModules: [],
|
|
1284
|
+
affectedFiles: ['src/routes.ts'],
|
|
1285
|
+
breakingChanges: false,
|
|
1286
|
+
requiresMigration: false,
|
|
1287
|
+
migrationReversible: false,
|
|
1288
|
+
requiresFeatureFlag: false,
|
|
1289
|
+
rollbackPlan: '',
|
|
1290
|
+
testingStrategy: {
|
|
1291
|
+
unitTests: [],
|
|
1292
|
+
integrationTests: [],
|
|
1293
|
+
e2eTests: [],
|
|
1294
|
+
manualTests: [],
|
|
1295
|
+
},
|
|
1296
|
+
environments: [],
|
|
1297
|
+
},
|
|
1298
|
+
});
|
|
1299
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1300
|
+
const p = String(path);
|
|
1301
|
+
if (p.includes('HU')) {
|
|
1302
|
+
return huContent;
|
|
1303
|
+
}
|
|
1304
|
+
if (p.includes('routes.ts')) {
|
|
1305
|
+
return 'app.get("/users", handler);';
|
|
1306
|
+
}
|
|
1307
|
+
throw new Error('ENOENT');
|
|
1308
|
+
}));
|
|
1309
|
+
const result = await validateSpec(spec, projectPath);
|
|
1310
|
+
expect(result.matches).toContain('Endpoint for users must return user list');
|
|
1311
|
+
});
|
|
1312
|
+
it('should handle endpoint not found in file contents', async () => {
|
|
1313
|
+
const huContent = [
|
|
1314
|
+
'## Acceptance Criteria',
|
|
1315
|
+
'- Route for payments must process transactions',
|
|
1316
|
+
].join('\n');
|
|
1317
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1318
|
+
if (String(path).includes('HU')) {
|
|
1319
|
+
return huContent;
|
|
1320
|
+
}
|
|
1321
|
+
throw new Error('ENOENT');
|
|
1322
|
+
}));
|
|
1323
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
1324
|
+
expect(result.missing).toContain('Route for payments must process transactions');
|
|
1325
|
+
});
|
|
1326
|
+
it('should detect "api" keyword for endpoint criterion', async () => {
|
|
1327
|
+
const huContent = [
|
|
1328
|
+
'## Acceptance Criteria',
|
|
1329
|
+
'- API products should list all products correctly',
|
|
1330
|
+
].join('\n');
|
|
1331
|
+
const spec = makeSpec({
|
|
1332
|
+
impactAnalysis: {
|
|
1333
|
+
affectedModules: [],
|
|
1334
|
+
affectedFiles: ['src/api.ts'],
|
|
1335
|
+
breakingChanges: false,
|
|
1336
|
+
requiresMigration: false,
|
|
1337
|
+
migrationReversible: false,
|
|
1338
|
+
requiresFeatureFlag: false,
|
|
1339
|
+
rollbackPlan: '',
|
|
1340
|
+
testingStrategy: {
|
|
1341
|
+
unitTests: [],
|
|
1342
|
+
integrationTests: [],
|
|
1343
|
+
e2eTests: [],
|
|
1344
|
+
manualTests: [],
|
|
1345
|
+
},
|
|
1346
|
+
environments: [],
|
|
1347
|
+
},
|
|
1348
|
+
});
|
|
1349
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1350
|
+
const p = String(path);
|
|
1351
|
+
if (p.includes('HU')) {
|
|
1352
|
+
return huContent;
|
|
1353
|
+
}
|
|
1354
|
+
if (p.includes('api.ts')) {
|
|
1355
|
+
return 'router.get("/products", listProducts);';
|
|
1356
|
+
}
|
|
1357
|
+
throw new Error('ENOENT');
|
|
1358
|
+
}));
|
|
1359
|
+
const result = await validateSpec(spec, projectPath);
|
|
1360
|
+
expect(result.matches).toContain('API products should list all products correctly');
|
|
1361
|
+
});
|
|
1362
|
+
// checkCriterion: endpoint matched but not in fileContents, falls through to optimistic check
|
|
1363
|
+
it('should pass endpoint criterion via optimistic fallback when affected files exist', async () => {
|
|
1364
|
+
const huContent = [
|
|
1365
|
+
'## Acceptance Criteria',
|
|
1366
|
+
'- Endpoint for orders must handle all requests',
|
|
1367
|
+
].join('\n');
|
|
1368
|
+
const spec = makeSpec({
|
|
1369
|
+
impactAnalysis: {
|
|
1370
|
+
affectedModules: [],
|
|
1371
|
+
affectedFiles: ['src/handler.ts'],
|
|
1372
|
+
breakingChanges: false,
|
|
1373
|
+
requiresMigration: false,
|
|
1374
|
+
migrationReversible: false,
|
|
1375
|
+
requiresFeatureFlag: false,
|
|
1376
|
+
rollbackPlan: '',
|
|
1377
|
+
testingStrategy: {
|
|
1378
|
+
unitTests: [],
|
|
1379
|
+
integrationTests: [],
|
|
1380
|
+
e2eTests: [],
|
|
1381
|
+
manualTests: [],
|
|
1382
|
+
},
|
|
1383
|
+
environments: [],
|
|
1384
|
+
},
|
|
1385
|
+
});
|
|
1386
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1387
|
+
const p = String(path);
|
|
1388
|
+
if (p.includes('HU')) {
|
|
1389
|
+
return huContent;
|
|
1390
|
+
}
|
|
1391
|
+
// handler.ts does NOT contain "orders" - endpoint not found in content
|
|
1392
|
+
if (p.includes('handler.ts')) {
|
|
1393
|
+
return 'export function handleRequest() {}';
|
|
1394
|
+
}
|
|
1395
|
+
throw new Error('ENOENT');
|
|
1396
|
+
}));
|
|
1397
|
+
const result = await validateSpec(spec, projectPath);
|
|
1398
|
+
// Endpoint "orders" not found in fileContents, but affectedFiles.length > 0 => optimistic pass
|
|
1399
|
+
expect(result.matches).toContain('Endpoint for orders must handle all requests');
|
|
1400
|
+
});
|
|
1401
|
+
// checkCriterion: fallback optimistic check when affectedFiles > 0
|
|
1402
|
+
it('should optimistically pass criteria when affected files exist', async () => {
|
|
1403
|
+
const huContent = [
|
|
1404
|
+
'## Acceptance Criteria',
|
|
1405
|
+
'- Some general criterion without specific keywords',
|
|
1406
|
+
].join('\n');
|
|
1407
|
+
const spec = makeSpec({
|
|
1408
|
+
impactAnalysis: {
|
|
1409
|
+
affectedModules: [],
|
|
1410
|
+
affectedFiles: ['src/feature.ts'],
|
|
1411
|
+
breakingChanges: false,
|
|
1412
|
+
requiresMigration: false,
|
|
1413
|
+
migrationReversible: false,
|
|
1414
|
+
requiresFeatureFlag: false,
|
|
1415
|
+
rollbackPlan: '',
|
|
1416
|
+
testingStrategy: {
|
|
1417
|
+
unitTests: [],
|
|
1418
|
+
integrationTests: [],
|
|
1419
|
+
e2eTests: [],
|
|
1420
|
+
manualTests: [],
|
|
1421
|
+
},
|
|
1422
|
+
environments: [],
|
|
1423
|
+
},
|
|
1424
|
+
});
|
|
1425
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1426
|
+
const p = String(path);
|
|
1427
|
+
if (p.includes('HU')) {
|
|
1428
|
+
return huContent;
|
|
1429
|
+
}
|
|
1430
|
+
if (p.includes('feature.ts')) {
|
|
1431
|
+
return 'export const feature = true;';
|
|
1432
|
+
}
|
|
1433
|
+
throw new Error('ENOENT');
|
|
1434
|
+
}));
|
|
1435
|
+
const result = await validateSpec(spec, projectPath);
|
|
1436
|
+
// criterion doesn't match file/component/endpoint patterns but affectedFiles > 0
|
|
1437
|
+
expect(result.matches).toContain('Some general criterion without specific keywords');
|
|
1438
|
+
});
|
|
1439
|
+
// checkCriterion: returns false when nothing matches
|
|
1440
|
+
it('should return false for criterion that matches nothing and no affected files', async () => {
|
|
1441
|
+
const huContent = [
|
|
1442
|
+
'## Acceptance Criteria',
|
|
1443
|
+
'- Some general criterion without specific patterns',
|
|
1444
|
+
].join('\n');
|
|
1445
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1446
|
+
if (String(path).includes('HU')) {
|
|
1447
|
+
return huContent;
|
|
1448
|
+
}
|
|
1449
|
+
throw new Error('ENOENT');
|
|
1450
|
+
}));
|
|
1451
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
1452
|
+
expect(result.missing).toContain('Some general criterion without specific patterns');
|
|
1453
|
+
});
|
|
1454
|
+
// quickQualityCheck: non-code extension skipped
|
|
1455
|
+
it('should not produce quality issues for non-code files', async () => {
|
|
1456
|
+
const spec = makeSpec({
|
|
1457
|
+
impactAnalysis: {
|
|
1458
|
+
affectedModules: [],
|
|
1459
|
+
affectedFiles: ['README.md'],
|
|
1460
|
+
breakingChanges: false,
|
|
1461
|
+
requiresMigration: false,
|
|
1462
|
+
migrationReversible: false,
|
|
1463
|
+
requiresFeatureFlag: false,
|
|
1464
|
+
rollbackPlan: '',
|
|
1465
|
+
testingStrategy: {
|
|
1466
|
+
unitTests: [],
|
|
1467
|
+
integrationTests: [],
|
|
1468
|
+
e2eTests: [],
|
|
1469
|
+
manualTests: [],
|
|
1470
|
+
},
|
|
1471
|
+
environments: [],
|
|
1472
|
+
},
|
|
1473
|
+
});
|
|
1474
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1475
|
+
const p = String(path);
|
|
1476
|
+
if (p.includes('README.md')) {
|
|
1477
|
+
return '# Readme\nSome content\nconsole.log("test")';
|
|
1478
|
+
}
|
|
1479
|
+
throw new Error('ENOENT');
|
|
1480
|
+
}));
|
|
1481
|
+
const result = await validateSpec(spec, projectPath);
|
|
1482
|
+
expect(result.qualityIssues).toHaveLength(0);
|
|
1483
|
+
});
|
|
1484
|
+
// quickQualityCheck: file length > 500
|
|
1485
|
+
it('should flag files with more than 500 lines', async () => {
|
|
1486
|
+
const longContent = Array.from({ length: 501 }, (_, i) => `line ${i}`).join('\n');
|
|
1487
|
+
const spec = makeSpec({
|
|
1488
|
+
impactAnalysis: {
|
|
1489
|
+
affectedModules: [],
|
|
1490
|
+
affectedFiles: ['src/big.ts'],
|
|
1491
|
+
breakingChanges: false,
|
|
1492
|
+
requiresMigration: false,
|
|
1493
|
+
migrationReversible: false,
|
|
1494
|
+
requiresFeatureFlag: false,
|
|
1495
|
+
rollbackPlan: '',
|
|
1496
|
+
testingStrategy: {
|
|
1497
|
+
unitTests: [],
|
|
1498
|
+
integrationTests: [],
|
|
1499
|
+
e2eTests: [],
|
|
1500
|
+
manualTests: [],
|
|
1501
|
+
},
|
|
1502
|
+
environments: [],
|
|
1503
|
+
},
|
|
1504
|
+
});
|
|
1505
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1506
|
+
const p = String(path);
|
|
1507
|
+
if (p.includes('big.ts')) {
|
|
1508
|
+
return longContent;
|
|
1509
|
+
}
|
|
1510
|
+
throw new Error('ENOENT');
|
|
1511
|
+
}));
|
|
1512
|
+
const result = await validateSpec(spec, projectPath);
|
|
1513
|
+
const fileLengthIssue = result.qualityIssues.find((i) => i.rule === 'file-length');
|
|
1514
|
+
expect(fileLengthIssue).toBeDefined();
|
|
1515
|
+
expect(fileLengthIssue?.severity).toBe('warning');
|
|
1516
|
+
expect(fileLengthIssue?.message).toContain('501 lines');
|
|
1517
|
+
});
|
|
1518
|
+
// quickQualityCheck: file length exactly 500 (no issue)
|
|
1519
|
+
it('should NOT flag files with exactly 500 lines', async () => {
|
|
1520
|
+
const content = Array.from({ length: 500 }, (_, i) => `line ${i}`).join('\n');
|
|
1521
|
+
const spec = makeSpec({
|
|
1522
|
+
impactAnalysis: {
|
|
1523
|
+
affectedModules: [],
|
|
1524
|
+
affectedFiles: ['src/ok.ts'],
|
|
1525
|
+
breakingChanges: false,
|
|
1526
|
+
requiresMigration: false,
|
|
1527
|
+
migrationReversible: false,
|
|
1528
|
+
requiresFeatureFlag: false,
|
|
1529
|
+
rollbackPlan: '',
|
|
1530
|
+
testingStrategy: {
|
|
1531
|
+
unitTests: [],
|
|
1532
|
+
integrationTests: [],
|
|
1533
|
+
e2eTests: [],
|
|
1534
|
+
manualTests: [],
|
|
1535
|
+
},
|
|
1536
|
+
environments: [],
|
|
1537
|
+
},
|
|
1538
|
+
});
|
|
1539
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1540
|
+
const p = String(path);
|
|
1541
|
+
if (p.includes('ok.ts')) {
|
|
1542
|
+
return content;
|
|
1543
|
+
}
|
|
1544
|
+
throw new Error('ENOENT');
|
|
1545
|
+
}));
|
|
1546
|
+
const result = await validateSpec(spec, projectPath);
|
|
1547
|
+
const fileLengthIssue = result.qualityIssues.find((i) => i.rule === 'file-length');
|
|
1548
|
+
expect(fileLengthIssue).toBeUndefined();
|
|
1549
|
+
});
|
|
1550
|
+
// quickQualityCheck: TODO/FIXME/HACK/XXX
|
|
1551
|
+
it('should flag TODO/FIXME/HACK/XXX markers', async () => {
|
|
1552
|
+
const content = [
|
|
1553
|
+
'const x = 1;',
|
|
1554
|
+
'// TODO: fix this later',
|
|
1555
|
+
'// FIXME: broken logic',
|
|
1556
|
+
'// HACK: workaround',
|
|
1557
|
+
'// XXX: needs review',
|
|
1558
|
+
'const y = 2;',
|
|
1559
|
+
].join('\n');
|
|
1560
|
+
const spec = makeSpec({
|
|
1561
|
+
impactAnalysis: {
|
|
1562
|
+
affectedModules: [],
|
|
1563
|
+
affectedFiles: ['src/dirty.ts'],
|
|
1564
|
+
breakingChanges: false,
|
|
1565
|
+
requiresMigration: false,
|
|
1566
|
+
migrationReversible: false,
|
|
1567
|
+
requiresFeatureFlag: false,
|
|
1568
|
+
rollbackPlan: '',
|
|
1569
|
+
testingStrategy: {
|
|
1570
|
+
unitTests: [],
|
|
1571
|
+
integrationTests: [],
|
|
1572
|
+
e2eTests: [],
|
|
1573
|
+
manualTests: [],
|
|
1574
|
+
},
|
|
1575
|
+
environments: [],
|
|
1576
|
+
},
|
|
1577
|
+
});
|
|
1578
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1579
|
+
const p = String(path);
|
|
1580
|
+
if (p.includes('dirty.ts')) {
|
|
1581
|
+
return content;
|
|
1582
|
+
}
|
|
1583
|
+
throw new Error('ENOENT');
|
|
1584
|
+
}));
|
|
1585
|
+
const result = await validateSpec(spec, projectPath);
|
|
1586
|
+
const pendingWork = result.qualityIssues.filter((i) => i.rule === 'pending-work');
|
|
1587
|
+
expect(pendingWork.length).toBe(4);
|
|
1588
|
+
expect(pendingWork[0]?.severity).toBe('info');
|
|
1589
|
+
});
|
|
1590
|
+
// quickQualityCheck: console.log detection (non-test file)
|
|
1591
|
+
it('should flag console statements in non-test files', async () => {
|
|
1592
|
+
const content = [
|
|
1593
|
+
'function doSomething() {',
|
|
1594
|
+
' console.log("debug");',
|
|
1595
|
+
' console.debug("details");',
|
|
1596
|
+
' console.info("info");',
|
|
1597
|
+
'}',
|
|
1598
|
+
].join('\n');
|
|
1599
|
+
const spec = makeSpec({
|
|
1600
|
+
impactAnalysis: {
|
|
1601
|
+
affectedModules: [],
|
|
1602
|
+
affectedFiles: ['src/service.ts'],
|
|
1603
|
+
breakingChanges: false,
|
|
1604
|
+
requiresMigration: false,
|
|
1605
|
+
migrationReversible: false,
|
|
1606
|
+
requiresFeatureFlag: false,
|
|
1607
|
+
rollbackPlan: '',
|
|
1608
|
+
testingStrategy: {
|
|
1609
|
+
unitTests: [],
|
|
1610
|
+
integrationTests: [],
|
|
1611
|
+
e2eTests: [],
|
|
1612
|
+
manualTests: [],
|
|
1613
|
+
},
|
|
1614
|
+
environments: [],
|
|
1615
|
+
},
|
|
1616
|
+
});
|
|
1617
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1618
|
+
const p = String(path);
|
|
1619
|
+
if (p.includes('service.ts')) {
|
|
1620
|
+
return content;
|
|
1621
|
+
}
|
|
1622
|
+
throw new Error('ENOENT');
|
|
1623
|
+
}));
|
|
1624
|
+
const result = await validateSpec(spec, projectPath);
|
|
1625
|
+
const consoleIssues = result.qualityIssues.filter((i) => i.rule === 'no-console');
|
|
1626
|
+
expect(consoleIssues.length).toBe(3);
|
|
1627
|
+
expect(consoleIssues[0]?.severity).toBe('warning');
|
|
1628
|
+
});
|
|
1629
|
+
// quickQualityCheck: console.log NOT flagged in test files
|
|
1630
|
+
it('should NOT flag console statements in test files', async () => {
|
|
1631
|
+
const content = 'console.log("test output");';
|
|
1632
|
+
const spec = makeSpec({
|
|
1633
|
+
impactAnalysis: {
|
|
1634
|
+
affectedModules: [],
|
|
1635
|
+
affectedFiles: ['src/service.test.ts'],
|
|
1636
|
+
breakingChanges: false,
|
|
1637
|
+
requiresMigration: false,
|
|
1638
|
+
migrationReversible: false,
|
|
1639
|
+
requiresFeatureFlag: false,
|
|
1640
|
+
rollbackPlan: '',
|
|
1641
|
+
testingStrategy: {
|
|
1642
|
+
unitTests: [],
|
|
1643
|
+
integrationTests: [],
|
|
1644
|
+
e2eTests: [],
|
|
1645
|
+
manualTests: [],
|
|
1646
|
+
},
|
|
1647
|
+
environments: [],
|
|
1648
|
+
},
|
|
1649
|
+
});
|
|
1650
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1651
|
+
const p = String(path);
|
|
1652
|
+
if (p.includes('service.test.ts')) {
|
|
1653
|
+
return content;
|
|
1654
|
+
}
|
|
1655
|
+
throw new Error('ENOENT');
|
|
1656
|
+
}));
|
|
1657
|
+
const result = await validateSpec(spec, projectPath);
|
|
1658
|
+
const consoleIssues = result.qualityIssues.filter((i) => i.rule === 'no-console');
|
|
1659
|
+
expect(consoleIssues).toHaveLength(0);
|
|
1660
|
+
});
|
|
1661
|
+
// quickQualityCheck: console.log NOT flagged in spec files
|
|
1662
|
+
it('should NOT flag console statements in spec files', async () => {
|
|
1663
|
+
const content = 'console.log("spec output");';
|
|
1664
|
+
const spec = makeSpec({
|
|
1665
|
+
impactAnalysis: {
|
|
1666
|
+
affectedModules: [],
|
|
1667
|
+
affectedFiles: ['src/feature.spec.ts'],
|
|
1668
|
+
breakingChanges: false,
|
|
1669
|
+
requiresMigration: false,
|
|
1670
|
+
migrationReversible: false,
|
|
1671
|
+
requiresFeatureFlag: false,
|
|
1672
|
+
rollbackPlan: '',
|
|
1673
|
+
testingStrategy: {
|
|
1674
|
+
unitTests: [],
|
|
1675
|
+
integrationTests: [],
|
|
1676
|
+
e2eTests: [],
|
|
1677
|
+
manualTests: [],
|
|
1678
|
+
},
|
|
1679
|
+
environments: [],
|
|
1680
|
+
},
|
|
1681
|
+
});
|
|
1682
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1683
|
+
const p = String(path);
|
|
1684
|
+
if (p.includes('feature.spec.ts')) {
|
|
1685
|
+
return content;
|
|
1686
|
+
}
|
|
1687
|
+
throw new Error('ENOENT');
|
|
1688
|
+
}));
|
|
1689
|
+
const result = await validateSpec(spec, projectPath);
|
|
1690
|
+
const consoleIssues = result.qualityIssues.filter((i) => i.rule === 'no-console');
|
|
1691
|
+
expect(consoleIssues).toHaveLength(0);
|
|
1692
|
+
});
|
|
1693
|
+
// quickQualityCheck: readFile error
|
|
1694
|
+
it('should handle readFile errors in quickQualityCheck gracefully', async () => {
|
|
1695
|
+
const spec = makeSpec({
|
|
1696
|
+
impactAnalysis: {
|
|
1697
|
+
affectedModules: [],
|
|
1698
|
+
affectedFiles: ['src/unreadable.ts'],
|
|
1699
|
+
breakingChanges: false,
|
|
1700
|
+
requiresMigration: false,
|
|
1701
|
+
migrationReversible: false,
|
|
1702
|
+
requiresFeatureFlag: false,
|
|
1703
|
+
rollbackPlan: '',
|
|
1704
|
+
testingStrategy: {
|
|
1705
|
+
unitTests: [],
|
|
1706
|
+
integrationTests: [],
|
|
1707
|
+
e2eTests: [],
|
|
1708
|
+
manualTests: [],
|
|
1709
|
+
},
|
|
1710
|
+
environments: [],
|
|
1711
|
+
},
|
|
1712
|
+
});
|
|
1713
|
+
// readFile always throws (default behavior)
|
|
1714
|
+
// BUT for scanCodeForSpec, reading impactAnalysis files also throws,
|
|
1715
|
+
// so affectedFiles stays empty. We need a special setup:
|
|
1716
|
+
// scanCodeForSpec reads the file for impactAnalysis => should succeed for that
|
|
1717
|
+
// quickQualityCheck reads the same file => should fail
|
|
1718
|
+
let readCount = 0;
|
|
1719
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1720
|
+
readCount++;
|
|
1721
|
+
const p = String(path);
|
|
1722
|
+
if (p.includes('unreadable.ts')) {
|
|
1723
|
+
if (readCount <= 1) {
|
|
1724
|
+
return 'const x = 1;';
|
|
1725
|
+
} // scanCodeForSpec succeeds
|
|
1726
|
+
throw new Error('EACCES'); // quickQualityCheck fails
|
|
1727
|
+
}
|
|
1728
|
+
throw new Error('ENOENT');
|
|
1729
|
+
}));
|
|
1730
|
+
const result = await validateSpec(spec, projectPath);
|
|
1731
|
+
// Should not crash, just no quality issues
|
|
1732
|
+
expect(result.qualityIssues).toHaveLength(0);
|
|
1733
|
+
});
|
|
1734
|
+
// quickQualityCheck: various code extensions
|
|
1735
|
+
it('should run quality check on various code extensions', async () => {
|
|
1736
|
+
const extensions = ['.tsx', '.js', '.jsx', '.py', '.go', '.rs', '.java', '.kt', '.rb', '.php'];
|
|
1737
|
+
for (const ext of extensions) {
|
|
1738
|
+
vi.clearAllMocks();
|
|
1739
|
+
mockGlob.mockResolvedValue([]);
|
|
1740
|
+
const filename = `src/file${ext}`;
|
|
1741
|
+
const spec = makeSpec({
|
|
1742
|
+
impactAnalysis: {
|
|
1743
|
+
affectedModules: [],
|
|
1744
|
+
affectedFiles: [filename],
|
|
1745
|
+
breakingChanges: false,
|
|
1746
|
+
requiresMigration: false,
|
|
1747
|
+
migrationReversible: false,
|
|
1748
|
+
requiresFeatureFlag: false,
|
|
1749
|
+
rollbackPlan: '',
|
|
1750
|
+
testingStrategy: {
|
|
1751
|
+
unitTests: [],
|
|
1752
|
+
integrationTests: [],
|
|
1753
|
+
e2eTests: [],
|
|
1754
|
+
manualTests: [],
|
|
1755
|
+
},
|
|
1756
|
+
environments: [],
|
|
1757
|
+
},
|
|
1758
|
+
});
|
|
1759
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1760
|
+
const p = String(path);
|
|
1761
|
+
if (p.includes(`file${ext}`)) {
|
|
1762
|
+
return '// TODO: implement this feature\nconst x = 1;';
|
|
1763
|
+
}
|
|
1764
|
+
throw new Error('ENOENT');
|
|
1765
|
+
}));
|
|
1766
|
+
const result = await validateSpec(spec, projectPath);
|
|
1767
|
+
const todoIssues = result.qualityIssues.filter((i) => i.rule === 'pending-work');
|
|
1768
|
+
expect(todoIssues.length).toBeGreaterThanOrEqual(1);
|
|
1769
|
+
}
|
|
1770
|
+
});
|
|
1771
|
+
// Score calculation
|
|
1772
|
+
it('should calculate score as 100 when all criteria match', async () => {
|
|
1773
|
+
const huContent = [
|
|
1774
|
+
'## Acceptance Criteria',
|
|
1775
|
+
'- Create file "src/new.ts" for the new feature',
|
|
1776
|
+
].join('\n');
|
|
1777
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1778
|
+
if (String(path).includes('HU')) {
|
|
1779
|
+
return huContent;
|
|
1780
|
+
}
|
|
1781
|
+
throw new Error('ENOENT');
|
|
1782
|
+
}));
|
|
1783
|
+
mockStat.mockResolvedValue({});
|
|
1784
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
1785
|
+
expect(result.score).toBe(100);
|
|
1786
|
+
expect(result.fieldsImplemented).toBe(1);
|
|
1787
|
+
expect(result.fieldsTotal).toBe(1);
|
|
1788
|
+
});
|
|
1789
|
+
it('should calculate partial score correctly', async () => {
|
|
1790
|
+
const huContent = [
|
|
1791
|
+
'## Acceptance Criteria',
|
|
1792
|
+
'- Create file "src/exists.ts" for first feature',
|
|
1793
|
+
'- Create file "src/missing.ts" for second feature',
|
|
1794
|
+
].join('\n');
|
|
1795
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1796
|
+
if (String(path).includes('HU')) {
|
|
1797
|
+
return huContent;
|
|
1798
|
+
}
|
|
1799
|
+
throw new Error('ENOENT');
|
|
1800
|
+
}));
|
|
1801
|
+
mockStat.mockImplementation(((path) => {
|
|
1802
|
+
if (String(path).includes('exists.ts')) {
|
|
1803
|
+
return {};
|
|
1804
|
+
}
|
|
1805
|
+
throw new Error('ENOENT');
|
|
1806
|
+
}));
|
|
1807
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
1808
|
+
expect(result.score).toBe(50);
|
|
1809
|
+
expect(result.fieldsImplemented).toBe(1);
|
|
1810
|
+
expect(result.fieldsTotal).toBe(2);
|
|
1811
|
+
});
|
|
1812
|
+
// slug with dashes transforms to pattern
|
|
1813
|
+
it('should transform slug dashes to [-_] pattern for glob', async () => {
|
|
1814
|
+
const spec = makeSpec({ slug: 'my-cool-feature' });
|
|
1815
|
+
mockGlob.mockResolvedValue([]);
|
|
1816
|
+
await validateSpec(spec, projectPath);
|
|
1817
|
+
// Check that glob was called with the pattern transformation
|
|
1818
|
+
expect(mockGlob).toHaveBeenCalledWith(expect.stringContaining('my[-_]cool[-_]feature'), expect.objectContaining({
|
|
1819
|
+
cwd: projectPath,
|
|
1820
|
+
nodir: true,
|
|
1821
|
+
ignore: ['node_modules/**', 'dist/**', 'build/**', '.git/**'],
|
|
1822
|
+
maxDepth: 6,
|
|
1823
|
+
}));
|
|
1824
|
+
});
|
|
1825
|
+
});
|
|
1826
|
+
// ============================================================
|
|
1827
|
+
// detectDrift
|
|
1828
|
+
// ============================================================
|
|
1829
|
+
describe('detectDrift', () => {
|
|
1830
|
+
const projectPath = '/test/project';
|
|
1831
|
+
it('should return a DriftReport with specId, checkType, and lastChecked', async () => {
|
|
1832
|
+
const result = await detectDrift(makeSpec(), projectPath);
|
|
1833
|
+
expect(result.specId).toBe('SPEC-001');
|
|
1834
|
+
expect(result.checkType).toBe('on-demand');
|
|
1835
|
+
expect(result.lastChecked).toBeDefined();
|
|
1836
|
+
expect(new Date(result.lastChecked).toISOString()).toBe(result.lastChecked);
|
|
1837
|
+
});
|
|
1838
|
+
it('should create drift items from missing criteria', async () => {
|
|
1839
|
+
const result = await detectDrift(makeSpec({ target: 'backend' }), projectPath);
|
|
1840
|
+
// Default spec with no files => all criteria are missing
|
|
1841
|
+
expect(result.drifts.length).toBeGreaterThan(0);
|
|
1842
|
+
for (const drift of result.drifts) {
|
|
1843
|
+
expect(drift.expected).toBe('Implemented');
|
|
1844
|
+
expect(drift.actual).toBe('Missing');
|
|
1845
|
+
expect(drift.autoFixable).toBe(false);
|
|
1846
|
+
}
|
|
1847
|
+
});
|
|
1848
|
+
it('should classify security drift as critical severity', async () => {
|
|
1849
|
+
const huContent = [
|
|
1850
|
+
'## Acceptance Criteria',
|
|
1851
|
+
'- Security authentication must be enforced properly',
|
|
1852
|
+
].join('\n');
|
|
1853
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1854
|
+
if (String(path).includes('HU')) {
|
|
1855
|
+
return huContent;
|
|
1856
|
+
}
|
|
1857
|
+
throw new Error('ENOENT');
|
|
1858
|
+
}));
|
|
1859
|
+
const result = await detectDrift(makeSpec(), projectPath);
|
|
1860
|
+
const secDrift = result.drifts.find((d) => d.specCriterion.includes('Security'));
|
|
1861
|
+
expect(secDrift?.severity).toBe('critical');
|
|
1862
|
+
});
|
|
1863
|
+
it('should classify auth criterion as critical severity', async () => {
|
|
1864
|
+
const huContent = [
|
|
1865
|
+
'## Acceptance Criteria',
|
|
1866
|
+
'- Auth token validation must work correctly',
|
|
1867
|
+
].join('\n');
|
|
1868
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1869
|
+
if (String(path).includes('HU')) {
|
|
1870
|
+
return huContent;
|
|
1871
|
+
}
|
|
1872
|
+
throw new Error('ENOENT');
|
|
1873
|
+
}));
|
|
1874
|
+
const result = await detectDrift(makeSpec(), projectPath);
|
|
1875
|
+
const authDrift = result.drifts.find((d) => d.specCriterion.toLowerCase().includes('auth'));
|
|
1876
|
+
expect(authDrift?.severity).toBe('critical');
|
|
1877
|
+
});
|
|
1878
|
+
it('should classify permission criterion as critical severity', async () => {
|
|
1879
|
+
const huContent = [
|
|
1880
|
+
'## Acceptance Criteria',
|
|
1881
|
+
'- Permission checks must prevent unauthorized access',
|
|
1882
|
+
].join('\n');
|
|
1883
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1884
|
+
if (String(path).includes('HU')) {
|
|
1885
|
+
return huContent;
|
|
1886
|
+
}
|
|
1887
|
+
throw new Error('ENOENT');
|
|
1888
|
+
}));
|
|
1889
|
+
const result = await detectDrift(makeSpec(), projectPath);
|
|
1890
|
+
const permDrift = result.drifts.find((d) => d.specCriterion.toLowerCase().includes('permission'));
|
|
1891
|
+
expect(permDrift?.severity).toBe('critical');
|
|
1892
|
+
});
|
|
1893
|
+
it('should classify error criterion as high severity', async () => {
|
|
1894
|
+
const huContent = [
|
|
1895
|
+
'## Acceptance Criteria',
|
|
1896
|
+
'- Error handling must be comprehensive everywhere',
|
|
1897
|
+
].join('\n');
|
|
1898
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1899
|
+
if (String(path).includes('HU')) {
|
|
1900
|
+
return huContent;
|
|
1901
|
+
}
|
|
1902
|
+
throw new Error('ENOENT');
|
|
1903
|
+
}));
|
|
1904
|
+
const result = await detectDrift(makeSpec(), projectPath);
|
|
1905
|
+
const errDrift = result.drifts.find((d) => d.specCriterion.toLowerCase().includes('error'));
|
|
1906
|
+
expect(errDrift?.severity).toBe('high');
|
|
1907
|
+
});
|
|
1908
|
+
it('should classify validation criterion as high severity', async () => {
|
|
1909
|
+
const huContent = [
|
|
1910
|
+
'## Acceptance Criteria',
|
|
1911
|
+
'- Validation of all user inputs must happen',
|
|
1912
|
+
].join('\n');
|
|
1913
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1914
|
+
if (String(path).includes('HU')) {
|
|
1915
|
+
return huContent;
|
|
1916
|
+
}
|
|
1917
|
+
throw new Error('ENOENT');
|
|
1918
|
+
}));
|
|
1919
|
+
const result = await detectDrift(makeSpec(), projectPath);
|
|
1920
|
+
const valDrift = result.drifts.find((d) => d.specCriterion.toLowerCase().includes('validation'));
|
|
1921
|
+
expect(valDrift?.severity).toBe('high');
|
|
1922
|
+
});
|
|
1923
|
+
it('should classify test criterion as high severity', async () => {
|
|
1924
|
+
const huContent = [
|
|
1925
|
+
'## Acceptance Criteria',
|
|
1926
|
+
'- Test coverage must meet minimum threshold',
|
|
1927
|
+
].join('\n');
|
|
1928
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1929
|
+
if (String(path).includes('HU')) {
|
|
1930
|
+
return huContent;
|
|
1931
|
+
}
|
|
1932
|
+
throw new Error('ENOENT');
|
|
1933
|
+
}));
|
|
1934
|
+
const result = await detectDrift(makeSpec(), projectPath);
|
|
1935
|
+
const testDrift = result.drifts.find((d) => d.specCriterion.toLowerCase().includes('test'));
|
|
1936
|
+
expect(testDrift?.severity).toBe('high');
|
|
1937
|
+
});
|
|
1938
|
+
it('should classify performance criterion as medium severity', async () => {
|
|
1939
|
+
const huContent = [
|
|
1940
|
+
'## Acceptance Criteria',
|
|
1941
|
+
'- Performance benchmark must meet requirements daily',
|
|
1942
|
+
].join('\n');
|
|
1943
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1944
|
+
if (String(path).includes('HU')) {
|
|
1945
|
+
return huContent;
|
|
1946
|
+
}
|
|
1947
|
+
throw new Error('ENOENT');
|
|
1948
|
+
}));
|
|
1949
|
+
const result = await detectDrift(makeSpec(), projectPath);
|
|
1950
|
+
const perfDrift = result.drifts.find((d) => d.specCriterion.toLowerCase().includes('performance'));
|
|
1951
|
+
expect(perfDrift?.severity).toBe('medium');
|
|
1952
|
+
});
|
|
1953
|
+
it('should classify cache criterion as medium severity', async () => {
|
|
1954
|
+
const huContent = [
|
|
1955
|
+
'## Acceptance Criteria',
|
|
1956
|
+
'- Cache expiry must work across replicas',
|
|
1957
|
+
].join('\n');
|
|
1958
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1959
|
+
if (String(path).includes('HU')) {
|
|
1960
|
+
return huContent;
|
|
1961
|
+
}
|
|
1962
|
+
throw new Error('ENOENT');
|
|
1963
|
+
}));
|
|
1964
|
+
const result = await detectDrift(makeSpec(), projectPath);
|
|
1965
|
+
const cacheDrift = result.drifts.find((d) => d.specCriterion.toLowerCase().includes('cache'));
|
|
1966
|
+
expect(cacheDrift?.severity).toBe('medium');
|
|
1967
|
+
});
|
|
1968
|
+
it('should classify optimize criterion as medium severity', async () => {
|
|
1969
|
+
const huContent = [
|
|
1970
|
+
'## Acceptance Criteria',
|
|
1971
|
+
'- Optimize database queries for large datasets',
|
|
1972
|
+
].join('\n');
|
|
1973
|
+
mockReadFile.mockImplementation(((path) => {
|
|
1974
|
+
if (String(path).includes('HU')) {
|
|
1975
|
+
return huContent;
|
|
1976
|
+
}
|
|
1977
|
+
throw new Error('ENOENT');
|
|
1978
|
+
}));
|
|
1979
|
+
const result = await detectDrift(makeSpec(), projectPath);
|
|
1980
|
+
const optDrift = result.drifts.find((d) => d.specCriterion.toLowerCase().includes('optimize'));
|
|
1981
|
+
expect(optDrift?.severity).toBe('medium');
|
|
1982
|
+
});
|
|
1983
|
+
it('should classify generic criterion as low severity', async () => {
|
|
1984
|
+
// The default fallback metadata criterion "Feature ... is implemented" is generic
|
|
1985
|
+
// Use a title without "test"/"error"/"validation"/"security"/"auth"/"performance"/"cache"/"optimize"
|
|
1986
|
+
const result = await detectDrift(makeSpec({ target: 'shared', title: 'User Profile Feature' }), projectPath);
|
|
1987
|
+
// "Feature ... is implemented" => low, "No regression in existing tests" => high (contains "test")
|
|
1988
|
+
const featureDrift = result.drifts.find((d) => d.specCriterion.includes('is implemented'));
|
|
1989
|
+
expect(featureDrift?.severity).toBe('low');
|
|
1990
|
+
});
|
|
1991
|
+
// Quality issues as drift items
|
|
1992
|
+
it('should include quality issues as drift items', async () => {
|
|
1993
|
+
const content = Array.from({ length: 501 }, (_, i) => `line ${i}`).join('\n');
|
|
1994
|
+
const spec = makeSpec({
|
|
1995
|
+
impactAnalysis: {
|
|
1996
|
+
affectedModules: [],
|
|
1997
|
+
affectedFiles: ['src/big.ts'],
|
|
1998
|
+
breakingChanges: false,
|
|
1999
|
+
requiresMigration: false,
|
|
2000
|
+
migrationReversible: false,
|
|
2001
|
+
requiresFeatureFlag: false,
|
|
2002
|
+
rollbackPlan: '',
|
|
2003
|
+
testingStrategy: {
|
|
2004
|
+
unitTests: [],
|
|
2005
|
+
integrationTests: [],
|
|
2006
|
+
e2eTests: [],
|
|
2007
|
+
manualTests: [],
|
|
2008
|
+
},
|
|
2009
|
+
environments: [],
|
|
2010
|
+
},
|
|
2011
|
+
});
|
|
2012
|
+
mockReadFile.mockImplementation(((path) => {
|
|
2013
|
+
const p = String(path);
|
|
2014
|
+
if (p.includes('big.ts')) {
|
|
2015
|
+
return content;
|
|
2016
|
+
}
|
|
2017
|
+
throw new Error('ENOENT');
|
|
2018
|
+
}));
|
|
2019
|
+
const result = await detectDrift(spec, projectPath);
|
|
2020
|
+
const qualityDrift = result.drifts.find((d) => d.specCriterion === 'file-length');
|
|
2021
|
+
expect(qualityDrift).toBeDefined();
|
|
2022
|
+
expect(qualityDrift?.file).toBe('src/big.ts');
|
|
2023
|
+
});
|
|
2024
|
+
// Quality issue severity mapping
|
|
2025
|
+
it('should map critical quality issue severity to critical drift', async () => {
|
|
2026
|
+
// We need a quality issue with severity "critical" - but quickQualityCheck only produces
|
|
2027
|
+
// 'warning' (file-length, no-console) and 'info' (pending-work).
|
|
2028
|
+
// The severity mapping in detectDrift converts: critical->critical, error->high, else->medium
|
|
2029
|
+
// Since quickQualityCheck can't produce critical/error severity directly, we test through
|
|
2030
|
+
// the existing severities. 'warning' should map to 'medium' and 'info' should map to 'medium'.
|
|
2031
|
+
const content = '// TODO: fix this critical issue later\n';
|
|
2032
|
+
const spec = makeSpec({
|
|
2033
|
+
impactAnalysis: {
|
|
2034
|
+
affectedModules: [],
|
|
2035
|
+
affectedFiles: ['src/todo.ts'],
|
|
2036
|
+
breakingChanges: false,
|
|
2037
|
+
requiresMigration: false,
|
|
2038
|
+
migrationReversible: false,
|
|
2039
|
+
requiresFeatureFlag: false,
|
|
2040
|
+
rollbackPlan: '',
|
|
2041
|
+
testingStrategy: {
|
|
2042
|
+
unitTests: [],
|
|
2043
|
+
integrationTests: [],
|
|
2044
|
+
e2eTests: [],
|
|
2045
|
+
manualTests: [],
|
|
2046
|
+
},
|
|
2047
|
+
environments: [],
|
|
2048
|
+
},
|
|
2049
|
+
});
|
|
2050
|
+
mockReadFile.mockImplementation(((path) => {
|
|
2051
|
+
const p = String(path);
|
|
2052
|
+
if (p.includes('todo.ts')) {
|
|
2053
|
+
return content;
|
|
2054
|
+
}
|
|
2055
|
+
throw new Error('ENOENT');
|
|
2056
|
+
}));
|
|
2057
|
+
const result = await detectDrift(spec, projectPath);
|
|
2058
|
+
const todoDrift = result.drifts.find((d) => d.specCriterion === 'pending-work');
|
|
2059
|
+
expect(todoDrift).toBeDefined();
|
|
2060
|
+
// 'info' severity maps to 'medium' in the ternary chain
|
|
2061
|
+
expect(todoDrift?.severity).toBe('medium');
|
|
2062
|
+
});
|
|
2063
|
+
it('should map warning quality issue severity to medium drift', async () => {
|
|
2064
|
+
const content = Array.from({ length: 501 }, (_, i) => `line ${i}`).join('\n');
|
|
2065
|
+
const spec = makeSpec({
|
|
2066
|
+
impactAnalysis: {
|
|
2067
|
+
affectedModules: [],
|
|
2068
|
+
affectedFiles: ['src/large.ts'],
|
|
2069
|
+
breakingChanges: false,
|
|
2070
|
+
requiresMigration: false,
|
|
2071
|
+
migrationReversible: false,
|
|
2072
|
+
requiresFeatureFlag: false,
|
|
2073
|
+
rollbackPlan: '',
|
|
2074
|
+
testingStrategy: {
|
|
2075
|
+
unitTests: [],
|
|
2076
|
+
integrationTests: [],
|
|
2077
|
+
e2eTests: [],
|
|
2078
|
+
manualTests: [],
|
|
2079
|
+
},
|
|
2080
|
+
environments: [],
|
|
2081
|
+
},
|
|
2082
|
+
});
|
|
2083
|
+
mockReadFile.mockImplementation(((path) => {
|
|
2084
|
+
const p = String(path);
|
|
2085
|
+
if (p.includes('large.ts')) {
|
|
2086
|
+
return content;
|
|
2087
|
+
}
|
|
2088
|
+
throw new Error('ENOENT');
|
|
2089
|
+
}));
|
|
2090
|
+
const result = await detectDrift(spec, projectPath);
|
|
2091
|
+
const fileLengthDrift = result.drifts.find((d) => d.specCriterion === 'file-length');
|
|
2092
|
+
expect(fileLengthDrift?.severity).toBe('medium');
|
|
2093
|
+
expect(fileLengthDrift?.suggestedFix).toBeDefined();
|
|
2094
|
+
});
|
|
2095
|
+
// Compliance threshold
|
|
2096
|
+
it('should mark as compliant when score >= threshold (default 80)', async () => {
|
|
2097
|
+
// Need a spec where all criteria pass => score 100 >= 80
|
|
2098
|
+
const huContent = [
|
|
2099
|
+
'## Acceptance Criteria',
|
|
2100
|
+
'- Create file "src/exists.ts" for the feature',
|
|
2101
|
+
].join('\n');
|
|
2102
|
+
mockReadFile.mockImplementation(((path) => {
|
|
2103
|
+
if (String(path).includes('HU')) {
|
|
2104
|
+
return huContent;
|
|
2105
|
+
}
|
|
2106
|
+
throw new Error('ENOENT');
|
|
2107
|
+
}));
|
|
2108
|
+
mockStat.mockResolvedValue({});
|
|
2109
|
+
const result = await detectDrift(makeSpec(), projectPath);
|
|
2110
|
+
expect(result.isCompliant).toBe(true);
|
|
2111
|
+
expect(result.driftScore).toBe(100);
|
|
2112
|
+
});
|
|
2113
|
+
it('should mark as non-compliant when score < threshold', async () => {
|
|
2114
|
+
const result = await detectDrift(makeSpec({ target: 'backend' }), projectPath);
|
|
2115
|
+
// All criteria missing => score 0 < 80
|
|
2116
|
+
expect(result.isCompliant).toBe(false);
|
|
2117
|
+
});
|
|
2118
|
+
it('should use custom threshold', async () => {
|
|
2119
|
+
const result = await detectDrift(makeSpec({ target: 'backend' }), projectPath, 'full', 0);
|
|
2120
|
+
// score 0 >= threshold 0
|
|
2121
|
+
expect(result.isCompliant).toBe(true);
|
|
2122
|
+
});
|
|
2123
|
+
it('should accept mode parameter (full)', async () => {
|
|
2124
|
+
const result = await detectDrift(makeSpec(), projectPath, 'full');
|
|
2125
|
+
expect(result).toBeDefined();
|
|
2126
|
+
});
|
|
2127
|
+
it('should accept mode parameter (quick)', async () => {
|
|
2128
|
+
const result = await detectDrift(makeSpec(), projectPath, 'quick');
|
|
2129
|
+
expect(result).toBeDefined();
|
|
2130
|
+
});
|
|
2131
|
+
it('should set driftScore from validation score', async () => {
|
|
2132
|
+
const result = await detectDrift(makeSpec({ target: 'shared' }), projectPath);
|
|
2133
|
+
expect(typeof result.driftScore).toBe('number');
|
|
2134
|
+
expect(result.driftScore).toBeGreaterThanOrEqual(0);
|
|
2135
|
+
expect(result.driftScore).toBeLessThanOrEqual(100);
|
|
2136
|
+
});
|
|
2137
|
+
});
|
|
2138
|
+
// ============================================================
|
|
2139
|
+
// Edge cases and integration scenarios
|
|
2140
|
+
// ============================================================
|
|
2141
|
+
describe('edge cases', () => {
|
|
2142
|
+
const projectPath = '/test/project';
|
|
2143
|
+
it('should handle spec with both GWT and AC sections combined', async () => {
|
|
2144
|
+
const huContent = [
|
|
2145
|
+
'## Acceptance Criteria',
|
|
2146
|
+
'- System handles concurrent requests properly',
|
|
2147
|
+
'',
|
|
2148
|
+
'Given a user is authenticated properly',
|
|
2149
|
+
'When they submit a request to the server',
|
|
2150
|
+
'Then the response returns within millisecond',
|
|
2151
|
+
].join('\n');
|
|
2152
|
+
mockReadFile.mockImplementation(((path) => {
|
|
2153
|
+
if (String(path).includes('HU')) {
|
|
2154
|
+
return huContent;
|
|
2155
|
+
}
|
|
2156
|
+
throw new Error('ENOENT');
|
|
2157
|
+
}));
|
|
2158
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
2159
|
+
// 1 AC item + 3 GWT items = 4 (deduplicated)
|
|
2160
|
+
expect(result.fieldsTotal).toBe(4);
|
|
2161
|
+
});
|
|
2162
|
+
it('should handle empty HU.md content', async () => {
|
|
2163
|
+
mockReadFile.mockImplementation(((path) => {
|
|
2164
|
+
if (String(path).includes('HU')) {
|
|
2165
|
+
return '';
|
|
2166
|
+
}
|
|
2167
|
+
throw new Error('ENOENT');
|
|
2168
|
+
}));
|
|
2169
|
+
const result = await validateSpec(makeSpec({ target: 'backend' }), projectPath);
|
|
2170
|
+
// Falls back to metadata criteria
|
|
2171
|
+
expect(result.fieldsTotal).toBe(3);
|
|
2172
|
+
});
|
|
2173
|
+
it('should handle HU content with no list items in AC section', async () => {
|
|
2174
|
+
const huContent = [
|
|
2175
|
+
'## Acceptance Criteria',
|
|
2176
|
+
'This section has text but no list items.',
|
|
2177
|
+
'Just plain paragraphs without bullets.',
|
|
2178
|
+
'## End',
|
|
2179
|
+
].join('\n');
|
|
2180
|
+
mockReadFile.mockImplementation(((path) => {
|
|
2181
|
+
if (String(path).includes('HU')) {
|
|
2182
|
+
return huContent;
|
|
2183
|
+
}
|
|
2184
|
+
throw new Error('ENOENT');
|
|
2185
|
+
}));
|
|
2186
|
+
const result = await validateSpec(makeSpec({ target: 'backend' }), projectPath);
|
|
2187
|
+
// No list items extracted, no GWT => fallback to metadata criteria
|
|
2188
|
+
expect(result.fieldsTotal).toBe(3);
|
|
2189
|
+
});
|
|
2190
|
+
it('should handle criterion with quoted file name', async () => {
|
|
2191
|
+
const huContent = [
|
|
2192
|
+
'## Acceptance Criteria',
|
|
2193
|
+
"- Create 'src/config.yaml' for project settings",
|
|
2194
|
+
].join('\n');
|
|
2195
|
+
mockReadFile.mockImplementation(((path) => {
|
|
2196
|
+
if (String(path).includes('HU')) {
|
|
2197
|
+
return huContent;
|
|
2198
|
+
}
|
|
2199
|
+
throw new Error('ENOENT');
|
|
2200
|
+
}));
|
|
2201
|
+
mockStat.mockResolvedValue({});
|
|
2202
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
2203
|
+
expect(result.matches.length).toBe(1);
|
|
2204
|
+
});
|
|
2205
|
+
it('should handle deeply nested heading in extractSection', async () => {
|
|
2206
|
+
const huContent = [
|
|
2207
|
+
'###### Acceptance Criteria',
|
|
2208
|
+
'- Deep nested criterion that should work',
|
|
2209
|
+
'###### Another Deep Section',
|
|
2210
|
+
'- Should not be captured here either',
|
|
2211
|
+
].join('\n');
|
|
2212
|
+
mockReadFile.mockImplementation(((path) => {
|
|
2213
|
+
if (String(path).includes('HU')) {
|
|
2214
|
+
return huContent;
|
|
2215
|
+
}
|
|
2216
|
+
throw new Error('ENOENT');
|
|
2217
|
+
}));
|
|
2218
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
2219
|
+
expect(result.fieldsTotal).toBe(1);
|
|
2220
|
+
});
|
|
2221
|
+
it('should handle non-heading lines before first heading', async () => {
|
|
2222
|
+
const huContent = [
|
|
2223
|
+
'Some preamble text that is not a heading',
|
|
2224
|
+
'',
|
|
2225
|
+
'## Acceptance Criteria',
|
|
2226
|
+
'- Valid criterion after preamble content',
|
|
2227
|
+
].join('\n');
|
|
2228
|
+
mockReadFile.mockImplementation(((path) => {
|
|
2229
|
+
if (String(path).includes('HU')) {
|
|
2230
|
+
return huContent;
|
|
2231
|
+
}
|
|
2232
|
+
throw new Error('ENOENT');
|
|
2233
|
+
}));
|
|
2234
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
2235
|
+
expect(result.fieldsTotal).toBe(1);
|
|
2236
|
+
});
|
|
2237
|
+
// Endpoint case-insensitive matching
|
|
2238
|
+
it('should match endpoint criterion case-insensitively', async () => {
|
|
2239
|
+
const huContent = [
|
|
2240
|
+
'## Acceptance Criteria',
|
|
2241
|
+
'- Endpoint for USERS must return all records',
|
|
2242
|
+
].join('\n');
|
|
2243
|
+
const spec = makeSpec({
|
|
2244
|
+
impactAnalysis: {
|
|
2245
|
+
affectedModules: [],
|
|
2246
|
+
affectedFiles: ['src/routes.ts'],
|
|
2247
|
+
breakingChanges: false,
|
|
2248
|
+
requiresMigration: false,
|
|
2249
|
+
migrationReversible: false,
|
|
2250
|
+
requiresFeatureFlag: false,
|
|
2251
|
+
rollbackPlan: '',
|
|
2252
|
+
testingStrategy: {
|
|
2253
|
+
unitTests: [],
|
|
2254
|
+
integrationTests: [],
|
|
2255
|
+
e2eTests: [],
|
|
2256
|
+
manualTests: [],
|
|
2257
|
+
},
|
|
2258
|
+
environments: [],
|
|
2259
|
+
},
|
|
2260
|
+
});
|
|
2261
|
+
mockReadFile.mockImplementation(((path) => {
|
|
2262
|
+
const p = String(path);
|
|
2263
|
+
if (p.includes('HU')) {
|
|
2264
|
+
return huContent;
|
|
2265
|
+
}
|
|
2266
|
+
if (p.includes('routes.ts')) {
|
|
2267
|
+
return 'app.get("/users", listUsers);';
|
|
2268
|
+
}
|
|
2269
|
+
throw new Error('ENOENT');
|
|
2270
|
+
}));
|
|
2271
|
+
const result = await validateSpec(spec, projectPath);
|
|
2272
|
+
// "USERS" in criterion matches "users" in code (case insensitive)
|
|
2273
|
+
expect(result.matches.length).toBe(1);
|
|
2274
|
+
});
|
|
2275
|
+
it('should handle indented list items in spec files', async () => {
|
|
2276
|
+
const huContent = [
|
|
2277
|
+
'## Acceptance Criteria',
|
|
2278
|
+
' - Indented bullet criterion that is valid',
|
|
2279
|
+
' - More deeply indented criterion valid',
|
|
2280
|
+
].join('\n');
|
|
2281
|
+
mockReadFile.mockImplementation(((path) => {
|
|
2282
|
+
if (String(path).includes('HU')) {
|
|
2283
|
+
return huContent;
|
|
2284
|
+
}
|
|
2285
|
+
throw new Error('ENOENT');
|
|
2286
|
+
}));
|
|
2287
|
+
const result = await validateSpec(makeSpec(), projectPath);
|
|
2288
|
+
expect(result.fieldsTotal).toBe(2);
|
|
2289
|
+
});
|
|
2290
|
+
});
|
|
2291
|
+
// ============================================================
|
|
2292
|
+
// checkDefinitionOfReady / checkDefinitionOfDone wrappers
|
|
2293
|
+
// (These are the generateDoR/generateDoD exports tested above,
|
|
2294
|
+
// but let's also verify they are re-exported if needed)
|
|
2295
|
+
// ============================================================
|
|
2296
|
+
describe('function export verification', () => {
|
|
2297
|
+
it('validateSpec is exported and callable', () => {
|
|
2298
|
+
expect(typeof validateSpec).toBe('function');
|
|
2299
|
+
});
|
|
2300
|
+
it('detectDrift is exported and callable', () => {
|
|
2301
|
+
expect(typeof detectDrift).toBe('function');
|
|
2302
|
+
});
|
|
2303
|
+
it('generateChecklist is exported and callable', () => {
|
|
2304
|
+
expect(typeof generateChecklist).toBe('function');
|
|
2305
|
+
});
|
|
2306
|
+
it('generateDoR is exported and callable', () => {
|
|
2307
|
+
expect(typeof generateDoR).toBe('function');
|
|
2308
|
+
});
|
|
2309
|
+
it('generateDoD is exported and callable', () => {
|
|
2310
|
+
expect(typeof generateDoD).toBe('function');
|
|
2311
|
+
});
|
|
2312
|
+
});
|
|
2313
|
+
// ============================================================
|
|
2314
|
+
// Additional branch coverage tests
|
|
2315
|
+
// ============================================================
|
|
2316
|
+
describe('generateDoR — empty target branch', () => {
|
|
2317
|
+
it('should fail target check when target is empty string', () => {
|
|
2318
|
+
const result = generateDoR(makeSpec({ target: '' }));
|
|
2319
|
+
const targetItem = result.items.find((i) => i.id === 'dor-10');
|
|
2320
|
+
expect(targetItem?.status).toBe('failed');
|
|
2321
|
+
});
|
|
2322
|
+
});
|
|
2323
|
+
describe('generateDoD — completedAt null when not all DoD passed', () => {
|
|
2324
|
+
it('should return completedAt=null when some required items are pending', () => {
|
|
2325
|
+
// Default spec: dod-3 (unit tests), dod-4, dod-5, dod-6 are pending => isDone=false
|
|
2326
|
+
const result = generateDoD(makeSpec({ status: 'implementing' }));
|
|
2327
|
+
expect(result.isDone).toBe(false);
|
|
2328
|
+
expect(result.completedAt).toBeNull();
|
|
2329
|
+
});
|
|
2330
|
+
});
|
|
2331
|
+
describe('checkCriterion — content.includes(name) in code state', () => {
|
|
2332
|
+
it('should match component name found in fileContents map', async () => {
|
|
2333
|
+
const projectPath = '/test/project';
|
|
2334
|
+
const huContent = [
|
|
2335
|
+
'## Acceptance Criteria',
|
|
2336
|
+
'- Component AuthValidator must work correctly',
|
|
2337
|
+
].join('\n');
|
|
2338
|
+
const spec = makeSpec({
|
|
2339
|
+
impactAnalysis: {
|
|
2340
|
+
affectedModules: [],
|
|
2341
|
+
affectedFiles: ['src/auth.ts'],
|
|
2342
|
+
breakingChanges: false,
|
|
2343
|
+
requiresMigration: false,
|
|
2344
|
+
migrationReversible: false,
|
|
2345
|
+
requiresFeatureFlag: false,
|
|
2346
|
+
rollbackPlan: '',
|
|
2347
|
+
testingStrategy: {
|
|
2348
|
+
unitTests: [],
|
|
2349
|
+
integrationTests: [],
|
|
2350
|
+
e2eTests: [],
|
|
2351
|
+
manualTests: [],
|
|
2352
|
+
},
|
|
2353
|
+
environments: [],
|
|
2354
|
+
},
|
|
2355
|
+
});
|
|
2356
|
+
mockReadFile.mockImplementation(((path) => {
|
|
2357
|
+
const p = String(path);
|
|
2358
|
+
if (p.includes('HU')) {
|
|
2359
|
+
return huContent;
|
|
2360
|
+
}
|
|
2361
|
+
if (p.includes('auth.ts')) {
|
|
2362
|
+
return 'export class AuthValidator { validate() {} }';
|
|
2363
|
+
}
|
|
2364
|
+
throw new Error('ENOENT');
|
|
2365
|
+
}));
|
|
2366
|
+
const result = await validateSpec(spec, projectPath);
|
|
2367
|
+
// AuthValidator found in fileContents via content.includes(name)
|
|
2368
|
+
expect(result.matches).toContain('Component AuthValidator must work correctly');
|
|
2369
|
+
});
|
|
2370
|
+
});
|
|
2371
|
+
//# sourceMappingURL=validator.test.js.map
|