@planu/cli 0.89.0 → 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 +2 -1
- 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/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 +8 -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.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/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.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 +25 -371
- 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/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/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-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 -481
- 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 +6 -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/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.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 +2 -1
- 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,2075 @@
|
|
|
1
|
+
// SpecForge — Auditor Engine Tests
|
|
2
|
+
// Comprehensive tests for auditCode and auditFile, exercising all internal
|
|
3
|
+
// check functions (checkCleanCode, checkSolid, checkArchitecture, checkSecurity,
|
|
4
|
+
// checkErrorHandling, checkPerformance, checkTestingPractices, checkDry,
|
|
5
|
+
// checkCustomRule) via the public API surface plus helpers
|
|
6
|
+
// (findFunctionBoundaries, findCodeFiles, buildSummary, calculateScore).
|
|
7
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
8
|
+
// ── Module-level mocks ──────────────────────────────────────────────────────
|
|
9
|
+
vi.mock('node:fs/promises', () => ({
|
|
10
|
+
readFile: vi.fn(),
|
|
11
|
+
stat: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
vi.mock('glob', () => ({
|
|
14
|
+
glob: vi.fn().mockResolvedValue([]),
|
|
15
|
+
}));
|
|
16
|
+
import { readFile } from 'node:fs/promises';
|
|
17
|
+
import { glob } from 'glob';
|
|
18
|
+
import { auditCode, auditFile } from './auditor.js';
|
|
19
|
+
const mockReadFile = vi.mocked(readFile);
|
|
20
|
+
const mockGlob = vi.mocked(glob);
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
mockGlob.mockResolvedValue([]);
|
|
24
|
+
});
|
|
25
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
26
|
+
function findFinding(findings, rule) {
|
|
27
|
+
return findings.find((f) => f.rule === rule);
|
|
28
|
+
}
|
|
29
|
+
function findFindings(findings, rule) {
|
|
30
|
+
return findings.filter((f) => f.rule === rule);
|
|
31
|
+
}
|
|
32
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
33
|
+
// auditFile — lightweight single-file auditing
|
|
34
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
35
|
+
describe('auditFile', () => {
|
|
36
|
+
it('returns empty array when readFile throws', async () => {
|
|
37
|
+
mockReadFile.mockRejectedValue(new Error('ENOENT'));
|
|
38
|
+
const findings = await auditFile('/missing.ts');
|
|
39
|
+
expect(findings).toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
it('uses default categories [clean-code, security] when none provided', async () => {
|
|
42
|
+
const content = [
|
|
43
|
+
`const api_key = "realSecretKeyValue1234";`,
|
|
44
|
+
`// TODO: fix this`,
|
|
45
|
+
].join('\n');
|
|
46
|
+
mockReadFile.mockResolvedValue(content);
|
|
47
|
+
const findings = await auditFile('/test/defaults.ts');
|
|
48
|
+
const cats = new Set(findings.map((f) => f.category));
|
|
49
|
+
// Should include both clean-code and security
|
|
50
|
+
expect(cats.has('clean-code')).toBe(true);
|
|
51
|
+
expect(cats.has('security')).toBe(true);
|
|
52
|
+
// Should NOT include error-handling (not in defaults)
|
|
53
|
+
expect(cats.has('error-handling')).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
it('runs error-handling checks when explicitly requested', async () => {
|
|
56
|
+
const content = `try { doSomething(); } catch (e) {}`;
|
|
57
|
+
mockReadFile.mockResolvedValue(content);
|
|
58
|
+
const findings = await auditFile('/test/err.ts', ['error-handling']);
|
|
59
|
+
expect(findFinding(findings, 'empty-catch')).toBeDefined();
|
|
60
|
+
});
|
|
61
|
+
it('only runs requested category', async () => {
|
|
62
|
+
const content = `const api_key = "superSecretValue12345"; eval("bad"); // TODO: fix`;
|
|
63
|
+
mockReadFile.mockResolvedValue(content);
|
|
64
|
+
const findings = await auditFile('/test/only-sec.ts', ['security']);
|
|
65
|
+
for (const f of findings) {
|
|
66
|
+
expect(f.category).toBe('security');
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
71
|
+
// auditCode — full project audit
|
|
72
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
73
|
+
describe('auditCode', () => {
|
|
74
|
+
// -- findCodeFiles --
|
|
75
|
+
it('returns result with zero files when glob returns empty', async () => {
|
|
76
|
+
mockGlob.mockResolvedValue([]);
|
|
77
|
+
const result = await auditCode('src', '/project');
|
|
78
|
+
expect(result.filesAnalyzed).toBe(0);
|
|
79
|
+
expect(result.findings).toEqual([]);
|
|
80
|
+
expect(result.score).toBe(100);
|
|
81
|
+
});
|
|
82
|
+
it('filters by code extensions from glob results', async () => {
|
|
83
|
+
mockGlob.mockResolvedValue(['app.ts', 'readme.md', 'logo.png', 'util.js']);
|
|
84
|
+
mockReadFile.mockResolvedValue('const x = 1;');
|
|
85
|
+
const result = await auditCode('src', '/project');
|
|
86
|
+
// Only .ts and .js should pass
|
|
87
|
+
expect(result.filesAnalyzed).toBe(2);
|
|
88
|
+
});
|
|
89
|
+
it('handles glob failure gracefully (returns empty files)', async () => {
|
|
90
|
+
mockGlob.mockRejectedValue(new Error('permission denied'));
|
|
91
|
+
const result = await auditCode('src', '/project');
|
|
92
|
+
expect(result.filesAnalyzed).toBe(0);
|
|
93
|
+
expect(result.score).toBe(100);
|
|
94
|
+
});
|
|
95
|
+
it('skips files that fail to read and continues', async () => {
|
|
96
|
+
mockGlob.mockResolvedValue(['good.ts', 'bad.ts']);
|
|
97
|
+
mockReadFile
|
|
98
|
+
.mockResolvedValueOnce('const x = 1;')
|
|
99
|
+
.mockRejectedValueOnce(new Error('ENOENT'));
|
|
100
|
+
const result = await auditCode('src', '/project');
|
|
101
|
+
expect(result.filesAnalyzed).toBe(2);
|
|
102
|
+
// Should not throw
|
|
103
|
+
});
|
|
104
|
+
it('resolves absolute path when path starts with /', async () => {
|
|
105
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
106
|
+
mockReadFile.mockResolvedValue('const x = 1;');
|
|
107
|
+
await auditCode('/absolute/src', '/project');
|
|
108
|
+
// glob should be called with the absolute path as cwd
|
|
109
|
+
expect(mockGlob).toHaveBeenCalledWith('**/*', expect.objectContaining({ cwd: '/absolute/src' }));
|
|
110
|
+
});
|
|
111
|
+
it('resolves relative path by joining with projectPath', async () => {
|
|
112
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
113
|
+
mockReadFile.mockResolvedValue('const x = 1;');
|
|
114
|
+
await auditCode('src', '/project');
|
|
115
|
+
expect(mockGlob).toHaveBeenCalledWith('**/*', expect.objectContaining({ cwd: '/project/src' }));
|
|
116
|
+
});
|
|
117
|
+
// -- Default categories --
|
|
118
|
+
it('uses all default categories when none provided', async () => {
|
|
119
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
120
|
+
// Content that triggers multiple categories
|
|
121
|
+
const content = [
|
|
122
|
+
'// TODO: fix',
|
|
123
|
+
'try { x(); } catch (e) {}',
|
|
124
|
+
].join('\n');
|
|
125
|
+
mockReadFile.mockResolvedValue(content);
|
|
126
|
+
const result = await auditCode('src', '/project');
|
|
127
|
+
const cats = new Set(result.findings.map((f) => f.category));
|
|
128
|
+
// Default categories include clean-code and error-handling
|
|
129
|
+
expect(cats.has('clean-code')).toBe(true);
|
|
130
|
+
expect(cats.has('error-handling')).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
// -- Profile overrides --
|
|
133
|
+
it('uses enabledCategories from profile when provided', async () => {
|
|
134
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
135
|
+
const content = `const api_key = "realSecretKeyValue1234"; // TODO: fix`;
|
|
136
|
+
mockReadFile.mockResolvedValue(content);
|
|
137
|
+
const profile = {
|
|
138
|
+
enabledCategories: ['security'],
|
|
139
|
+
customRules: [],
|
|
140
|
+
principles: [],
|
|
141
|
+
strictness: 'standard',
|
|
142
|
+
};
|
|
143
|
+
const result = await auditCode('src', '/project', ['clean-code'], profile);
|
|
144
|
+
// Should ONLY have security findings (profile overrides passed categories)
|
|
145
|
+
for (const f of result.findings) {
|
|
146
|
+
expect(f.category).toBe('security');
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
// -- Custom rules via profile --
|
|
150
|
+
it('runs custom rules that match enabled categories', async () => {
|
|
151
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
152
|
+
const content = `const DEBUG_MODE = true; console.log("debug");`;
|
|
153
|
+
mockReadFile.mockResolvedValue(content);
|
|
154
|
+
const customRule = {
|
|
155
|
+
id: 'no-debug-mode',
|
|
156
|
+
description: 'DEBUG_MODE should not be true in production',
|
|
157
|
+
category: 'clean-code',
|
|
158
|
+
severity: 'warning',
|
|
159
|
+
source: 'team-rules',
|
|
160
|
+
pattern: 'DEBUG_MODE\\s*=\\s*true',
|
|
161
|
+
};
|
|
162
|
+
const profile = {
|
|
163
|
+
enabledCategories: ['clean-code'],
|
|
164
|
+
customRules: [customRule],
|
|
165
|
+
principles: [],
|
|
166
|
+
strictness: 'standard',
|
|
167
|
+
};
|
|
168
|
+
const result = await auditCode('src', '/project', [], profile);
|
|
169
|
+
const finding = findFinding(result.findings, 'no-debug-mode');
|
|
170
|
+
expect(finding).toBeDefined();
|
|
171
|
+
expect(finding.message).toBe('DEBUG_MODE should not be true in production');
|
|
172
|
+
expect(finding.suggestion).toContain('team-rules');
|
|
173
|
+
});
|
|
174
|
+
it('skips custom rules whose category is not enabled', async () => {
|
|
175
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
176
|
+
const content = `const DEBUG_MODE = true;`;
|
|
177
|
+
mockReadFile.mockResolvedValue(content);
|
|
178
|
+
const customRule = {
|
|
179
|
+
id: 'no-debug-mode',
|
|
180
|
+
description: 'No debug mode',
|
|
181
|
+
category: 'security', // Not in enabledCategories
|
|
182
|
+
severity: 'warning',
|
|
183
|
+
source: 'team-rules',
|
|
184
|
+
pattern: 'DEBUG_MODE',
|
|
185
|
+
};
|
|
186
|
+
const profile = {
|
|
187
|
+
enabledCategories: ['clean-code'],
|
|
188
|
+
customRules: [customRule],
|
|
189
|
+
principles: [],
|
|
190
|
+
strictness: 'standard',
|
|
191
|
+
};
|
|
192
|
+
const result = await auditCode('src', '/project', [], profile);
|
|
193
|
+
expect(findFinding(result.findings, 'no-debug-mode')).toBeUndefined();
|
|
194
|
+
});
|
|
195
|
+
it('handles custom rule with no pattern (returns null)', async () => {
|
|
196
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
197
|
+
mockReadFile.mockResolvedValue('const x = 1;');
|
|
198
|
+
const customRule = {
|
|
199
|
+
id: 'no-pattern-rule',
|
|
200
|
+
description: 'Rule without pattern',
|
|
201
|
+
category: 'clean-code',
|
|
202
|
+
severity: 'info',
|
|
203
|
+
source: 'team',
|
|
204
|
+
// no pattern property
|
|
205
|
+
};
|
|
206
|
+
const profile = {
|
|
207
|
+
enabledCategories: ['clean-code'],
|
|
208
|
+
customRules: [customRule],
|
|
209
|
+
principles: [],
|
|
210
|
+
strictness: 'standard',
|
|
211
|
+
};
|
|
212
|
+
const result = await auditCode('src', '/project', [], profile);
|
|
213
|
+
expect(findFinding(result.findings, 'no-pattern-rule')).toBeUndefined();
|
|
214
|
+
});
|
|
215
|
+
it('handles custom rule with invalid regex pattern gracefully', async () => {
|
|
216
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
217
|
+
mockReadFile.mockResolvedValue('const x = 1;');
|
|
218
|
+
const customRule = {
|
|
219
|
+
id: 'bad-regex-rule',
|
|
220
|
+
description: 'Rule with broken regex',
|
|
221
|
+
category: 'clean-code',
|
|
222
|
+
severity: 'error',
|
|
223
|
+
source: 'team',
|
|
224
|
+
pattern: '[invalid(regex',
|
|
225
|
+
};
|
|
226
|
+
const profile = {
|
|
227
|
+
enabledCategories: ['clean-code'],
|
|
228
|
+
customRules: [customRule],
|
|
229
|
+
principles: [],
|
|
230
|
+
strictness: 'standard',
|
|
231
|
+
};
|
|
232
|
+
const result = await auditCode('src', '/project', [], profile);
|
|
233
|
+
// Should not throw and finding should not appear
|
|
234
|
+
expect(findFinding(result.findings, 'bad-regex-rule')).toBeUndefined();
|
|
235
|
+
});
|
|
236
|
+
it('handles custom rule whose pattern does not match', async () => {
|
|
237
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
238
|
+
mockReadFile.mockResolvedValue('const x = 1;');
|
|
239
|
+
const customRule = {
|
|
240
|
+
id: 'never-matches',
|
|
241
|
+
description: 'Never triggers',
|
|
242
|
+
category: 'clean-code',
|
|
243
|
+
severity: 'info',
|
|
244
|
+
source: 'team',
|
|
245
|
+
pattern: 'NEVER_MATCH_THIS_PATTERN_ABCXYZ',
|
|
246
|
+
};
|
|
247
|
+
const profile = {
|
|
248
|
+
enabledCategories: ['clean-code'],
|
|
249
|
+
customRules: [customRule],
|
|
250
|
+
principles: [],
|
|
251
|
+
strictness: 'standard',
|
|
252
|
+
};
|
|
253
|
+
const result = await auditCode('src', '/project', [], profile);
|
|
254
|
+
expect(findFinding(result.findings, 'never-matches')).toBeUndefined();
|
|
255
|
+
});
|
|
256
|
+
// -- Summary & Score --
|
|
257
|
+
it('returns summary with byCategory, topIssues, strengths, score', async () => {
|
|
258
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
259
|
+
const content = `// TODO: fix\n// FIXME: broken`;
|
|
260
|
+
mockReadFile.mockResolvedValue(content);
|
|
261
|
+
const result = await auditCode('src', '/project', ['clean-code', 'security']);
|
|
262
|
+
expect(result.summary).toBeDefined();
|
|
263
|
+
expect(result.summary.byCategory).toBeDefined();
|
|
264
|
+
expect(result.summary.topIssues).toBeInstanceOf(Array);
|
|
265
|
+
expect(result.summary.strengths).toBeInstanceOf(Array);
|
|
266
|
+
expect(typeof result.summary.score).toBe('number');
|
|
267
|
+
});
|
|
268
|
+
it('summary includes strengths for categories with zero findings', async () => {
|
|
269
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
270
|
+
const content = `function add(a: number, b: number) { return a + b; }`;
|
|
271
|
+
mockReadFile.mockResolvedValue(content);
|
|
272
|
+
const result = await auditCode('src', '/project', ['clean-code', 'security']);
|
|
273
|
+
// security should have zero findings for clean code
|
|
274
|
+
const securityStrength = result.summary.strengths.find((s) => s.includes('security'));
|
|
275
|
+
expect(securityStrength).toBeDefined();
|
|
276
|
+
});
|
|
277
|
+
it('summary topIssues lists up to 5 most frequent rules', async () => {
|
|
278
|
+
mockGlob.mockResolvedValue(['a.ts', 'b.ts', 'c.ts', 'd.ts', 'e.ts', 'f.ts']);
|
|
279
|
+
// Each file has a TODO
|
|
280
|
+
mockReadFile.mockResolvedValue('// TODO: fixme\n// HACK: workaround');
|
|
281
|
+
const result = await auditCode('src', '/project', ['clean-code']);
|
|
282
|
+
expect(result.summary.topIssues.length).toBeLessThanOrEqual(5);
|
|
283
|
+
if (result.summary.topIssues.length > 0) {
|
|
284
|
+
expect(result.summary.topIssues[0]).toContain('occurrences');
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
it('score is 100 when no findings exist', async () => {
|
|
288
|
+
mockGlob.mockResolvedValue([]);
|
|
289
|
+
const result = await auditCode('src', '/project');
|
|
290
|
+
expect(result.score).toBe(100);
|
|
291
|
+
});
|
|
292
|
+
it('score decreases with critical findings', async () => {
|
|
293
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
294
|
+
const content = `const api_key = "realSecretKeyValue1234";`;
|
|
295
|
+
mockReadFile.mockResolvedValue(content);
|
|
296
|
+
const result = await auditCode('src', '/project', ['security']);
|
|
297
|
+
expect(result.score).toBeLessThan(100);
|
|
298
|
+
});
|
|
299
|
+
it('score is non-negative even with many findings', async () => {
|
|
300
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
301
|
+
// Create content with many critical findings
|
|
302
|
+
const lines = [];
|
|
303
|
+
for (let i = 0; i < 20; i++) {
|
|
304
|
+
lines.push(`const secret${i} = "realApiKeyValue${i}pad_pad";`);
|
|
305
|
+
lines.push(`eval("code${i}");`);
|
|
306
|
+
}
|
|
307
|
+
mockReadFile.mockResolvedValue(lines.join('\n'));
|
|
308
|
+
const result = await auditCode('src', '/project', ['security']);
|
|
309
|
+
expect(result.score).toBeGreaterThanOrEqual(0);
|
|
310
|
+
});
|
|
311
|
+
// -- All category branches in auditCode --
|
|
312
|
+
it('runs performance checks when category is performance', async () => {
|
|
313
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
314
|
+
const content = `for (let i = 0; i < items.length; i++) {\n await fetch("/api/" + items[i]);\n}`;
|
|
315
|
+
mockReadFile.mockResolvedValue(content);
|
|
316
|
+
const result = await auditCode('src', '/project', ['performance']);
|
|
317
|
+
const finding = findFinding(result.findings, 'n-plus-one');
|
|
318
|
+
expect(finding).toBeDefined();
|
|
319
|
+
});
|
|
320
|
+
it('runs testing checks when category is testing', async () => {
|
|
321
|
+
mockGlob.mockResolvedValue(['app.test.ts']);
|
|
322
|
+
const content = `it.skip('broken test', () => { expect(true).toBe(true); });`;
|
|
323
|
+
mockReadFile.mockResolvedValue(content);
|
|
324
|
+
const result = await auditCode('src', '/project', ['testing']);
|
|
325
|
+
const finding = findFinding(result.findings, 'test-skip');
|
|
326
|
+
expect(finding).toBeDefined();
|
|
327
|
+
});
|
|
328
|
+
it('runs dry checks when category is dry', async () => {
|
|
329
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
330
|
+
const dupLine = 'const result = processItem(item, config, options);';
|
|
331
|
+
const content = Array.from({ length: 5 }, () => dupLine).join('\n');
|
|
332
|
+
mockReadFile.mockResolvedValue(content);
|
|
333
|
+
const result = await auditCode('src', '/project', ['dry']);
|
|
334
|
+
const finding = findFinding(result.findings, 'duplicate-code');
|
|
335
|
+
expect(finding).toBeDefined();
|
|
336
|
+
});
|
|
337
|
+
it('runs solid checks when category is solid', async () => {
|
|
338
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
339
|
+
// 3 classes => SRP violation
|
|
340
|
+
const content = `class A {}\nclass B {}\nclass C {}`;
|
|
341
|
+
mockReadFile.mockResolvedValue(content);
|
|
342
|
+
const result = await auditCode('src', '/project', ['solid']);
|
|
343
|
+
const finding = findFinding(result.findings, 'srp-multiple-classes');
|
|
344
|
+
expect(finding).toBeDefined();
|
|
345
|
+
});
|
|
346
|
+
it('runs architecture checks when category is architecture', async () => {
|
|
347
|
+
mockGlob.mockResolvedValue(['repositories/user.ts']);
|
|
348
|
+
const content = `import { useState } from 'react';`;
|
|
349
|
+
mockReadFile.mockResolvedValue(content);
|
|
350
|
+
const result = await auditCode('.', '/project');
|
|
351
|
+
expect(findFinding(result.findings, 'layer-violation-ui-in-data')).toBeDefined();
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
355
|
+
// checkCleanCode — detailed branch coverage
|
|
356
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
357
|
+
describe('checkCleanCode', () => {
|
|
358
|
+
// -- file-too-long --
|
|
359
|
+
it('flags files exceeding 500 lines', async () => {
|
|
360
|
+
const longContent = Array.from({ length: 550 }, (_, i) => `const x${i} = ${i};`).join('\n');
|
|
361
|
+
mockReadFile.mockResolvedValue(longContent);
|
|
362
|
+
const findings = await auditFile('/test/long.ts', ['clean-code']);
|
|
363
|
+
const finding = findFinding(findings, 'file-too-long');
|
|
364
|
+
expect(finding).toBeDefined();
|
|
365
|
+
expect(finding.severity).toBe('warning');
|
|
366
|
+
expect(finding.message).toContain('550');
|
|
367
|
+
expect(finding.message).toContain('500');
|
|
368
|
+
});
|
|
369
|
+
it('does not flag file with exactly 500 lines', async () => {
|
|
370
|
+
const content = Array.from({ length: 500 }, (_, i) => `const x${i} = ${i};`).join('\n');
|
|
371
|
+
mockReadFile.mockResolvedValue(content);
|
|
372
|
+
const findings = await auditFile('/test/exact.ts', ['clean-code']);
|
|
373
|
+
expect(findFinding(findings, 'file-too-long')).toBeUndefined();
|
|
374
|
+
});
|
|
375
|
+
// -- function-too-long --
|
|
376
|
+
it('flags functions exceeding 50 lines', async () => {
|
|
377
|
+
const body = Array.from({ length: 55 }, (_, i) => ` const v${i} = ${i};`).join('\n');
|
|
378
|
+
const content = `function longFunction() {\n${body}\n}`;
|
|
379
|
+
mockReadFile.mockResolvedValue(content);
|
|
380
|
+
const findings = await auditFile('/test/long-fn.ts', ['clean-code']);
|
|
381
|
+
const finding = findFinding(findings, 'function-too-long');
|
|
382
|
+
expect(finding).toBeDefined();
|
|
383
|
+
expect(finding.message).toContain('longFunction');
|
|
384
|
+
});
|
|
385
|
+
it('does not flag functions within 50 lines', async () => {
|
|
386
|
+
const body = Array.from({ length: 10 }, (_, i) => ` const v${i} = ${i};`).join('\n');
|
|
387
|
+
const content = `function shortFunction() {\n${body}\n}`;
|
|
388
|
+
mockReadFile.mockResolvedValue(content);
|
|
389
|
+
const findings = await auditFile('/test/short-fn.ts', ['clean-code']);
|
|
390
|
+
expect(findFinding(findings, 'function-too-long')).toBeUndefined();
|
|
391
|
+
});
|
|
392
|
+
it('detects arrow function declarations', async () => {
|
|
393
|
+
const body = Array.from({ length: 55 }, (_, i) => ` const v${i} = ${i};`).join('\n');
|
|
394
|
+
const content = `const myArrowFn = () => {\n${body}\n}`;
|
|
395
|
+
mockReadFile.mockResolvedValue(content);
|
|
396
|
+
const findings = await auditFile('/test/arrow-fn.ts', ['clean-code']);
|
|
397
|
+
const finding = findFinding(findings, 'function-too-long');
|
|
398
|
+
expect(finding).toBeDefined();
|
|
399
|
+
expect(finding.message).toContain('myArrowFn');
|
|
400
|
+
});
|
|
401
|
+
it('detects python def functions', async () => {
|
|
402
|
+
// Python-style function detection
|
|
403
|
+
const body = Array.from({ length: 55 }, (_, i) => ` v${i} = ${i}`).join('\n');
|
|
404
|
+
const content = `def long_function():\n${body}\n`;
|
|
405
|
+
mockReadFile.mockResolvedValue(content);
|
|
406
|
+
const findings = await auditFile('/test/long-fn.py', ['clean-code']);
|
|
407
|
+
const finding = findFinding(findings, 'function-too-long');
|
|
408
|
+
expect(finding).toBeDefined();
|
|
409
|
+
expect(finding.message).toContain('long_function');
|
|
410
|
+
});
|
|
411
|
+
it('detects Go func functions', async () => {
|
|
412
|
+
const body = Array.from({ length: 55 }, (_, i) => ` v${i} := ${i}`).join('\n');
|
|
413
|
+
const content = `func longGoFunction() {\n${body}\n}`;
|
|
414
|
+
mockReadFile.mockResolvedValue(content);
|
|
415
|
+
const findings = await auditFile('/test/long-fn.go', ['clean-code']);
|
|
416
|
+
const finding = findFinding(findings, 'function-too-long');
|
|
417
|
+
expect(finding).toBeDefined();
|
|
418
|
+
expect(finding.message).toContain('longGoFunction');
|
|
419
|
+
});
|
|
420
|
+
it('detects Rust fn functions', async () => {
|
|
421
|
+
const body = Array.from({ length: 55 }, (_, i) => ` let v${i} = ${i};`).join('\n');
|
|
422
|
+
const content = `fn long_rust_function() {\n${body}\n}`;
|
|
423
|
+
mockReadFile.mockResolvedValue(content);
|
|
424
|
+
const findings = await auditFile('/test/long-fn.rs', ['clean-code']);
|
|
425
|
+
const finding = findFinding(findings, 'function-too-long');
|
|
426
|
+
expect(finding).toBeDefined();
|
|
427
|
+
expect(finding.message).toContain('long_rust_function');
|
|
428
|
+
});
|
|
429
|
+
it('detects Kotlin fun functions', async () => {
|
|
430
|
+
const body = Array.from({ length: 55 }, (_, i) => ` val v${i} = ${i}`).join('\n');
|
|
431
|
+
const content = `fun longKotlinFunction() {\n${body}\n}`;
|
|
432
|
+
mockReadFile.mockResolvedValue(content);
|
|
433
|
+
const findings = await auditFile('/test/long-fn.kt', ['clean-code']);
|
|
434
|
+
const finding = findFinding(findings, 'function-too-long');
|
|
435
|
+
expect(finding).toBeDefined();
|
|
436
|
+
expect(finding.message).toContain('longKotlinFunction');
|
|
437
|
+
});
|
|
438
|
+
it('detects async function declarations', async () => {
|
|
439
|
+
const body = Array.from({ length: 55 }, (_, i) => ` const v${i} = ${i};`).join('\n');
|
|
440
|
+
const content = `async function asyncLongFn() {\n${body}\n}`;
|
|
441
|
+
mockReadFile.mockResolvedValue(content);
|
|
442
|
+
const findings = await auditFile('/test/async-long.ts', ['clean-code']);
|
|
443
|
+
const finding = findFinding(findings, 'function-too-long');
|
|
444
|
+
expect(finding).toBeDefined();
|
|
445
|
+
expect(finding.message).toContain('asyncLongFn');
|
|
446
|
+
});
|
|
447
|
+
it('detects export function declarations', async () => {
|
|
448
|
+
const body = Array.from({ length: 55 }, (_, i) => ` const v${i} = ${i};`).join('\n');
|
|
449
|
+
const content = `export function exportedLongFn() {\n${body}\n}`;
|
|
450
|
+
mockReadFile.mockResolvedValue(content);
|
|
451
|
+
const findings = await auditFile('/test/exported-long.ts', ['clean-code']);
|
|
452
|
+
const finding = findFinding(findings, 'function-too-long');
|
|
453
|
+
expect(finding).toBeDefined();
|
|
454
|
+
expect(finding.message).toContain('exportedLongFn');
|
|
455
|
+
});
|
|
456
|
+
it('detects export async function declarations', async () => {
|
|
457
|
+
const body = Array.from({ length: 55 }, (_, i) => ` const v${i} = ${i};`).join('\n');
|
|
458
|
+
const content = `export async function exportAsyncLongFn() {\n${body}\n}`;
|
|
459
|
+
mockReadFile.mockResolvedValue(content);
|
|
460
|
+
const findings = await auditFile('/test/export-async-long.ts', ['clean-code']);
|
|
461
|
+
const finding = findFinding(findings, 'function-too-long');
|
|
462
|
+
expect(finding).toBeDefined();
|
|
463
|
+
expect(finding.message).toContain('exportAsyncLongFn');
|
|
464
|
+
});
|
|
465
|
+
// -- too-many-params --
|
|
466
|
+
it('flags functions with more than 4 parameters', async () => {
|
|
467
|
+
// Must be 80+ chars inside parens AND more than 4 params
|
|
468
|
+
const content = `function bigFn(paramAlpha: string, paramBeta: number, paramGamma: boolean, paramDelta: object, paramEpsilon: any) { return 1; }`;
|
|
469
|
+
mockReadFile.mockResolvedValue(content);
|
|
470
|
+
const findings = await auditFile('/test/params.ts', ['clean-code']);
|
|
471
|
+
const finding = findFinding(findings, 'too-many-params');
|
|
472
|
+
expect(finding).toBeDefined();
|
|
473
|
+
expect(finding.severity).toBe('warning');
|
|
474
|
+
expect(finding.message).toContain('5');
|
|
475
|
+
});
|
|
476
|
+
it('does not flag functions with 4 or fewer parameters', async () => {
|
|
477
|
+
const content = `function smallFn(a: string, b: number, c: boolean, d: object) { return 1; }`;
|
|
478
|
+
mockReadFile.mockResolvedValue(content);
|
|
479
|
+
const findings = await auditFile('/test/few-params.ts', ['clean-code']);
|
|
480
|
+
expect(findFinding(findings, 'too-many-params')).toBeUndefined();
|
|
481
|
+
});
|
|
482
|
+
it('does not flag function params under 80 chars (short signature)', async () => {
|
|
483
|
+
// Even with 5 params, if they're short enough not to exceed 80 chars, regex won't match
|
|
484
|
+
const content = `function fn(a, b, c, d, e) { return 1; }`;
|
|
485
|
+
mockReadFile.mockResolvedValue(content);
|
|
486
|
+
const findings = await auditFile('/test/short-sig.ts', ['clean-code']);
|
|
487
|
+
expect(findFinding(findings, 'too-many-params')).toBeUndefined();
|
|
488
|
+
});
|
|
489
|
+
it('detects def with many params (Python)', async () => {
|
|
490
|
+
const content = `def big_fn(param_alpha, param_beta, param_gamma, param_delta, param_epsilon, param_zeta_extra) :\n pass`;
|
|
491
|
+
mockReadFile.mockResolvedValue(content);
|
|
492
|
+
const findings = await auditFile('/test/params.py', ['clean-code']);
|
|
493
|
+
const finding = findFinding(findings, 'too-many-params');
|
|
494
|
+
expect(finding).toBeDefined();
|
|
495
|
+
});
|
|
496
|
+
// -- pending-marker --
|
|
497
|
+
it('detects TODO markers', async () => {
|
|
498
|
+
const content = `function foo() {\n // TODO: fix this later\n return 42;\n}`;
|
|
499
|
+
mockReadFile.mockResolvedValue(content);
|
|
500
|
+
const findings = await auditFile('/test/todo.ts', ['clean-code']);
|
|
501
|
+
const finding = findFinding(findings, 'pending-marker');
|
|
502
|
+
expect(finding).toBeDefined();
|
|
503
|
+
expect(finding.severity).toBe('info');
|
|
504
|
+
expect(finding.message).toContain('TODO');
|
|
505
|
+
expect(finding.line).toBe(2);
|
|
506
|
+
});
|
|
507
|
+
it('detects FIXME markers', async () => {
|
|
508
|
+
const content = `// FIXME: memory leak here`;
|
|
509
|
+
mockReadFile.mockResolvedValue(content);
|
|
510
|
+
const findings = await auditFile('/test/fixme.ts', ['clean-code']);
|
|
511
|
+
expect(findFinding(findings, 'pending-marker').message).toContain('FIXME');
|
|
512
|
+
});
|
|
513
|
+
it('detects HACK markers', async () => {
|
|
514
|
+
const content = `const x = 1; // HACK: workaround for issue`;
|
|
515
|
+
mockReadFile.mockResolvedValue(content);
|
|
516
|
+
const findings = await auditFile('/test/hack.ts', ['clean-code']);
|
|
517
|
+
expect(findFinding(findings, 'pending-marker').message).toContain('HACK');
|
|
518
|
+
});
|
|
519
|
+
it('detects XXX markers', async () => {
|
|
520
|
+
const content = `// XXX: needs review`;
|
|
521
|
+
mockReadFile.mockResolvedValue(content);
|
|
522
|
+
const findings = await auditFile('/test/xxx.ts', ['clean-code']);
|
|
523
|
+
expect(findFinding(findings, 'pending-marker').message).toContain('XXX');
|
|
524
|
+
});
|
|
525
|
+
it('handles TODO with no description (empty match group)', async () => {
|
|
526
|
+
const content = `// TODO:`;
|
|
527
|
+
mockReadFile.mockResolvedValue(content);
|
|
528
|
+
const findings = await auditFile('/test/todo-empty.ts', ['clean-code']);
|
|
529
|
+
const finding = findFinding(findings, 'pending-marker');
|
|
530
|
+
expect(finding).toBeDefined();
|
|
531
|
+
// message should include "(no description)" or empty string
|
|
532
|
+
expect(finding.message).toContain('TODO');
|
|
533
|
+
});
|
|
534
|
+
// -- deep-nesting --
|
|
535
|
+
it('flags deeply nested code (more than 5 indentation levels)', async () => {
|
|
536
|
+
const content = [
|
|
537
|
+
'function deep() {',
|
|
538
|
+
' if (true) {',
|
|
539
|
+
' if (true) {',
|
|
540
|
+
' if (true) {',
|
|
541
|
+
' if (true) {',
|
|
542
|
+
' if (true) {',
|
|
543
|
+
' const x = 1;', // 12 spaces = 6 levels
|
|
544
|
+
' }',
|
|
545
|
+
' }',
|
|
546
|
+
' }',
|
|
547
|
+
' }',
|
|
548
|
+
' }',
|
|
549
|
+
'}',
|
|
550
|
+
].join('\n');
|
|
551
|
+
mockReadFile.mockResolvedValue(content);
|
|
552
|
+
const findings = await auditFile('/test/deep.ts', ['clean-code']);
|
|
553
|
+
const finding = findFinding(findings, 'deep-nesting');
|
|
554
|
+
expect(finding).toBeDefined();
|
|
555
|
+
expect(finding.severity).toBe('warning');
|
|
556
|
+
});
|
|
557
|
+
it('does not flag moderately nested code', async () => {
|
|
558
|
+
const content = [
|
|
559
|
+
'function shallow() {',
|
|
560
|
+
' if (true) {',
|
|
561
|
+
' if (true) {',
|
|
562
|
+
' const x = 1;', // 6 spaces = 3 levels
|
|
563
|
+
' }',
|
|
564
|
+
' }',
|
|
565
|
+
'}',
|
|
566
|
+
].join('\n');
|
|
567
|
+
mockReadFile.mockResolvedValue(content);
|
|
568
|
+
const findings = await auditFile('/test/shallow.ts', ['clean-code']);
|
|
569
|
+
expect(findFinding(findings, 'deep-nesting')).toBeUndefined();
|
|
570
|
+
});
|
|
571
|
+
it('reports only first deep nesting occurrence per file (break)', async () => {
|
|
572
|
+
const content = [
|
|
573
|
+
' const x = 1;', // deep
|
|
574
|
+
' const y = 2;', // also deep
|
|
575
|
+
].join('\n');
|
|
576
|
+
mockReadFile.mockResolvedValue(content);
|
|
577
|
+
const findings = await auditFile('/test/multi-deep.ts', ['clean-code']);
|
|
578
|
+
const deepFindings = findFindings(findings, 'deep-nesting');
|
|
579
|
+
expect(deepFindings).toHaveLength(1);
|
|
580
|
+
});
|
|
581
|
+
it('skips blank deeply indented lines', async () => {
|
|
582
|
+
const content = [
|
|
583
|
+
' ', // deep but empty (trim().length === 0)
|
|
584
|
+
' const x = 1;',
|
|
585
|
+
].join('\n');
|
|
586
|
+
mockReadFile.mockResolvedValue(content);
|
|
587
|
+
const findings = await auditFile('/test/blank-deep.ts', ['clean-code']);
|
|
588
|
+
expect(findFinding(findings, 'deep-nesting')).toBeUndefined();
|
|
589
|
+
});
|
|
590
|
+
// -- magic-number --
|
|
591
|
+
it('detects magic numbers in code', async () => {
|
|
592
|
+
const content = `function calc(x: number) {\n return x * 42;\n}`;
|
|
593
|
+
mockReadFile.mockResolvedValue(content);
|
|
594
|
+
const findings = await auditFile('/test/magic.ts', ['clean-code']);
|
|
595
|
+
const finding = findFinding(findings, 'magic-number');
|
|
596
|
+
expect(finding).toBeDefined();
|
|
597
|
+
expect(finding.severity).toBe('info');
|
|
598
|
+
expect(finding.message).toContain('42');
|
|
599
|
+
});
|
|
600
|
+
it('does not flag common HTTP status codes', async () => {
|
|
601
|
+
const content = `if (status === 404) { return; }`;
|
|
602
|
+
mockReadFile.mockResolvedValue(content);
|
|
603
|
+
const findings = await auditFile('/test/status.ts', ['clean-code']);
|
|
604
|
+
expect(findFinding(findings, 'magic-number')).toBeUndefined();
|
|
605
|
+
});
|
|
606
|
+
it('does not flag 0 or 1', async () => {
|
|
607
|
+
// 0 and 1 are single digit, so the regex won't match (needs 2+ digits)
|
|
608
|
+
const content = `const x = 0; const y = 1;`;
|
|
609
|
+
mockReadFile.mockResolvedValue(content);
|
|
610
|
+
const findings = await auditFile('/test/zero-one.ts', ['clean-code']);
|
|
611
|
+
expect(findFinding(findings, 'magic-number')).toBeUndefined();
|
|
612
|
+
});
|
|
613
|
+
it('does not flag numbers on lines with "port" or "PORT"', async () => {
|
|
614
|
+
const content = `const port = 3000;`;
|
|
615
|
+
mockReadFile.mockResolvedValue(content);
|
|
616
|
+
const findings = await auditFile('/test/port.ts', ['clean-code']);
|
|
617
|
+
expect(findFinding(findings, 'magic-number')).toBeUndefined();
|
|
618
|
+
});
|
|
619
|
+
it('does not flag numbers on lines with "timeout"', async () => {
|
|
620
|
+
const content = `const timeout = 5000;`;
|
|
621
|
+
mockReadFile.mockResolvedValue(content);
|
|
622
|
+
const findings = await auditFile('/test/timeout.ts', ['clean-code']);
|
|
623
|
+
expect(findFinding(findings, 'magic-number')).toBeUndefined();
|
|
624
|
+
});
|
|
625
|
+
it('skips comment lines starting with //', async () => {
|
|
626
|
+
const content = ` // Magic number 42 in comment`;
|
|
627
|
+
mockReadFile.mockResolvedValue(content);
|
|
628
|
+
const findings = await auditFile('/test/comment.ts', ['clean-code']);
|
|
629
|
+
expect(findFinding(findings, 'magic-number')).toBeUndefined();
|
|
630
|
+
});
|
|
631
|
+
it('skips comment lines starting with #', async () => {
|
|
632
|
+
const content = ` # Magic number 42 in comment`;
|
|
633
|
+
mockReadFile.mockResolvedValue(content);
|
|
634
|
+
const findings = await auditFile('/test/hash-comment.ts', ['clean-code']);
|
|
635
|
+
expect(findFinding(findings, 'magic-number')).toBeUndefined();
|
|
636
|
+
});
|
|
637
|
+
it('skips comment lines starting with *', async () => {
|
|
638
|
+
const content = ` * Magic number 42 in JSDoc`;
|
|
639
|
+
mockReadFile.mockResolvedValue(content);
|
|
640
|
+
const findings = await auditFile('/test/jsdoc.ts', ['clean-code']);
|
|
641
|
+
expect(findFinding(findings, 'magic-number')).toBeUndefined();
|
|
642
|
+
});
|
|
643
|
+
it('skips import lines', async () => {
|
|
644
|
+
const content = `import { something42 } from 'module';`;
|
|
645
|
+
mockReadFile.mockResolvedValue(content);
|
|
646
|
+
const findings = await auditFile('/test/import.ts', ['clean-code']);
|
|
647
|
+
expect(findFinding(findings, 'magic-number')).toBeUndefined();
|
|
648
|
+
});
|
|
649
|
+
it('skips require lines', async () => {
|
|
650
|
+
const content = `const mod = require('module42');`;
|
|
651
|
+
mockReadFile.mockResolvedValue(content);
|
|
652
|
+
const findings = await auditFile('/test/require.ts', ['clean-code']);
|
|
653
|
+
expect(findFinding(findings, 'magic-number')).toBeUndefined();
|
|
654
|
+
});
|
|
655
|
+
it('reports only one magic number per file (break)', async () => {
|
|
656
|
+
const content = [
|
|
657
|
+
'const a = 42;',
|
|
658
|
+
'const b = 99;',
|
|
659
|
+
].join('\n');
|
|
660
|
+
mockReadFile.mockResolvedValue(content);
|
|
661
|
+
const findings = await auditFile('/test/multi-magic.ts', ['clean-code']);
|
|
662
|
+
const magicFindings = findFindings(findings, 'magic-number');
|
|
663
|
+
expect(magicFindings).toHaveLength(1);
|
|
664
|
+
});
|
|
665
|
+
it('does not flag common non-magic numbers like 100, 200, etc.', async () => {
|
|
666
|
+
const content = `const pct = 100; const ok = 200; const created = 201;`;
|
|
667
|
+
mockReadFile.mockResolvedValue(content);
|
|
668
|
+
const findings = await auditFile('/test/common-nums.ts', ['clean-code']);
|
|
669
|
+
expect(findFinding(findings, 'magic-number')).toBeUndefined();
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
673
|
+
// checkSolid — SOLID principle checks
|
|
674
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
675
|
+
describe('checkSolid', () => {
|
|
676
|
+
// -- srp-multiple-classes --
|
|
677
|
+
it('flags files with more than 2 classes', async () => {
|
|
678
|
+
mockGlob.mockResolvedValue(['multi-class.ts']);
|
|
679
|
+
const content = `class A {}\nclass B {}\nclass C {}`;
|
|
680
|
+
mockReadFile.mockResolvedValue(content);
|
|
681
|
+
const result = await auditCode('src', '/project', ['solid']);
|
|
682
|
+
const finding = findFinding(result.findings, 'srp-multiple-classes');
|
|
683
|
+
expect(finding).toBeDefined();
|
|
684
|
+
expect(finding.severity).toBe('warning');
|
|
685
|
+
expect(finding.message).toContain('3');
|
|
686
|
+
expect(finding.reference).toContain('Single Responsibility');
|
|
687
|
+
});
|
|
688
|
+
it('does not flag files with 2 or fewer classes', async () => {
|
|
689
|
+
mockGlob.mockResolvedValue(['two-class.ts']);
|
|
690
|
+
const content = `class A {}\nclass B {}`;
|
|
691
|
+
mockReadFile.mockResolvedValue(content);
|
|
692
|
+
const result = await auditCode('src', '/project', ['solid']);
|
|
693
|
+
expect(findFinding(result.findings, 'srp-multiple-classes')).toBeUndefined();
|
|
694
|
+
});
|
|
695
|
+
// -- srp-too-many-exports --
|
|
696
|
+
it('flags files with more than 15 exports', async () => {
|
|
697
|
+
mockGlob.mockResolvedValue(['exports.ts']);
|
|
698
|
+
const exports = Array.from({ length: 16 }, (_, i) => `export const x${i} = ${i};`).join('\n');
|
|
699
|
+
mockReadFile.mockResolvedValue(exports);
|
|
700
|
+
const result = await auditCode('src', '/project', ['solid']);
|
|
701
|
+
const finding = findFinding(result.findings, 'srp-too-many-exports');
|
|
702
|
+
expect(finding).toBeDefined();
|
|
703
|
+
expect(finding.severity).toBe('info');
|
|
704
|
+
});
|
|
705
|
+
it('does not flag files with 15 or fewer exports', async () => {
|
|
706
|
+
mockGlob.mockResolvedValue(['exports.ts']);
|
|
707
|
+
const exports = Array.from({ length: 15 }, (_, i) => `export const x${i} = ${i};`).join('\n');
|
|
708
|
+
mockReadFile.mockResolvedValue(exports);
|
|
709
|
+
const result = await auditCode('src', '/project', ['solid']);
|
|
710
|
+
expect(findFinding(result.findings, 'srp-too-many-exports')).toBeUndefined();
|
|
711
|
+
});
|
|
712
|
+
// -- ocp-long-switch --
|
|
713
|
+
it('flags switch statements with more than 7 cases', async () => {
|
|
714
|
+
mockGlob.mockResolvedValue(['switch.ts']);
|
|
715
|
+
const cases = Array.from({ length: 8 }, (_, i) => ` case ${i}: return ${i};`).join('\n');
|
|
716
|
+
const content = `function handler(x: number) {\n switch (x) {\n${cases}\n }\n}`;
|
|
717
|
+
mockReadFile.mockResolvedValue(content);
|
|
718
|
+
const result = await auditCode('src', '/project', ['solid']);
|
|
719
|
+
const finding = findFinding(result.findings, 'ocp-long-switch');
|
|
720
|
+
expect(finding).toBeDefined();
|
|
721
|
+
expect(finding.severity).toBe('warning');
|
|
722
|
+
expect(finding.message).toContain('8');
|
|
723
|
+
expect(finding.reference).toContain('Open/Closed');
|
|
724
|
+
});
|
|
725
|
+
it('does not flag switch with 7 or fewer cases', async () => {
|
|
726
|
+
mockGlob.mockResolvedValue(['switch.ts']);
|
|
727
|
+
const cases = Array.from({ length: 7 }, (_, i) => ` case ${i}: return ${i};`).join('\n');
|
|
728
|
+
const content = `switch (x) {\n${cases}\n}`;
|
|
729
|
+
mockReadFile.mockResolvedValue(content);
|
|
730
|
+
const result = await auditCode('src', '/project', ['solid']);
|
|
731
|
+
expect(findFinding(result.findings, 'ocp-long-switch')).toBeUndefined();
|
|
732
|
+
});
|
|
733
|
+
it('does not flag when no switch exists', async () => {
|
|
734
|
+
mockGlob.mockResolvedValue(['no-switch.ts']);
|
|
735
|
+
const content = `if (x === 1) { return 1; } else if (x === 2) { return 2; }`;
|
|
736
|
+
mockReadFile.mockResolvedValue(content);
|
|
737
|
+
const result = await auditCode('src', '/project', ['solid']);
|
|
738
|
+
expect(findFinding(result.findings, 'ocp-long-switch')).toBeUndefined();
|
|
739
|
+
});
|
|
740
|
+
// -- isp-large-interface --
|
|
741
|
+
it('flags interfaces with more than 8 members (TypeScript files)', async () => {
|
|
742
|
+
mockGlob.mockResolvedValue(['types.ts']);
|
|
743
|
+
const members = Array.from({ length: 10 }, (_, i) => ` field${i}: string;`).join('\n');
|
|
744
|
+
const content = `interface BigInterface {\n${members}\n}`;
|
|
745
|
+
mockReadFile.mockResolvedValue(content);
|
|
746
|
+
const result = await auditCode('src', '/project', ['solid']);
|
|
747
|
+
const finding = findFinding(result.findings, 'isp-large-interface');
|
|
748
|
+
expect(finding).toBeDefined();
|
|
749
|
+
expect(finding.severity).toBe('info');
|
|
750
|
+
expect(finding.message).toContain('BigInterface');
|
|
751
|
+
expect(finding.message).toContain('10');
|
|
752
|
+
expect(finding.reference).toContain('Interface Segregation');
|
|
753
|
+
});
|
|
754
|
+
it('does not flag interfaces with 8 or fewer members', async () => {
|
|
755
|
+
mockGlob.mockResolvedValue(['types.ts']);
|
|
756
|
+
const members = Array.from({ length: 8 }, (_, i) => ` field${i}: string;`).join('\n');
|
|
757
|
+
const content = `interface SmallInterface {\n${members}\n}`;
|
|
758
|
+
mockReadFile.mockResolvedValue(content);
|
|
759
|
+
const result = await auditCode('src', '/project', ['solid']);
|
|
760
|
+
expect(findFinding(result.findings, 'isp-large-interface')).toBeUndefined();
|
|
761
|
+
});
|
|
762
|
+
it('does not check interfaces in non-TS files', async () => {
|
|
763
|
+
mockGlob.mockResolvedValue(['types.js']);
|
|
764
|
+
const members = Array.from({ length: 10 }, (_, i) => ` field${i}: string;`).join('\n');
|
|
765
|
+
const content = `interface BigInterface {\n${members}\n}`;
|
|
766
|
+
mockReadFile.mockResolvedValue(content);
|
|
767
|
+
const result = await auditCode('src', '/project', ['solid']);
|
|
768
|
+
expect(findFinding(result.findings, 'isp-large-interface')).toBeUndefined();
|
|
769
|
+
});
|
|
770
|
+
it('checks interfaces in .tsx files too', async () => {
|
|
771
|
+
mockGlob.mockResolvedValue(['types.tsx']);
|
|
772
|
+
const members = Array.from({ length: 10 }, (_, i) => ` field${i}: string;`).join('\n');
|
|
773
|
+
const content = `interface BigTsxInterface {\n${members}\n}`;
|
|
774
|
+
mockReadFile.mockResolvedValue(content);
|
|
775
|
+
const result = await auditCode('src', '/project', ['solid']);
|
|
776
|
+
const finding = findFinding(result.findings, 'isp-large-interface');
|
|
777
|
+
expect(finding).toBeDefined();
|
|
778
|
+
expect(finding.message).toContain('BigTsxInterface');
|
|
779
|
+
});
|
|
780
|
+
// -- dip-direct-instantiation --
|
|
781
|
+
it('flags direct instantiation of infrastructure classes', async () => {
|
|
782
|
+
mockGlob.mockResolvedValue(['service.ts']);
|
|
783
|
+
const content = `const db = new DatabaseConnection("localhost");`;
|
|
784
|
+
mockReadFile.mockResolvedValue(content);
|
|
785
|
+
const result = await auditCode('src', '/project', ['solid']);
|
|
786
|
+
const finding = findFinding(result.findings, 'dip-direct-instantiation');
|
|
787
|
+
expect(finding).toBeDefined();
|
|
788
|
+
expect(finding.severity).toBe('warning');
|
|
789
|
+
expect(finding.message).toContain('Database');
|
|
790
|
+
expect(finding.reference).toContain('Dependency Inversion');
|
|
791
|
+
});
|
|
792
|
+
it('flags new HttpClient', async () => {
|
|
793
|
+
mockGlob.mockResolvedValue(['service.ts']);
|
|
794
|
+
const content = `const client = new HttpClient();`;
|
|
795
|
+
mockReadFile.mockResolvedValue(content);
|
|
796
|
+
const result = await auditCode('src', '/project', ['solid']);
|
|
797
|
+
expect(findFinding(result.findings, 'dip-direct-instantiation')).toBeDefined();
|
|
798
|
+
});
|
|
799
|
+
it('flags new RedisClient', async () => {
|
|
800
|
+
mockGlob.mockResolvedValue(['service.ts']);
|
|
801
|
+
const content = `const cache = new RedisClient();`;
|
|
802
|
+
mockReadFile.mockResolvedValue(content);
|
|
803
|
+
const result = await auditCode('src', '/project', ['solid']);
|
|
804
|
+
expect(findFinding(result.findings, 'dip-direct-instantiation')).toBeDefined();
|
|
805
|
+
});
|
|
806
|
+
it('flags new MongoClient', async () => {
|
|
807
|
+
mockGlob.mockResolvedValue(['service.ts']);
|
|
808
|
+
const content = `const mongo = new MongoClient("uri");`;
|
|
809
|
+
mockReadFile.mockResolvedValue(content);
|
|
810
|
+
const result = await auditCode('src', '/project', ['solid']);
|
|
811
|
+
expect(findFinding(result.findings, 'dip-direct-instantiation')).toBeDefined();
|
|
812
|
+
});
|
|
813
|
+
it('flags new PrismaClient', async () => {
|
|
814
|
+
mockGlob.mockResolvedValue(['service.ts']);
|
|
815
|
+
const content = `const prisma = new PrismaClient();`;
|
|
816
|
+
mockReadFile.mockResolvedValue(content);
|
|
817
|
+
const result = await auditCode('src', '/project', ['solid']);
|
|
818
|
+
expect(findFinding(result.findings, 'dip-direct-instantiation')).toBeDefined();
|
|
819
|
+
});
|
|
820
|
+
it('flags new SequelizeConnection', async () => {
|
|
821
|
+
mockGlob.mockResolvedValue(['service.ts']);
|
|
822
|
+
const content = `const seq = new SequelizeConnection();`;
|
|
823
|
+
mockReadFile.mockResolvedValue(content);
|
|
824
|
+
const result = await auditCode('src', '/project', ['solid']);
|
|
825
|
+
expect(findFinding(result.findings, 'dip-direct-instantiation')).toBeDefined();
|
|
826
|
+
});
|
|
827
|
+
it('flags new MongooseModel', async () => {
|
|
828
|
+
mockGlob.mockResolvedValue(['service.ts']);
|
|
829
|
+
const content = `const model = new MongooseModel();`;
|
|
830
|
+
mockReadFile.mockResolvedValue(content);
|
|
831
|
+
const result = await auditCode('src', '/project', ['solid']);
|
|
832
|
+
expect(findFinding(result.findings, 'dip-direct-instantiation')).toBeDefined();
|
|
833
|
+
});
|
|
834
|
+
it('does not flag new for non-infrastructure classes', async () => {
|
|
835
|
+
mockGlob.mockResolvedValue(['service.ts']);
|
|
836
|
+
const content = `const user = new User("John");`;
|
|
837
|
+
mockReadFile.mockResolvedValue(content);
|
|
838
|
+
const result = await auditCode('src', '/project', ['solid']);
|
|
839
|
+
expect(findFinding(result.findings, 'dip-direct-instantiation')).toBeUndefined();
|
|
840
|
+
});
|
|
841
|
+
});
|
|
842
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
843
|
+
// checkArchitecture — layer violation and dependency checks
|
|
844
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
845
|
+
describe('checkArchitecture', () => {
|
|
846
|
+
// -- layer-violation-ui-in-data --
|
|
847
|
+
it('flags UI logic in data/repository layer', async () => {
|
|
848
|
+
mockGlob.mockResolvedValue(['repositories/user-repo.ts']);
|
|
849
|
+
const content = `import { useState } from 'react';`;
|
|
850
|
+
mockReadFile.mockResolvedValue(content);
|
|
851
|
+
const result = await auditCode('.', '/project', ['architecture']);
|
|
852
|
+
const finding = findFinding(result.findings, 'layer-violation-ui-in-data');
|
|
853
|
+
expect(finding).toBeDefined();
|
|
854
|
+
expect(finding.severity).toBe('error');
|
|
855
|
+
expect(finding.reference).toContain('Separation of Concerns');
|
|
856
|
+
});
|
|
857
|
+
it('flags UI logic in models directory', async () => {
|
|
858
|
+
mockGlob.mockResolvedValue(['models/user.ts']);
|
|
859
|
+
const content = `const el = createElement('div');`;
|
|
860
|
+
mockReadFile.mockResolvedValue(content);
|
|
861
|
+
const result = await auditCode('.', '/project', ['architecture']);
|
|
862
|
+
expect(findFinding(result.findings, 'layer-violation-ui-in-data')).toBeDefined();
|
|
863
|
+
});
|
|
864
|
+
it('flags UI logic in db directory', async () => {
|
|
865
|
+
mockGlob.mockResolvedValue(['db/connection.ts']);
|
|
866
|
+
const content = `useEffect(() => { fetchData(); }, []);`;
|
|
867
|
+
mockReadFile.mockResolvedValue(content);
|
|
868
|
+
const result = await auditCode('.', '/project', ['architecture']);
|
|
869
|
+
expect(findFinding(result.findings, 'layer-violation-ui-in-data')).toBeDefined();
|
|
870
|
+
});
|
|
871
|
+
it('flags component render in data layer', async () => {
|
|
872
|
+
mockGlob.mockResolvedValue(['data/store.tsx']);
|
|
873
|
+
const content = `function render() { return <div>Hello</div>; }`;
|
|
874
|
+
mockReadFile.mockResolvedValue(content);
|
|
875
|
+
const result = await auditCode('.', '/project', ['architecture']);
|
|
876
|
+
expect(findFinding(result.findings, 'layer-violation-ui-in-data')).toBeDefined();
|
|
877
|
+
});
|
|
878
|
+
it('does not flag non-data-layer files with UI logic', async () => {
|
|
879
|
+
mockGlob.mockResolvedValue(['utils/helper.ts']);
|
|
880
|
+
const content = `import { useState } from 'react';`;
|
|
881
|
+
mockReadFile.mockResolvedValue(content);
|
|
882
|
+
const result = await auditCode('.', '/project', ['architecture']);
|
|
883
|
+
expect(findFinding(result.findings, 'layer-violation-ui-in-data')).toBeUndefined();
|
|
884
|
+
});
|
|
885
|
+
// -- layer-violation-data-in-ui --
|
|
886
|
+
it('flags data access in presentation layer', async () => {
|
|
887
|
+
mockGlob.mockResolvedValue(['components/UserList.ts']);
|
|
888
|
+
const content = `const users = await db.query("SELECT * FROM users");`;
|
|
889
|
+
mockReadFile.mockResolvedValue(content);
|
|
890
|
+
const result = await auditCode('.', '/project', ['architecture']);
|
|
891
|
+
const finding = findFinding(result.findings, 'layer-violation-data-in-ui');
|
|
892
|
+
expect(finding).toBeDefined();
|
|
893
|
+
expect(finding.severity).toBe('warning');
|
|
894
|
+
});
|
|
895
|
+
it('flags prisma in pages directory', async () => {
|
|
896
|
+
mockGlob.mockResolvedValue(['pages/index.ts']);
|
|
897
|
+
const content = `const data = await prisma.user.findMany();`;
|
|
898
|
+
mockReadFile.mockResolvedValue(content);
|
|
899
|
+
const result = await auditCode('.', '/project', ['architecture']);
|
|
900
|
+
expect(findFinding(result.findings, 'layer-violation-data-in-ui')).toBeDefined();
|
|
901
|
+
});
|
|
902
|
+
it('flags SQL keywords in views directory', async () => {
|
|
903
|
+
mockGlob.mockResolvedValue(['views/dashboard.ts']);
|
|
904
|
+
const content = `const result = execute("INSERT INTO logs VALUES (1)");`;
|
|
905
|
+
mockReadFile.mockResolvedValue(content);
|
|
906
|
+
const result = await auditCode('.', '/project', ['architecture']);
|
|
907
|
+
expect(findFinding(result.findings, 'layer-violation-data-in-ui')).toBeDefined();
|
|
908
|
+
});
|
|
909
|
+
it('flags mongoose in screens directory', async () => {
|
|
910
|
+
mockGlob.mockResolvedValue(['screens/Home.ts']);
|
|
911
|
+
const content = `const users = await mongoose.model('User').find();`;
|
|
912
|
+
mockReadFile.mockResolvedValue(content);
|
|
913
|
+
const result = await auditCode('.', '/project', ['architecture']);
|
|
914
|
+
expect(findFinding(result.findings, 'layer-violation-data-in-ui')).toBeDefined();
|
|
915
|
+
});
|
|
916
|
+
it('does not flag react-query (false positive exclusion)', async () => {
|
|
917
|
+
mockGlob.mockResolvedValue(['components/UserList.ts']);
|
|
918
|
+
const content = `import { useQuery } from 'react-query';\nconst data = query({ key: 'users' });`;
|
|
919
|
+
mockReadFile.mockResolvedValue(content);
|
|
920
|
+
const result = await auditCode('.', '/project', ['architecture']);
|
|
921
|
+
expect(findFinding(result.findings, 'layer-violation-data-in-ui')).toBeUndefined();
|
|
922
|
+
});
|
|
923
|
+
it('does not flag tanstack (false positive exclusion)', async () => {
|
|
924
|
+
mockGlob.mockResolvedValue(['components/UserList.ts']);
|
|
925
|
+
const content = `import { useQuery } from '@tanstack/react-query';\nconst data = query({ key: 'users' });`;
|
|
926
|
+
mockReadFile.mockResolvedValue(content);
|
|
927
|
+
const result = await auditCode('.', '/project', ['architecture']);
|
|
928
|
+
expect(findFinding(result.findings, 'layer-violation-data-in-ui')).toBeUndefined();
|
|
929
|
+
});
|
|
930
|
+
// -- too-many-dependencies --
|
|
931
|
+
it('flags files with more than 20 imports', async () => {
|
|
932
|
+
mockGlob.mockResolvedValue(['god-module.ts']);
|
|
933
|
+
const imports = Array.from({ length: 21 }, (_, i) => `import { mod${i} } from './mod${i}';`).join('\n');
|
|
934
|
+
const content = imports + '\n\nexport function main() { return 1; }';
|
|
935
|
+
mockReadFile.mockResolvedValue(content);
|
|
936
|
+
const result = await auditCode('.', '/project', ['architecture']);
|
|
937
|
+
const finding = findFinding(result.findings, 'too-many-dependencies');
|
|
938
|
+
expect(finding).toBeDefined();
|
|
939
|
+
expect(finding.severity).toBe('warning');
|
|
940
|
+
expect(finding.message).toContain('21');
|
|
941
|
+
expect(finding.reference).toContain('High Cohesion');
|
|
942
|
+
});
|
|
943
|
+
it('does not flag files with 20 or fewer imports', async () => {
|
|
944
|
+
mockGlob.mockResolvedValue(['normal.ts']);
|
|
945
|
+
const imports = Array.from({ length: 20 }, (_, i) => `import { mod${i} } from './mod${i}';`).join('\n');
|
|
946
|
+
mockReadFile.mockResolvedValue(imports);
|
|
947
|
+
const result = await auditCode('.', '/project', ['architecture']);
|
|
948
|
+
expect(findFinding(result.findings, 'too-many-dependencies')).toBeUndefined();
|
|
949
|
+
});
|
|
950
|
+
it('counts require-style imports', async () => {
|
|
951
|
+
mockGlob.mockResolvedValue(['old-module.js']);
|
|
952
|
+
const requires = Array.from({ length: 21 }, (_, i) => ` const mod${i} = require('./mod${i}');`).join('\n');
|
|
953
|
+
mockReadFile.mockResolvedValue(requires);
|
|
954
|
+
const result = await auditCode('.', '/project', ['architecture']);
|
|
955
|
+
expect(findFinding(result.findings, 'too-many-dependencies')).toBeDefined();
|
|
956
|
+
});
|
|
957
|
+
});
|
|
958
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
959
|
+
// checkSecurity — security vulnerability detection
|
|
960
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
961
|
+
describe('checkSecurity', () => {
|
|
962
|
+
// -- hardcoded-secret --
|
|
963
|
+
it('flags hardcoded API keys', async () => {
|
|
964
|
+
const content = `const api_key = "sk-abc123defghijklmno";`;
|
|
965
|
+
mockReadFile.mockResolvedValue(content);
|
|
966
|
+
const findings = await auditFile('/test/secrets.ts', ['security']);
|
|
967
|
+
const finding = findFinding(findings, 'hardcoded-secret');
|
|
968
|
+
expect(finding).toBeDefined();
|
|
969
|
+
expect(finding.severity).toBe('critical');
|
|
970
|
+
expect(finding.reference).toContain('OWASP');
|
|
971
|
+
});
|
|
972
|
+
it('flags hardcoded passwords', async () => {
|
|
973
|
+
const content = `const password = "superSecret123!";`;
|
|
974
|
+
mockReadFile.mockResolvedValue(content);
|
|
975
|
+
const findings = await auditFile('/test/pass.ts', ['security']);
|
|
976
|
+
expect(findFinding(findings, 'hardcoded-secret')).toBeDefined();
|
|
977
|
+
});
|
|
978
|
+
it('flags hardcoded tokens', async () => {
|
|
979
|
+
const content = `const token = "eyJhbGciOiJIUzI1NiIs";`;
|
|
980
|
+
mockReadFile.mockResolvedValue(content);
|
|
981
|
+
const findings = await auditFile('/test/token.ts', ['security']);
|
|
982
|
+
expect(findFinding(findings, 'hardcoded-secret')).toBeDefined();
|
|
983
|
+
});
|
|
984
|
+
it('flags hardcoded private keys', async () => {
|
|
985
|
+
const content = `const private_key = "MIIEvgIBADANBgkqhkiG9w0BAQ";`;
|
|
986
|
+
mockReadFile.mockResolvedValue(content);
|
|
987
|
+
const findings = await auditFile('/test/pk.ts', ['security']);
|
|
988
|
+
expect(findFinding(findings, 'hardcoded-secret')).toBeDefined();
|
|
989
|
+
});
|
|
990
|
+
it('flags hardcoded secret values', async () => {
|
|
991
|
+
const content = `const secret = "my-super-secret-value-123";`;
|
|
992
|
+
mockReadFile.mockResolvedValue(content);
|
|
993
|
+
const findings = await auditFile('/test/secret.ts', ['security']);
|
|
994
|
+
expect(findFinding(findings, 'hardcoded-secret')).toBeDefined();
|
|
995
|
+
});
|
|
996
|
+
it('does not flag placeholder secrets (your-)', async () => {
|
|
997
|
+
const content = `const api_key = "your-api-key-here";`;
|
|
998
|
+
mockReadFile.mockResolvedValue(content);
|
|
999
|
+
const findings = await auditFile('/test/placeholder.ts', ['security']);
|
|
1000
|
+
expect(findFinding(findings, 'hardcoded-secret')).toBeUndefined();
|
|
1001
|
+
});
|
|
1002
|
+
it('does not flag placeholder secrets (example)', async () => {
|
|
1003
|
+
const content = `const api_key = "example-api-key-value";`;
|
|
1004
|
+
mockReadFile.mockResolvedValue(content);
|
|
1005
|
+
const findings = await auditFile('/test/example.ts', ['security']);
|
|
1006
|
+
expect(findFinding(findings, 'hardcoded-secret')).toBeUndefined();
|
|
1007
|
+
});
|
|
1008
|
+
it('does not flag placeholder secrets (TODO)', async () => {
|
|
1009
|
+
const content = `const api_key = "TODO_replace_me_12345";`;
|
|
1010
|
+
mockReadFile.mockResolvedValue(content);
|
|
1011
|
+
const findings = await auditFile('/test/todo.ts', ['security']);
|
|
1012
|
+
expect(findFinding(findings, 'hardcoded-secret')).toBeUndefined();
|
|
1013
|
+
});
|
|
1014
|
+
it('does not flag placeholder secrets (change-me)', async () => {
|
|
1015
|
+
const content = `const api_key = "change-me-placeholder_val";`;
|
|
1016
|
+
mockReadFile.mockResolvedValue(content);
|
|
1017
|
+
const findings = await auditFile('/test/change-me.ts', ['security']);
|
|
1018
|
+
expect(findFinding(findings, 'hardcoded-secret')).toBeUndefined();
|
|
1019
|
+
});
|
|
1020
|
+
it('does not flag placeholder secrets (xxx)', async () => {
|
|
1021
|
+
const content = `const api_key = "xxx-xxx-xxx-placeholder";`;
|
|
1022
|
+
mockReadFile.mockResolvedValue(content);
|
|
1023
|
+
const findings = await auditFile('/test/xxx-secret.ts', ['security']);
|
|
1024
|
+
expect(findFinding(findings, 'hardcoded-secret')).toBeUndefined();
|
|
1025
|
+
});
|
|
1026
|
+
it('does not flag short values (under 8 chars)', async () => {
|
|
1027
|
+
const content = `const api_key = "short";`;
|
|
1028
|
+
mockReadFile.mockResolvedValue(content);
|
|
1029
|
+
const findings = await auditFile('/test/short-secret.ts', ['security']);
|
|
1030
|
+
expect(findFinding(findings, 'hardcoded-secret')).toBeUndefined();
|
|
1031
|
+
});
|
|
1032
|
+
// -- sql-injection-risk --
|
|
1033
|
+
it('flags SQL injection with template literals', async () => {
|
|
1034
|
+
const content = 'const result = query(`SELECT * FROM users WHERE id = ${userId}`);';
|
|
1035
|
+
mockReadFile.mockResolvedValue(content);
|
|
1036
|
+
const findings = await auditFile('/test/sqli.ts', ['security']);
|
|
1037
|
+
const finding = findFinding(findings, 'sql-injection-risk');
|
|
1038
|
+
expect(finding).toBeDefined();
|
|
1039
|
+
expect(finding.severity).toBe('critical');
|
|
1040
|
+
expect(finding.reference).toContain('SQL Injection');
|
|
1041
|
+
});
|
|
1042
|
+
// -- no-eval --
|
|
1043
|
+
it('flags eval() usage', async () => {
|
|
1044
|
+
const content = `const result = eval("1 + 2");`;
|
|
1045
|
+
mockReadFile.mockResolvedValue(content);
|
|
1046
|
+
const findings = await auditFile('/test/eval.ts', ['security']);
|
|
1047
|
+
const finding = findFinding(findings, 'no-eval');
|
|
1048
|
+
expect(finding).toBeDefined();
|
|
1049
|
+
expect(finding.severity).toBe('critical');
|
|
1050
|
+
expect(finding.reference).toContain('Code Injection');
|
|
1051
|
+
});
|
|
1052
|
+
it('does not flag evaluate or evaluation (not eval())', async () => {
|
|
1053
|
+
const content = `const evaluate = (x: number) => x * 2; evaluate(5);`;
|
|
1054
|
+
mockReadFile.mockResolvedValue(content);
|
|
1055
|
+
const findings = await auditFile('/test/evaluate.ts', ['security']);
|
|
1056
|
+
expect(findFinding(findings, 'no-eval')).toBeUndefined();
|
|
1057
|
+
});
|
|
1058
|
+
// -- xss-risk --
|
|
1059
|
+
it('flags innerHTML assignment', async () => {
|
|
1060
|
+
const content = `element.innerHTML = userInput;`;
|
|
1061
|
+
mockReadFile.mockResolvedValue(content);
|
|
1062
|
+
const findings = await auditFile('/test/xss.ts', ['security']);
|
|
1063
|
+
const finding = findFinding(findings, 'xss-risk');
|
|
1064
|
+
expect(finding).toBeDefined();
|
|
1065
|
+
expect(finding.severity).toBe('error');
|
|
1066
|
+
expect(finding.reference).toContain('XSS');
|
|
1067
|
+
});
|
|
1068
|
+
it('flags dangerouslySetInnerHTML', async () => {
|
|
1069
|
+
const content = `<div dangerouslySetInnerHTML={{ __html: data }} />`;
|
|
1070
|
+
mockReadFile.mockResolvedValue(content);
|
|
1071
|
+
const findings = await auditFile('/test/react-xss.tsx', ['security']);
|
|
1072
|
+
expect(findFinding(findings, 'xss-risk')).toBeDefined();
|
|
1073
|
+
});
|
|
1074
|
+
// -- cors-wildcard --
|
|
1075
|
+
it('flags CORS wildcard configuration', async () => {
|
|
1076
|
+
const content = `const cors = "*";`;
|
|
1077
|
+
mockReadFile.mockResolvedValue(content);
|
|
1078
|
+
const findings = await auditFile('/test/cors.ts', ['security']);
|
|
1079
|
+
const finding = findFinding(findings, 'cors-wildcard');
|
|
1080
|
+
expect(finding).toBeDefined();
|
|
1081
|
+
expect(finding.severity).toBe('warning');
|
|
1082
|
+
expect(finding.reference).toContain('OWASP');
|
|
1083
|
+
});
|
|
1084
|
+
it('flags access-control-allow-origin wildcard', async () => {
|
|
1085
|
+
const content = `access-control-allow-origin: "*"`;
|
|
1086
|
+
mockReadFile.mockResolvedValue(content);
|
|
1087
|
+
const findings = await auditFile('/test/cors2.ts', ['security']);
|
|
1088
|
+
expect(findFinding(findings, 'cors-wildcard')).toBeDefined();
|
|
1089
|
+
});
|
|
1090
|
+
it('does not flag non-wildcard CORS', async () => {
|
|
1091
|
+
const content = `cors = "https://example.com";`;
|
|
1092
|
+
mockReadFile.mockResolvedValue(content);
|
|
1093
|
+
const findings = await auditFile('/test/cors-safe.ts', ['security']);
|
|
1094
|
+
expect(findFinding(findings, 'cors-wildcard')).toBeUndefined();
|
|
1095
|
+
});
|
|
1096
|
+
});
|
|
1097
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1098
|
+
// checkErrorHandling — error handling patterns
|
|
1099
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1100
|
+
describe('checkErrorHandling', () => {
|
|
1101
|
+
// -- empty-catch --
|
|
1102
|
+
it('flags empty catch blocks', async () => {
|
|
1103
|
+
const content = `try { doSomething(); } catch (e) {}`;
|
|
1104
|
+
mockReadFile.mockResolvedValue(content);
|
|
1105
|
+
const findings = await auditFile('/test/empty-catch.ts', ['error-handling']);
|
|
1106
|
+
const finding = findFinding(findings, 'empty-catch');
|
|
1107
|
+
expect(finding).toBeDefined();
|
|
1108
|
+
expect(finding.severity).toBe('error');
|
|
1109
|
+
expect(finding.line).toBeDefined();
|
|
1110
|
+
});
|
|
1111
|
+
it('does not flag catch blocks with code', async () => {
|
|
1112
|
+
const content = `try { doSomething(); } catch (e) { throw e; }`;
|
|
1113
|
+
mockReadFile.mockResolvedValue(content);
|
|
1114
|
+
const findings = await auditFile('/test/good-catch.ts', ['error-handling']);
|
|
1115
|
+
expect(findFinding(findings, 'empty-catch')).toBeUndefined();
|
|
1116
|
+
});
|
|
1117
|
+
// -- catch-only-logs --
|
|
1118
|
+
it('flags catch blocks that only log (console.log)', async () => {
|
|
1119
|
+
const content = [
|
|
1120
|
+
'try {',
|
|
1121
|
+
' doSomething();',
|
|
1122
|
+
'} catch (e) {',
|
|
1123
|
+
' console.log(e);',
|
|
1124
|
+
'}',
|
|
1125
|
+
].join('\n');
|
|
1126
|
+
mockReadFile.mockResolvedValue(content);
|
|
1127
|
+
const findings = await auditFile('/test/log-catch.ts', ['error-handling']);
|
|
1128
|
+
expect(findFinding(findings, 'catch-only-logs')).toBeDefined();
|
|
1129
|
+
});
|
|
1130
|
+
it('flags catch blocks that only log (console.error)', async () => {
|
|
1131
|
+
const content = [
|
|
1132
|
+
'try {',
|
|
1133
|
+
' doSomething();',
|
|
1134
|
+
'} catch (e) {',
|
|
1135
|
+
' console.error(e);',
|
|
1136
|
+
'}',
|
|
1137
|
+
].join('\n');
|
|
1138
|
+
mockReadFile.mockResolvedValue(content);
|
|
1139
|
+
const findings = await auditFile('/test/error-log-catch.ts', ['error-handling']);
|
|
1140
|
+
expect(findFinding(findings, 'catch-only-logs')).toBeDefined();
|
|
1141
|
+
});
|
|
1142
|
+
it('does not flag catch blocks that log and throw', async () => {
|
|
1143
|
+
const content = [
|
|
1144
|
+
'try {',
|
|
1145
|
+
' doSomething();',
|
|
1146
|
+
'}',
|
|
1147
|
+
'catch (e) {',
|
|
1148
|
+
' console.error(e);',
|
|
1149
|
+
' throw e;',
|
|
1150
|
+
'}',
|
|
1151
|
+
].join('\n');
|
|
1152
|
+
mockReadFile.mockResolvedValue(content);
|
|
1153
|
+
const findings = await auditFile('/test/log-throw.ts', ['error-handling']);
|
|
1154
|
+
expect(findFinding(findings, 'catch-only-logs')).toBeUndefined();
|
|
1155
|
+
});
|
|
1156
|
+
it('does not flag catch blocks that log and return', async () => {
|
|
1157
|
+
const content = [
|
|
1158
|
+
'try {',
|
|
1159
|
+
' doSomething();',
|
|
1160
|
+
'}',
|
|
1161
|
+
'catch (e) {',
|
|
1162
|
+
' console.error(e);',
|
|
1163
|
+
' return null;',
|
|
1164
|
+
'}',
|
|
1165
|
+
].join('\n');
|
|
1166
|
+
mockReadFile.mockResolvedValue(content);
|
|
1167
|
+
const findings = await auditFile('/test/log-return.ts', ['error-handling']);
|
|
1168
|
+
expect(findFinding(findings, 'catch-only-logs')).toBeUndefined();
|
|
1169
|
+
});
|
|
1170
|
+
it('does not flag catch blocks that log and reject', async () => {
|
|
1171
|
+
const content = [
|
|
1172
|
+
'try {',
|
|
1173
|
+
' doSomething();',
|
|
1174
|
+
'}',
|
|
1175
|
+
'catch (e) {',
|
|
1176
|
+
' console.error(e);',
|
|
1177
|
+
' reject(e);',
|
|
1178
|
+
'}',
|
|
1179
|
+
].join('\n');
|
|
1180
|
+
mockReadFile.mockResolvedValue(content);
|
|
1181
|
+
const findings = await auditFile('/test/log-reject.ts', ['error-handling']);
|
|
1182
|
+
expect(findFinding(findings, 'catch-only-logs')).toBeUndefined();
|
|
1183
|
+
});
|
|
1184
|
+
it('does not flag catch blocks without console.log/error', async () => {
|
|
1185
|
+
const content = [
|
|
1186
|
+
'try {',
|
|
1187
|
+
' doSomething();',
|
|
1188
|
+
'} catch (e) {',
|
|
1189
|
+
' logger.warn(e);',
|
|
1190
|
+
'}',
|
|
1191
|
+
].join('\n');
|
|
1192
|
+
mockReadFile.mockResolvedValue(content);
|
|
1193
|
+
const findings = await auditFile('/test/custom-logger.ts', ['error-handling']);
|
|
1194
|
+
expect(findFinding(findings, 'catch-only-logs')).toBeUndefined();
|
|
1195
|
+
});
|
|
1196
|
+
// -- unhandled-async --
|
|
1197
|
+
it('flags async functions without try-catch', async () => {
|
|
1198
|
+
const content = [
|
|
1199
|
+
'async function fetchData() {',
|
|
1200
|
+
' const res = await fetch("/api/data");',
|
|
1201
|
+
' return res.json();',
|
|
1202
|
+
'}',
|
|
1203
|
+
].join('\n');
|
|
1204
|
+
mockReadFile.mockResolvedValue(content);
|
|
1205
|
+
const findings = await auditFile('/test/unhandled.ts', ['error-handling']);
|
|
1206
|
+
const finding = findFinding(findings, 'unhandled-async');
|
|
1207
|
+
expect(finding).toBeDefined();
|
|
1208
|
+
expect(finding.severity).toBe('info');
|
|
1209
|
+
expect(finding.message).toContain('fetchData');
|
|
1210
|
+
});
|
|
1211
|
+
it('does not flag async functions with try-catch', async () => {
|
|
1212
|
+
const content = [
|
|
1213
|
+
'async function fetchData() {',
|
|
1214
|
+
' try {',
|
|
1215
|
+
' const res = await fetch("/api/data");',
|
|
1216
|
+
' return res.json();',
|
|
1217
|
+
' } catch (e) {',
|
|
1218
|
+
' throw new Error("failed");',
|
|
1219
|
+
' }',
|
|
1220
|
+
'}',
|
|
1221
|
+
].join('\n');
|
|
1222
|
+
mockReadFile.mockResolvedValue(content);
|
|
1223
|
+
const findings = await auditFile('/test/handled.ts', ['error-handling']);
|
|
1224
|
+
expect(findFinding(findings, 'unhandled-async')).toBeUndefined();
|
|
1225
|
+
});
|
|
1226
|
+
it('does not flag async functions with .catch', async () => {
|
|
1227
|
+
const content = [
|
|
1228
|
+
'async function fetchData() {',
|
|
1229
|
+
' const res = await fetch("/api/data").catch(handleError);',
|
|
1230
|
+
' return res;',
|
|
1231
|
+
'}',
|
|
1232
|
+
].join('\n');
|
|
1233
|
+
mockReadFile.mockResolvedValue(content);
|
|
1234
|
+
const findings = await auditFile('/test/catch-chain.ts', ['error-handling']);
|
|
1235
|
+
expect(findFinding(findings, 'unhandled-async')).toBeUndefined();
|
|
1236
|
+
});
|
|
1237
|
+
it('only checks async for JS/TS extensions', async () => {
|
|
1238
|
+
const content = [
|
|
1239
|
+
'async function fetchData() {',
|
|
1240
|
+
' const res = await fetch("/api/data");',
|
|
1241
|
+
' return res;',
|
|
1242
|
+
'}',
|
|
1243
|
+
].join('\n');
|
|
1244
|
+
mockReadFile.mockResolvedValue(content);
|
|
1245
|
+
// .py extension, should not check for unhandled-async
|
|
1246
|
+
const findings = await auditFile('/test/async.py', ['error-handling']);
|
|
1247
|
+
expect(findFinding(findings, 'unhandled-async')).toBeUndefined();
|
|
1248
|
+
});
|
|
1249
|
+
it('checks async in .jsx files', async () => {
|
|
1250
|
+
const content = [
|
|
1251
|
+
'async function fetchData() {',
|
|
1252
|
+
' const res = await fetch("/api/data");',
|
|
1253
|
+
' return res;',
|
|
1254
|
+
'}',
|
|
1255
|
+
].join('\n');
|
|
1256
|
+
mockReadFile.mockResolvedValue(content);
|
|
1257
|
+
const findings = await auditFile('/test/async.jsx', ['error-handling']);
|
|
1258
|
+
expect(findFinding(findings, 'unhandled-async')).toBeDefined();
|
|
1259
|
+
});
|
|
1260
|
+
});
|
|
1261
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1262
|
+
// checkPerformance — performance pattern detection
|
|
1263
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1264
|
+
describe('checkPerformance', () => {
|
|
1265
|
+
// -- n-plus-one --
|
|
1266
|
+
it('flags N+1 query patterns (await in loop)', async () => {
|
|
1267
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1268
|
+
const content = `for (const item of items) {\n const data = await fetch("/api/" + item.id);\n}`;
|
|
1269
|
+
mockReadFile.mockResolvedValue(content);
|
|
1270
|
+
const result = await auditCode('src', '/project', ['performance']);
|
|
1271
|
+
const finding = findFinding(result.findings, 'n-plus-one');
|
|
1272
|
+
expect(finding).toBeDefined();
|
|
1273
|
+
expect(finding.severity).toBe('warning');
|
|
1274
|
+
});
|
|
1275
|
+
it('flags N+1 with query in loop', async () => {
|
|
1276
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1277
|
+
const content = `for (let i = 0; i < users.length; i++) {\n const user = query(users[i]);\n}`;
|
|
1278
|
+
mockReadFile.mockResolvedValue(content);
|
|
1279
|
+
const result = await auditCode('src', '/project', ['performance']);
|
|
1280
|
+
expect(findFinding(result.findings, 'n-plus-one')).toBeDefined();
|
|
1281
|
+
});
|
|
1282
|
+
it('does not flag when no loop+query pattern', async () => {
|
|
1283
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1284
|
+
const content = `const data = await fetchAll(ids);`;
|
|
1285
|
+
mockReadFile.mockResolvedValue(content);
|
|
1286
|
+
const result = await auditCode('src', '/project', ['performance']);
|
|
1287
|
+
expect(findFinding(result.findings, 'n-plus-one')).toBeUndefined();
|
|
1288
|
+
});
|
|
1289
|
+
// -- sync-io --
|
|
1290
|
+
it('flags synchronous file reads in TS/JS files', async () => {
|
|
1291
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1292
|
+
const content = `const data = readFileSync("/path/to/file", "utf-8");`;
|
|
1293
|
+
mockReadFile.mockResolvedValue(content);
|
|
1294
|
+
const result = await auditCode('src', '/project', ['performance']);
|
|
1295
|
+
const finding = findFinding(result.findings, 'sync-io');
|
|
1296
|
+
expect(finding).toBeDefined();
|
|
1297
|
+
expect(finding.severity).toBe('warning');
|
|
1298
|
+
});
|
|
1299
|
+
it('flags writeFileSync', async () => {
|
|
1300
|
+
mockGlob.mockResolvedValue(['app.js']);
|
|
1301
|
+
const content = `writeFileSync("/path", data);`;
|
|
1302
|
+
mockReadFile.mockResolvedValue(content);
|
|
1303
|
+
const result = await auditCode('src', '/project', ['performance']);
|
|
1304
|
+
expect(findFinding(result.findings, 'sync-io')).toBeDefined();
|
|
1305
|
+
});
|
|
1306
|
+
it('flags readdirSync', async () => {
|
|
1307
|
+
mockGlob.mockResolvedValue(['app.mjs']);
|
|
1308
|
+
const content = `const files = readdirSync("/path");`;
|
|
1309
|
+
mockReadFile.mockResolvedValue(content);
|
|
1310
|
+
const result = await auditCode('src', '/project', ['performance']);
|
|
1311
|
+
expect(findFinding(result.findings, 'sync-io')).toBeDefined();
|
|
1312
|
+
});
|
|
1313
|
+
it('flags statSync', async () => {
|
|
1314
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1315
|
+
const content = `const stats = statSync("/path");`;
|
|
1316
|
+
mockReadFile.mockResolvedValue(content);
|
|
1317
|
+
const result = await auditCode('src', '/project', ['performance']);
|
|
1318
|
+
expect(findFinding(result.findings, 'sync-io')).toBeDefined();
|
|
1319
|
+
});
|
|
1320
|
+
it('flags existsSync', async () => {
|
|
1321
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1322
|
+
const content = `if (existsSync("/path")) { }`;
|
|
1323
|
+
mockReadFile.mockResolvedValue(content);
|
|
1324
|
+
const result = await auditCode('src', '/project', ['performance']);
|
|
1325
|
+
expect(findFinding(result.findings, 'sync-io')).toBeDefined();
|
|
1326
|
+
});
|
|
1327
|
+
it('does not flag sync I/O in config files', async () => {
|
|
1328
|
+
mockGlob.mockResolvedValue(['config/settings.ts']);
|
|
1329
|
+
const content = `const data = readFileSync("config.json", "utf-8");`;
|
|
1330
|
+
mockReadFile.mockResolvedValue(content);
|
|
1331
|
+
const result = await auditCode('src', '/project', ['performance']);
|
|
1332
|
+
expect(findFinding(result.findings, 'sync-io')).toBeUndefined();
|
|
1333
|
+
});
|
|
1334
|
+
it('does not flag sync I/O in setup files', async () => {
|
|
1335
|
+
mockGlob.mockResolvedValue(['setup/init.ts']);
|
|
1336
|
+
const content = `const data = readFileSync("setup.json", "utf-8");`;
|
|
1337
|
+
mockReadFile.mockResolvedValue(content);
|
|
1338
|
+
const result = await auditCode('src', '/project', ['performance']);
|
|
1339
|
+
expect(findFinding(result.findings, 'sync-io')).toBeUndefined();
|
|
1340
|
+
});
|
|
1341
|
+
it('does not flag sync I/O in init files', async () => {
|
|
1342
|
+
mockGlob.mockResolvedValue(['init/bootstrap.ts']);
|
|
1343
|
+
const content = `const data = readFileSync("init.json", "utf-8");`;
|
|
1344
|
+
mockReadFile.mockResolvedValue(content);
|
|
1345
|
+
const result = await auditCode('src', '/project', ['performance']);
|
|
1346
|
+
expect(findFinding(result.findings, 'sync-io')).toBeUndefined();
|
|
1347
|
+
});
|
|
1348
|
+
it('does not flag sync I/O in non-JS files', async () => {
|
|
1349
|
+
mockGlob.mockResolvedValue(['app.py']);
|
|
1350
|
+
const content = `readFileSync("/path")`;
|
|
1351
|
+
mockReadFile.mockResolvedValue(content);
|
|
1352
|
+
const result = await auditCode('src', '/project', ['performance']);
|
|
1353
|
+
expect(findFinding(result.findings, 'sync-io')).toBeUndefined();
|
|
1354
|
+
});
|
|
1355
|
+
// -- spread-in-loop --
|
|
1356
|
+
it('flags spread in for loop', async () => {
|
|
1357
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1358
|
+
const content = [
|
|
1359
|
+
'for (const item of items) {',
|
|
1360
|
+
' result = { ...result, [item.id]: item };',
|
|
1361
|
+
'}',
|
|
1362
|
+
].join('\n');
|
|
1363
|
+
mockReadFile.mockResolvedValue(content);
|
|
1364
|
+
const result = await auditCode('src', '/project', ['performance']);
|
|
1365
|
+
const finding = findFinding(result.findings, 'spread-in-loop');
|
|
1366
|
+
expect(finding).toBeDefined();
|
|
1367
|
+
expect(finding.severity).toBe('info');
|
|
1368
|
+
});
|
|
1369
|
+
it('flags spread in forEach', async () => {
|
|
1370
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1371
|
+
const content = [
|
|
1372
|
+
'items.forEach((item) => {',
|
|
1373
|
+
' result = [...result, item];',
|
|
1374
|
+
'});',
|
|
1375
|
+
].join('\n');
|
|
1376
|
+
mockReadFile.mockResolvedValue(content);
|
|
1377
|
+
const result = await auditCode('src', '/project', ['performance']);
|
|
1378
|
+
expect(findFinding(result.findings, 'spread-in-loop')).toBeDefined();
|
|
1379
|
+
});
|
|
1380
|
+
it('does not flag spread outside loops', async () => {
|
|
1381
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1382
|
+
const content = `const merged = { ...defaults, ...options };`;
|
|
1383
|
+
mockReadFile.mockResolvedValue(content);
|
|
1384
|
+
const result = await auditCode('src', '/project', ['performance']);
|
|
1385
|
+
expect(findFinding(result.findings, 'spread-in-loop')).toBeUndefined();
|
|
1386
|
+
});
|
|
1387
|
+
});
|
|
1388
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1389
|
+
// checkTestingPractices — test file quality checks
|
|
1390
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1391
|
+
describe('checkTestingPractices', () => {
|
|
1392
|
+
it('flags it.skip() in test files', async () => {
|
|
1393
|
+
mockGlob.mockResolvedValue(['app.test.ts']);
|
|
1394
|
+
const content = `it.skip('broken test', () => { expect(true).toBe(true); });`;
|
|
1395
|
+
mockReadFile.mockResolvedValue(content);
|
|
1396
|
+
const result = await auditCode('src', '/project', ['testing']);
|
|
1397
|
+
const finding = findFinding(result.findings, 'test-skip');
|
|
1398
|
+
expect(finding).toBeDefined();
|
|
1399
|
+
expect(finding.severity).toBe('warning');
|
|
1400
|
+
expect(finding.message).toContain('.skip()');
|
|
1401
|
+
});
|
|
1402
|
+
it('flags test.skip() in test files', async () => {
|
|
1403
|
+
mockGlob.mockResolvedValue(['app.spec.ts']);
|
|
1404
|
+
const content = `test.skip('broken test', () => { expect(true).toBe(true); });`;
|
|
1405
|
+
mockReadFile.mockResolvedValue(content);
|
|
1406
|
+
const result = await auditCode('src', '/project', ['testing']);
|
|
1407
|
+
expect(findFinding(result.findings, 'test-skip')).toBeDefined();
|
|
1408
|
+
});
|
|
1409
|
+
it('flags describe.skip() in test files', async () => {
|
|
1410
|
+
mockGlob.mockResolvedValue(['app.test.ts']);
|
|
1411
|
+
const content = `describe.skip('suite', () => { it('test', () => {}); });`;
|
|
1412
|
+
mockReadFile.mockResolvedValue(content);
|
|
1413
|
+
const result = await auditCode('src', '/project', ['testing']);
|
|
1414
|
+
expect(findFinding(result.findings, 'test-skip')).toBeDefined();
|
|
1415
|
+
});
|
|
1416
|
+
it('flags it.only() with error severity', async () => {
|
|
1417
|
+
mockGlob.mockResolvedValue(['app.test.ts']);
|
|
1418
|
+
const content = `it.only('focused test', () => { expect(true).toBe(true); });`;
|
|
1419
|
+
mockReadFile.mockResolvedValue(content);
|
|
1420
|
+
const result = await auditCode('src', '/project', ['testing']);
|
|
1421
|
+
const finding = findFinding(result.findings, 'test-only');
|
|
1422
|
+
expect(finding).toBeDefined();
|
|
1423
|
+
expect(finding.severity).toBe('error');
|
|
1424
|
+
expect(finding.message).toContain('.only()');
|
|
1425
|
+
expect(finding.suggestion).toContain('Remove .only()');
|
|
1426
|
+
});
|
|
1427
|
+
it('flags test.only() in test files', async () => {
|
|
1428
|
+
mockGlob.mockResolvedValue(['app.test.ts']);
|
|
1429
|
+
const content = `test.only('focused test', () => { expect(true).toBe(true); });`;
|
|
1430
|
+
mockReadFile.mockResolvedValue(content);
|
|
1431
|
+
const result = await auditCode('src', '/project', ['testing']);
|
|
1432
|
+
expect(findFinding(result.findings, 'test-only')).toBeDefined();
|
|
1433
|
+
});
|
|
1434
|
+
it('flags describe.only() in test files', async () => {
|
|
1435
|
+
mockGlob.mockResolvedValue(['app.test.ts']);
|
|
1436
|
+
const content = `describe.only('focused suite', () => { it('test', () => {}); });`;
|
|
1437
|
+
mockReadFile.mockResolvedValue(content);
|
|
1438
|
+
const result = await auditCode('src', '/project', ['testing']);
|
|
1439
|
+
expect(findFinding(result.findings, 'test-only')).toBeDefined();
|
|
1440
|
+
});
|
|
1441
|
+
it('returns no testing findings for non-test files', async () => {
|
|
1442
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1443
|
+
const content = `it.skip('not a test file', () => {});`;
|
|
1444
|
+
mockReadFile.mockResolvedValue(content);
|
|
1445
|
+
const result = await auditCode('src', '/project', ['testing']);
|
|
1446
|
+
expect(findFinding(result.findings, 'test-skip')).toBeUndefined();
|
|
1447
|
+
});
|
|
1448
|
+
it('returns no testing findings for .spec. files without skip/only', async () => {
|
|
1449
|
+
mockGlob.mockResolvedValue(['app.spec.ts']);
|
|
1450
|
+
const content = `it('normal test', () => { expect(1).toBe(1); });`;
|
|
1451
|
+
mockReadFile.mockResolvedValue(content);
|
|
1452
|
+
const result = await auditCode('src', '/project', ['testing']);
|
|
1453
|
+
expect(result.findings).toHaveLength(0);
|
|
1454
|
+
});
|
|
1455
|
+
});
|
|
1456
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1457
|
+
// checkDry — duplicate code detection
|
|
1458
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1459
|
+
describe('checkDry', () => {
|
|
1460
|
+
it('flags lines duplicated 4+ times', async () => {
|
|
1461
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1462
|
+
const dupLine = 'const result = processItem(item, config, options);';
|
|
1463
|
+
// 4 identical lines
|
|
1464
|
+
const content = Array.from({ length: 4 }, () => dupLine).join('\n');
|
|
1465
|
+
mockReadFile.mockResolvedValue(content);
|
|
1466
|
+
const result = await auditCode('src', '/project', ['dry']);
|
|
1467
|
+
const finding = findFinding(result.findings, 'duplicate-code');
|
|
1468
|
+
expect(finding).toBeDefined();
|
|
1469
|
+
expect(finding.severity).toBe('info');
|
|
1470
|
+
expect(finding.message).toContain('4 times');
|
|
1471
|
+
});
|
|
1472
|
+
it('does not flag lines duplicated 3 times (threshold is 4)', async () => {
|
|
1473
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1474
|
+
const dupLine = 'const result = processItem(item, config, options);';
|
|
1475
|
+
const content = Array.from({ length: 3 }, () => dupLine).join('\n');
|
|
1476
|
+
mockReadFile.mockResolvedValue(content);
|
|
1477
|
+
const result = await auditCode('src', '/project', ['dry']);
|
|
1478
|
+
expect(findFinding(result.findings, 'duplicate-code')).toBeUndefined();
|
|
1479
|
+
});
|
|
1480
|
+
it('skips short lines (under 10 chars)', async () => {
|
|
1481
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1482
|
+
const content = Array.from({ length: 10 }, () => 'x = 1;').join('\n');
|
|
1483
|
+
mockReadFile.mockResolvedValue(content);
|
|
1484
|
+
const result = await auditCode('src', '/project', ['dry']);
|
|
1485
|
+
expect(findFinding(result.findings, 'duplicate-code')).toBeUndefined();
|
|
1486
|
+
});
|
|
1487
|
+
it('skips comment lines starting with //', async () => {
|
|
1488
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1489
|
+
const dupLine = '// This is a long comment line that repeats';
|
|
1490
|
+
const content = Array.from({ length: 5 }, () => dupLine).join('\n');
|
|
1491
|
+
mockReadFile.mockResolvedValue(content);
|
|
1492
|
+
const result = await auditCode('src', '/project', ['dry']);
|
|
1493
|
+
expect(findFinding(result.findings, 'duplicate-code')).toBeUndefined();
|
|
1494
|
+
});
|
|
1495
|
+
it('skips comment lines starting with #', async () => {
|
|
1496
|
+
mockGlob.mockResolvedValue(['app.py']);
|
|
1497
|
+
const dupLine = '# This is a long comment line that repeats';
|
|
1498
|
+
const content = Array.from({ length: 5 }, () => dupLine).join('\n');
|
|
1499
|
+
mockReadFile.mockResolvedValue(content);
|
|
1500
|
+
const result = await auditCode('src', '/project', ['dry']);
|
|
1501
|
+
expect(findFinding(result.findings, 'duplicate-code')).toBeUndefined();
|
|
1502
|
+
});
|
|
1503
|
+
it('skips comment lines starting with *', async () => {
|
|
1504
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1505
|
+
const dupLine = '* @param something long description here';
|
|
1506
|
+
const content = Array.from({ length: 5 }, () => dupLine).join('\n');
|
|
1507
|
+
mockReadFile.mockResolvedValue(content);
|
|
1508
|
+
const result = await auditCode('src', '/project', ['dry']);
|
|
1509
|
+
expect(findFinding(result.findings, 'duplicate-code')).toBeUndefined();
|
|
1510
|
+
});
|
|
1511
|
+
it('skips common patterns like closing braces', async () => {
|
|
1512
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1513
|
+
// Single closing brace is < 10 chars, but closing patterns are also explicitly filtered
|
|
1514
|
+
// Let's use a pattern that would reach the filter: return statements
|
|
1515
|
+
const content = Array.from({ length: 5 }, () => 'return undefined;').join('\n');
|
|
1516
|
+
mockReadFile.mockResolvedValue(content);
|
|
1517
|
+
const result = await auditCode('src', '/project', ['dry']);
|
|
1518
|
+
expect(findFinding(result.findings, 'duplicate-code')).toBeUndefined();
|
|
1519
|
+
});
|
|
1520
|
+
it('skips break statements', async () => {
|
|
1521
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1522
|
+
const content = Array.from({ length: 5 }, () => ' break; ').join('\n');
|
|
1523
|
+
mockReadFile.mockResolvedValue(content);
|
|
1524
|
+
const result = await auditCode('src', '/project', ['dry']);
|
|
1525
|
+
expect(findFinding(result.findings, 'duplicate-code')).toBeUndefined();
|
|
1526
|
+
});
|
|
1527
|
+
it('reports only first duplicate per file (break)', async () => {
|
|
1528
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1529
|
+
const dup1 = 'const alpha = processAlpha(item, config, options);';
|
|
1530
|
+
const dup2 = 'const beta = processBeta(item, config, options);;';
|
|
1531
|
+
// Both appear 4+ times
|
|
1532
|
+
const content = [
|
|
1533
|
+
...Array.from({ length: 4 }, () => dup1),
|
|
1534
|
+
...Array.from({ length: 4 }, () => dup2),
|
|
1535
|
+
].join('\n');
|
|
1536
|
+
mockReadFile.mockResolvedValue(content);
|
|
1537
|
+
const result = await auditCode('src', '/project', ['dry']);
|
|
1538
|
+
const dupFindings = findFindings(result.findings, 'duplicate-code');
|
|
1539
|
+
expect(dupFindings).toHaveLength(1);
|
|
1540
|
+
});
|
|
1541
|
+
it('truncates long duplicate lines in message to 60 chars with ellipsis', async () => {
|
|
1542
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1543
|
+
const longLine = 'const result = processItemWithVeryLongFunctionNameThatExceedsSixtyCharactersInLength(item, config, options);';
|
|
1544
|
+
const content = Array.from({ length: 4 }, () => longLine).join('\n');
|
|
1545
|
+
mockReadFile.mockResolvedValue(content);
|
|
1546
|
+
const result = await auditCode('src', '/project', ['dry']);
|
|
1547
|
+
const finding = findFinding(result.findings, 'duplicate-code');
|
|
1548
|
+
expect(finding).toBeDefined();
|
|
1549
|
+
expect(finding.message).toContain('...');
|
|
1550
|
+
});
|
|
1551
|
+
it('does not add ellipsis for lines under 60 chars', async () => {
|
|
1552
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1553
|
+
const shortLine = 'const result = processItem(item, config);';
|
|
1554
|
+
const content = Array.from({ length: 4 }, () => shortLine).join('\n');
|
|
1555
|
+
mockReadFile.mockResolvedValue(content);
|
|
1556
|
+
const result = await auditCode('src', '/project', ['dry']);
|
|
1557
|
+
const finding = findFinding(result.findings, 'duplicate-code');
|
|
1558
|
+
expect(finding).toBeDefined();
|
|
1559
|
+
expect(finding.message).not.toContain('...');
|
|
1560
|
+
});
|
|
1561
|
+
});
|
|
1562
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1563
|
+
// buildSummary & calculateScore — scoring and summary logic
|
|
1564
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1565
|
+
describe('buildSummary and calculateScore', () => {
|
|
1566
|
+
it('calculates score 100 when no findings', async () => {
|
|
1567
|
+
mockGlob.mockResolvedValue([]);
|
|
1568
|
+
const result = await auditCode('src', '/project');
|
|
1569
|
+
expect(result.score).toBe(100);
|
|
1570
|
+
expect(result.summary.score).toBe(100);
|
|
1571
|
+
});
|
|
1572
|
+
it('calculates score with weighted severity penalties', async () => {
|
|
1573
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1574
|
+
// Critical finding: penalty = 10
|
|
1575
|
+
const content = `const api_key = "realSecretKeyAbcde123";`;
|
|
1576
|
+
mockReadFile.mockResolvedValue(content);
|
|
1577
|
+
const result = await auditCode('src', '/project', ['security']);
|
|
1578
|
+
// score should be penalized
|
|
1579
|
+
expect(result.score).toBeLessThan(100);
|
|
1580
|
+
expect(result.score).toBeGreaterThanOrEqual(0);
|
|
1581
|
+
});
|
|
1582
|
+
it('normalizes score by file count (more files = more lenient)', async () => {
|
|
1583
|
+
// Single file with many issues
|
|
1584
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1585
|
+
const content = [
|
|
1586
|
+
`const api_key = "realSecretKeyAbcde123";`,
|
|
1587
|
+
`eval("bad");`,
|
|
1588
|
+
`element.innerHTML = data;`,
|
|
1589
|
+
].join('\n');
|
|
1590
|
+
mockReadFile.mockResolvedValue(content);
|
|
1591
|
+
const result1 = await auditCode('src', '/project', ['security']);
|
|
1592
|
+
// Reset and test with more files
|
|
1593
|
+
vi.clearAllMocks();
|
|
1594
|
+
const files = Array.from({ length: 10 }, (_, i) => `app${i}.ts`);
|
|
1595
|
+
mockGlob.mockResolvedValue(files);
|
|
1596
|
+
mockReadFile
|
|
1597
|
+
.mockResolvedValueOnce(content) // First file has issues
|
|
1598
|
+
.mockResolvedValue('const x = 1;'); // Rest are clean
|
|
1599
|
+
const result2 = await auditCode('src', '/project', ['security']);
|
|
1600
|
+
// More files = higher score for same findings
|
|
1601
|
+
expect(result2.score).toBeGreaterThan(result1.score);
|
|
1602
|
+
});
|
|
1603
|
+
it('summary.byCategory includes all requested categories', async () => {
|
|
1604
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1605
|
+
mockReadFile.mockResolvedValue('const x = 1;');
|
|
1606
|
+
const categories = ['clean-code', 'security', 'solid'];
|
|
1607
|
+
const result = await auditCode('src', '/project', categories);
|
|
1608
|
+
for (const cat of categories) {
|
|
1609
|
+
expect(result.summary.byCategory[cat]).toBeDefined();
|
|
1610
|
+
expect(result.summary.byCategory[cat].count).toBeDefined();
|
|
1611
|
+
expect(result.summary.byCategory[cat].severity).toBeDefined();
|
|
1612
|
+
}
|
|
1613
|
+
});
|
|
1614
|
+
it('summary.byCategory has worst severity per category', async () => {
|
|
1615
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1616
|
+
const content = [
|
|
1617
|
+
`const api_key = "realSecretKeyAbcde123";`, // critical
|
|
1618
|
+
`element.innerHTML = data;`, // error
|
|
1619
|
+
].join('\n');
|
|
1620
|
+
mockReadFile.mockResolvedValue(content);
|
|
1621
|
+
const result = await auditCode('src', '/project', ['security']);
|
|
1622
|
+
// Security should have "critical" as worst severity
|
|
1623
|
+
expect(result.summary.byCategory.security?.severity).toBe('critical');
|
|
1624
|
+
});
|
|
1625
|
+
it('summary includes strengths for categories with no findings', async () => {
|
|
1626
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1627
|
+
mockReadFile.mockResolvedValue('function add(a: number, b: number) { return a + b; }');
|
|
1628
|
+
const result = await auditCode('src', '/project', ['clean-code', 'security', 'solid']);
|
|
1629
|
+
// At least some categories should have no findings
|
|
1630
|
+
expect(result.summary.strengths.length).toBeGreaterThan(0);
|
|
1631
|
+
for (const s of result.summary.strengths) {
|
|
1632
|
+
expect(s).toContain('No issues found');
|
|
1633
|
+
}
|
|
1634
|
+
});
|
|
1635
|
+
it('handles unknown severity in calculateScore using default weight', async () => {
|
|
1636
|
+
// This tests the fallback `?? 1` in the weights lookup
|
|
1637
|
+
// We can't directly trigger unknown severity through normal flow,
|
|
1638
|
+
// but we can verify the score handles it through custom rules
|
|
1639
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1640
|
+
mockReadFile.mockResolvedValue('const x = 1;');
|
|
1641
|
+
const result = await auditCode('src', '/project', ['clean-code']);
|
|
1642
|
+
expect(typeof result.score).toBe('number');
|
|
1643
|
+
});
|
|
1644
|
+
});
|
|
1645
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1646
|
+
// findFunctionBoundaries — function detection across languages
|
|
1647
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1648
|
+
describe('findFunctionBoundaries', () => {
|
|
1649
|
+
it('detects standard JS/TS function declarations', async () => {
|
|
1650
|
+
const body = Array.from({ length: 55 }, (_, i) => ` const v${i} = ${i};`).join('\n');
|
|
1651
|
+
const content = `function myFunction() {\n${body}\n}`;
|
|
1652
|
+
mockReadFile.mockResolvedValue(content);
|
|
1653
|
+
const findings = await auditFile('/test/fn.ts', ['clean-code']);
|
|
1654
|
+
const finding = findFinding(findings, 'function-too-long');
|
|
1655
|
+
expect(finding).toBeDefined();
|
|
1656
|
+
expect(finding.message).toContain('myFunction');
|
|
1657
|
+
});
|
|
1658
|
+
it('detects const arrow functions', async () => {
|
|
1659
|
+
const body = Array.from({ length: 55 }, (_, i) => ` const v${i} = ${i};`).join('\n');
|
|
1660
|
+
const content = `const myArrow = (x: number) => {\n${body}\n}`;
|
|
1661
|
+
mockReadFile.mockResolvedValue(content);
|
|
1662
|
+
const findings = await auditFile('/test/arrow.ts', ['clean-code']);
|
|
1663
|
+
const finding = findFinding(findings, 'function-too-long');
|
|
1664
|
+
expect(finding).toBeDefined();
|
|
1665
|
+
expect(finding.message).toContain('myArrow');
|
|
1666
|
+
});
|
|
1667
|
+
it('detects let arrow functions', async () => {
|
|
1668
|
+
const body = Array.from({ length: 55 }, (_, i) => ` const v${i} = ${i};`).join('\n');
|
|
1669
|
+
const content = `let myLetFn = (x: number) => {\n${body}\n}`;
|
|
1670
|
+
mockReadFile.mockResolvedValue(content);
|
|
1671
|
+
const findings = await auditFile('/test/let-arrow.ts', ['clean-code']);
|
|
1672
|
+
const finding = findFinding(findings, 'function-too-long');
|
|
1673
|
+
expect(finding).toBeDefined();
|
|
1674
|
+
expect(finding.message).toContain('myLetFn');
|
|
1675
|
+
});
|
|
1676
|
+
it('detects Python def', async () => {
|
|
1677
|
+
// Python function detection is indent-based
|
|
1678
|
+
const body = Array.from({ length: 55 }, (_, i) => ` v${i} = ${i}`).join('\n');
|
|
1679
|
+
const content = `def my_python_fn():\n${body}\n`;
|
|
1680
|
+
mockReadFile.mockResolvedValue(content);
|
|
1681
|
+
const findings = await auditFile('/test/fn.py', ['clean-code']);
|
|
1682
|
+
const finding = findFinding(findings, 'function-too-long');
|
|
1683
|
+
expect(finding).toBeDefined();
|
|
1684
|
+
expect(finding.message).toContain('my_python_fn');
|
|
1685
|
+
});
|
|
1686
|
+
it('detects Python def and ends at de-indent', async () => {
|
|
1687
|
+
// Python function with next function at same indent level
|
|
1688
|
+
const body = Array.from({ length: 55 }, (_, i) => ` v${i} = ${i}`).join('\n');
|
|
1689
|
+
const content = [
|
|
1690
|
+
`def long_fn():`,
|
|
1691
|
+
body,
|
|
1692
|
+
`def next_fn():`,
|
|
1693
|
+
` pass`,
|
|
1694
|
+
].join('\n');
|
|
1695
|
+
mockReadFile.mockResolvedValue(content);
|
|
1696
|
+
const findings = await auditFile('/test/py-indent.py', ['clean-code']);
|
|
1697
|
+
const finding = findFinding(findings, 'function-too-long');
|
|
1698
|
+
expect(finding).toBeDefined();
|
|
1699
|
+
expect(finding.message).toContain('long_fn');
|
|
1700
|
+
});
|
|
1701
|
+
it('handles functions without braces (fallback to endLine = j)', async () => {
|
|
1702
|
+
// A function where braceCount never reaches 0 - single-line function
|
|
1703
|
+
const content = `function foo() { return 1; }`;
|
|
1704
|
+
mockReadFile.mockResolvedValue(content);
|
|
1705
|
+
const findings = await auditFile('/test/oneliner.ts', ['clean-code']);
|
|
1706
|
+
// Should not crash, function is short
|
|
1707
|
+
expect(findFinding(findings, 'function-too-long')).toBeUndefined();
|
|
1708
|
+
});
|
|
1709
|
+
it('assigns "anonymous" when no name group matches', async () => {
|
|
1710
|
+
// var arrow function
|
|
1711
|
+
const body = Array.from({ length: 55 }, (_, i) => ` const v${i} = ${i};`).join('\n');
|
|
1712
|
+
const content = `var myVarFn = (x: number) => {\n${body}\n}`;
|
|
1713
|
+
mockReadFile.mockResolvedValue(content);
|
|
1714
|
+
const findings = await auditFile('/test/var-arrow.ts', ['clean-code']);
|
|
1715
|
+
const finding = findFinding(findings, 'function-too-long');
|
|
1716
|
+
expect(finding).toBeDefined();
|
|
1717
|
+
expect(finding.message).toContain('myVarFn');
|
|
1718
|
+
});
|
|
1719
|
+
});
|
|
1720
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1721
|
+
// Edge cases and integration scenarios
|
|
1722
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1723
|
+
describe('edge cases', () => {
|
|
1724
|
+
it('handles empty file content gracefully', async () => {
|
|
1725
|
+
mockReadFile.mockResolvedValue('');
|
|
1726
|
+
const findings = await auditFile('/test/empty.ts', ['clean-code', 'security', 'error-handling']);
|
|
1727
|
+
// Should not crash, should have no findings
|
|
1728
|
+
expect(findings).toEqual([]);
|
|
1729
|
+
});
|
|
1730
|
+
it('handles file with only whitespace', async () => {
|
|
1731
|
+
mockReadFile.mockResolvedValue(' \n \n \n');
|
|
1732
|
+
const findings = await auditFile('/test/whitespace.ts', ['clean-code']);
|
|
1733
|
+
expect(findings).toEqual([]);
|
|
1734
|
+
});
|
|
1735
|
+
it('handles file with only comments', async () => {
|
|
1736
|
+
const content = [
|
|
1737
|
+
'// This is a comment',
|
|
1738
|
+
'// Another comment',
|
|
1739
|
+
'# Python comment',
|
|
1740
|
+
].join('\n');
|
|
1741
|
+
mockReadFile.mockResolvedValue(content);
|
|
1742
|
+
const findings = await auditFile('/test/comments.ts', ['clean-code']);
|
|
1743
|
+
// Should not flag magic numbers in comments
|
|
1744
|
+
expect(findFinding(findings, 'magic-number')).toBeUndefined();
|
|
1745
|
+
});
|
|
1746
|
+
it('auditCode runs all 8 categories when specified', async () => {
|
|
1747
|
+
mockGlob.mockResolvedValue(['app.test.ts']);
|
|
1748
|
+
const content = [
|
|
1749
|
+
'// TODO: fix this',
|
|
1750
|
+
'class A {}',
|
|
1751
|
+
'class B {}',
|
|
1752
|
+
'class C {}',
|
|
1753
|
+
`const secret = "realSecretKeyAbcde123";`,
|
|
1754
|
+
'try { x(); } catch (e) {}',
|
|
1755
|
+
'for (const i of arr) { await fetch(i); }',
|
|
1756
|
+
'it.skip("broken", () => {});',
|
|
1757
|
+
'const result = processItem(item, config, options);',
|
|
1758
|
+
'const result = processItem(item, config, options);',
|
|
1759
|
+
'const result = processItem(item, config, options);',
|
|
1760
|
+
'const result = processItem(item, config, options);',
|
|
1761
|
+
].join('\n');
|
|
1762
|
+
mockReadFile.mockResolvedValue(content);
|
|
1763
|
+
const allCategories = [
|
|
1764
|
+
'solid',
|
|
1765
|
+
'clean-code',
|
|
1766
|
+
'architecture',
|
|
1767
|
+
'security',
|
|
1768
|
+
'error-handling',
|
|
1769
|
+
'performance',
|
|
1770
|
+
'testing',
|
|
1771
|
+
'dry',
|
|
1772
|
+
];
|
|
1773
|
+
const result = await auditCode('.', '/project', allCategories);
|
|
1774
|
+
// Should have findings from multiple categories
|
|
1775
|
+
const cats = new Set(result.findings.map((f) => f.category));
|
|
1776
|
+
expect(cats.size).toBeGreaterThanOrEqual(3);
|
|
1777
|
+
});
|
|
1778
|
+
it('handles files with all supported code extensions', async () => {
|
|
1779
|
+
const exts = [
|
|
1780
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
1781
|
+
'.py', '.go', '.rs', '.java', '.kt', '.kts',
|
|
1782
|
+
'.rb', '.php', '.cs', '.swift', '.dart',
|
|
1783
|
+
'.ex', '.exs', '.scala', '.clj',
|
|
1784
|
+
];
|
|
1785
|
+
const files = exts.map((ext) => `app${ext}`);
|
|
1786
|
+
mockGlob.mockResolvedValue(files);
|
|
1787
|
+
mockReadFile.mockResolvedValue('const x = 1;');
|
|
1788
|
+
const result = await auditCode('src', '/project', ['clean-code']);
|
|
1789
|
+
expect(result.filesAnalyzed).toBe(exts.length);
|
|
1790
|
+
});
|
|
1791
|
+
it('limits glob results to 200 files', async () => {
|
|
1792
|
+
const files = Array.from({ length: 250 }, (_, i) => `file${i}.ts`);
|
|
1793
|
+
mockGlob.mockResolvedValue(files);
|
|
1794
|
+
mockReadFile.mockResolvedValue('const x = 1;');
|
|
1795
|
+
const result = await auditCode('src', '/project', ['clean-code']);
|
|
1796
|
+
expect(result.filesAnalyzed).toBe(200);
|
|
1797
|
+
});
|
|
1798
|
+
it('all findings have required fields: file, category, severity, rule, message, suggestion', async () => {
|
|
1799
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1800
|
+
const content = [
|
|
1801
|
+
`const api_key = "realSecretKeyAbcde123";`,
|
|
1802
|
+
`eval("bad");`,
|
|
1803
|
+
'// TODO: fix this',
|
|
1804
|
+
'try { x(); } catch (e) {}',
|
|
1805
|
+
].join('\n');
|
|
1806
|
+
mockReadFile.mockResolvedValue(content);
|
|
1807
|
+
const result = await auditCode('src', '/project', [
|
|
1808
|
+
'clean-code',
|
|
1809
|
+
'security',
|
|
1810
|
+
'error-handling',
|
|
1811
|
+
]);
|
|
1812
|
+
for (const finding of result.findings) {
|
|
1813
|
+
expect(finding.file).toBeDefined();
|
|
1814
|
+
expect(finding.category).toBeDefined();
|
|
1815
|
+
expect(finding.severity).toBeDefined();
|
|
1816
|
+
expect(finding.rule).toBeDefined();
|
|
1817
|
+
expect(finding.message).toBeTruthy();
|
|
1818
|
+
expect(finding.suggestion).toBeTruthy();
|
|
1819
|
+
expect(['info', 'warning', 'error', 'critical']).toContain(finding.severity);
|
|
1820
|
+
}
|
|
1821
|
+
});
|
|
1822
|
+
it('catch-only-logs handles body lines at end of file', async () => {
|
|
1823
|
+
// Catch block near end of file (Math.min boundary)
|
|
1824
|
+
const content = [
|
|
1825
|
+
'try { x(); }',
|
|
1826
|
+
'catch (e) {',
|
|
1827
|
+
' console.log(e);',
|
|
1828
|
+
'}',
|
|
1829
|
+
].join('\n');
|
|
1830
|
+
mockReadFile.mockResolvedValue(content);
|
|
1831
|
+
const findings = await auditFile('/test/eof-catch.ts', ['error-handling']);
|
|
1832
|
+
expect(findFinding(findings, 'catch-only-logs')).toBeDefined();
|
|
1833
|
+
});
|
|
1834
|
+
it('async function detection uses 500-char window', async () => {
|
|
1835
|
+
// Create a long async function body (>500 chars) with a try far away
|
|
1836
|
+
const longBody = Array.from({ length: 60 }, (_, i) => ` const longVarName${i} = ${i};`).join('\n');
|
|
1837
|
+
const content = [
|
|
1838
|
+
'async function farTry() {',
|
|
1839
|
+
longBody,
|
|
1840
|
+
' try {',
|
|
1841
|
+
' return 1;',
|
|
1842
|
+
' } catch (e) { throw e; }',
|
|
1843
|
+
'}',
|
|
1844
|
+
].join('\n');
|
|
1845
|
+
mockReadFile.mockResolvedValue(content);
|
|
1846
|
+
const findings = await auditFile('/test/far-try.ts', ['error-handling']);
|
|
1847
|
+
// The try is beyond the 500-char window, so it might still detect unhandled-async
|
|
1848
|
+
findFinding(findings, 'unhandled-async');
|
|
1849
|
+
// Depending on whether "try" falls within the 500-char window
|
|
1850
|
+
// The heuristic only looks at 500 chars after the async keyword
|
|
1851
|
+
// This test just ensures it doesn't crash
|
|
1852
|
+
expect(Array.isArray(findings)).toBe(true);
|
|
1853
|
+
});
|
|
1854
|
+
it('multiple files produce aggregated findings and summary', async () => {
|
|
1855
|
+
mockGlob.mockResolvedValue(['a.ts', 'b.ts']);
|
|
1856
|
+
mockReadFile
|
|
1857
|
+
.mockResolvedValueOnce('// TODO: fix a')
|
|
1858
|
+
.mockResolvedValueOnce('// TODO: fix b');
|
|
1859
|
+
const result = await auditCode('src', '/project', ['clean-code']);
|
|
1860
|
+
expect(result.filesAnalyzed).toBe(2);
|
|
1861
|
+
const todoFindings = findFindings(result.findings, 'pending-marker');
|
|
1862
|
+
expect(todoFindings.length).toBe(2);
|
|
1863
|
+
});
|
|
1864
|
+
it('summary.byCategory.severity defaults to info when no findings', async () => {
|
|
1865
|
+
mockGlob.mockResolvedValue(['app.ts']);
|
|
1866
|
+
mockReadFile.mockResolvedValue('function add(a: number, b: number) { return a + b; }');
|
|
1867
|
+
const result = await auditCode('src', '/project', ['security']);
|
|
1868
|
+
expect(result.summary.byCategory.security?.severity).toBe('info');
|
|
1869
|
+
expect(result.summary.byCategory.security?.count).toBe(0);
|
|
1870
|
+
});
|
|
1871
|
+
});
|
|
1872
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1873
|
+
// Sparse/undefined array element guards — defensive coverage
|
|
1874
|
+
// Lines 719, 781, 856, 883: continue when lines[i] is undefined.
|
|
1875
|
+
// These guards protect against sparse arrays. Since content.split('\n')
|
|
1876
|
+
// never produces holes, we exercise the surrounding code paths thoroughly.
|
|
1877
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1878
|
+
describe('checkPerformance — spread-in-loop detection with edge-case line patterns', () => {
|
|
1879
|
+
it('detects spread in a for-loop at the boundary of the 10-line window', async () => {
|
|
1880
|
+
mockGlob.mockResolvedValue(['spread-boundary.ts']);
|
|
1881
|
+
// Spread appears at line i+9 (last line within the 10-line window)
|
|
1882
|
+
const lines = [
|
|
1883
|
+
'for (let i = 0; i < items.length; i++) {',
|
|
1884
|
+
' const a = 1;',
|
|
1885
|
+
' const b = 2;',
|
|
1886
|
+
' const c = 3;',
|
|
1887
|
+
' const d = 4;',
|
|
1888
|
+
' const e = 5;',
|
|
1889
|
+
' const f = 6;',
|
|
1890
|
+
' const g = 7;',
|
|
1891
|
+
' const h = 8;',
|
|
1892
|
+
' const merged = { ...items[i] };',
|
|
1893
|
+
'}',
|
|
1894
|
+
];
|
|
1895
|
+
mockReadFile.mockResolvedValue(lines.join('\n'));
|
|
1896
|
+
const result = await auditCode('src', '/project', ['performance']);
|
|
1897
|
+
expect(findFinding(result.findings, 'spread-in-loop')).toBeDefined();
|
|
1898
|
+
});
|
|
1899
|
+
it('does not detect spread beyond the 10-line window', async () => {
|
|
1900
|
+
mockGlob.mockResolvedValue(['spread-far.ts']);
|
|
1901
|
+
const lines = [
|
|
1902
|
+
'for (let i = 0; i < items.length; i++) {',
|
|
1903
|
+
' const a = 1;',
|
|
1904
|
+
' const b = 2;',
|
|
1905
|
+
' const c = 3;',
|
|
1906
|
+
' const d = 4;',
|
|
1907
|
+
' const e = 5;',
|
|
1908
|
+
' const f = 6;',
|
|
1909
|
+
' const g = 7;',
|
|
1910
|
+
' const h = 8;',
|
|
1911
|
+
' const j = 9;',
|
|
1912
|
+
' const k = 10;',
|
|
1913
|
+
' const merged = { ...items[i] };', // line i+11, beyond window
|
|
1914
|
+
'}',
|
|
1915
|
+
];
|
|
1916
|
+
mockReadFile.mockResolvedValue(lines.join('\n'));
|
|
1917
|
+
const result = await auditCode('src', '/project', ['performance']);
|
|
1918
|
+
expect(findFinding(result.findings, 'spread-in-loop')).toBeUndefined();
|
|
1919
|
+
});
|
|
1920
|
+
it('detects spread inside forEach callback', async () => {
|
|
1921
|
+
mockGlob.mockResolvedValue(['spread-foreach.ts']);
|
|
1922
|
+
const content = [
|
|
1923
|
+
'items.forEach((item) => {',
|
|
1924
|
+
' const result = { ...item, extra: true };',
|
|
1925
|
+
'});',
|
|
1926
|
+
].join('\n');
|
|
1927
|
+
mockReadFile.mockResolvedValue(content);
|
|
1928
|
+
const result = await auditCode('src', '/project', ['performance']);
|
|
1929
|
+
expect(findFinding(result.findings, 'spread-in-loop')).toBeDefined();
|
|
1930
|
+
});
|
|
1931
|
+
it('handles for-loop at the very end of a file (window extends past EOF)', async () => {
|
|
1932
|
+
mockGlob.mockResolvedValue(['spread-eof.ts']);
|
|
1933
|
+
// Only 2 lines total; the 10-line window clips to lines.length
|
|
1934
|
+
const content = [
|
|
1935
|
+
'for (let i = 0; i < arr.length; i++) {',
|
|
1936
|
+
' const copy = { ...arr[i] };',
|
|
1937
|
+
].join('\n');
|
|
1938
|
+
mockReadFile.mockResolvedValue(content);
|
|
1939
|
+
const result = await auditCode('src', '/project', ['performance']);
|
|
1940
|
+
expect(findFinding(result.findings, 'spread-in-loop')).toBeDefined();
|
|
1941
|
+
});
|
|
1942
|
+
it('handles empty lines within the for-loop 10-line window', async () => {
|
|
1943
|
+
mockGlob.mockResolvedValue(['spread-empty.ts']);
|
|
1944
|
+
const content = [
|
|
1945
|
+
'for (let i = 0; i < items.length; i++) {',
|
|
1946
|
+
'',
|
|
1947
|
+
'',
|
|
1948
|
+
'',
|
|
1949
|
+
'',
|
|
1950
|
+
'',
|
|
1951
|
+
'',
|
|
1952
|
+
'',
|
|
1953
|
+
'',
|
|
1954
|
+
' const merged = { ...items[i] };',
|
|
1955
|
+
'}',
|
|
1956
|
+
].join('\n');
|
|
1957
|
+
mockReadFile.mockResolvedValue(content);
|
|
1958
|
+
const result = await auditCode('src', '/project', ['performance']);
|
|
1959
|
+
expect(findFinding(result.findings, 'spread-in-loop')).toBeDefined();
|
|
1960
|
+
});
|
|
1961
|
+
});
|
|
1962
|
+
describe('checkDry — duplicate code detection edge cases', () => {
|
|
1963
|
+
it('detects duplicates with many identical long lines', async () => {
|
|
1964
|
+
mockGlob.mockResolvedValue(['dup-lines.ts']);
|
|
1965
|
+
const duplicateLine = 'const result = processItem(item, config, options, extra);';
|
|
1966
|
+
const content = Array.from({ length: 5 }, () => duplicateLine).join('\n');
|
|
1967
|
+
mockReadFile.mockResolvedValue(content);
|
|
1968
|
+
const result = await auditCode('src', '/project', ['dry']);
|
|
1969
|
+
expect(findFinding(result.findings, 'duplicate-code')).toBeDefined();
|
|
1970
|
+
});
|
|
1971
|
+
it('does not flag short lines (under 10 chars) as duplicates', async () => {
|
|
1972
|
+
mockGlob.mockResolvedValue(['short-dup.ts']);
|
|
1973
|
+
const content = Array.from({ length: 10 }, () => 'x = 1;').join('\n');
|
|
1974
|
+
mockReadFile.mockResolvedValue(content);
|
|
1975
|
+
const result = await auditCode('src', '/project', ['dry']);
|
|
1976
|
+
expect(findFinding(result.findings, 'duplicate-code')).toBeUndefined();
|
|
1977
|
+
});
|
|
1978
|
+
it('skips comment lines starting with // in duplicate detection', async () => {
|
|
1979
|
+
mockGlob.mockResolvedValue(['comment-dup.ts']);
|
|
1980
|
+
const comment = '// This is a long comment line that repeats many times in the code';
|
|
1981
|
+
const content = Array.from({ length: 10 }, () => comment).join('\n');
|
|
1982
|
+
mockReadFile.mockResolvedValue(content);
|
|
1983
|
+
const result = await auditCode('src', '/project', ['dry']);
|
|
1984
|
+
expect(findFinding(result.findings, 'duplicate-code')).toBeUndefined();
|
|
1985
|
+
});
|
|
1986
|
+
it('skips comment lines starting with # in duplicate detection', async () => {
|
|
1987
|
+
mockGlob.mockResolvedValue(['hash-comment-dup.py']);
|
|
1988
|
+
const comment = '# This is a long Python comment line that repeats many times';
|
|
1989
|
+
const content = Array.from({ length: 10 }, () => comment).join('\n');
|
|
1990
|
+
mockReadFile.mockResolvedValue(content);
|
|
1991
|
+
const result = await auditCode('src', '/project', ['dry']);
|
|
1992
|
+
expect(findFinding(result.findings, 'duplicate-code')).toBeUndefined();
|
|
1993
|
+
});
|
|
1994
|
+
it('skips comment lines starting with * in duplicate detection', async () => {
|
|
1995
|
+
mockGlob.mockResolvedValue(['jsdoc-dup.ts']);
|
|
1996
|
+
const comment = ' * This is a long JSDoc comment line that appears many times';
|
|
1997
|
+
const content = Array.from({ length: 10 }, () => comment).join('\n');
|
|
1998
|
+
mockReadFile.mockResolvedValue(content);
|
|
1999
|
+
const result = await auditCode('src', '/project', ['dry']);
|
|
2000
|
+
expect(findFinding(result.findings, 'duplicate-code')).toBeUndefined();
|
|
2001
|
+
});
|
|
2002
|
+
it('handles content with only empty lines', async () => {
|
|
2003
|
+
mockGlob.mockResolvedValue(['empty-lines.ts']);
|
|
2004
|
+
const content = '\n\n\n\n\n\n\n\n\n\n';
|
|
2005
|
+
mockReadFile.mockResolvedValue(content);
|
|
2006
|
+
const result = await auditCode('src', '/project', ['dry']);
|
|
2007
|
+
expect(findFinding(result.findings, 'duplicate-code')).toBeUndefined();
|
|
2008
|
+
});
|
|
2009
|
+
});
|
|
2010
|
+
describe('findFunctionBoundaries — edge cases for function detection', () => {
|
|
2011
|
+
it('handles content with no functions at all', async () => {
|
|
2012
|
+
const content = 'const x = 1;\nconst y = 2;\nconst z = 3;';
|
|
2013
|
+
mockReadFile.mockResolvedValue(content);
|
|
2014
|
+
const findings = await auditFile('/test/no-fn.ts', ['clean-code']);
|
|
2015
|
+
expect(findFinding(findings, 'function-too-long')).toBeUndefined();
|
|
2016
|
+
});
|
|
2017
|
+
it('handles function with empty body (no brace counting needed)', async () => {
|
|
2018
|
+
const content = 'function empty() {}';
|
|
2019
|
+
mockReadFile.mockResolvedValue(content);
|
|
2020
|
+
const findings = await auditFile('/test/empty-fn.ts', ['clean-code']);
|
|
2021
|
+
expect(findFinding(findings, 'function-too-long')).toBeUndefined();
|
|
2022
|
+
});
|
|
2023
|
+
it('handles multiple functions where only one exceeds the line limit', async () => {
|
|
2024
|
+
const shortBody = Array.from({ length: 5 }, (_, i) => ` const v${i} = ${i};`).join('\n');
|
|
2025
|
+
const longBody = Array.from({ length: 55 }, (_, i) => ` const v${i} = ${i};`).join('\n');
|
|
2026
|
+
const content = [
|
|
2027
|
+
`function shortOne() {`,
|
|
2028
|
+
shortBody,
|
|
2029
|
+
`}`,
|
|
2030
|
+
`function longOne() {`,
|
|
2031
|
+
longBody,
|
|
2032
|
+
`}`,
|
|
2033
|
+
].join('\n');
|
|
2034
|
+
mockReadFile.mockResolvedValue(content);
|
|
2035
|
+
const findings = await auditFile('/test/mixed-fn.ts', ['clean-code']);
|
|
2036
|
+
const longFindings = findFindings(findings, 'function-too-long');
|
|
2037
|
+
expect(longFindings).toHaveLength(1);
|
|
2038
|
+
expect(longFindings[0]?.message).toContain('longOne');
|
|
2039
|
+
});
|
|
2040
|
+
it('handles deeply nested braces within a function', async () => {
|
|
2041
|
+
const body = [
|
|
2042
|
+
' if (true) {',
|
|
2043
|
+
' if (true) {',
|
|
2044
|
+
' if (true) {',
|
|
2045
|
+
...Array.from({ length: 50 }, (_, i) => ` const v${i} = ${i};`),
|
|
2046
|
+
' }',
|
|
2047
|
+
' }',
|
|
2048
|
+
' }',
|
|
2049
|
+
].join('\n');
|
|
2050
|
+
const content = `function deepBraces() {\n${body}\n}`;
|
|
2051
|
+
mockReadFile.mockResolvedValue(content);
|
|
2052
|
+
const findings = await auditFile('/test/deep-braces.ts', ['clean-code']);
|
|
2053
|
+
expect(findFinding(findings, 'function-too-long')).toBeDefined();
|
|
2054
|
+
});
|
|
2055
|
+
it('handles function followed immediately by another function', async () => {
|
|
2056
|
+
const body = Array.from({ length: 55 }, (_, i) => ` const v${i} = ${i};`).join('\n');
|
|
2057
|
+
const content = [
|
|
2058
|
+
`function first() {\n${body}\n}`,
|
|
2059
|
+
`function second() {\n${body}\n}`,
|
|
2060
|
+
].join('\n');
|
|
2061
|
+
mockReadFile.mockResolvedValue(content);
|
|
2062
|
+
const findings = await auditFile('/test/back-to-back.ts', ['clean-code']);
|
|
2063
|
+
const longFindings = findFindings(findings, 'function-too-long');
|
|
2064
|
+
expect(longFindings.length).toBeGreaterThanOrEqual(2);
|
|
2065
|
+
});
|
|
2066
|
+
it('handles content with many empty lines between code lines', async () => {
|
|
2067
|
+
const body = Array.from({ length: 55 }, (_, i) => ` const v${i} = ${i};\n`).join('\n');
|
|
2068
|
+
const content = `function spacey() {\n${body}\n}`;
|
|
2069
|
+
mockReadFile.mockResolvedValue(content);
|
|
2070
|
+
const findings = await auditFile('/test/spacey-fn.ts', ['clean-code']);
|
|
2071
|
+
// Function is long due to empty lines counted
|
|
2072
|
+
expect(findFinding(findings, 'function-too-long')).toBeDefined();
|
|
2073
|
+
});
|
|
2074
|
+
});
|
|
2075
|
+
//# sourceMappingURL=auditor.test.js.map
|