@codeyam/codeyam-cli 0.1.0-staging.103bd05 → 0.1.0-staging.121e983
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/analyzer-template/.build-info.json +8 -8
- package/analyzer-template/log.txt +3 -3
- package/analyzer-template/package.json +4 -4
- package/analyzer-template/packages/ai/src/lib/astScopes/methodSemantics.ts +135 -0
- package/analyzer-template/packages/ai/src/lib/astScopes/nodeToSource.ts +19 -0
- package/analyzer-template/packages/ai/src/lib/astScopes/paths.ts +11 -4
- package/analyzer-template/packages/ai/src/lib/dataStructure/ScopeDataStructure.ts +36 -9
- package/analyzer-template/packages/ai/src/lib/dataStructure/equivalencyManagers/ParentScopeManager.ts +10 -3
- package/analyzer-template/packages/ai/src/lib/dataStructure/helpers/cleanKnownObjectFunctions.ts +16 -6
- package/analyzer-template/packages/analyze/index.ts +4 -1
- package/analyzer-template/packages/analyze/src/lib/files/analyze/analyzeEntities/prepareDataStructures.ts +28 -2
- package/analyzer-template/packages/analyze/src/lib/files/analyze/analyzeEntities.ts +5 -36
- package/analyzer-template/packages/analyze/src/lib/files/analyze/findOrCreateEntity.ts +10 -6
- package/analyzer-template/packages/analyze/src/lib/files/analyze/gatherEntityMap.ts +9 -12
- package/analyzer-template/packages/analyze/src/lib/files/analyze/trackEntityCircularDependencies.ts +21 -0
- package/analyzer-template/packages/analyze/src/lib/files/analyze/validateDependencyAnalyses.ts +82 -10
- package/analyzer-template/packages/analyze/src/lib/files/analyzeChange.ts +4 -0
- package/analyzer-template/packages/analyze/src/lib/files/analyzeInitial.ts +4 -0
- package/analyzer-template/packages/analyze/src/lib/files/analyzeNextRoute.ts +8 -3
- package/analyzer-template/packages/analyze/src/lib/files/scenarios/generateDataStructure.ts +239 -58
- package/analyzer-template/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.ts +1684 -1462
- package/analyzer-template/packages/aws/package.json +6 -6
- package/analyzer-template/packages/database/package.json +2 -2
- package/analyzer-template/packages/database/src/lib/loadAnalysis.ts +25 -15
- package/analyzer-template/packages/database/src/lib/loadEntity.ts +19 -8
- package/analyzer-template/packages/github/dist/database/src/lib/loadAnalysis.d.ts.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/loadAnalysis.js +7 -1
- package/analyzer-template/packages/github/dist/database/src/lib/loadAnalysis.js.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.d.ts +4 -1
- package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.d.ts.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.js +5 -5
- package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.js.map +1 -1
- package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.d.ts +3 -1
- package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.d.ts.map +1 -1
- package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.js +22 -1
- package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.js.map +1 -1
- package/analyzer-template/packages/utils/src/lib/fs/rsyncCopy.ts +27 -0
- package/analyzer-template/project/analyzeFileEntities.ts +26 -0
- package/analyzer-template/project/runMultiScenarioServer.ts +26 -3
- package/background/src/lib/virtualized/project/analyzeFileEntities.js +22 -0
- package/background/src/lib/virtualized/project/analyzeFileEntities.js.map +1 -1
- package/background/src/lib/virtualized/project/runMultiScenarioServer.js +23 -3
- package/background/src/lib/virtualized/project/runMultiScenarioServer.js.map +1 -1
- package/codeyam-cli/src/cli.js +24 -0
- package/codeyam-cli/src/cli.js.map +1 -1
- package/codeyam-cli/src/commands/__tests__/editor.analyzeImportsArgs.test.js +47 -0
- package/codeyam-cli/src/commands/__tests__/editor.analyzeImportsArgs.test.js.map +1 -0
- package/codeyam-cli/src/commands/__tests__/editor.auditNoAutoAnalysis.test.js +71 -0
- package/codeyam-cli/src/commands/__tests__/editor.auditNoAutoAnalysis.test.js.map +1 -0
- package/codeyam-cli/src/commands/__tests__/editor.designSystem.test.js +30 -0
- package/codeyam-cli/src/commands/__tests__/editor.designSystem.test.js.map +1 -0
- package/codeyam-cli/src/commands/__tests__/editor.isolateArgs.test.js +51 -0
- package/codeyam-cli/src/commands/__tests__/editor.isolateArgs.test.js.map +1 -0
- package/codeyam-cli/src/commands/__tests__/editor.statePersistence.test.js +55 -0
- package/codeyam-cli/src/commands/__tests__/editor.statePersistence.test.js.map +1 -0
- package/codeyam-cli/src/commands/__tests__/editor.stepDispatch.test.js +9 -9
- package/codeyam-cli/src/commands/__tests__/init.gitignore.test.js +39 -3
- package/codeyam-cli/src/commands/__tests__/init.gitignore.test.js.map +1 -1
- package/codeyam-cli/src/commands/editor.js +2407 -466
- package/codeyam-cli/src/commands/editor.js.map +1 -1
- package/codeyam-cli/src/commands/editorAnalyzeImportsArgs.js +23 -0
- package/codeyam-cli/src/commands/editorAnalyzeImportsArgs.js.map +1 -0
- package/codeyam-cli/src/commands/editorIsolateArgs.js +25 -0
- package/codeyam-cli/src/commands/editorIsolateArgs.js.map +1 -0
- package/codeyam-cli/src/commands/init.js +21 -0
- package/codeyam-cli/src/commands/init.js.map +1 -1
- package/codeyam-cli/src/commands/telemetry.js +37 -0
- package/codeyam-cli/src/commands/telemetry.js.map +1 -0
- package/codeyam-cli/src/data/designSystems.js +27 -0
- package/codeyam-cli/src/data/designSystems.js.map +1 -0
- package/codeyam-cli/src/data/techStacks.js +1 -1
- package/codeyam-cli/src/utils/__tests__/devServerState.test.js +93 -1
- package/codeyam-cli/src/utils/__tests__/devServerState.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/editorApi.test.js +44 -0
- package/codeyam-cli/src/utils/__tests__/editorApi.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/editorAudit.test.js +2328 -47
- package/codeyam-cli/src/utils/__tests__/editorAudit.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/editorCaptureScenarioSeeding.test.js +137 -0
- package/codeyam-cli/src/utils/__tests__/editorCaptureScenarioSeeding.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorEntityChangeStatus.test.js +70 -0
- package/codeyam-cli/src/utils/__tests__/editorEntityChangeStatus.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/editorEntityHelpers.test.js +124 -4
- package/codeyam-cli/src/utils/__tests__/editorEntityHelpers.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/editorGuardMiddleware.test.js +67 -0
- package/codeyam-cli/src/utils/__tests__/editorGuardMiddleware.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorPreview.test.js +11 -3
- package/codeyam-cli/src/utils/__tests__/editorPreview.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/editorProxySession.test.js +98 -1
- package/codeyam-cli/src/utils/__tests__/editorProxySession.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/editorRoadmap.test.js +1108 -0
- package/codeyam-cli/src/utils/__tests__/editorRoadmap.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorScenarioSwitch.test.js +190 -0
- package/codeyam-cli/src/utils/__tests__/editorScenarioSwitch.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/editorScenarios.test.js +286 -1
- package/codeyam-cli/src/utils/__tests__/editorScenarios.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/editorSeedAdapter.test.js +134 -1
- package/codeyam-cli/src/utils/__tests__/editorSeedAdapter.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/entityChangeStatus.test.js +266 -2
- package/codeyam-cli/src/utils/__tests__/entityChangeStatus.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/envFile.test.js +125 -0
- package/codeyam-cli/src/utils/__tests__/envFile.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/glossaryAdd.test.js +177 -0
- package/codeyam-cli/src/utils/__tests__/glossaryAdd.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/handoffContext.test.js +500 -0
- package/codeyam-cli/src/utils/__tests__/handoffContext.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/journalCaptureStabilization.test.js +16 -1
- package/codeyam-cli/src/utils/__tests__/journalCaptureStabilization.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/manualEntityAnalysis.test.js +302 -0
- package/codeyam-cli/src/utils/__tests__/manualEntityAnalysis.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/registerScenarioResult.test.js +127 -0
- package/codeyam-cli/src/utils/__tests__/registerScenarioResult.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/scenarioCoverage.test.js +57 -0
- package/codeyam-cli/src/utils/__tests__/scenarioCoverage.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/scenariosManifest.test.js +180 -1
- package/codeyam-cli/src/utils/__tests__/scenariosManifest.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/screenshotHash.test.js +84 -0
- package/codeyam-cli/src/utils/__tests__/screenshotHash.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/telemetry.test.js +159 -0
- package/codeyam-cli/src/utils/__tests__/telemetry.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/testRunner.test.js +216 -0
- package/codeyam-cli/src/utils/__tests__/testRunner.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/webappDetection.test.js +6 -0
- package/codeyam-cli/src/utils/__tests__/webappDetection.test.js.map +1 -1
- package/codeyam-cli/src/utils/analysisRunner.js +36 -7
- package/codeyam-cli/src/utils/analysisRunner.js.map +1 -1
- package/codeyam-cli/src/utils/analyzer.js +11 -1
- package/codeyam-cli/src/utils/analyzer.js.map +1 -1
- package/codeyam-cli/src/utils/backgroundServer.js +1 -1
- package/codeyam-cli/src/utils/backgroundServer.js.map +1 -1
- package/codeyam-cli/src/utils/designSystemShowcase.js +810 -0
- package/codeyam-cli/src/utils/designSystemShowcase.js.map +1 -0
- package/codeyam-cli/src/utils/devServerState.js +32 -0
- package/codeyam-cli/src/utils/devServerState.js.map +1 -1
- package/codeyam-cli/src/utils/editorApi.js +16 -0
- package/codeyam-cli/src/utils/editorApi.js.map +1 -1
- package/codeyam-cli/src/utils/editorAudit.js +532 -38
- package/codeyam-cli/src/utils/editorAudit.js.map +1 -1
- package/codeyam-cli/src/utils/editorEntityChangeStatus.js +13 -7
- package/codeyam-cli/src/utils/editorEntityChangeStatus.js.map +1 -1
- package/codeyam-cli/src/utils/editorEntityHelpers.js +18 -3
- package/codeyam-cli/src/utils/editorEntityHelpers.js.map +1 -1
- package/codeyam-cli/src/utils/editorGuard.js +36 -0
- package/codeyam-cli/src/utils/editorGuard.js.map +1 -0
- package/codeyam-cli/src/utils/editorPreview.js +5 -3
- package/codeyam-cli/src/utils/editorPreview.js.map +1 -1
- package/codeyam-cli/src/utils/editorRecapture.js +109 -0
- package/codeyam-cli/src/utils/editorRecapture.js.map +1 -0
- package/codeyam-cli/src/utils/editorRoadmap.js +574 -0
- package/codeyam-cli/src/utils/editorRoadmap.js.map +1 -0
- package/codeyam-cli/src/utils/editorScenarioSwitch.js +39 -2
- package/codeyam-cli/src/utils/editorScenarioSwitch.js.map +1 -1
- package/codeyam-cli/src/utils/editorScenarios.js +146 -7
- package/codeyam-cli/src/utils/editorScenarios.js.map +1 -1
- package/codeyam-cli/src/utils/editorSeedAdapter.js +69 -16
- package/codeyam-cli/src/utils/editorSeedAdapter.js.map +1 -1
- package/codeyam-cli/src/utils/entityChangeStatus.js +31 -3
- package/codeyam-cli/src/utils/entityChangeStatus.js.map +1 -1
- package/codeyam-cli/src/utils/entityChangeStatus.server.js +31 -0
- package/codeyam-cli/src/utils/entityChangeStatus.server.js.map +1 -1
- package/codeyam-cli/src/utils/envFile.js +90 -0
- package/codeyam-cli/src/utils/envFile.js.map +1 -0
- package/codeyam-cli/src/utils/glossaryAdd.js +74 -0
- package/codeyam-cli/src/utils/glossaryAdd.js.map +1 -0
- package/codeyam-cli/src/utils/handoffContext.js +257 -0
- package/codeyam-cli/src/utils/handoffContext.js.map +1 -0
- package/codeyam-cli/src/utils/install-skills.js +41 -6
- package/codeyam-cli/src/utils/install-skills.js.map +1 -1
- package/codeyam-cli/src/utils/manualEntityAnalysis.js +196 -0
- package/codeyam-cli/src/utils/manualEntityAnalysis.js.map +1 -0
- package/codeyam-cli/src/utils/queue/__tests__/job.interactiveStart.test.js +159 -0
- package/codeyam-cli/src/utils/queue/__tests__/job.interactiveStart.test.js.map +1 -0
- package/codeyam-cli/src/utils/queue/job.js +35 -6
- package/codeyam-cli/src/utils/queue/job.js.map +1 -1
- package/codeyam-cli/src/utils/registerScenarioResult.js +52 -0
- package/codeyam-cli/src/utils/registerScenarioResult.js.map +1 -0
- package/codeyam-cli/src/utils/scenarioCoverage.js +4 -1
- package/codeyam-cli/src/utils/scenarioCoverage.js.map +1 -1
- package/codeyam-cli/src/utils/scenariosManifest.js +66 -2
- package/codeyam-cli/src/utils/scenariosManifest.js.map +1 -1
- package/codeyam-cli/src/utils/screenshotHash.js +26 -0
- package/codeyam-cli/src/utils/screenshotHash.js.map +1 -0
- package/codeyam-cli/src/utils/simulationGateMiddleware.js +9 -0
- package/codeyam-cli/src/utils/simulationGateMiddleware.js.map +1 -1
- package/codeyam-cli/src/utils/techStackConfig.js +38 -0
- package/codeyam-cli/src/utils/techStackConfig.js.map +1 -0
- package/codeyam-cli/src/utils/techStackConfig.test.js +85 -0
- package/codeyam-cli/src/utils/techStackConfig.test.js.map +1 -0
- package/codeyam-cli/src/utils/telemetry.js +106 -0
- package/codeyam-cli/src/utils/telemetry.js.map +1 -0
- package/codeyam-cli/src/utils/telemetryMiddleware.js +22 -0
- package/codeyam-cli/src/utils/telemetryMiddleware.js.map +1 -0
- package/codeyam-cli/src/utils/testResultCache.js +53 -0
- package/codeyam-cli/src/utils/testResultCache.js.map +1 -0
- package/codeyam-cli/src/utils/testResultCache.server.js +81 -0
- package/codeyam-cli/src/utils/testResultCache.server.js.map +1 -0
- package/codeyam-cli/src/utils/testResultCache.server.test.js +187 -0
- package/codeyam-cli/src/utils/testResultCache.server.test.js.map +1 -0
- package/codeyam-cli/src/utils/testResultCache.test.js +230 -0
- package/codeyam-cli/src/utils/testResultCache.test.js.map +1 -0
- package/codeyam-cli/src/utils/testRunner.js +193 -1
- package/codeyam-cli/src/utils/testRunner.js.map +1 -1
- package/codeyam-cli/src/utils/webappDetection.js +4 -2
- package/codeyam-cli/src/utils/webappDetection.js.map +1 -1
- package/codeyam-cli/src/webserver/__tests__/api.interactive-switch-scenario.test.js +99 -0
- package/codeyam-cli/src/webserver/__tests__/api.interactive-switch-scenario.test.js.map +1 -0
- package/codeyam-cli/src/webserver/__tests__/buildPtyEnv.test.js +119 -1
- package/codeyam-cli/src/webserver/__tests__/buildPtyEnv.test.js.map +1 -1
- package/codeyam-cli/src/webserver/__tests__/clientErrors.test.js +68 -1
- package/codeyam-cli/src/webserver/__tests__/clientErrors.test.js.map +1 -1
- package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js +412 -12
- package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js.map +1 -1
- package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js +190 -21
- package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js.map +1 -1
- package/codeyam-cli/src/webserver/__tests__/stripClaudeCommand.test.js +135 -0
- package/codeyam-cli/src/webserver/__tests__/stripClaudeCommand.test.js.map +1 -0
- package/codeyam-cli/src/webserver/app/lib/clientErrors.js +22 -1
- package/codeyam-cli/src/webserver/app/lib/clientErrors.js.map +1 -1
- package/codeyam-cli/src/webserver/app/lib/database.js.map +1 -1
- package/codeyam-cli/src/webserver/app/routes/api.interactive-switch-scenario.js +34 -0
- package/codeyam-cli/src/webserver/app/routes/api.interactive-switch-scenario.js.map +1 -0
- package/codeyam-cli/src/webserver/backgroundServer.js +42 -57
- package/codeyam-cli/src/webserver/backgroundServer.js.map +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{CopyButton-CzTDWkF2.js → CopyButton-DTBZZfSk.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{EntityItem-BFbq6iFk.js → EntityItem-BxclONWq.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{EntityTypeIcon-B6OMi58N.js → EntityTypeIcon-BsnEOJZ_.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{InlineSpinner-DuYodzo1.js → InlineSpinner-ByaELMbv.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{InteractivePreview-CXo9EeCl.js → InteractivePreview-6WjVfhxX.js} +2 -2
- package/codeyam-cli/src/webserver/build/client/assets/{LibraryFunctionPreview-DYCNb2It.js → LibraryFunctionPreview-ChX-Hp7W.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{LogViewer-CZgY3sxX.js → LogViewer-C-9zQdXg.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/MiniClaudeChat-Bs2_Oua4.js +36 -0
- package/codeyam-cli/src/webserver/build/client/assets/{ReportIssueModal-CnYYwRDw.js → ReportIssueModal-DQsceHVv.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{SafeScreenshot-CDoF7ZpU.js → SafeScreenshot-DThcm_9M.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{ScenarioViewer-DrnfvaLL.js → ScenarioViewer-Cl4oOA3A.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/Spinner-CIil5-gb.js +34 -0
- package/codeyam-cli/src/webserver/build/client/assets/{ViewportInspectBar-DRKR9T0U.js → ViewportInspectBar-BqkA9zyZ.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{_index-ClR-g3tY.js → _index-DnOgyseQ.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{activity.(_tab)-DTH6ydEA.js → activity.(_tab)-DqM9hbNE.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{addon-web-links-74hnHF59.js → addon-web-links-C58dYPwR.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{agent-transcripts-B8CYhCO9.js → agent-transcripts-B8NCeOrm.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-database-verify-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-github-verify-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-handoff-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-hosting-verify-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-recapture-stale-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-roadmap-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-save-scenario-data-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-schema-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-verify-routes-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.interactive-switch-scenario-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/{book-open-CLaoh4ac.js → book-open-BFSIqZgO.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{chevron-down-BZ2DZxbW.js → chevron-down-B9fDzFVh.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/chunk-UVKPFVEO-Bmq2apuh.js +43 -0
- package/codeyam-cli/src/webserver/build/client/assets/{circle-check-CT4unAk-.js → circle-check-DLPObLUx.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{copy-zK0B6Nu-.js → copy-DXEmO0TD.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{createLucideIcon-DJB0YQJL.js → createLucideIcon-BwyFiRot.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/cy-logo-cli-Coe5NhbS.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/{cy-logo-cli-CCKUIm0S.svg → cy-logo-cli-DoA97ML3.svg} +2 -2
- package/codeyam-cli/src/webserver/build/client/assets/{dev.empty-CkXFP_i-.js → dev.empty-iRhRIFlp.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/editor._tab-BZPBzV73.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-DhtVC4aI.js +161 -0
- package/codeyam-cli/src/webserver/build/client/assets/editorPreview-C6fEYHrh.js +41 -0
- package/codeyam-cli/src/webserver/build/client/assets/{entity._sha._-BqAN7hyG.js → entity._sha._-pc-vc6wO.js} +13 -12
- package/codeyam-cli/src/webserver/build/client/assets/{entity._sha.scenarios._scenarioId.dev-BOi8kpwd.js → entity._sha.scenarios._scenarioId.dev-C8AyYgYT.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{entity._sha.scenarios._scenarioId.fullscreen-Dg1NhIms.js → entity._sha.scenarios._scenarioId.fullscreen-DziaVQX1.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{entity._sha_.create-scenario-CJX6kkkV.js → entity._sha_.create-scenario-BTcpgIpC.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{entity._sha_.edit._scenarioId-BhVjZhKg.js → entity._sha_.edit._scenarioId-D_O_ajfZ.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{entry.client-_gzKltPN.js → entry.client-j1Vi0bco.js} +6 -6
- package/codeyam-cli/src/webserver/build/client/assets/{files-CV_17tZS.js → files-kuny2Q_s.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{git-D-YXmMbR.js → git-DgCZPMie.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/globals-L-aUIeux.css +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/{index-CCrgCshv.js → index-BliGSSpl.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{index-Blo6EK8G.js → index-SqjQKTdH.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{index-BsX0F-9C.js → index-vyrZD2g4.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{labs-Byazq8Pv.js → labs-c3yLxSEp.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{loader-circle-DVQ0oHR7.js → loader-circle-D-q28GLF.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/manifest-79d0d81a.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/{memory-b-VmA2Vj.js → memory-CEWIUC4t.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{pause-DGcndCAa.js → pause-BP6fitdh.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/root-L2V0jea7.js +80 -0
- package/codeyam-cli/src/webserver/build/client/assets/{search-C0Uw0bcK.js → search-BooqacKS.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{settings-OoNgHIfW.js → settings-BM0nbryO.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{simulations-Bcemfu8a.js → simulations-ovy6FjRY.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{terminal-BgMmG7R9.js → terminal-DHemCJIs.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{triangle-alert-Cs87hJYK.js → triangle-alert-D87ekDl8.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{useCustomSizes-BR3Rs7JY.js → useCustomSizes-Dk0Tciqg.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/useLastLogLine-C8QvIe05.js +2 -0
- package/codeyam-cli/src/webserver/build/client/assets/{useReportContext-BermyNU5.js → useReportContext-jkCytuYz.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{useToast-a_QN_W9_.js → useToast-BgqkixU9.js} +1 -1
- package/codeyam-cli/src/webserver/build/server/assets/analysisRunner-QgInFGdU.js +16 -0
- package/codeyam-cli/src/webserver/build/server/assets/{index-Cr7d_IsG.js → index-zblh9auj.js} +1 -1
- package/codeyam-cli/src/webserver/build/server/assets/init-DaE0CBjk.js +14 -0
- package/codeyam-cli/src/webserver/build/server/assets/server-build-CNvgz1cC.js +853 -0
- package/codeyam-cli/src/webserver/build/server/index.js +1 -1
- package/codeyam-cli/src/webserver/build-info.json +5 -5
- package/codeyam-cli/src/webserver/editorProxy.js +329 -25
- package/codeyam-cli/src/webserver/editorProxy.js.map +1 -1
- package/codeyam-cli/src/webserver/idleDetector.js +65 -8
- package/codeyam-cli/src/webserver/idleDetector.js.map +1 -1
- package/codeyam-cli/src/webserver/scripts/journalCapture.ts +53 -0
- package/codeyam-cli/src/webserver/server.js +151 -14
- package/codeyam-cli/src/webserver/server.js.map +1 -1
- package/codeyam-cli/src/webserver/terminalServer.js +253 -41
- package/codeyam-cli/src/webserver/terminalServer.js.map +1 -1
- package/codeyam-cli/templates/__tests__/editor-step-hook.prompt-capture.test.ts +118 -0
- package/codeyam-cli/templates/codeyam-editor-claude.md +2 -0
- package/codeyam-cli/templates/codeyam-editor-codex.md +61 -0
- package/codeyam-cli/templates/codeyam-editor-gemini.md +59 -0
- package/codeyam-cli/templates/codeyam-editor-reference.md +216 -0
- package/codeyam-cli/templates/design-systems/clean-dashboard-design-system.md +255 -0
- package/codeyam-cli/templates/design-systems/editorial-design-system.md +267 -0
- package/codeyam-cli/templates/design-systems/mono-brutalist-design-system.md +256 -0
- package/codeyam-cli/templates/design-systems/neo-brutalist-design-system.md +294 -0
- package/codeyam-cli/templates/editor-step-hook.py +93 -46
- package/codeyam-cli/templates/expo-react-native/MOBILE_SETUP.md +204 -5
- package/codeyam-cli/templates/expo-react-native/__tests__/.gitkeep +0 -0
- package/codeyam-cli/templates/expo-react-native/app/_layout.tsx +6 -3
- package/codeyam-cli/templates/expo-react-native/app/index.tsx +36 -0
- package/codeyam-cli/templates/expo-react-native/app.json +11 -0
- package/codeyam-cli/templates/expo-react-native/babel.config.js +1 -0
- package/codeyam-cli/templates/expo-react-native/gitignore +2 -0
- package/codeyam-cli/templates/expo-react-native/global.css +7 -0
- package/codeyam-cli/templates/expo-react-native/lib/theme.ts +73 -0
- package/codeyam-cli/templates/expo-react-native/package.json +32 -16
- package/codeyam-cli/templates/expo-react-native/patches/expo-modules-autolinking+3.0.24.patch +29 -0
- package/codeyam-cli/templates/isolation-route/expo-router.tsx.template +54 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/gitignore +1 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/package.json +1 -1
- package/codeyam-cli/templates/nextjs-prisma-sqlite/seed-adapter.ts +47 -34
- package/codeyam-cli/templates/nextjs-prisma-supabase/package.json +1 -1
- package/codeyam-cli/templates/seed-adapters/supabase.ts +271 -78
- package/codeyam-cli/templates/skills/codeyam-editor/SKILL.md +34 -1
- package/package.json +2 -1
- package/packages/ai/src/lib/astScopes/methodSemantics.js +99 -0
- package/packages/ai/src/lib/astScopes/methodSemantics.js.map +1 -1
- package/packages/ai/src/lib/astScopes/nodeToSource.js +16 -0
- package/packages/ai/src/lib/astScopes/nodeToSource.js.map +1 -1
- package/packages/ai/src/lib/astScopes/paths.js +12 -3
- package/packages/ai/src/lib/astScopes/paths.js.map +1 -1
- package/packages/ai/src/lib/dataStructure/ScopeDataStructure.js +27 -10
- package/packages/ai/src/lib/dataStructure/ScopeDataStructure.js.map +1 -1
- package/packages/ai/src/lib/dataStructure/equivalencyManagers/ParentScopeManager.js +9 -2
- package/packages/ai/src/lib/dataStructure/equivalencyManagers/ParentScopeManager.js.map +1 -1
- package/packages/ai/src/lib/dataStructure/helpers/cleanKnownObjectFunctions.js +14 -4
- package/packages/ai/src/lib/dataStructure/helpers/cleanKnownObjectFunctions.js.map +1 -1
- package/packages/analyze/index.js +1 -1
- package/packages/analyze/index.js.map +1 -1
- package/packages/analyze/src/lib/files/analyze/analyzeEntities/prepareDataStructures.js +16 -2
- package/packages/analyze/src/lib/files/analyze/analyzeEntities/prepareDataStructures.js.map +1 -1
- package/packages/analyze/src/lib/files/analyze/analyzeEntities.js +6 -26
- package/packages/analyze/src/lib/files/analyze/analyzeEntities.js.map +1 -1
- package/packages/analyze/src/lib/files/analyze/findOrCreateEntity.js +3 -2
- package/packages/analyze/src/lib/files/analyze/findOrCreateEntity.js.map +1 -1
- package/packages/analyze/src/lib/files/analyze/gatherEntityMap.js +9 -7
- package/packages/analyze/src/lib/files/analyze/gatherEntityMap.js.map +1 -1
- package/packages/analyze/src/lib/files/analyze/trackEntityCircularDependencies.js +14 -0
- package/packages/analyze/src/lib/files/analyze/trackEntityCircularDependencies.js.map +1 -1
- package/packages/analyze/src/lib/files/analyze/validateDependencyAnalyses.js +44 -11
- package/packages/analyze/src/lib/files/analyze/validateDependencyAnalyses.js.map +1 -1
- package/packages/analyze/src/lib/files/analyzeChange.js +1 -0
- package/packages/analyze/src/lib/files/analyzeChange.js.map +1 -1
- package/packages/analyze/src/lib/files/analyzeInitial.js +1 -0
- package/packages/analyze/src/lib/files/analyzeInitial.js.map +1 -1
- package/packages/analyze/src/lib/files/analyzeNextRoute.js +5 -1
- package/packages/analyze/src/lib/files/analyzeNextRoute.js.map +1 -1
- package/packages/analyze/src/lib/files/scenarios/generateDataStructure.js +120 -28
- package/packages/analyze/src/lib/files/scenarios/generateDataStructure.js.map +1 -1
- package/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.js +1368 -1193
- package/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.js.map +1 -1
- package/packages/database/src/lib/loadAnalysis.js +7 -1
- package/packages/database/src/lib/loadAnalysis.js.map +1 -1
- package/packages/database/src/lib/loadEntity.js +5 -5
- package/packages/database/src/lib/loadEntity.js.map +1 -1
- package/packages/utils/src/lib/fs/rsyncCopy.js +22 -1
- package/packages/utils/src/lib/fs/rsyncCopy.js.map +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/Spinner-Df3UCi8k.js +0 -34
- package/codeyam-cli/src/webserver/build/client/assets/chunk-JZWAC4HX-BBXArFPl.js +0 -43
- package/codeyam-cli/src/webserver/build/client/assets/cy-logo-cli-DcX-ZS3p.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/editor._tab-DPw7NZHc.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-DmBK1JBK.js +0 -58
- package/codeyam-cli/src/webserver/build/client/assets/editorPreview-DBa7T2FK.js +0 -41
- package/codeyam-cli/src/webserver/build/client/assets/globals-CGrDAxj0.css +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/manifest-b3f77062.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/root-D5Zi3U2Z.js +0 -67
- package/codeyam-cli/src/webserver/build/client/assets/useLastLogLine-BxxP_XF9.js +0 -2
- package/codeyam-cli/src/webserver/build/server/assets/analysisRunner-yTyb36j3.js +0 -13
- package/codeyam-cli/src/webserver/build/server/assets/init-M_wqNAfu.js +0 -10
- package/codeyam-cli/src/webserver/build/server/assets/server-build-_ybRgrlc.js +0 -551
- package/codeyam-cli/templates/expo-react-native/app/(tabs)/_layout.tsx +0 -33
- package/codeyam-cli/templates/expo-react-native/app/(tabs)/index.tsx +0 -12
- package/codeyam-cli/templates/expo-react-native/app/(tabs)/settings.tsx +0 -12
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
2
|
import { Kysely, SqliteDialect } from 'kysely';
|
|
3
|
-
import { isComponent, classifyGlossaryEntries, computeAudit, filterGlossaryByChangeStatus, resolveAuditSessionScope, queryScenarioCounts, queryIncompleteEntities, queryMiscategorizedScenarios, isOnlyIncompleteEntities, isAutoRemediable, } from "../editorAudit.js";
|
|
3
|
+
import { isComponent, classifyGlossaryEntries, computeAudit, filterGlossaryByChangeStatus, resolveAuditSessionScope, queryScenarioCounts, queryPageScenarioCounts, queryIncompleteEntities, queryMiscategorizedScenarios, queryUnassociatedScenarios, isOnlyIncompleteEntities, isOnlyPreExistingIncomplete, isAutoRemediable, identifyScenariosNeedingRecapture, detectDuplicateNames, aggregateClientErrorsByComponent, determineTargetedAnalysisPaths, shouldAutoRecapture, } from "../editorAudit.js";
|
|
4
4
|
describe('editorAudit', () => {
|
|
5
5
|
describe('isComponent', () => {
|
|
6
6
|
it('should return true for JSX.Element return type', () => {
|
|
@@ -29,6 +29,16 @@ describe('editorAudit', () => {
|
|
|
29
29
|
it('should return false when returnType is empty string', () => {
|
|
30
30
|
expect(isComponent({ returnType: '' })).toBe(false);
|
|
31
31
|
});
|
|
32
|
+
it('should return true for page files (index.tsx) even without returnType', () => {
|
|
33
|
+
expect(isComponent({ filePath: 'app/(tabs)/index.tsx' })).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
it('should return true for page.tsx files even without returnType', () => {
|
|
36
|
+
expect(isComponent({ filePath: 'app/dashboard/page.tsx' })).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
it('should return false for non-page files without returnType', () => {
|
|
39
|
+
expect(isComponent({ filePath: 'app/hooks/useCounter.ts' })).toBe(false);
|
|
40
|
+
expect(isComponent({ filePath: 'lib/storage.ts' })).toBe(false);
|
|
41
|
+
});
|
|
32
42
|
});
|
|
33
43
|
describe('classifyGlossaryEntries', () => {
|
|
34
44
|
it('should classify entries with JSX return types as components', () => {
|
|
@@ -290,6 +300,29 @@ describe('editorAudit', () => {
|
|
|
290
300
|
expect(result.functions[0].testsPassing).toBe(false);
|
|
291
301
|
expect(result.functions[0].testsVisibleInUi).toBe(true);
|
|
292
302
|
});
|
|
303
|
+
it('should distinguish runner errors from test failures and include error message', () => {
|
|
304
|
+
const result = computeAudit({
|
|
305
|
+
components: [],
|
|
306
|
+
functions: [
|
|
307
|
+
{
|
|
308
|
+
name: 'getTimeAgo',
|
|
309
|
+
filePath: 'src/lib/format.ts',
|
|
310
|
+
testFile: 'src/lib/format.test.ts',
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
scenarioCounts: {},
|
|
314
|
+
testFileExistence: { 'src/lib/format.test.ts': true },
|
|
315
|
+
testResults: {
|
|
316
|
+
'src/lib/format.test.ts': {
|
|
317
|
+
passing: false,
|
|
318
|
+
hasEntityNameDescribe: false,
|
|
319
|
+
errorMessage: 'Error: Cannot find module "@/lib/format" from "src/lib/format.test.ts"',
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
expect(result.functions[0].status).toBe('runner_error');
|
|
324
|
+
expect(result.functions[0].errorMessage).toBe('Error: Cannot find module "@/lib/format" from "src/lib/format.test.ts"');
|
|
325
|
+
});
|
|
293
326
|
it('should mark function as name_mismatch when tests pass but no describe matches', () => {
|
|
294
327
|
const result = computeAudit({
|
|
295
328
|
components: [],
|
|
@@ -530,6 +563,142 @@ describe('editorAudit', () => {
|
|
|
530
563
|
expect(result.summary.componentsOk).toBe(1);
|
|
531
564
|
expect(result.summary.componentsWithErrors).toBe(1);
|
|
532
565
|
});
|
|
566
|
+
// ── Test case count integration ─────────────────────────────────
|
|
567
|
+
it('should include testCaseCount when testCaseCounts is provided', () => {
|
|
568
|
+
const result = computeAudit({
|
|
569
|
+
components: [],
|
|
570
|
+
functions: [
|
|
571
|
+
{
|
|
572
|
+
name: 'calculatePrice',
|
|
573
|
+
filePath: 'app/lib/pricing.ts',
|
|
574
|
+
testFile: 'app/lib/pricing.test.ts',
|
|
575
|
+
},
|
|
576
|
+
],
|
|
577
|
+
scenarioCounts: {},
|
|
578
|
+
testFileExistence: { 'app/lib/pricing.test.ts': true },
|
|
579
|
+
testResults: {
|
|
580
|
+
'app/lib/pricing.test.ts': {
|
|
581
|
+
passing: true,
|
|
582
|
+
hasEntityNameDescribe: true,
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
testCaseCounts: { calculatePrice: 5 },
|
|
586
|
+
});
|
|
587
|
+
expect(result.functions[0].testCaseCount).toBe(5);
|
|
588
|
+
});
|
|
589
|
+
it('should not include testCaseCount when testCaseCounts is not provided', () => {
|
|
590
|
+
const result = computeAudit({
|
|
591
|
+
components: [],
|
|
592
|
+
functions: [
|
|
593
|
+
{
|
|
594
|
+
name: 'calculatePrice',
|
|
595
|
+
filePath: 'app/lib/pricing.ts',
|
|
596
|
+
testFile: 'app/lib/pricing.test.ts',
|
|
597
|
+
},
|
|
598
|
+
],
|
|
599
|
+
scenarioCounts: {},
|
|
600
|
+
testFileExistence: { 'app/lib/pricing.test.ts': true },
|
|
601
|
+
testResults: {
|
|
602
|
+
'app/lib/pricing.test.ts': {
|
|
603
|
+
passing: true,
|
|
604
|
+
hasEntityNameDescribe: true,
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
});
|
|
608
|
+
expect(result.functions[0].testCaseCount).toBeUndefined();
|
|
609
|
+
});
|
|
610
|
+
it('should count functionsThinCoverage for ok functions with fewer than 3 test cases', () => {
|
|
611
|
+
const result = computeAudit({
|
|
612
|
+
components: [],
|
|
613
|
+
functions: [
|
|
614
|
+
{
|
|
615
|
+
name: 'fnWellTested',
|
|
616
|
+
filePath: 'a.ts',
|
|
617
|
+
testFile: 'a.test.ts',
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
name: 'fnThinCoverage',
|
|
621
|
+
filePath: 'b.ts',
|
|
622
|
+
testFile: 'b.test.ts',
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
name: 'fnAlsoThin',
|
|
626
|
+
filePath: 'c.ts',
|
|
627
|
+
testFile: 'c.test.ts',
|
|
628
|
+
},
|
|
629
|
+
],
|
|
630
|
+
scenarioCounts: {},
|
|
631
|
+
testFileExistence: {
|
|
632
|
+
'a.test.ts': true,
|
|
633
|
+
'b.test.ts': true,
|
|
634
|
+
'c.test.ts': true,
|
|
635
|
+
},
|
|
636
|
+
testResults: {
|
|
637
|
+
'a.test.ts': { passing: true, hasEntityNameDescribe: true },
|
|
638
|
+
'b.test.ts': { passing: true, hasEntityNameDescribe: true },
|
|
639
|
+
'c.test.ts': { passing: true, hasEntityNameDescribe: true },
|
|
640
|
+
},
|
|
641
|
+
testCaseCounts: { fnWellTested: 5, fnThinCoverage: 2, fnAlsoThin: 1 },
|
|
642
|
+
});
|
|
643
|
+
expect(result.summary.functionsThinCoverage).toBe(2);
|
|
644
|
+
});
|
|
645
|
+
it('should not count failing functions in functionsThinCoverage', () => {
|
|
646
|
+
const result = computeAudit({
|
|
647
|
+
components: [],
|
|
648
|
+
functions: [
|
|
649
|
+
{
|
|
650
|
+
name: 'fnFailing',
|
|
651
|
+
filePath: 'a.ts',
|
|
652
|
+
testFile: 'a.test.ts',
|
|
653
|
+
},
|
|
654
|
+
],
|
|
655
|
+
scenarioCounts: {},
|
|
656
|
+
testFileExistence: { 'a.test.ts': true },
|
|
657
|
+
testResults: {
|
|
658
|
+
'a.test.ts': { passing: false, hasEntityNameDescribe: true },
|
|
659
|
+
},
|
|
660
|
+
testCaseCounts: { fnFailing: 1 },
|
|
661
|
+
});
|
|
662
|
+
expect(result.summary.functionsThinCoverage).toBe(0);
|
|
663
|
+
});
|
|
664
|
+
it('should not block allPassing due to thin test coverage', () => {
|
|
665
|
+
const result = computeAudit({
|
|
666
|
+
components: [],
|
|
667
|
+
functions: [
|
|
668
|
+
{
|
|
669
|
+
name: 'fnThin',
|
|
670
|
+
filePath: 'a.ts',
|
|
671
|
+
testFile: 'a.test.ts',
|
|
672
|
+
},
|
|
673
|
+
],
|
|
674
|
+
scenarioCounts: {},
|
|
675
|
+
testFileExistence: { 'a.test.ts': true },
|
|
676
|
+
testResults: {
|
|
677
|
+
'a.test.ts': { passing: true, hasEntityNameDescribe: true },
|
|
678
|
+
},
|
|
679
|
+
testCaseCounts: { fnThin: 1 },
|
|
680
|
+
});
|
|
681
|
+
expect(result.summary.allPassing).toBe(true);
|
|
682
|
+
expect(result.summary.functionsThinCoverage).toBe(1);
|
|
683
|
+
});
|
|
684
|
+
it('should default functionsThinCoverage to 0 when no testCaseCounts provided', () => {
|
|
685
|
+
const result = computeAudit({
|
|
686
|
+
components: [],
|
|
687
|
+
functions: [
|
|
688
|
+
{
|
|
689
|
+
name: 'fn',
|
|
690
|
+
filePath: 'a.ts',
|
|
691
|
+
testFile: 'a.test.ts',
|
|
692
|
+
},
|
|
693
|
+
],
|
|
694
|
+
scenarioCounts: {},
|
|
695
|
+
testFileExistence: { 'a.test.ts': true },
|
|
696
|
+
testResults: {
|
|
697
|
+
'a.test.ts': { passing: true, hasEntityNameDescribe: true },
|
|
698
|
+
},
|
|
699
|
+
});
|
|
700
|
+
expect(result.summary.functionsThinCoverage).toBe(0);
|
|
701
|
+
});
|
|
533
702
|
});
|
|
534
703
|
// ── filterGlossaryByChangeStatus ──────────────────────────────────
|
|
535
704
|
describe('filterGlossaryByChangeStatus', () => {
|
|
@@ -983,6 +1152,158 @@ describe('editorAudit', () => {
|
|
|
983
1152
|
expect(counts).toEqual({ ArticleCard: 1, ArticleRow: 1 });
|
|
984
1153
|
});
|
|
985
1154
|
});
|
|
1155
|
+
// ── queryPageScenarioCounts ──────────────────────────────────────────
|
|
1156
|
+
describe('queryPageScenarioCounts', () => {
|
|
1157
|
+
let db;
|
|
1158
|
+
let rawDb;
|
|
1159
|
+
const projectId = 'test-project-id';
|
|
1160
|
+
beforeEach(async () => {
|
|
1161
|
+
rawDb = new Database(':memory:');
|
|
1162
|
+
db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
|
|
1163
|
+
await db.schema
|
|
1164
|
+
.createTable('editor_scenarios')
|
|
1165
|
+
.addColumn('id', 'varchar', (col) => col.primaryKey())
|
|
1166
|
+
.addColumn('project_id', 'varchar', (col) => col.notNull())
|
|
1167
|
+
.addColumn('name', 'varchar', (col) => col.notNull())
|
|
1168
|
+
.addColumn('description', 'text')
|
|
1169
|
+
.addColumn('component_name', 'varchar')
|
|
1170
|
+
.addColumn('component_path', 'varchar')
|
|
1171
|
+
.addColumn('page_file_path', 'varchar')
|
|
1172
|
+
.addColumn('url', 'varchar')
|
|
1173
|
+
.addColumn('type', 'varchar')
|
|
1174
|
+
.addColumn('screenshot_path', 'varchar')
|
|
1175
|
+
.addColumn('viewport_width', 'integer')
|
|
1176
|
+
.addColumn('viewport_height', 'integer')
|
|
1177
|
+
.addColumn('dimension', 'varchar')
|
|
1178
|
+
.addColumn('created_at', 'datetime')
|
|
1179
|
+
.addColumn('updated_at', 'datetime')
|
|
1180
|
+
.execute();
|
|
1181
|
+
});
|
|
1182
|
+
afterEach(async () => {
|
|
1183
|
+
await db.destroy();
|
|
1184
|
+
});
|
|
1185
|
+
it('should count app-level scenarios by page_file_path', async () => {
|
|
1186
|
+
// App-level scenario: has page_file_path but no component_name
|
|
1187
|
+
await db
|
|
1188
|
+
.insertInto('editor_scenarios')
|
|
1189
|
+
.values({
|
|
1190
|
+
id: 'sc-1',
|
|
1191
|
+
project_id: projectId,
|
|
1192
|
+
name: 'Library - Default',
|
|
1193
|
+
component_name: null,
|
|
1194
|
+
page_file_path: 'app/library/page.tsx',
|
|
1195
|
+
created_at: '2026-03-12 13:00:00',
|
|
1196
|
+
})
|
|
1197
|
+
.execute();
|
|
1198
|
+
await db
|
|
1199
|
+
.insertInto('editor_scenarios')
|
|
1200
|
+
.values({
|
|
1201
|
+
id: 'sc-2',
|
|
1202
|
+
project_id: projectId,
|
|
1203
|
+
name: 'Library - Empty',
|
|
1204
|
+
component_name: null,
|
|
1205
|
+
page_file_path: 'app/library/page.tsx',
|
|
1206
|
+
created_at: '2026-03-12 13:05:00',
|
|
1207
|
+
})
|
|
1208
|
+
.execute();
|
|
1209
|
+
const counts = await queryPageScenarioCounts(db, projectId, null);
|
|
1210
|
+
expect(counts).toEqual({ 'app/library/page.tsx': 2 });
|
|
1211
|
+
});
|
|
1212
|
+
it('should not count component scenarios (those have component_name)', async () => {
|
|
1213
|
+
// Component scenario — should NOT appear in page counts
|
|
1214
|
+
await db
|
|
1215
|
+
.insertInto('editor_scenarios')
|
|
1216
|
+
.values({
|
|
1217
|
+
id: 'sc-1',
|
|
1218
|
+
project_id: projectId,
|
|
1219
|
+
name: 'ArticleRow - Default',
|
|
1220
|
+
component_name: 'ArticleRow',
|
|
1221
|
+
page_file_path: null,
|
|
1222
|
+
created_at: '2026-03-12 13:00:00',
|
|
1223
|
+
})
|
|
1224
|
+
.execute();
|
|
1225
|
+
const counts = await queryPageScenarioCounts(db, projectId, null);
|
|
1226
|
+
expect(counts).toEqual({});
|
|
1227
|
+
});
|
|
1228
|
+
it('should respect featureStartedAt filter', async () => {
|
|
1229
|
+
await db
|
|
1230
|
+
.insertInto('editor_scenarios')
|
|
1231
|
+
.values({
|
|
1232
|
+
id: 'sc-1',
|
|
1233
|
+
project_id: projectId,
|
|
1234
|
+
name: 'Library - Old',
|
|
1235
|
+
component_name: null,
|
|
1236
|
+
page_file_path: 'app/library/page.tsx',
|
|
1237
|
+
created_at: '2026-03-12 13:00:00',
|
|
1238
|
+
})
|
|
1239
|
+
.execute();
|
|
1240
|
+
await db
|
|
1241
|
+
.insertInto('editor_scenarios')
|
|
1242
|
+
.values({
|
|
1243
|
+
id: 'sc-2',
|
|
1244
|
+
project_id: projectId,
|
|
1245
|
+
name: 'Library - New',
|
|
1246
|
+
component_name: null,
|
|
1247
|
+
page_file_path: 'app/library/page.tsx',
|
|
1248
|
+
created_at: '2026-03-12 15:00:00',
|
|
1249
|
+
})
|
|
1250
|
+
.execute();
|
|
1251
|
+
const counts = await queryPageScenarioCounts(db, projectId, '2026-03-12T14:01:31.291Z');
|
|
1252
|
+
expect(counts).toEqual({ 'app/library/page.tsx': 1 });
|
|
1253
|
+
});
|
|
1254
|
+
it('should count re-registered page scenarios via updated_at', async () => {
|
|
1255
|
+
await db
|
|
1256
|
+
.insertInto('editor_scenarios')
|
|
1257
|
+
.values({
|
|
1258
|
+
id: 'sc-1',
|
|
1259
|
+
project_id: projectId,
|
|
1260
|
+
name: 'Library - Default',
|
|
1261
|
+
component_name: null,
|
|
1262
|
+
page_file_path: 'app/library/page.tsx',
|
|
1263
|
+
created_at: '2026-03-12 13:00:00',
|
|
1264
|
+
updated_at: '2026-03-12 15:00:00',
|
|
1265
|
+
})
|
|
1266
|
+
.execute();
|
|
1267
|
+
const counts = await queryPageScenarioCounts(db, projectId, '2026-03-12T14:01:31.291Z');
|
|
1268
|
+
expect(counts).toEqual({ 'app/library/page.tsx': 1 });
|
|
1269
|
+
});
|
|
1270
|
+
it('should group counts by page_file_path across multiple pages', async () => {
|
|
1271
|
+
await db
|
|
1272
|
+
.insertInto('editor_scenarios')
|
|
1273
|
+
.values([
|
|
1274
|
+
{
|
|
1275
|
+
id: 'sc-1',
|
|
1276
|
+
project_id: projectId,
|
|
1277
|
+
name: 'Library - Default',
|
|
1278
|
+
component_name: null,
|
|
1279
|
+
page_file_path: 'app/library/page.tsx',
|
|
1280
|
+
created_at: '2026-03-12 13:00:00',
|
|
1281
|
+
},
|
|
1282
|
+
{
|
|
1283
|
+
id: 'sc-2',
|
|
1284
|
+
project_id: projectId,
|
|
1285
|
+
name: 'Library - Rich',
|
|
1286
|
+
component_name: null,
|
|
1287
|
+
page_file_path: 'app/library/page.tsx',
|
|
1288
|
+
created_at: '2026-03-12 13:00:00',
|
|
1289
|
+
},
|
|
1290
|
+
{
|
|
1291
|
+
id: 'sc-3',
|
|
1292
|
+
project_id: projectId,
|
|
1293
|
+
name: 'Collections - Default',
|
|
1294
|
+
component_name: null,
|
|
1295
|
+
page_file_path: 'app/library/collections/page.tsx',
|
|
1296
|
+
created_at: '2026-03-12 13:00:00',
|
|
1297
|
+
},
|
|
1298
|
+
])
|
|
1299
|
+
.execute();
|
|
1300
|
+
const counts = await queryPageScenarioCounts(db, projectId, null);
|
|
1301
|
+
expect(counts).toEqual({
|
|
1302
|
+
'app/library/page.tsx': 2,
|
|
1303
|
+
'app/library/collections/page.tsx': 1,
|
|
1304
|
+
});
|
|
1305
|
+
});
|
|
1306
|
+
});
|
|
986
1307
|
// ── Audit + entity completeness integration ─────────────────────────
|
|
987
1308
|
describe('audit should catch incomplete entities (bug reproduction)', () => {
|
|
988
1309
|
let db;
|
|
@@ -1291,8 +1612,11 @@ describe('editorAudit', () => {
|
|
|
1291
1612
|
expect(auditResult.summary.incompleteEntities).toBeUndefined();
|
|
1292
1613
|
});
|
|
1293
1614
|
});
|
|
1294
|
-
// ──
|
|
1295
|
-
describe('
|
|
1615
|
+
// ── filterToIncompleteFilePaths ──────────────────────────────────────
|
|
1616
|
+
describe('filterToIncompleteFilePaths', () => {
|
|
1617
|
+
// analyze-imports processes ALL file paths (~120 files) even when only
|
|
1618
|
+
// a few need analysis. This function filters to files that have no
|
|
1619
|
+
// entity with an analysis record.
|
|
1296
1620
|
let db;
|
|
1297
1621
|
let rawDb;
|
|
1298
1622
|
const projectId = 'test-project-id';
|
|
@@ -1300,39 +1624,309 @@ describe('editorAudit', () => {
|
|
|
1300
1624
|
rawDb = new Database(':memory:');
|
|
1301
1625
|
db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
|
|
1302
1626
|
await db.schema
|
|
1303
|
-
.createTable('
|
|
1627
|
+
.createTable('analyses')
|
|
1304
1628
|
.addColumn('id', 'varchar', (col) => col.primaryKey())
|
|
1305
|
-
.addColumn('project_id', 'varchar', (col) => col.notNull())
|
|
1306
|
-
.addColumn('name', 'varchar', (col) => col.notNull())
|
|
1307
|
-
.addColumn('component_name', 'varchar')
|
|
1308
|
-
.addColumn('component_path', 'varchar')
|
|
1309
1629
|
.addColumn('entity_sha', 'varchar')
|
|
1310
|
-
.addColumn('
|
|
1311
|
-
.addColumn('
|
|
1312
|
-
.
|
|
1313
|
-
|
|
1314
|
-
.
|
|
1630
|
+
.addColumn('entity_name', 'varchar')
|
|
1631
|
+
.addColumn('project_id', 'varchar')
|
|
1632
|
+
.execute();
|
|
1633
|
+
await db.schema
|
|
1634
|
+
.createTable('entities')
|
|
1635
|
+
.addColumn('sha', 'varchar', (col) => col.primaryKey())
|
|
1636
|
+
.addColumn('name', 'varchar')
|
|
1637
|
+
.addColumn('entity_type', 'varchar')
|
|
1638
|
+
.addColumn('file_path', 'varchar')
|
|
1315
1639
|
.execute();
|
|
1316
1640
|
});
|
|
1317
1641
|
afterEach(async () => {
|
|
1318
1642
|
await db.destroy();
|
|
1319
1643
|
});
|
|
1320
|
-
it('should
|
|
1644
|
+
it('should exclude files that have an entity with an analysis', async () => {
|
|
1645
|
+
const { filterToIncompleteFilePaths } = require('../editorAudit');
|
|
1646
|
+
// Entity with analysis — skip
|
|
1321
1647
|
await db
|
|
1322
|
-
.insertInto('
|
|
1648
|
+
.insertInto('entities')
|
|
1323
1649
|
.values({
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
url: '/isolated-components/LibraryCard?s=Default',
|
|
1329
|
-
created_at: '2026-03-17 12:00:00',
|
|
1650
|
+
sha: 'sha-header',
|
|
1651
|
+
name: 'Header',
|
|
1652
|
+
entity_type: 'visual',
|
|
1653
|
+
file_path: 'app/components/Header.tsx',
|
|
1330
1654
|
})
|
|
1331
1655
|
.execute();
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1656
|
+
await db
|
|
1657
|
+
.insertInto('analyses')
|
|
1658
|
+
.values({
|
|
1659
|
+
id: 'a-1',
|
|
1660
|
+
entity_sha: 'sha-header',
|
|
1661
|
+
entity_name: 'Header',
|
|
1662
|
+
project_id: projectId,
|
|
1663
|
+
})
|
|
1664
|
+
.execute();
|
|
1665
|
+
// Entity without analysis — needs analysis
|
|
1666
|
+
await db
|
|
1667
|
+
.insertInto('entities')
|
|
1668
|
+
.values({
|
|
1669
|
+
sha: 'sha-rule',
|
|
1670
|
+
name: 'RuleBuilder',
|
|
1671
|
+
entity_type: 'visual',
|
|
1672
|
+
file_path: 'app/components/RuleBuilder.tsx',
|
|
1673
|
+
})
|
|
1674
|
+
.execute();
|
|
1675
|
+
const allFilePaths = [
|
|
1676
|
+
'app/components/Header.tsx',
|
|
1677
|
+
'app/components/RuleBuilder.tsx',
|
|
1678
|
+
'app/components/Footer.tsx', // no entity — needs analysis
|
|
1679
|
+
];
|
|
1680
|
+
const result = await filterToIncompleteFilePaths(db, projectId, allFilePaths);
|
|
1681
|
+
expect(result).toContain('app/components/RuleBuilder.tsx');
|
|
1682
|
+
expect(result).toContain('app/components/Footer.tsx');
|
|
1683
|
+
expect(result).not.toContain('app/components/Header.tsx');
|
|
1684
|
+
});
|
|
1685
|
+
it('should return all file paths when no entities exist yet', async () => {
|
|
1686
|
+
const { filterToIncompleteFilePaths } = require('../editorAudit');
|
|
1687
|
+
const filePaths = ['app/Foo.tsx', 'app/Bar.tsx'];
|
|
1688
|
+
const result = await filterToIncompleteFilePaths(db, projectId, filePaths);
|
|
1689
|
+
expect(result).toEqual(filePaths);
|
|
1690
|
+
});
|
|
1691
|
+
it('should return empty when all files have analyzed entities', async () => {
|
|
1692
|
+
const { filterToIncompleteFilePaths } = require('../editorAudit');
|
|
1693
|
+
await db
|
|
1694
|
+
.insertInto('entities')
|
|
1695
|
+
.values({
|
|
1696
|
+
sha: 'sha-a',
|
|
1697
|
+
name: 'CompA',
|
|
1698
|
+
entity_type: 'visual',
|
|
1699
|
+
file_path: 'app/CompA.tsx',
|
|
1700
|
+
})
|
|
1701
|
+
.execute();
|
|
1702
|
+
await db
|
|
1703
|
+
.insertInto('analyses')
|
|
1704
|
+
.values({
|
|
1705
|
+
id: 'a-1',
|
|
1706
|
+
entity_sha: 'sha-a',
|
|
1707
|
+
entity_name: 'CompA',
|
|
1708
|
+
project_id: projectId,
|
|
1709
|
+
})
|
|
1710
|
+
.execute();
|
|
1711
|
+
const result = await filterToIncompleteFilePaths(db, projectId, [
|
|
1712
|
+
'app/CompA.tsx',
|
|
1713
|
+
]);
|
|
1714
|
+
expect(result).toEqual([]);
|
|
1715
|
+
});
|
|
1716
|
+
it('should skip files with analysis even without scenarios', async () => {
|
|
1717
|
+
const { filterToIncompleteFilePaths } = require('../editorAudit');
|
|
1718
|
+
// Entity with analysis but NO scenarios — still complete
|
|
1719
|
+
await db
|
|
1720
|
+
.insertInto('entities')
|
|
1721
|
+
.values({
|
|
1722
|
+
sha: 'sha-util',
|
|
1723
|
+
name: 'utils',
|
|
1724
|
+
entity_type: 'library',
|
|
1725
|
+
file_path: 'app/lib/utils.ts',
|
|
1726
|
+
})
|
|
1727
|
+
.execute();
|
|
1728
|
+
await db
|
|
1729
|
+
.insertInto('analyses')
|
|
1730
|
+
.values({
|
|
1731
|
+
id: 'a-1',
|
|
1732
|
+
entity_sha: 'sha-util',
|
|
1733
|
+
entity_name: 'utils',
|
|
1734
|
+
project_id: projectId,
|
|
1735
|
+
})
|
|
1736
|
+
.execute();
|
|
1737
|
+
const result = await filterToIncompleteFilePaths(db, projectId, [
|
|
1738
|
+
'app/lib/utils.ts',
|
|
1739
|
+
]);
|
|
1740
|
+
expect(result).toEqual([]);
|
|
1741
|
+
});
|
|
1742
|
+
});
|
|
1743
|
+
// ── phantom entity SHAs ─────────────────────────────────────────────
|
|
1744
|
+
describe('queryIncompleteEntities with phantom entity SHAs', () => {
|
|
1745
|
+
// Root cause of the Margo/reader step-blocking bug:
|
|
1746
|
+
// Scenarios registered without component_path got a phantom entity_sha
|
|
1747
|
+
// computed from component_name alone. These SHAs have NO entity record
|
|
1748
|
+
// in the entities table and NO analyses. syncScenarioEntityShas can't
|
|
1749
|
+
// fix them (it skips scenarios without component_path). So they remain
|
|
1750
|
+
// "incomplete" forever, blocking step progression.
|
|
1751
|
+
//
|
|
1752
|
+
// Fix: queryIncompleteEntities should not report scenarios whose
|
|
1753
|
+
// entity_sha has no entity record — these are orphaned data, not
|
|
1754
|
+
// fixable by running analyze-imports.
|
|
1755
|
+
let db;
|
|
1756
|
+
let rawDb;
|
|
1757
|
+
const projectId = 'test-project-id';
|
|
1758
|
+
beforeEach(async () => {
|
|
1759
|
+
rawDb = new Database(':memory:');
|
|
1760
|
+
db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
|
|
1761
|
+
await db.schema
|
|
1762
|
+
.createTable('editor_scenarios')
|
|
1763
|
+
.addColumn('id', 'varchar', (col) => col.primaryKey())
|
|
1764
|
+
.addColumn('project_id', 'varchar', (col) => col.notNull())
|
|
1765
|
+
.addColumn('name', 'varchar', (col) => col.notNull())
|
|
1766
|
+
.addColumn('component_name', 'varchar')
|
|
1767
|
+
.addColumn('component_path', 'varchar')
|
|
1768
|
+
.addColumn('entity_sha', 'varchar')
|
|
1769
|
+
.addColumn('display_name', 'varchar')
|
|
1770
|
+
.addColumn('page_file_path', 'varchar')
|
|
1771
|
+
.addColumn('url', 'varchar')
|
|
1772
|
+
.addColumn('created_at', 'datetime')
|
|
1773
|
+
.addColumn('updated_at', 'datetime')
|
|
1774
|
+
.execute();
|
|
1775
|
+
await db.schema
|
|
1776
|
+
.createTable('analyses')
|
|
1777
|
+
.addColumn('id', 'varchar', (col) => col.primaryKey())
|
|
1778
|
+
.addColumn('entity_sha', 'varchar')
|
|
1779
|
+
.addColumn('entity_name', 'varchar')
|
|
1780
|
+
.addColumn('project_id', 'varchar')
|
|
1781
|
+
.execute();
|
|
1782
|
+
await db.schema
|
|
1783
|
+
.createTable('entities')
|
|
1784
|
+
.addColumn('sha', 'varchar', (col) => col.primaryKey())
|
|
1785
|
+
.addColumn('name', 'varchar')
|
|
1786
|
+
.addColumn('entity_type', 'varchar')
|
|
1787
|
+
.addColumn('file_path', 'varchar')
|
|
1788
|
+
.execute();
|
|
1789
|
+
});
|
|
1790
|
+
afterEach(async () => {
|
|
1791
|
+
await db.destroy();
|
|
1792
|
+
});
|
|
1793
|
+
it('should not report scenarios with phantom entity SHAs (no entity record exists)', async () => {
|
|
1794
|
+
// Real entity — has an entity record, analyses, and scenarios. Complete.
|
|
1795
|
+
await db
|
|
1796
|
+
.insertInto('entities')
|
|
1797
|
+
.values({
|
|
1798
|
+
sha: 'sha-real',
|
|
1799
|
+
name: 'RuleBuilder',
|
|
1800
|
+
entity_type: 'visual',
|
|
1801
|
+
file_path: 'app/components/RuleBuilder.tsx',
|
|
1802
|
+
})
|
|
1803
|
+
.execute();
|
|
1804
|
+
await db
|
|
1805
|
+
.insertInto('analyses')
|
|
1806
|
+
.values({
|
|
1807
|
+
id: 'a-1',
|
|
1808
|
+
entity_sha: 'sha-real',
|
|
1809
|
+
entity_name: 'RuleBuilder',
|
|
1810
|
+
project_id: projectId,
|
|
1811
|
+
})
|
|
1812
|
+
.execute();
|
|
1813
|
+
await db
|
|
1814
|
+
.insertInto('editor_scenarios')
|
|
1815
|
+
.values({
|
|
1816
|
+
id: 'sc-good',
|
|
1817
|
+
project_id: projectId,
|
|
1818
|
+
name: 'RuleBuilder - Empty',
|
|
1819
|
+
component_name: 'RuleBuilder',
|
|
1820
|
+
component_path: 'app/components/RuleBuilder.tsx',
|
|
1821
|
+
entity_sha: 'sha-real',
|
|
1822
|
+
created_at: '2026-03-16 23:00:00',
|
|
1823
|
+
})
|
|
1824
|
+
.execute();
|
|
1825
|
+
// Phantom entity — scenario points to a SHA that doesn't exist
|
|
1826
|
+
// in the entities table (registered without component_path).
|
|
1827
|
+
// No entity record, no analyses, unfixable by analyze-imports.
|
|
1828
|
+
await db
|
|
1829
|
+
.insertInto('editor_scenarios')
|
|
1830
|
+
.values({
|
|
1831
|
+
id: 'sc-phantom',
|
|
1832
|
+
project_id: projectId,
|
|
1833
|
+
name: 'Empty',
|
|
1834
|
+
component_name: 'RuleBuilder',
|
|
1835
|
+
component_path: null,
|
|
1836
|
+
entity_sha: 'sha-phantom-no-entity-record',
|
|
1837
|
+
created_at: '2026-03-16 22:00:00',
|
|
1838
|
+
})
|
|
1839
|
+
.execute();
|
|
1840
|
+
const incomplete = await queryIncompleteEntities(db, projectId, null);
|
|
1841
|
+
// Should NOT report phantom SHAs as incomplete — they can't be fixed
|
|
1842
|
+
// by running analyze-imports (no entity record exists to resolve).
|
|
1843
|
+
expect(incomplete).toHaveLength(0);
|
|
1844
|
+
});
|
|
1845
|
+
it('should still report real incomplete entities (entity exists but no analysis)', async () => {
|
|
1846
|
+
// Real entity without analysis — this IS a legitimate incomplete entity
|
|
1847
|
+
await db
|
|
1848
|
+
.insertInto('entities')
|
|
1849
|
+
.values({
|
|
1850
|
+
sha: 'sha-noanalysis',
|
|
1851
|
+
name: 'Footer',
|
|
1852
|
+
entity_type: 'visual',
|
|
1853
|
+
file_path: 'app/components/Footer.tsx',
|
|
1854
|
+
})
|
|
1855
|
+
.execute();
|
|
1856
|
+
await db
|
|
1857
|
+
.insertInto('editor_scenarios')
|
|
1858
|
+
.values({
|
|
1859
|
+
id: 'sc-1',
|
|
1860
|
+
project_id: projectId,
|
|
1861
|
+
name: 'Footer - Default',
|
|
1862
|
+
component_name: 'Footer',
|
|
1863
|
+
component_path: 'app/components/Footer.tsx',
|
|
1864
|
+
entity_sha: 'sha-noanalysis',
|
|
1865
|
+
created_at: '2026-03-16 23:00:00',
|
|
1866
|
+
})
|
|
1867
|
+
.execute();
|
|
1868
|
+
// Phantom scenario (shouldn't affect results)
|
|
1869
|
+
await db
|
|
1870
|
+
.insertInto('editor_scenarios')
|
|
1871
|
+
.values({
|
|
1872
|
+
id: 'sc-phantom',
|
|
1873
|
+
project_id: projectId,
|
|
1874
|
+
name: 'Footer - Alt',
|
|
1875
|
+
component_name: 'Footer',
|
|
1876
|
+
component_path: null,
|
|
1877
|
+
entity_sha: 'sha-phantom-does-not-exist',
|
|
1878
|
+
created_at: '2026-03-16 22:00:00',
|
|
1879
|
+
})
|
|
1880
|
+
.execute();
|
|
1881
|
+
const incomplete = await queryIncompleteEntities(db, projectId, null);
|
|
1882
|
+
// Should report Footer (real entity, no analysis) but NOT the phantom
|
|
1883
|
+
expect(incomplete).toHaveLength(1);
|
|
1884
|
+
expect(incomplete[0].name).toBe('Footer');
|
|
1885
|
+
expect(incomplete[0].entitySha).toBe('sha-noanalysis');
|
|
1886
|
+
});
|
|
1887
|
+
});
|
|
1888
|
+
// ── queryMiscategorizedScenarios ─────────────────────────────────────
|
|
1889
|
+
describe('queryMiscategorizedScenarios', () => {
|
|
1890
|
+
let db;
|
|
1891
|
+
let rawDb;
|
|
1892
|
+
const projectId = 'test-project-id';
|
|
1893
|
+
beforeEach(async () => {
|
|
1894
|
+
rawDb = new Database(':memory:');
|
|
1895
|
+
db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
|
|
1896
|
+
await db.schema
|
|
1897
|
+
.createTable('editor_scenarios')
|
|
1898
|
+
.addColumn('id', 'varchar', (col) => col.primaryKey())
|
|
1899
|
+
.addColumn('project_id', 'varchar', (col) => col.notNull())
|
|
1900
|
+
.addColumn('name', 'varchar', (col) => col.notNull())
|
|
1901
|
+
.addColumn('component_name', 'varchar')
|
|
1902
|
+
.addColumn('component_path', 'varchar')
|
|
1903
|
+
.addColumn('entity_sha', 'varchar')
|
|
1904
|
+
.addColumn('display_name', 'varchar')
|
|
1905
|
+
.addColumn('page_file_path', 'varchar')
|
|
1906
|
+
.addColumn('url', 'varchar')
|
|
1907
|
+
.addColumn('created_at', 'datetime')
|
|
1908
|
+
.addColumn('updated_at', 'datetime')
|
|
1909
|
+
.execute();
|
|
1910
|
+
});
|
|
1911
|
+
afterEach(async () => {
|
|
1912
|
+
await db.destroy();
|
|
1913
|
+
});
|
|
1914
|
+
it('should return empty when all component scenarios use isolation routes', async () => {
|
|
1915
|
+
await db
|
|
1916
|
+
.insertInto('editor_scenarios')
|
|
1917
|
+
.values({
|
|
1918
|
+
id: 'sc-1',
|
|
1919
|
+
project_id: projectId,
|
|
1920
|
+
name: 'LibraryCard - Default',
|
|
1921
|
+
component_name: 'LibraryCard',
|
|
1922
|
+
url: '/isolated-components/LibraryCard?s=Default',
|
|
1923
|
+
created_at: '2026-03-17 12:00:00',
|
|
1924
|
+
})
|
|
1925
|
+
.execute();
|
|
1926
|
+
const result = await queryMiscategorizedScenarios(db, projectId, null);
|
|
1927
|
+
expect(result).toEqual([]);
|
|
1928
|
+
});
|
|
1929
|
+
it('should flag component scenarios that use non-isolation URLs', async () => {
|
|
1336
1930
|
// This is the bug: "Full Library Page" registered as component_name=LibraryPage
|
|
1337
1931
|
// but url=/library — it's pointing at the real page, not an isolation route
|
|
1338
1932
|
await db
|
|
@@ -1539,10 +2133,31 @@ describe('editorAudit', () => {
|
|
|
1539
2133
|
allPassing: false,
|
|
1540
2134
|
})).toBe(true);
|
|
1541
2135
|
});
|
|
2136
|
+
it('should return false when there are also runner errors', () => {
|
|
2137
|
+
// functionsRunnerError means the test runner crashed — a real failure
|
|
2138
|
+
// that cannot be fixed by entity SHA backfill or analyze-imports.
|
|
2139
|
+
// If this returns true, checkAuditGate would attempt a useless backfill
|
|
2140
|
+
// instead of reporting the runner error, and isOnlyPreExistingIncomplete
|
|
2141
|
+
// could let the gate pass entirely.
|
|
2142
|
+
expect(isOnlyIncompleteEntities({
|
|
2143
|
+
componentsMissing: 0,
|
|
2144
|
+
componentsWithErrors: 0,
|
|
2145
|
+
functionsFailing: 0,
|
|
2146
|
+
functionsRunnerError: 2,
|
|
2147
|
+
functionsNameMismatch: 0,
|
|
2148
|
+
functionsMissing: 0,
|
|
2149
|
+
missingFromGlossary: 0,
|
|
2150
|
+
incompleteEntities: 1,
|
|
2151
|
+
allPassing: false,
|
|
2152
|
+
})).toBe(false);
|
|
2153
|
+
});
|
|
1542
2154
|
});
|
|
1543
2155
|
// ── isAutoRemediable ─────────────────────────────────────────────────
|
|
1544
2156
|
describe('isAutoRemediable', () => {
|
|
1545
|
-
|
|
2157
|
+
// isAutoRemediable always returns false — the audit never triggers
|
|
2158
|
+
// full analyze-imports inline. It takes minutes on large projects.
|
|
2159
|
+
// Only lightweight backfill is acceptable during audit.
|
|
2160
|
+
it('should return false even on first attempt with only incomplete entities', () => {
|
|
1546
2161
|
const result = isAutoRemediable({
|
|
1547
2162
|
componentsMissing: 0,
|
|
1548
2163
|
componentsWithErrors: 0,
|
|
@@ -1554,11 +2169,9 @@ describe('editorAudit', () => {
|
|
|
1554
2169
|
incompleteEntities: 3,
|
|
1555
2170
|
allPassing: false,
|
|
1556
2171
|
}, false);
|
|
1557
|
-
expect(result).toBe(
|
|
2172
|
+
expect(result).toBe(false);
|
|
1558
2173
|
});
|
|
1559
|
-
it('should return false on second attempt
|
|
1560
|
-
// This is the key fix: if we already tried analyze-imports and
|
|
1561
|
-
// entities are STILL incomplete, don't try again — report the failure
|
|
2174
|
+
it('should return false on second attempt', () => {
|
|
1562
2175
|
const result = isAutoRemediable({
|
|
1563
2176
|
componentsMissing: 0,
|
|
1564
2177
|
componentsWithErrors: 0,
|
|
@@ -1698,7 +2311,12 @@ describe('editorAudit', () => {
|
|
|
1698
2311
|
.execute();
|
|
1699
2312
|
const result = await queryIncompleteEntities(db, projectId, null);
|
|
1700
2313
|
expect(result).toEqual([
|
|
1701
|
-
{
|
|
2314
|
+
{
|
|
2315
|
+
entitySha: 'sha-chips',
|
|
2316
|
+
name: 'CollectionChips',
|
|
2317
|
+
scenarioCount: 2,
|
|
2318
|
+
preExisting: false,
|
|
2319
|
+
},
|
|
1702
2320
|
]);
|
|
1703
2321
|
});
|
|
1704
2322
|
it('should only return entities without analyses, not those with analyses', async () => {
|
|
@@ -1755,10 +2373,15 @@ describe('editorAudit', () => {
|
|
|
1755
2373
|
.execute();
|
|
1756
2374
|
const result = await queryIncompleteEntities(db, projectId, null);
|
|
1757
2375
|
expect(result).toEqual([
|
|
1758
|
-
{
|
|
2376
|
+
{
|
|
2377
|
+
entitySha: 'sha-picker',
|
|
2378
|
+
name: 'CollectionPicker',
|
|
2379
|
+
scenarioCount: 1,
|
|
2380
|
+
preExisting: false,
|
|
2381
|
+
},
|
|
1759
2382
|
]);
|
|
1760
2383
|
});
|
|
1761
|
-
it('should
|
|
2384
|
+
it('should return both pre-session and in-session entities with preExisting flags', async () => {
|
|
1762
2385
|
// Entity without analysis, scenario created BEFORE session
|
|
1763
2386
|
await db
|
|
1764
2387
|
.insertInto('entities')
|
|
@@ -1802,12 +2425,24 @@ describe('editorAudit', () => {
|
|
|
1802
2425
|
})
|
|
1803
2426
|
.execute();
|
|
1804
2427
|
const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
|
|
1805
|
-
//
|
|
1806
|
-
expect(result).toEqual([
|
|
1807
|
-
{
|
|
1808
|
-
|
|
2428
|
+
// Both should be returned — OldComponent is preExisting, NewComponent is not
|
|
2429
|
+
expect(result).toEqual(expect.arrayContaining([
|
|
2430
|
+
{
|
|
2431
|
+
entitySha: 'sha-old',
|
|
2432
|
+
name: 'OldComponent',
|
|
2433
|
+
scenarioCount: 1,
|
|
2434
|
+
preExisting: true,
|
|
2435
|
+
},
|
|
2436
|
+
{
|
|
2437
|
+
entitySha: 'sha-new',
|
|
2438
|
+
name: 'NewComponent',
|
|
2439
|
+
scenarioCount: 1,
|
|
2440
|
+
preExisting: false,
|
|
2441
|
+
},
|
|
2442
|
+
]));
|
|
2443
|
+
expect(result).toHaveLength(2);
|
|
1809
2444
|
});
|
|
1810
|
-
it('should
|
|
2445
|
+
it('should flag preExisting: false when scenario was updated in session even if created before', async () => {
|
|
1811
2446
|
await db
|
|
1812
2447
|
.insertInto('entities')
|
|
1813
2448
|
.values({
|
|
@@ -1835,6 +2470,7 @@ describe('editorAudit', () => {
|
|
|
1835
2470
|
entitySha: 'sha-updated',
|
|
1836
2471
|
name: 'UpdatedComponent',
|
|
1837
2472
|
scenarioCount: 1,
|
|
2473
|
+
preExisting: false,
|
|
1838
2474
|
},
|
|
1839
2475
|
]);
|
|
1840
2476
|
});
|
|
@@ -1856,23 +2492,1668 @@ describe('editorAudit', () => {
|
|
|
1856
2492
|
const result = await queryIncompleteEntities(db, projectId, null);
|
|
1857
2493
|
expect(result).toEqual([]);
|
|
1858
2494
|
});
|
|
1859
|
-
it('should
|
|
1860
|
-
//
|
|
2495
|
+
it('should not flag entities when a sibling version (same name+filePath) has analyses', async () => {
|
|
2496
|
+
// Old entity version WITH analysis
|
|
2497
|
+
await db
|
|
2498
|
+
.insertInto('entities')
|
|
2499
|
+
.values({
|
|
2500
|
+
sha: 'sha-btn-v1',
|
|
2501
|
+
name: 'OpenLibraryButton',
|
|
2502
|
+
entity_type: 'visual',
|
|
2503
|
+
file_path: 'src/components/OpenLibraryButton.tsx',
|
|
2504
|
+
})
|
|
2505
|
+
.execute();
|
|
2506
|
+
await db
|
|
2507
|
+
.insertInto('analyses')
|
|
2508
|
+
.values({
|
|
2509
|
+
id: 'a-btn-v1',
|
|
2510
|
+
entity_sha: 'sha-btn-v1',
|
|
2511
|
+
entity_name: 'OpenLibraryButton',
|
|
2512
|
+
project_id: projectId,
|
|
2513
|
+
})
|
|
2514
|
+
.execute();
|
|
2515
|
+
// New entity version WITHOUT analysis (created by file watcher)
|
|
2516
|
+
await db
|
|
2517
|
+
.insertInto('entities')
|
|
2518
|
+
.values({
|
|
2519
|
+
sha: 'sha-btn-v2',
|
|
2520
|
+
name: 'OpenLibraryButton',
|
|
2521
|
+
entity_type: 'visual',
|
|
2522
|
+
file_path: 'src/components/OpenLibraryButton.tsx',
|
|
2523
|
+
})
|
|
2524
|
+
.execute();
|
|
2525
|
+
// Scenario points to the NEW version (backfilled after file watcher)
|
|
1861
2526
|
await db
|
|
1862
2527
|
.insertInto('editor_scenarios')
|
|
1863
2528
|
.values({
|
|
1864
|
-
id: 'sc-
|
|
2529
|
+
id: 'sc-btn',
|
|
1865
2530
|
project_id: projectId,
|
|
1866
|
-
name: '
|
|
1867
|
-
component_name: '
|
|
1868
|
-
entity_sha: 'sha-
|
|
2531
|
+
name: 'OpenLibraryButton - Default',
|
|
2532
|
+
component_name: 'OpenLibraryButton',
|
|
2533
|
+
entity_sha: 'sha-btn-v2',
|
|
1869
2534
|
created_at: '2026-03-16 23:00:00',
|
|
1870
2535
|
})
|
|
1871
2536
|
.execute();
|
|
2537
|
+
// Should NOT flag as incomplete — sibling version has analyses
|
|
1872
2538
|
const result = await queryIncompleteEntities(db, projectId, null);
|
|
1873
|
-
expect(result).toEqual([
|
|
1874
|
-
|
|
1875
|
-
|
|
2539
|
+
expect(result).toEqual([]);
|
|
2540
|
+
});
|
|
2541
|
+
it('should flag entity when sibling has analyses but different filePath (extracted component)', async () => {
|
|
2542
|
+
// Old entity version WITH analysis at ORIGINAL file path
|
|
2543
|
+
await db
|
|
2544
|
+
.insertInto('entities')
|
|
2545
|
+
.values({
|
|
2546
|
+
sha: 'sha-card-v1',
|
|
2547
|
+
name: 'TaskCard',
|
|
2548
|
+
entity_type: 'visual',
|
|
2549
|
+
file_path: 'app/page.tsx',
|
|
2550
|
+
})
|
|
2551
|
+
.execute();
|
|
2552
|
+
await db
|
|
2553
|
+
.insertInto('analyses')
|
|
2554
|
+
.values({
|
|
2555
|
+
id: 'a-card-v1',
|
|
2556
|
+
entity_sha: 'sha-card-v1',
|
|
2557
|
+
entity_name: 'TaskCard',
|
|
2558
|
+
project_id: projectId,
|
|
2559
|
+
})
|
|
2560
|
+
.execute();
|
|
2561
|
+
// New entity version WITHOUT analysis at EXTRACTED file path
|
|
2562
|
+
await db
|
|
2563
|
+
.insertInto('entities')
|
|
2564
|
+
.values({
|
|
2565
|
+
sha: 'sha-card-v2',
|
|
2566
|
+
name: 'TaskCard',
|
|
2567
|
+
entity_type: 'visual',
|
|
2568
|
+
file_path: 'app/components/TaskCard.tsx',
|
|
2569
|
+
})
|
|
2570
|
+
.execute();
|
|
2571
|
+
// Scenario points to the new version (synced by syncScenarioEntityShas)
|
|
2572
|
+
await db
|
|
2573
|
+
.insertInto('editor_scenarios')
|
|
2574
|
+
.values({
|
|
2575
|
+
id: 'sc-card',
|
|
2576
|
+
project_id: projectId,
|
|
2577
|
+
name: 'TaskCard - Default',
|
|
2578
|
+
component_name: 'TaskCard',
|
|
2579
|
+
entity_sha: 'sha-card-v2',
|
|
2580
|
+
created_at: '2026-03-16 23:00:00',
|
|
2581
|
+
})
|
|
2582
|
+
.execute();
|
|
2583
|
+
// SHOULD flag as incomplete — sibling has analyses but at a different filePath,
|
|
2584
|
+
// so getAllEntities() won't inherit (it matches by name+filePath)
|
|
2585
|
+
const result = await queryIncompleteEntities(db, projectId, null);
|
|
2586
|
+
expect(result).toEqual([
|
|
2587
|
+
{
|
|
2588
|
+
entitySha: 'sha-card-v2',
|
|
2589
|
+
name: 'TaskCard',
|
|
2590
|
+
scenarioCount: 1,
|
|
2591
|
+
preExisting: false,
|
|
2592
|
+
},
|
|
2593
|
+
]);
|
|
2594
|
+
});
|
|
2595
|
+
it('should still flag entities when no sibling version has analyses', async () => {
|
|
2596
|
+
// Only one version, no analyses
|
|
2597
|
+
await db
|
|
2598
|
+
.insertInto('entities')
|
|
2599
|
+
.values({
|
|
2600
|
+
sha: 'sha-icon',
|
|
2601
|
+
name: 'ExternalLinkIcon',
|
|
2602
|
+
entity_type: 'visual',
|
|
2603
|
+
file_path: 'src/components/ExternalLinkIcon.tsx',
|
|
2604
|
+
})
|
|
2605
|
+
.execute();
|
|
2606
|
+
await db
|
|
2607
|
+
.insertInto('editor_scenarios')
|
|
2608
|
+
.values({
|
|
2609
|
+
id: 'sc-icon',
|
|
2610
|
+
project_id: projectId,
|
|
2611
|
+
name: 'ExternalLinkIcon - Default',
|
|
2612
|
+
component_name: 'ExternalLinkIcon',
|
|
2613
|
+
entity_sha: 'sha-icon',
|
|
2614
|
+
created_at: '2026-03-16 23:00:00',
|
|
2615
|
+
})
|
|
2616
|
+
.execute();
|
|
2617
|
+
// Should flag as incomplete — no version has analyses
|
|
2618
|
+
const result = await queryIncompleteEntities(db, projectId, null);
|
|
2619
|
+
expect(result).toEqual([
|
|
2620
|
+
{
|
|
2621
|
+
entitySha: 'sha-icon',
|
|
2622
|
+
name: 'ExternalLinkIcon',
|
|
2623
|
+
scenarioCount: 1,
|
|
2624
|
+
preExisting: false,
|
|
2625
|
+
},
|
|
2626
|
+
]);
|
|
2627
|
+
});
|
|
2628
|
+
it('should skip phantom SHAs (entity_sha with no entity record)', async () => {
|
|
2629
|
+
// Scenario has entity_sha but entity record doesn't exist.
|
|
2630
|
+
// These are "phantom SHAs" created when scenarios were registered
|
|
2631
|
+
// without component_path — they can never be fixed by analyze-imports
|
|
2632
|
+
// and should not block audit progression.
|
|
2633
|
+
await db
|
|
2634
|
+
.insertInto('editor_scenarios')
|
|
2635
|
+
.values({
|
|
2636
|
+
id: 'sc-1',
|
|
2637
|
+
project_id: projectId,
|
|
2638
|
+
name: 'Ghost - Default',
|
|
2639
|
+
component_name: 'GhostComponent',
|
|
2640
|
+
entity_sha: 'sha-ghost',
|
|
2641
|
+
created_at: '2026-03-16 23:00:00',
|
|
2642
|
+
})
|
|
2643
|
+
.execute();
|
|
2644
|
+
const result = await queryIncompleteEntities(db, projectId, null);
|
|
2645
|
+
// Phantom SHAs are excluded — not reportable as incomplete
|
|
2646
|
+
expect(result).toEqual([]);
|
|
2647
|
+
});
|
|
2648
|
+
it('should detect incomplete entity whose scenario predates the session', async () => {
|
|
2649
|
+
// Entity with no analyses, scenario created BEFORE session
|
|
2650
|
+
await db
|
|
2651
|
+
.insertInto('entities')
|
|
2652
|
+
.values({
|
|
2653
|
+
sha: 'sha-preexisting',
|
|
2654
|
+
name: 'PreExistingComponent',
|
|
2655
|
+
entity_type: 'visual',
|
|
2656
|
+
file_path: 'src/PreExistingComponent.tsx',
|
|
2657
|
+
})
|
|
2658
|
+
.execute();
|
|
2659
|
+
await db
|
|
2660
|
+
.insertInto('editor_scenarios')
|
|
2661
|
+
.values({
|
|
2662
|
+
id: 'sc-preexisting',
|
|
2663
|
+
project_id: projectId,
|
|
2664
|
+
name: 'PreExistingComponent - Default',
|
|
2665
|
+
component_name: 'PreExistingComponent',
|
|
2666
|
+
entity_sha: 'sha-preexisting',
|
|
2667
|
+
created_at: '2026-03-16 20:00:00',
|
|
2668
|
+
updated_at: '2026-03-16 20:00:00',
|
|
2669
|
+
})
|
|
2670
|
+
.execute();
|
|
2671
|
+
// Session started well after scenario was created/updated
|
|
2672
|
+
const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
|
|
2673
|
+
// Should still be detected — the old time filter would have excluded it
|
|
2674
|
+
expect(result).toEqual([
|
|
2675
|
+
{
|
|
2676
|
+
entitySha: 'sha-preexisting',
|
|
2677
|
+
name: 'PreExistingComponent',
|
|
2678
|
+
scenarioCount: 1,
|
|
2679
|
+
preExisting: true,
|
|
2680
|
+
},
|
|
2681
|
+
]);
|
|
2682
|
+
});
|
|
2683
|
+
it('should flag preExisting: true when all scenarios predate the session', async () => {
|
|
2684
|
+
await db
|
|
2685
|
+
.insertInto('entities')
|
|
2686
|
+
.values({
|
|
2687
|
+
sha: 'sha-old-entity',
|
|
2688
|
+
name: 'OldEntity',
|
|
2689
|
+
entity_type: 'visual',
|
|
2690
|
+
file_path: 'src/OldEntity.tsx',
|
|
2691
|
+
})
|
|
2692
|
+
.execute();
|
|
2693
|
+
// Two scenarios, both before session
|
|
2694
|
+
await db
|
|
2695
|
+
.insertInto('editor_scenarios')
|
|
2696
|
+
.values({
|
|
2697
|
+
id: 'sc-old-1',
|
|
2698
|
+
project_id: projectId,
|
|
2699
|
+
name: 'OldEntity - Default',
|
|
2700
|
+
component_name: 'OldEntity',
|
|
2701
|
+
entity_sha: 'sha-old-entity',
|
|
2702
|
+
created_at: '2026-03-16 19:00:00',
|
|
2703
|
+
updated_at: '2026-03-16 19:00:00',
|
|
2704
|
+
})
|
|
2705
|
+
.execute();
|
|
2706
|
+
await db
|
|
2707
|
+
.insertInto('editor_scenarios')
|
|
2708
|
+
.values({
|
|
2709
|
+
id: 'sc-old-2',
|
|
2710
|
+
project_id: projectId,
|
|
2711
|
+
name: 'OldEntity - Hover',
|
|
2712
|
+
component_name: 'OldEntity',
|
|
2713
|
+
entity_sha: 'sha-old-entity',
|
|
2714
|
+
created_at: '2026-03-16 19:30:00',
|
|
2715
|
+
updated_at: '2026-03-16 19:30:00',
|
|
2716
|
+
})
|
|
2717
|
+
.execute();
|
|
2718
|
+
const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
|
|
2719
|
+
expect(result).toEqual([
|
|
2720
|
+
{
|
|
2721
|
+
entitySha: 'sha-old-entity',
|
|
2722
|
+
name: 'OldEntity',
|
|
2723
|
+
scenarioCount: 2,
|
|
2724
|
+
preExisting: true,
|
|
2725
|
+
},
|
|
2726
|
+
]);
|
|
2727
|
+
});
|
|
2728
|
+
it('should flag preExisting: false when scenario is from the current session', async () => {
|
|
2729
|
+
await db
|
|
2730
|
+
.insertInto('entities')
|
|
2731
|
+
.values({
|
|
2732
|
+
sha: 'sha-session-entity',
|
|
2733
|
+
name: 'SessionEntity',
|
|
2734
|
+
entity_type: 'visual',
|
|
2735
|
+
file_path: 'src/SessionEntity.tsx',
|
|
2736
|
+
})
|
|
2737
|
+
.execute();
|
|
2738
|
+
await db
|
|
2739
|
+
.insertInto('editor_scenarios')
|
|
2740
|
+
.values({
|
|
2741
|
+
id: 'sc-session',
|
|
2742
|
+
project_id: projectId,
|
|
2743
|
+
name: 'SessionEntity - Default',
|
|
2744
|
+
component_name: 'SessionEntity',
|
|
2745
|
+
entity_sha: 'sha-session-entity',
|
|
2746
|
+
created_at: '2026-03-16 23:30:00',
|
|
2747
|
+
updated_at: '2026-03-16 23:30:00',
|
|
2748
|
+
})
|
|
2749
|
+
.execute();
|
|
2750
|
+
const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
|
|
2751
|
+
expect(result).toEqual([
|
|
2752
|
+
{
|
|
2753
|
+
entitySha: 'sha-session-entity',
|
|
2754
|
+
name: 'SessionEntity',
|
|
2755
|
+
scenarioCount: 1,
|
|
2756
|
+
preExisting: false,
|
|
2757
|
+
},
|
|
2758
|
+
]);
|
|
2759
|
+
});
|
|
2760
|
+
});
|
|
2761
|
+
// ── identifyScenariosNeedingRecapture ──────────────────────────────
|
|
2762
|
+
describe('identifyScenariosNeedingRecapture', () => {
|
|
2763
|
+
// Reproduces the Margo bug: Feature 1 built app-level popup scenarios,
|
|
2764
|
+
// Feature 2 edited LibraryView (used by App), but app-level scenarios
|
|
2765
|
+
// were never flagged for recapture because the audit only checked
|
|
2766
|
+
// component scenario existence — not whether app-level scenarios are stale.
|
|
2767
|
+
//
|
|
2768
|
+
// Each scenario's entityName is resolved by the caller via
|
|
2769
|
+
// entity_sha → entities.name (the default export for app-level scenarios).
|
|
2770
|
+
it('should flag app-level scenario when its entity is impacted by transitive dependency change', () => {
|
|
2771
|
+
// LibraryView was edited → App is impacted (imports LibraryView)
|
|
2772
|
+
// App-level scenario "Library - Rich Library" has entity_sha pointing to App
|
|
2773
|
+
// It was NOT recaptured during Feature 2 → should be flagged
|
|
2774
|
+
const entityChangeStatus = {
|
|
2775
|
+
LibraryView: { status: 'edited' },
|
|
2776
|
+
App: {
|
|
2777
|
+
status: 'impacted',
|
|
2778
|
+
impactedBy: [
|
|
2779
|
+
{
|
|
2780
|
+
name: 'LibraryView',
|
|
2781
|
+
filePath: 'src/components/LibraryView.tsx',
|
|
2782
|
+
changeType: 'edited',
|
|
2783
|
+
},
|
|
2784
|
+
],
|
|
2785
|
+
},
|
|
2786
|
+
};
|
|
2787
|
+
const result = identifyScenariosNeedingRecapture({
|
|
2788
|
+
scenarios: [
|
|
2789
|
+
{
|
|
2790
|
+
name: 'Library - Rich Library',
|
|
2791
|
+
entityName: 'App', // resolved from entity_sha → entities.name
|
|
2792
|
+
updatedInSession: false,
|
|
2793
|
+
},
|
|
2794
|
+
],
|
|
2795
|
+
entityChangeStatus,
|
|
2796
|
+
});
|
|
2797
|
+
expect(result).toHaveLength(1);
|
|
2798
|
+
expect(result[0].scenarioName).toBe('Library - Rich Library');
|
|
2799
|
+
expect(result[0].entityName).toBe('App');
|
|
2800
|
+
expect(result[0].status.status).toBe('impacted');
|
|
2801
|
+
});
|
|
2802
|
+
it('should flag component scenario when its entity is directly edited and not recaptured', () => {
|
|
2803
|
+
const entityChangeStatus = {
|
|
2804
|
+
LibraryView: { status: 'edited' },
|
|
2805
|
+
};
|
|
2806
|
+
const result = identifyScenariosNeedingRecapture({
|
|
2807
|
+
scenarios: [
|
|
2808
|
+
{
|
|
2809
|
+
name: 'LibraryView - Empty',
|
|
2810
|
+
entityName: 'LibraryView',
|
|
2811
|
+
updatedInSession: false,
|
|
2812
|
+
},
|
|
2813
|
+
],
|
|
2814
|
+
entityChangeStatus,
|
|
2815
|
+
});
|
|
2816
|
+
expect(result).toHaveLength(1);
|
|
2817
|
+
expect(result[0].scenarioName).toBe('LibraryView - Empty');
|
|
2818
|
+
expect(result[0].entityName).toBe('LibraryView');
|
|
2819
|
+
expect(result[0].status.status).toBe('edited');
|
|
2820
|
+
});
|
|
2821
|
+
it('should NOT flag scenario that was already recaptured in the current session', () => {
|
|
2822
|
+
const entityChangeStatus = {
|
|
2823
|
+
App: {
|
|
2824
|
+
status: 'impacted',
|
|
2825
|
+
impactedBy: [
|
|
2826
|
+
{
|
|
2827
|
+
name: 'LibraryView',
|
|
2828
|
+
filePath: 'src/components/LibraryView.tsx',
|
|
2829
|
+
changeType: 'edited',
|
|
2830
|
+
},
|
|
2831
|
+
],
|
|
2832
|
+
},
|
|
2833
|
+
};
|
|
2834
|
+
const result = identifyScenariosNeedingRecapture({
|
|
2835
|
+
scenarios: [
|
|
2836
|
+
{
|
|
2837
|
+
name: 'App - Default',
|
|
2838
|
+
entityName: 'App',
|
|
2839
|
+
updatedInSession: true, // re-registered during Feature 2
|
|
2840
|
+
},
|
|
2841
|
+
],
|
|
2842
|
+
entityChangeStatus,
|
|
2843
|
+
});
|
|
2844
|
+
expect(result).toHaveLength(0);
|
|
2845
|
+
});
|
|
2846
|
+
it('should NOT flag scenario whose entity has no change status', () => {
|
|
2847
|
+
const entityChangeStatus = {
|
|
2848
|
+
LibraryView: { status: 'edited' },
|
|
2849
|
+
};
|
|
2850
|
+
const result = identifyScenariosNeedingRecapture({
|
|
2851
|
+
scenarios: [
|
|
2852
|
+
{
|
|
2853
|
+
name: 'WelcomeScreen - Default',
|
|
2854
|
+
entityName: 'WelcomeScreen',
|
|
2855
|
+
updatedInSession: false,
|
|
2856
|
+
},
|
|
2857
|
+
],
|
|
2858
|
+
entityChangeStatus,
|
|
2859
|
+
});
|
|
2860
|
+
expect(result).toHaveLength(0);
|
|
2861
|
+
});
|
|
2862
|
+
it('should return empty array when entityChangeStatus is undefined', () => {
|
|
2863
|
+
const result = identifyScenariosNeedingRecapture({
|
|
2864
|
+
scenarios: [
|
|
2865
|
+
{
|
|
2866
|
+
name: 'Library - Rich Library',
|
|
2867
|
+
entityName: 'App',
|
|
2868
|
+
updatedInSession: false,
|
|
2869
|
+
},
|
|
2870
|
+
],
|
|
2871
|
+
entityChangeStatus: undefined,
|
|
2872
|
+
});
|
|
2873
|
+
expect(result).toHaveLength(0);
|
|
2874
|
+
});
|
|
2875
|
+
it('should return empty array when entityChangeStatus is empty', () => {
|
|
2876
|
+
const result = identifyScenariosNeedingRecapture({
|
|
2877
|
+
scenarios: [
|
|
2878
|
+
{
|
|
2879
|
+
name: 'Library - Rich Library',
|
|
2880
|
+
entityName: 'App',
|
|
2881
|
+
updatedInSession: false,
|
|
2882
|
+
},
|
|
2883
|
+
],
|
|
2884
|
+
entityChangeStatus: {},
|
|
2885
|
+
});
|
|
2886
|
+
expect(result).toHaveLength(0);
|
|
2887
|
+
});
|
|
2888
|
+
it('should flag multiple app-level scenarios sharing the same impacted entity', () => {
|
|
2889
|
+
const entityChangeStatus = {
|
|
2890
|
+
LibraryView: { status: 'edited' },
|
|
2891
|
+
App: {
|
|
2892
|
+
status: 'impacted',
|
|
2893
|
+
impactedBy: [
|
|
2894
|
+
{
|
|
2895
|
+
name: 'LibraryView',
|
|
2896
|
+
filePath: 'src/components/LibraryView.tsx',
|
|
2897
|
+
changeType: 'edited',
|
|
2898
|
+
},
|
|
2899
|
+
],
|
|
2900
|
+
},
|
|
2901
|
+
};
|
|
2902
|
+
const result = identifyScenariosNeedingRecapture({
|
|
2903
|
+
scenarios: [
|
|
2904
|
+
{
|
|
2905
|
+
name: 'Library - Empty',
|
|
2906
|
+
entityName: 'App',
|
|
2907
|
+
updatedInSession: false,
|
|
2908
|
+
},
|
|
2909
|
+
{
|
|
2910
|
+
name: 'Library - Rich Library',
|
|
2911
|
+
entityName: 'App',
|
|
2912
|
+
updatedInSession: false,
|
|
2913
|
+
},
|
|
2914
|
+
{
|
|
2915
|
+
name: 'First Article Saved',
|
|
2916
|
+
entityName: 'App',
|
|
2917
|
+
updatedInSession: false,
|
|
2918
|
+
},
|
|
2919
|
+
],
|
|
2920
|
+
entityChangeStatus,
|
|
2921
|
+
});
|
|
2922
|
+
expect(result).toHaveLength(3);
|
|
2923
|
+
expect(result.map((r) => r.scenarioName).sort()).toEqual([
|
|
2924
|
+
'First Article Saved',
|
|
2925
|
+
'Library - Empty',
|
|
2926
|
+
'Library - Rich Library',
|
|
2927
|
+
]);
|
|
2928
|
+
expect(result.every((r) => r.entityName === 'App')).toBe(true);
|
|
2929
|
+
});
|
|
2930
|
+
it('should skip scenarios with null entityName (no entity_sha set)', () => {
|
|
2931
|
+
const entityChangeStatus = {
|
|
2932
|
+
App: { status: 'edited' },
|
|
2933
|
+
};
|
|
2934
|
+
const result = identifyScenariosNeedingRecapture({
|
|
2935
|
+
scenarios: [
|
|
2936
|
+
{
|
|
2937
|
+
name: 'Mystery Scenario',
|
|
2938
|
+
entityName: null, // no entity_sha → no entity name
|
|
2939
|
+
updatedInSession: false,
|
|
2940
|
+
},
|
|
2941
|
+
],
|
|
2942
|
+
entityChangeStatus,
|
|
2943
|
+
});
|
|
2944
|
+
expect(result).toHaveLength(0);
|
|
2945
|
+
});
|
|
2946
|
+
it('should NOT flag scenarios for new entities (they need creation, not recapture)', () => {
|
|
2947
|
+
// "new" entities are being seen for the first time. Their scenarios need
|
|
2948
|
+
// initial creation, not recapture of old screenshots. Flagging them as
|
|
2949
|
+
// "needs_recapture" sends the wrong remediation signal to Claude.
|
|
2950
|
+
const entityChangeStatus = {
|
|
2951
|
+
NewComponent: { status: 'new' },
|
|
2952
|
+
};
|
|
2953
|
+
const result = identifyScenariosNeedingRecapture({
|
|
2954
|
+
scenarios: [
|
|
2955
|
+
{
|
|
2956
|
+
name: 'NewComponent - Default',
|
|
2957
|
+
entityName: 'NewComponent',
|
|
2958
|
+
updatedInSession: false,
|
|
2959
|
+
},
|
|
2960
|
+
],
|
|
2961
|
+
entityChangeStatus,
|
|
2962
|
+
});
|
|
2963
|
+
expect(result).toHaveLength(0);
|
|
2964
|
+
});
|
|
2965
|
+
});
|
|
2966
|
+
// ── detectDuplicateNames ──────────────────────────────────────────
|
|
2967
|
+
describe('detectDuplicateNames', () => {
|
|
2968
|
+
it('should return empty map when no duplicates exist', () => {
|
|
2969
|
+
const entries = [
|
|
2970
|
+
{ name: 'Header', filePath: 'app/components/Header.tsx' },
|
|
2971
|
+
{ name: 'Footer', filePath: 'app/components/Footer.tsx' },
|
|
2972
|
+
{ name: 'Sidebar', filePath: 'app/components/Sidebar.tsx' },
|
|
2973
|
+
];
|
|
2974
|
+
const result = detectDuplicateNames(entries);
|
|
2975
|
+
expect(result.size).toBe(0);
|
|
2976
|
+
});
|
|
2977
|
+
it('should group entries that share a name', () => {
|
|
2978
|
+
const entries = [
|
|
2979
|
+
{ name: 'Page', filePath: 'app/isolated-components/Foo/page.tsx' },
|
|
2980
|
+
{ name: 'Page', filePath: 'app/isolated-components/Bar/page.tsx' },
|
|
2981
|
+
{ name: 'Page', filePath: 'app/isolated-components/Baz/page.tsx' },
|
|
2982
|
+
{ name: 'Header', filePath: 'app/components/Header.tsx' },
|
|
2983
|
+
];
|
|
2984
|
+
const result = detectDuplicateNames(entries);
|
|
2985
|
+
expect(result.size).toBe(1);
|
|
2986
|
+
expect(result.has('Page')).toBe(true);
|
|
2987
|
+
const pageGroup = result.get('Page');
|
|
2988
|
+
expect(pageGroup).toHaveLength(3);
|
|
2989
|
+
expect(pageGroup.map((e) => e.filePath)).toEqual([
|
|
2990
|
+
'app/isolated-components/Foo/page.tsx',
|
|
2991
|
+
'app/isolated-components/Bar/page.tsx',
|
|
2992
|
+
'app/isolated-components/Baz/page.tsx',
|
|
2993
|
+
]);
|
|
2994
|
+
});
|
|
2995
|
+
it('should exclude single-occurrence names', () => {
|
|
2996
|
+
const entries = [
|
|
2997
|
+
{ name: 'Page', filePath: 'app/isolated-components/Foo/page.tsx' },
|
|
2998
|
+
{ name: 'Page', filePath: 'app/isolated-components/Bar/page.tsx' },
|
|
2999
|
+
{ name: 'Header', filePath: 'app/components/Header.tsx' },
|
|
3000
|
+
{ name: 'Footer', filePath: 'app/components/Footer.tsx' },
|
|
3001
|
+
];
|
|
3002
|
+
const result = detectDuplicateNames(entries);
|
|
3003
|
+
expect(result.size).toBe(1);
|
|
3004
|
+
expect(result.has('Header')).toBe(false);
|
|
3005
|
+
expect(result.has('Footer')).toBe(false);
|
|
3006
|
+
});
|
|
3007
|
+
it('should handle multiple duplicate groups', () => {
|
|
3008
|
+
const entries = [
|
|
3009
|
+
{ name: 'Page', filePath: 'app/isolated-components/A/page.tsx' },
|
|
3010
|
+
{ name: 'Page', filePath: 'app/isolated-components/B/page.tsx' },
|
|
3011
|
+
{ name: 'Layout', filePath: 'app/isolated-components/A/layout.tsx' },
|
|
3012
|
+
{ name: 'Layout', filePath: 'app/isolated-components/B/layout.tsx' },
|
|
3013
|
+
{ name: 'Unique', filePath: 'app/components/Unique.tsx' },
|
|
3014
|
+
];
|
|
3015
|
+
const result = detectDuplicateNames(entries);
|
|
3016
|
+
expect(result.size).toBe(2);
|
|
3017
|
+
expect(result.has('Page')).toBe(true);
|
|
3018
|
+
expect(result.has('Layout')).toBe(true);
|
|
3019
|
+
expect(result.get('Page')).toHaveLength(2);
|
|
3020
|
+
expect(result.get('Layout')).toHaveLength(2);
|
|
3021
|
+
});
|
|
3022
|
+
it('should return empty map for empty input', () => {
|
|
3023
|
+
const result = detectDuplicateNames([]);
|
|
3024
|
+
expect(result.size).toBe(0);
|
|
3025
|
+
});
|
|
3026
|
+
});
|
|
3027
|
+
// ── computeAudit: impacted components with stale scenarios ──────────
|
|
3028
|
+
describe('computeAudit — impacted components with stale scenarios', () => {
|
|
3029
|
+
it('should mark impacted component as needs_recapture when it has total scenarios but none in session', () => {
|
|
3030
|
+
// Library page has 3 scenarios from Feature 1 (totalScenarioCounts),
|
|
3031
|
+
// 0 in the current session (scenarioCounts), and is "impacted" in entityChangeStatus.
|
|
3032
|
+
// It should NOT be marked "missing" — it needs recapture, not new scenarios.
|
|
3033
|
+
const result = computeAudit({
|
|
3034
|
+
components: [
|
|
3035
|
+
{
|
|
3036
|
+
name: 'Library',
|
|
3037
|
+
filePath: 'app/library/page.tsx',
|
|
3038
|
+
returnType: 'JSX.Element',
|
|
3039
|
+
},
|
|
3040
|
+
{
|
|
3041
|
+
name: 'ArticleTable',
|
|
3042
|
+
filePath: 'app/components/ArticleTable.tsx',
|
|
3043
|
+
returnType: 'JSX.Element',
|
|
3044
|
+
},
|
|
3045
|
+
],
|
|
3046
|
+
functions: [],
|
|
3047
|
+
scenarioCounts: { ArticleTable: 2 },
|
|
3048
|
+
testFileExistence: {},
|
|
3049
|
+
totalScenarioCounts: { Library: 3 },
|
|
3050
|
+
entityChangeStatus: {
|
|
3051
|
+
Library: { status: 'impacted' },
|
|
3052
|
+
ArticleTable: { status: 'edited' },
|
|
3053
|
+
},
|
|
3054
|
+
});
|
|
3055
|
+
// Library: impacted + has total scenarios but 0 in session → needs_recapture
|
|
3056
|
+
expect(result.components[0].status).toBe('needs_recapture');
|
|
3057
|
+
expect(result.components[0].scenarioCount).toBe(3);
|
|
3058
|
+
// ArticleTable: edited + has session scenarios → ok
|
|
3059
|
+
expect(result.components[1].status).toBe('ok');
|
|
3060
|
+
// needs_recapture should NOT count as missing
|
|
3061
|
+
expect(result.summary.componentsMissing).toBe(0);
|
|
3062
|
+
expect(result.summary.componentsNeedingRecapture).toBe(1);
|
|
3063
|
+
// should NOT fail the audit (scenariosNeedingRecapture handles it)
|
|
3064
|
+
expect(result.summary.allPassing).toBe(true);
|
|
3065
|
+
});
|
|
3066
|
+
it('should still mark component as missing when impacted but has zero total scenarios', () => {
|
|
3067
|
+
// New page added to glossary but never had scenarios — truly missing
|
|
3068
|
+
const result = computeAudit({
|
|
3069
|
+
components: [
|
|
3070
|
+
{
|
|
3071
|
+
name: 'NewPage',
|
|
3072
|
+
filePath: 'app/new/page.tsx',
|
|
3073
|
+
returnType: 'JSX.Element',
|
|
3074
|
+
},
|
|
3075
|
+
],
|
|
3076
|
+
functions: [],
|
|
3077
|
+
scenarioCounts: {},
|
|
3078
|
+
testFileExistence: {},
|
|
3079
|
+
totalScenarioCounts: {},
|
|
3080
|
+
entityChangeStatus: {
|
|
3081
|
+
NewPage: { status: 'impacted' },
|
|
3082
|
+
},
|
|
3083
|
+
});
|
|
3084
|
+
expect(result.components[0].status).toBe('missing');
|
|
3085
|
+
expect(result.summary.componentsMissing).toBe(1);
|
|
3086
|
+
});
|
|
3087
|
+
it('should use needs_recapture for edited entities with existing scenarios from prior sessions', () => {
|
|
3088
|
+
// Edited entities that already have scenarios from prior sessions
|
|
3089
|
+
// need recapture, not re-registration. The code changed, but the
|
|
3090
|
+
// scenarios exist — they just need fresh screenshots.
|
|
3091
|
+
const result = computeAudit({
|
|
3092
|
+
components: [
|
|
3093
|
+
{
|
|
3094
|
+
name: 'EditedComp',
|
|
3095
|
+
filePath: 'app/components/Edited.tsx',
|
|
3096
|
+
returnType: 'JSX.Element',
|
|
3097
|
+
},
|
|
3098
|
+
],
|
|
3099
|
+
functions: [],
|
|
3100
|
+
scenarioCounts: {},
|
|
3101
|
+
testFileExistence: {},
|
|
3102
|
+
totalScenarioCounts: { EditedComp: 2 },
|
|
3103
|
+
entityChangeStatus: {
|
|
3104
|
+
EditedComp: { status: 'edited' },
|
|
3105
|
+
},
|
|
3106
|
+
});
|
|
3107
|
+
expect(result.components[0].status).toBe('needs_recapture');
|
|
3108
|
+
expect(result.summary.componentsMissing).toBe(0);
|
|
3109
|
+
expect(result.summary.componentsNeedingRecapture).toBe(1);
|
|
3110
|
+
});
|
|
3111
|
+
it('should still mark as missing when new entity has zero total scenarios', () => {
|
|
3112
|
+
// Truly new component with no scenarios ever — needs scenarios created
|
|
3113
|
+
const result = computeAudit({
|
|
3114
|
+
components: [
|
|
3115
|
+
{
|
|
3116
|
+
name: 'BrandNew',
|
|
3117
|
+
filePath: 'app/components/BrandNew.tsx',
|
|
3118
|
+
returnType: 'JSX.Element',
|
|
3119
|
+
},
|
|
3120
|
+
],
|
|
3121
|
+
functions: [],
|
|
3122
|
+
scenarioCounts: {},
|
|
3123
|
+
testFileExistence: {},
|
|
3124
|
+
totalScenarioCounts: {},
|
|
3125
|
+
entityChangeStatus: {
|
|
3126
|
+
BrandNew: { status: 'new' },
|
|
3127
|
+
},
|
|
3128
|
+
});
|
|
3129
|
+
expect(result.components[0].status).toBe('missing');
|
|
3130
|
+
expect(result.summary.componentsMissing).toBe(1);
|
|
3131
|
+
});
|
|
3132
|
+
it('should not count needs_recapture components as componentsOk', () => {
|
|
3133
|
+
// A needs_recapture component is not "ok" — it needs action. Counting it
|
|
3134
|
+
// in componentsOk is misleading: if totalComponents=2, componentsOk=2,
|
|
3135
|
+
// and componentsNeedingRecapture=1, the numbers don't add up (2+1 > 2).
|
|
3136
|
+
const result = computeAudit({
|
|
3137
|
+
components: [
|
|
3138
|
+
{ name: 'Library', filePath: 'app/library/page.tsx' },
|
|
3139
|
+
{ name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
|
|
3140
|
+
],
|
|
3141
|
+
functions: [],
|
|
3142
|
+
scenarioCounts: { DrinkCard: 2 },
|
|
3143
|
+
testFileExistence: {},
|
|
3144
|
+
totalScenarioCounts: { Library: 3 },
|
|
3145
|
+
entityChangeStatus: { Library: { status: 'impacted' } },
|
|
3146
|
+
});
|
|
3147
|
+
expect(result.components[0].status).toBe('needs_recapture');
|
|
3148
|
+
expect(result.components[1].status).toBe('ok');
|
|
3149
|
+
// needs_recapture is not "ok" — should be counted separately
|
|
3150
|
+
expect(result.summary.componentsOk).toBe(1);
|
|
3151
|
+
expect(result.summary.componentsNeedingRecapture).toBe(1);
|
|
3152
|
+
expect(result.summary.totalComponents).toBe(2);
|
|
3153
|
+
});
|
|
3154
|
+
});
|
|
3155
|
+
// ── queryUnassociatedScenarios ──────────────────────────────────────
|
|
3156
|
+
describe('queryUnassociatedScenarios', () => {
|
|
3157
|
+
let db;
|
|
3158
|
+
let rawDb;
|
|
3159
|
+
const projectId = 'test-project-id';
|
|
3160
|
+
beforeEach(async () => {
|
|
3161
|
+
rawDb = new Database(':memory:');
|
|
3162
|
+
db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
|
|
3163
|
+
await db.schema
|
|
3164
|
+
.createTable('editor_scenarios')
|
|
3165
|
+
.addColumn('id', 'varchar', (col) => col.primaryKey())
|
|
3166
|
+
.addColumn('project_id', 'varchar', (col) => col.notNull())
|
|
3167
|
+
.addColumn('name', 'varchar', (col) => col.notNull())
|
|
3168
|
+
.addColumn('component_name', 'varchar')
|
|
3169
|
+
.addColumn('component_path', 'varchar')
|
|
3170
|
+
.addColumn('entity_sha', 'varchar')
|
|
3171
|
+
.addColumn('display_name', 'varchar')
|
|
3172
|
+
.addColumn('page_file_path', 'varchar')
|
|
3173
|
+
.addColumn('url', 'varchar')
|
|
3174
|
+
.addColumn('type', 'varchar')
|
|
3175
|
+
.addColumn('created_at', 'datetime')
|
|
3176
|
+
.addColumn('updated_at', 'datetime')
|
|
3177
|
+
.execute();
|
|
3178
|
+
});
|
|
3179
|
+
afterEach(async () => {
|
|
3180
|
+
await db.destroy();
|
|
3181
|
+
});
|
|
3182
|
+
it('should return empty when all scenarios have entity_sha', async () => {
|
|
3183
|
+
await db
|
|
3184
|
+
.insertInto('editor_scenarios')
|
|
3185
|
+
.values({
|
|
3186
|
+
id: 'sc-1',
|
|
3187
|
+
project_id: projectId,
|
|
3188
|
+
name: 'Header - Default',
|
|
3189
|
+
component_name: 'Header',
|
|
3190
|
+
component_path: 'src/components/Header.tsx',
|
|
3191
|
+
entity_sha: 'sha-header',
|
|
3192
|
+
created_at: '2026-03-16 23:00:00',
|
|
3193
|
+
updated_at: '2026-03-16 23:00:00',
|
|
3194
|
+
})
|
|
3195
|
+
.execute();
|
|
3196
|
+
const result = await queryUnassociatedScenarios(db, projectId, null);
|
|
3197
|
+
expect(result).toEqual([]);
|
|
3198
|
+
});
|
|
3199
|
+
it('should find component scenarios with NULL entity_sha', async () => {
|
|
3200
|
+
// This reproduces the Margo testapp bug: subagent registered scenarios
|
|
3201
|
+
// but entity records didn't exist yet, so entity_sha was never set
|
|
3202
|
+
await db
|
|
3203
|
+
.insertInto('editor_scenarios')
|
|
3204
|
+
.values({
|
|
3205
|
+
id: 'sc-1',
|
|
3206
|
+
project_id: projectId,
|
|
3207
|
+
name: 'SearchBar - Default',
|
|
3208
|
+
component_name: 'SearchBar',
|
|
3209
|
+
component_path: 'src/components/SearchBar.tsx',
|
|
3210
|
+
entity_sha: null,
|
|
3211
|
+
created_at: '2026-03-20 18:45:00',
|
|
3212
|
+
updated_at: '2026-03-20 18:45:00',
|
|
3213
|
+
})
|
|
3214
|
+
.execute();
|
|
3215
|
+
await db
|
|
3216
|
+
.insertInto('editor_scenarios')
|
|
3217
|
+
.values({
|
|
3218
|
+
id: 'sc-2',
|
|
3219
|
+
project_id: projectId,
|
|
3220
|
+
name: 'SearchBar - With Results',
|
|
3221
|
+
component_name: 'SearchBar',
|
|
3222
|
+
component_path: 'src/components/SearchBar.tsx',
|
|
3223
|
+
entity_sha: null,
|
|
3224
|
+
created_at: '2026-03-20 18:45:05',
|
|
3225
|
+
updated_at: '2026-03-20 18:45:05',
|
|
3226
|
+
})
|
|
3227
|
+
.execute();
|
|
3228
|
+
const result = await queryUnassociatedScenarios(db, projectId, null);
|
|
3229
|
+
expect(result).toHaveLength(1);
|
|
3230
|
+
expect(result[0].name).toBe('SearchBar');
|
|
3231
|
+
expect(result[0].filePath).toBe('src/components/SearchBar.tsx');
|
|
3232
|
+
expect(result[0].scenarioCount).toBe(2);
|
|
3233
|
+
expect(result[0].scenarioNames).toEqual(expect.arrayContaining([
|
|
3234
|
+
'SearchBar - Default',
|
|
3235
|
+
'SearchBar - With Results',
|
|
3236
|
+
]));
|
|
3237
|
+
});
|
|
3238
|
+
it('should find page scenarios with NULL entity_sha', async () => {
|
|
3239
|
+
await db
|
|
3240
|
+
.insertInto('editor_scenarios')
|
|
3241
|
+
.values({
|
|
3242
|
+
id: 'sc-1',
|
|
3243
|
+
project_id: projectId,
|
|
3244
|
+
name: 'Full Page — Rich Library',
|
|
3245
|
+
component_name: null,
|
|
3246
|
+
component_path: null,
|
|
3247
|
+
page_file_path: 'src/library/LibraryApp.tsx',
|
|
3248
|
+
entity_sha: null,
|
|
3249
|
+
created_at: '2026-03-20 18:50:00',
|
|
3250
|
+
updated_at: '2026-03-20 18:50:00',
|
|
3251
|
+
})
|
|
3252
|
+
.execute();
|
|
3253
|
+
const result = await queryUnassociatedScenarios(db, projectId, null);
|
|
3254
|
+
expect(result).toHaveLength(1);
|
|
3255
|
+
expect(result[0].name).toBe('LibraryApp');
|
|
3256
|
+
expect(result[0].filePath).toBe('src/library/LibraryApp.tsx');
|
|
3257
|
+
expect(result[0].scenarioCount).toBe(1);
|
|
3258
|
+
});
|
|
3259
|
+
it('should ignore scenarios without any file path (orphans without component_path or page_file_path)', async () => {
|
|
3260
|
+
// Scenarios with no file path at all can't be associated — they're not
|
|
3261
|
+
// actionable, so don't report them as unassociated
|
|
3262
|
+
await db
|
|
3263
|
+
.insertInto('editor_scenarios')
|
|
3264
|
+
.values({
|
|
3265
|
+
id: 'sc-1',
|
|
3266
|
+
project_id: projectId,
|
|
3267
|
+
name: 'Some Orphan',
|
|
3268
|
+
component_name: null,
|
|
3269
|
+
component_path: null,
|
|
3270
|
+
page_file_path: null,
|
|
3271
|
+
entity_sha: null,
|
|
3272
|
+
created_at: '2026-03-20 18:50:00',
|
|
3273
|
+
updated_at: '2026-03-20 18:50:00',
|
|
3274
|
+
})
|
|
3275
|
+
.execute();
|
|
3276
|
+
const result = await queryUnassociatedScenarios(db, projectId, null);
|
|
3277
|
+
expect(result).toEqual([]);
|
|
3278
|
+
});
|
|
3279
|
+
it('should group multiple components separately', async () => {
|
|
3280
|
+
// Two different components both missing entity_sha
|
|
3281
|
+
await db
|
|
3282
|
+
.insertInto('editor_scenarios')
|
|
3283
|
+
.values({
|
|
3284
|
+
id: 'sc-1',
|
|
3285
|
+
project_id: projectId,
|
|
3286
|
+
name: 'FullPageHeader - Default',
|
|
3287
|
+
component_name: 'FullPageHeader',
|
|
3288
|
+
component_path: 'src/components/FullPageHeader.tsx',
|
|
3289
|
+
entity_sha: null,
|
|
3290
|
+
created_at: '2026-03-20 18:45:00',
|
|
3291
|
+
updated_at: '2026-03-20 18:45:00',
|
|
3292
|
+
})
|
|
3293
|
+
.execute();
|
|
3294
|
+
await db
|
|
3295
|
+
.insertInto('editor_scenarios')
|
|
3296
|
+
.values({
|
|
3297
|
+
id: 'sc-2',
|
|
3298
|
+
project_id: projectId,
|
|
3299
|
+
name: 'SaveConfirmation - Visible',
|
|
3300
|
+
component_name: 'SaveConfirmation',
|
|
3301
|
+
component_path: 'src/components/SaveConfirmation.tsx',
|
|
3302
|
+
entity_sha: null,
|
|
3303
|
+
created_at: '2026-03-20 19:00:00',
|
|
3304
|
+
updated_at: '2026-03-20 19:00:00',
|
|
3305
|
+
})
|
|
3306
|
+
.execute();
|
|
3307
|
+
const result = await queryUnassociatedScenarios(db, projectId, null);
|
|
3308
|
+
expect(result).toHaveLength(2);
|
|
3309
|
+
const names = result.map((r) => r.name).sort();
|
|
3310
|
+
expect(names).toEqual(['FullPageHeader', 'SaveConfirmation']);
|
|
3311
|
+
});
|
|
3312
|
+
it('should only include scenarios from the specified project', async () => {
|
|
3313
|
+
await db
|
|
3314
|
+
.insertInto('editor_scenarios')
|
|
3315
|
+
.values({
|
|
3316
|
+
id: 'sc-1',
|
|
3317
|
+
project_id: 'other-project',
|
|
3318
|
+
name: 'SearchBar - Default',
|
|
3319
|
+
component_name: 'SearchBar',
|
|
3320
|
+
component_path: 'src/components/SearchBar.tsx',
|
|
3321
|
+
entity_sha: null,
|
|
3322
|
+
created_at: '2026-03-20 18:45:00',
|
|
3323
|
+
updated_at: '2026-03-20 18:45:00',
|
|
3324
|
+
})
|
|
3325
|
+
.execute();
|
|
3326
|
+
const result = await queryUnassociatedScenarios(db, projectId, null);
|
|
3327
|
+
expect(result).toEqual([]);
|
|
3328
|
+
});
|
|
3329
|
+
it('should scope to feature session when featureStartedAt is provided', async () => {
|
|
3330
|
+
// Pre-existing unassociated scenario (before session)
|
|
3331
|
+
await db
|
|
3332
|
+
.insertInto('editor_scenarios')
|
|
3333
|
+
.values({
|
|
3334
|
+
id: 'sc-old',
|
|
3335
|
+
project_id: projectId,
|
|
3336
|
+
name: 'OldComponent - Default',
|
|
3337
|
+
component_name: 'OldComponent',
|
|
3338
|
+
component_path: 'src/components/OldComponent.tsx',
|
|
3339
|
+
entity_sha: null,
|
|
3340
|
+
created_at: '2026-03-19 10:00:00',
|
|
3341
|
+
updated_at: '2026-03-19 10:00:00',
|
|
3342
|
+
})
|
|
3343
|
+
.execute();
|
|
3344
|
+
// New unassociated scenario (during session)
|
|
3345
|
+
await db
|
|
3346
|
+
.insertInto('editor_scenarios')
|
|
3347
|
+
.values({
|
|
3348
|
+
id: 'sc-new',
|
|
3349
|
+
project_id: projectId,
|
|
3350
|
+
name: 'NewComponent - Default',
|
|
3351
|
+
component_name: 'NewComponent',
|
|
3352
|
+
component_path: 'src/components/NewComponent.tsx',
|
|
3353
|
+
entity_sha: null,
|
|
3354
|
+
created_at: '2026-03-20 18:45:00',
|
|
3355
|
+
updated_at: '2026-03-20 18:45:00',
|
|
3356
|
+
})
|
|
3357
|
+
.execute();
|
|
3358
|
+
const result = await queryUnassociatedScenarios(db, projectId, '2026-03-20T18:00:00.000Z');
|
|
3359
|
+
// Should only find the session-scoped one
|
|
3360
|
+
expect(result).toHaveLength(1);
|
|
3361
|
+
expect(result[0].name).toBe('NewComponent');
|
|
3362
|
+
});
|
|
3363
|
+
it('should include re-registered scenarios (updated_at in session) even if created before', async () => {
|
|
3364
|
+
await db
|
|
3365
|
+
.insertInto('editor_scenarios')
|
|
3366
|
+
.values({
|
|
3367
|
+
id: 'sc-1',
|
|
3368
|
+
project_id: projectId,
|
|
3369
|
+
name: 'SearchBar - Default',
|
|
3370
|
+
component_name: 'SearchBar',
|
|
3371
|
+
component_path: 'src/components/SearchBar.tsx',
|
|
3372
|
+
entity_sha: null,
|
|
3373
|
+
created_at: '2026-03-19 10:00:00',
|
|
3374
|
+
updated_at: '2026-03-20 18:45:00', // re-registered during session
|
|
3375
|
+
})
|
|
3376
|
+
.execute();
|
|
3377
|
+
const result = await queryUnassociatedScenarios(db, projectId, '2026-03-20T18:00:00.000Z');
|
|
3378
|
+
expect(result).toHaveLength(1);
|
|
3379
|
+
expect(result[0].name).toBe('SearchBar');
|
|
3380
|
+
});
|
|
3381
|
+
it('should not include scenarios with entity_sha set (even if stale)', async () => {
|
|
3382
|
+
// This scenario has an entity_sha — even if it's stale, that's a
|
|
3383
|
+
// different problem (handled by queryIncompleteEntities)
|
|
3384
|
+
await db
|
|
3385
|
+
.insertInto('editor_scenarios')
|
|
3386
|
+
.values({
|
|
3387
|
+
id: 'sc-1',
|
|
3388
|
+
project_id: projectId,
|
|
3389
|
+
name: 'Header - Default',
|
|
3390
|
+
component_name: 'Header',
|
|
3391
|
+
component_path: 'src/components/Header.tsx',
|
|
3392
|
+
entity_sha: 'sha-old-version',
|
|
3393
|
+
created_at: '2026-03-20 18:45:00',
|
|
3394
|
+
updated_at: '2026-03-20 18:45:00',
|
|
3395
|
+
})
|
|
3396
|
+
.execute();
|
|
3397
|
+
const result = await queryUnassociatedScenarios(db, projectId, null);
|
|
3398
|
+
expect(result).toEqual([]);
|
|
3399
|
+
});
|
|
3400
|
+
});
|
|
3401
|
+
// ── isAutoRemediable with unassociatedScenarios ────────────────────
|
|
3402
|
+
describe('isAutoRemediable always returns false (no inline full analysis)', () => {
|
|
3403
|
+
// Full analyze-imports takes minutes on large projects. The audit should
|
|
3404
|
+
// never trigger it — only the lightweight backfill path is acceptable.
|
|
3405
|
+
it('should return false for unassociatedScenarios only', () => {
|
|
3406
|
+
expect(isAutoRemediable({ unassociatedScenarios: 3 }, false)).toBe(false);
|
|
3407
|
+
});
|
|
3408
|
+
it('should return false for incompleteEntities + unassociatedScenarios', () => {
|
|
3409
|
+
expect(isAutoRemediable({ incompleteEntities: 1, unassociatedScenarios: 2 }, false)).toBe(false);
|
|
3410
|
+
});
|
|
3411
|
+
it('should return false even with no other failures', () => {
|
|
3412
|
+
expect(isAutoRemediable({ unassociatedScenarios: 2 }, false)).toBe(false);
|
|
3413
|
+
});
|
|
3414
|
+
it('should return false when already attempted', () => {
|
|
3415
|
+
expect(isAutoRemediable({ unassociatedScenarios: 3 }, true)).toBe(false);
|
|
3416
|
+
});
|
|
3417
|
+
});
|
|
3418
|
+
describe('suggestedTestFile for functions without testFile', () => {
|
|
3419
|
+
it('should suggest conventional .test.ts path when testFile is undefined', () => {
|
|
3420
|
+
const result = computeAudit({
|
|
3421
|
+
components: [],
|
|
3422
|
+
functions: [
|
|
3423
|
+
{ name: 'useLibraryShell', filePath: 'app/library/context.tsx' },
|
|
3424
|
+
],
|
|
3425
|
+
scenarioCounts: {},
|
|
3426
|
+
testFileExistence: {},
|
|
3427
|
+
});
|
|
3428
|
+
expect(result.functions[0].suggestedTestFile).toBe('app/library/context.test.ts');
|
|
3429
|
+
expect(result.functions[0].status).toBe('missing');
|
|
3430
|
+
});
|
|
3431
|
+
it('should suggest .test.ts for .ts files', () => {
|
|
3432
|
+
const result = computeAudit({
|
|
3433
|
+
components: [],
|
|
3434
|
+
functions: [{ name: 'calculatePrice', filePath: 'app/lib/pricing.ts' }],
|
|
3435
|
+
scenarioCounts: {},
|
|
3436
|
+
testFileExistence: {},
|
|
3437
|
+
});
|
|
3438
|
+
expect(result.functions[0].suggestedTestFile).toBe('app/lib/pricing.test.ts');
|
|
3439
|
+
});
|
|
3440
|
+
it('should not set suggestedTestFile when testFile is already specified', () => {
|
|
3441
|
+
const result = computeAudit({
|
|
3442
|
+
components: [],
|
|
3443
|
+
functions: [
|
|
3444
|
+
{
|
|
3445
|
+
name: 'calculatePrice',
|
|
3446
|
+
filePath: 'app/lib/pricing.ts',
|
|
3447
|
+
testFile: 'app/lib/pricing.test.ts',
|
|
3448
|
+
},
|
|
3449
|
+
],
|
|
3450
|
+
scenarioCounts: {},
|
|
3451
|
+
testFileExistence: { 'app/lib/pricing.test.ts': true },
|
|
3452
|
+
});
|
|
3453
|
+
expect(result.functions[0].suggestedTestFile).toBeUndefined();
|
|
3454
|
+
});
|
|
3455
|
+
});
|
|
3456
|
+
describe('hint for function audit entries', () => {
|
|
3457
|
+
it('should include a hint for name_mismatch functions explaining the fix', () => {
|
|
3458
|
+
// Claude sees "name mismatch" with no guidance on what it means or how
|
|
3459
|
+
// to fix it. The hint should explain that a top-level describe block
|
|
3460
|
+
// matching the function name is required for the CodeYam UI.
|
|
3461
|
+
const result = computeAudit({
|
|
3462
|
+
components: [],
|
|
3463
|
+
functions: [
|
|
3464
|
+
{
|
|
3465
|
+
name: 'useDrinks',
|
|
3466
|
+
filePath: 'app/hooks/useDrinks.ts',
|
|
3467
|
+
testFile: 'app/hooks/useDrinks.test.ts',
|
|
3468
|
+
},
|
|
3469
|
+
],
|
|
3470
|
+
scenarioCounts: {},
|
|
3471
|
+
testFileExistence: { 'app/hooks/useDrinks.test.ts': true },
|
|
3472
|
+
testResults: {
|
|
3473
|
+
'app/hooks/useDrinks.test.ts': {
|
|
3474
|
+
passing: true,
|
|
3475
|
+
hasEntityNameDescribe: false,
|
|
3476
|
+
},
|
|
3477
|
+
},
|
|
3478
|
+
});
|
|
3479
|
+
expect(result.functions[0].status).toBe('name_mismatch');
|
|
3480
|
+
expect(result.functions[0].hint).toBeDefined();
|
|
3481
|
+
expect(result.functions[0].hint).toContain('describe');
|
|
3482
|
+
expect(result.functions[0].hint).toContain('useDrinks');
|
|
3483
|
+
});
|
|
3484
|
+
it('should include a hint for runner_error functions showing the error', () => {
|
|
3485
|
+
// When the test runner crashes, Claude needs to see WHY it crashed
|
|
3486
|
+
// to fix the underlying issue. Without this, Claude loops re-running audit.
|
|
3487
|
+
const result = computeAudit({
|
|
3488
|
+
components: [],
|
|
3489
|
+
functions: [
|
|
3490
|
+
{
|
|
3491
|
+
name: 'getTimeAgo',
|
|
3492
|
+
filePath: 'src/lib/format.ts',
|
|
3493
|
+
testFile: 'src/lib/format.test.ts',
|
|
3494
|
+
},
|
|
3495
|
+
],
|
|
3496
|
+
scenarioCounts: {},
|
|
3497
|
+
testFileExistence: { 'src/lib/format.test.ts': true },
|
|
3498
|
+
testResults: {
|
|
3499
|
+
'src/lib/format.test.ts': {
|
|
3500
|
+
passing: false,
|
|
3501
|
+
hasEntityNameDescribe: false,
|
|
3502
|
+
errorMessage: 'Cannot find module "@/lib/format"',
|
|
3503
|
+
},
|
|
3504
|
+
},
|
|
3505
|
+
});
|
|
3506
|
+
expect(result.functions[0].status).toBe('runner_error');
|
|
3507
|
+
expect(result.functions[0].hint).toBeDefined();
|
|
3508
|
+
expect(result.functions[0].hint).toContain('Cannot find module');
|
|
3509
|
+
});
|
|
3510
|
+
});
|
|
3511
|
+
describe('hint for missing components', () => {
|
|
3512
|
+
it('should hint that layout files need app-level scenarios', () => {
|
|
3513
|
+
const result = computeAudit({
|
|
3514
|
+
components: [
|
|
3515
|
+
{ name: 'LibraryLayout', filePath: 'app/library/layout.tsx' },
|
|
3516
|
+
],
|
|
3517
|
+
functions: [],
|
|
3518
|
+
scenarioCounts: {},
|
|
3519
|
+
testFileExistence: {},
|
|
3520
|
+
});
|
|
3521
|
+
expect(result.components[0].hint).toContain('layout');
|
|
3522
|
+
expect(result.components[0].hint).toContain('pageFilePath');
|
|
3523
|
+
});
|
|
3524
|
+
it('should hint that page files need app-level scenarios', () => {
|
|
3525
|
+
const result = computeAudit({
|
|
3526
|
+
components: [
|
|
3527
|
+
{ name: 'InboxPage', filePath: 'app/library/inbox/page.tsx' },
|
|
3528
|
+
],
|
|
3529
|
+
functions: [],
|
|
3530
|
+
scenarioCounts: {},
|
|
3531
|
+
testFileExistence: {},
|
|
3532
|
+
});
|
|
3533
|
+
expect(result.components[0].hint).toContain('page');
|
|
3534
|
+
expect(result.components[0].hint).toContain('pageFilePath');
|
|
3535
|
+
});
|
|
3536
|
+
it('should hint that regular components need isolation routes', () => {
|
|
3537
|
+
const result = computeAudit({
|
|
3538
|
+
components: [
|
|
3539
|
+
{ name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
|
|
3540
|
+
],
|
|
3541
|
+
functions: [],
|
|
3542
|
+
scenarioCounts: {},
|
|
3543
|
+
testFileExistence: {},
|
|
3544
|
+
});
|
|
3545
|
+
expect(result.components[0].hint).toContain('isolated-components');
|
|
3546
|
+
});
|
|
3547
|
+
it('should not set hint when component has scenarios', () => {
|
|
3548
|
+
const result = computeAudit({
|
|
3549
|
+
components: [
|
|
3550
|
+
{ name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
|
|
3551
|
+
],
|
|
3552
|
+
functions: [],
|
|
3553
|
+
scenarioCounts: { DrinkCard: 2 },
|
|
3554
|
+
testFileExistence: {},
|
|
3555
|
+
});
|
|
3556
|
+
expect(result.components[0].hint).toBeUndefined();
|
|
3557
|
+
});
|
|
3558
|
+
it('should provide a hint for needs_recapture components', () => {
|
|
3559
|
+
// Components with needs_recapture status need guidance on what to do.
|
|
3560
|
+
// Without a hint, Claude has no instructions for fixing the issue.
|
|
3561
|
+
const result = computeAudit({
|
|
3562
|
+
components: [{ name: 'Library', filePath: 'app/library/page.tsx' }],
|
|
3563
|
+
functions: [],
|
|
3564
|
+
scenarioCounts: {},
|
|
3565
|
+
testFileExistence: {},
|
|
3566
|
+
totalScenarioCounts: { Library: 3 },
|
|
3567
|
+
entityChangeStatus: { Library: { status: 'impacted' } },
|
|
3568
|
+
});
|
|
3569
|
+
expect(result.components[0].status).toBe('needs_recapture');
|
|
3570
|
+
expect(result.components[0].hint).toBeDefined();
|
|
3571
|
+
expect(result.components[0].hint).toContain('recapture');
|
|
3572
|
+
});
|
|
3573
|
+
});
|
|
3574
|
+
describe('formatIncompleteEntityGuidance', () => {
|
|
3575
|
+
it('should include the entity name and scenario count', () => {
|
|
3576
|
+
const { formatIncompleteEntityGuidance } = require('../editorAudit');
|
|
3577
|
+
const result = formatIncompleteEntityGuidance({
|
|
3578
|
+
entitySha: 'abc123',
|
|
3579
|
+
name: 'RuleBuilder',
|
|
3580
|
+
scenarioCount: 5,
|
|
3581
|
+
preExisting: false,
|
|
3582
|
+
});
|
|
3583
|
+
expect(result).toContain('RuleBuilder');
|
|
3584
|
+
expect(result).toContain('5');
|
|
3585
|
+
});
|
|
3586
|
+
it('should tell Claude the exact fix command', () => {
|
|
3587
|
+
const { formatIncompleteEntityGuidance } = require('../editorAudit');
|
|
3588
|
+
const result = formatIncompleteEntityGuidance({
|
|
3589
|
+
entitySha: 'abc123',
|
|
3590
|
+
name: 'RuleBuilder',
|
|
3591
|
+
scenarioCount: 5,
|
|
3592
|
+
preExisting: false,
|
|
3593
|
+
});
|
|
3594
|
+
expect(result).toContain('codeyam editor analyze-imports');
|
|
3595
|
+
});
|
|
3596
|
+
it('should flag pre-existing issues as non-blocking', () => {
|
|
3597
|
+
const { formatIncompleteEntityGuidance } = require('../editorAudit');
|
|
3598
|
+
const result = formatIncompleteEntityGuidance({
|
|
3599
|
+
entitySha: 'abc123',
|
|
3600
|
+
name: 'RuleBuilder',
|
|
3601
|
+
scenarioCount: 5,
|
|
3602
|
+
preExisting: true,
|
|
3603
|
+
});
|
|
3604
|
+
expect(result).toContain('pre-existing');
|
|
3605
|
+
});
|
|
3606
|
+
it('should explain what incomplete means', () => {
|
|
3607
|
+
const { formatIncompleteEntityGuidance } = require('../editorAudit');
|
|
3608
|
+
const result = formatIncompleteEntityGuidance({
|
|
3609
|
+
entitySha: 'abc123',
|
|
3610
|
+
name: 'RuleBuilder',
|
|
3611
|
+
scenarioCount: 5,
|
|
3612
|
+
preExisting: false,
|
|
3613
|
+
});
|
|
3614
|
+
// Should explain the root cause, not just the symptom
|
|
3615
|
+
expect(result).toMatch(/scenario.*without.*import graph|import graph.*not.*built/i);
|
|
3616
|
+
});
|
|
3617
|
+
});
|
|
3618
|
+
describe('formatManualAnalysisGuidance', () => {
|
|
3619
|
+
it('should include entity name, scenario count, and error message', () => {
|
|
3620
|
+
const { formatManualAnalysisGuidance } = require('../editorAudit');
|
|
3621
|
+
const result = formatManualAnalysisGuidance({
|
|
3622
|
+
name: 'Header',
|
|
3623
|
+
filePath: 'src/components/Header.tsx',
|
|
3624
|
+
scenarioCount: 3,
|
|
3625
|
+
error: 'TypeScript parsing error: unexpected token',
|
|
3626
|
+
});
|
|
3627
|
+
expect(result).toContain('Header');
|
|
3628
|
+
expect(result).toContain('3 scenario(s)');
|
|
3629
|
+
expect(result).toContain('TypeScript parsing error');
|
|
3630
|
+
});
|
|
3631
|
+
it('should include MANUAL ANALYSIS REQUIRED header', () => {
|
|
3632
|
+
const { formatManualAnalysisGuidance } = require('../editorAudit');
|
|
3633
|
+
const result = formatManualAnalysisGuidance({
|
|
3634
|
+
name: 'Header',
|
|
3635
|
+
filePath: 'src/components/Header.tsx',
|
|
3636
|
+
scenarioCount: 2,
|
|
3637
|
+
error: 'Parse error',
|
|
3638
|
+
});
|
|
3639
|
+
expect(result).toContain('MANUAL ANALYSIS REQUIRED');
|
|
3640
|
+
});
|
|
3641
|
+
it('should tell Claude to read the source file', () => {
|
|
3642
|
+
const { formatManualAnalysisGuidance } = require('../editorAudit');
|
|
3643
|
+
const result = formatManualAnalysisGuidance({
|
|
3644
|
+
name: 'Header',
|
|
3645
|
+
filePath: 'src/components/Header.tsx',
|
|
3646
|
+
scenarioCount: 2,
|
|
3647
|
+
error: 'Parse error',
|
|
3648
|
+
});
|
|
3649
|
+
expect(result).toContain('Read src/components/Header.tsx');
|
|
3650
|
+
});
|
|
3651
|
+
it('should include the manual-entity command', () => {
|
|
3652
|
+
const { formatManualAnalysisGuidance } = require('../editorAudit');
|
|
3653
|
+
const result = formatManualAnalysisGuidance({
|
|
3654
|
+
name: 'Header',
|
|
3655
|
+
filePath: 'src/components/Header.tsx',
|
|
3656
|
+
scenarioCount: 2,
|
|
3657
|
+
error: 'Parse error',
|
|
3658
|
+
});
|
|
3659
|
+
expect(result).toContain('codeyam editor manual-entity');
|
|
3660
|
+
});
|
|
3661
|
+
it('should tell Claude to check glossary for imports', () => {
|
|
3662
|
+
const { formatManualAnalysisGuidance } = require('../editorAudit');
|
|
3663
|
+
const result = formatManualAnalysisGuidance({
|
|
3664
|
+
name: 'Header',
|
|
3665
|
+
filePath: 'src/components/Header.tsx',
|
|
3666
|
+
scenarioCount: 2,
|
|
3667
|
+
error: 'Parse error',
|
|
3668
|
+
});
|
|
3669
|
+
expect(result).toContain('glossary');
|
|
3670
|
+
});
|
|
3671
|
+
});
|
|
3672
|
+
describe('getIncompleteEntityFilePaths', () => {
|
|
3673
|
+
// The audit should auto-fix incomplete entities by running analysis on
|
|
3674
|
+
// just their specific file paths, not all 117+ files. This function
|
|
3675
|
+
// resolves entity SHAs to file paths for targeted analysis.
|
|
3676
|
+
let db;
|
|
3677
|
+
let rawDb;
|
|
3678
|
+
beforeEach(async () => {
|
|
3679
|
+
rawDb = new Database(':memory:');
|
|
3680
|
+
db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
|
|
3681
|
+
await db.schema
|
|
3682
|
+
.createTable('entities')
|
|
3683
|
+
.addColumn('sha', 'varchar', (col) => col.primaryKey())
|
|
3684
|
+
.addColumn('name', 'varchar')
|
|
3685
|
+
.addColumn('file_path', 'varchar')
|
|
3686
|
+
.addColumn('project_id', 'varchar')
|
|
3687
|
+
.addColumn('entity_type', 'varchar')
|
|
3688
|
+
.execute();
|
|
3689
|
+
});
|
|
3690
|
+
afterEach(() => {
|
|
3691
|
+
rawDb.close();
|
|
3692
|
+
});
|
|
3693
|
+
it('should resolve entity SHAs to file paths from the entities table', async () => {
|
|
3694
|
+
const { getIncompleteEntityFilePaths } = require('../editorAudit');
|
|
3695
|
+
await db
|
|
3696
|
+
.insertInto('entities')
|
|
3697
|
+
.values([
|
|
3698
|
+
{
|
|
3699
|
+
sha: 'sha-rule',
|
|
3700
|
+
name: 'RuleBuilder',
|
|
3701
|
+
file_path: 'app/components/RuleBuilder.tsx',
|
|
3702
|
+
project_id: 'p1',
|
|
3703
|
+
entity_type: 'component',
|
|
3704
|
+
},
|
|
3705
|
+
{
|
|
3706
|
+
sha: 'sha-row',
|
|
3707
|
+
name: 'ArticleTableRow',
|
|
3708
|
+
file_path: 'app/components/ArticleTableRow.tsx',
|
|
3709
|
+
project_id: 'p1',
|
|
3710
|
+
entity_type: 'component',
|
|
3711
|
+
},
|
|
3712
|
+
])
|
|
3713
|
+
.execute();
|
|
3714
|
+
const result = await getIncompleteEntityFilePaths(db, [
|
|
3715
|
+
{
|
|
3716
|
+
entitySha: 'sha-rule',
|
|
3717
|
+
name: 'RuleBuilder',
|
|
3718
|
+
scenarioCount: 5,
|
|
3719
|
+
preExisting: false,
|
|
3720
|
+
},
|
|
3721
|
+
{
|
|
3722
|
+
entitySha: 'sha-row',
|
|
3723
|
+
name: 'ArticleTableRow',
|
|
3724
|
+
scenarioCount: 2,
|
|
3725
|
+
preExisting: false,
|
|
3726
|
+
},
|
|
3727
|
+
]);
|
|
3728
|
+
expect(result).toContain('app/components/RuleBuilder.tsx');
|
|
3729
|
+
expect(result).toContain('app/components/ArticleTableRow.tsx');
|
|
3730
|
+
expect(result).toHaveLength(2);
|
|
3731
|
+
});
|
|
3732
|
+
it('should skip entities whose SHA is not in the entities table', async () => {
|
|
3733
|
+
const { getIncompleteEntityFilePaths } = require('../editorAudit');
|
|
3734
|
+
const result = await getIncompleteEntityFilePaths(db, [
|
|
3735
|
+
{
|
|
3736
|
+
entitySha: 'nonexistent-sha',
|
|
3737
|
+
name: 'Ghost',
|
|
3738
|
+
scenarioCount: 1,
|
|
3739
|
+
preExisting: false,
|
|
3740
|
+
},
|
|
3741
|
+
]);
|
|
3742
|
+
expect(result).toHaveLength(0);
|
|
3743
|
+
});
|
|
3744
|
+
it('should deduplicate file paths', async () => {
|
|
3745
|
+
const { getIncompleteEntityFilePaths } = require('../editorAudit');
|
|
3746
|
+
await db
|
|
3747
|
+
.insertInto('entities')
|
|
3748
|
+
.values([
|
|
3749
|
+
{
|
|
3750
|
+
sha: 'sha-v1',
|
|
3751
|
+
name: 'Foo',
|
|
3752
|
+
file_path: 'app/Foo.tsx',
|
|
3753
|
+
project_id: 'p1',
|
|
3754
|
+
entity_type: 'component',
|
|
3755
|
+
},
|
|
3756
|
+
{
|
|
3757
|
+
sha: 'sha-v2',
|
|
3758
|
+
name: 'Foo',
|
|
3759
|
+
file_path: 'app/Foo.tsx',
|
|
3760
|
+
project_id: 'p1',
|
|
3761
|
+
entity_type: 'component',
|
|
3762
|
+
},
|
|
3763
|
+
])
|
|
3764
|
+
.execute();
|
|
3765
|
+
const result = await getIncompleteEntityFilePaths(db, [
|
|
3766
|
+
{
|
|
3767
|
+
entitySha: 'sha-v1',
|
|
3768
|
+
name: 'Foo',
|
|
3769
|
+
scenarioCount: 3,
|
|
3770
|
+
preExisting: false,
|
|
3771
|
+
},
|
|
3772
|
+
{
|
|
3773
|
+
entitySha: 'sha-v2',
|
|
3774
|
+
name: 'Foo',
|
|
3775
|
+
scenarioCount: 1,
|
|
3776
|
+
preExisting: false,
|
|
3777
|
+
},
|
|
3778
|
+
]);
|
|
3779
|
+
expect(result).toEqual(['app/Foo.tsx']);
|
|
3780
|
+
});
|
|
3781
|
+
});
|
|
3782
|
+
describe('isAutoRemediable never triggers full analysis', () => {
|
|
3783
|
+
// The audit must NEVER run handleAnalyzeImports inline — it takes minutes
|
|
3784
|
+
// for large projects. Auto-remediation should only do the lightweight
|
|
3785
|
+
// entity SHA backfill. isAutoRemediable is now always false; the callers
|
|
3786
|
+
// use needsBackfillOnly for the fast path instead.
|
|
3787
|
+
it('should always return false regardless of summary state', () => {
|
|
3788
|
+
expect(isAutoRemediable({ incompleteEntities: 5 }, false)).toBe(false);
|
|
3789
|
+
expect(isAutoRemediable({ unassociatedScenarios: 3 }, false)).toBe(false);
|
|
3790
|
+
expect(isAutoRemediable({ incompleteEntities: 1, unassociatedScenarios: 2 }, false)).toBe(false);
|
|
3791
|
+
});
|
|
3792
|
+
});
|
|
3793
|
+
// ── isOnlyPreExistingIncomplete ─────────────────────────────────────
|
|
3794
|
+
describe('isOnlyPreExistingIncomplete', () => {
|
|
3795
|
+
it('should return true when all incomplete entities are pre-existing and no other failures', () => {
|
|
3796
|
+
expect(isOnlyPreExistingIncomplete({
|
|
3797
|
+
incompleteEntities: 2,
|
|
3798
|
+
preExistingIncompleteEntities: 2,
|
|
3799
|
+
componentsMissing: 0,
|
|
3800
|
+
componentsWithErrors: 0,
|
|
3801
|
+
functionsFailing: 0,
|
|
3802
|
+
functionsNameMismatch: 0,
|
|
3803
|
+
functionsMissing: 0,
|
|
3804
|
+
missingFromGlossary: 0,
|
|
3805
|
+
miscategorizedScenarios: 0,
|
|
3806
|
+
scenariosNeedingRecapture: 0,
|
|
3807
|
+
}, [
|
|
3808
|
+
{
|
|
3809
|
+
entitySha: 'a',
|
|
3810
|
+
name: 'RuleBuilder',
|
|
3811
|
+
scenarioCount: 5,
|
|
3812
|
+
preExisting: true,
|
|
3813
|
+
},
|
|
3814
|
+
{
|
|
3815
|
+
entitySha: 'b',
|
|
3816
|
+
name: 'ArticleTableRow',
|
|
3817
|
+
scenarioCount: 2,
|
|
3818
|
+
preExisting: true,
|
|
3819
|
+
},
|
|
3820
|
+
])).toBe(true);
|
|
3821
|
+
});
|
|
3822
|
+
it('should return false when there are runner errors even if all incompletes are pre-existing', () => {
|
|
3823
|
+
// Safety net: runner errors (crashed test runner) must NEVER bypass the gate.
|
|
3824
|
+
// Without this test, a regression removing functionsRunnerError from
|
|
3825
|
+
// isOnlyIncompleteEntities would silently let broken test infrastructure through.
|
|
3826
|
+
expect(isOnlyPreExistingIncomplete({
|
|
3827
|
+
incompleteEntities: 1,
|
|
3828
|
+
preExistingIncompleteEntities: 1,
|
|
3829
|
+
componentsMissing: 0,
|
|
3830
|
+
componentsWithErrors: 0,
|
|
3831
|
+
functionsFailing: 0,
|
|
3832
|
+
functionsRunnerError: 1,
|
|
3833
|
+
functionsNameMismatch: 0,
|
|
3834
|
+
functionsMissing: 0,
|
|
3835
|
+
missingFromGlossary: 0,
|
|
3836
|
+
}, [
|
|
3837
|
+
{
|
|
3838
|
+
entitySha: 'a',
|
|
3839
|
+
name: 'OldComponent',
|
|
3840
|
+
scenarioCount: 1,
|
|
3841
|
+
preExisting: true,
|
|
3842
|
+
},
|
|
3843
|
+
])).toBe(false);
|
|
3844
|
+
});
|
|
3845
|
+
it('should return false when some incomplete entities are NOT pre-existing', () => {
|
|
3846
|
+
expect(isOnlyPreExistingIncomplete({
|
|
3847
|
+
incompleteEntities: 2,
|
|
3848
|
+
preExistingIncompleteEntities: 1,
|
|
3849
|
+
componentsMissing: 0,
|
|
3850
|
+
componentsWithErrors: 0,
|
|
3851
|
+
functionsFailing: 0,
|
|
3852
|
+
functionsNameMismatch: 0,
|
|
3853
|
+
functionsMissing: 0,
|
|
3854
|
+
missingFromGlossary: 0,
|
|
3855
|
+
miscategorizedScenarios: 0,
|
|
3856
|
+
scenariosNeedingRecapture: 0,
|
|
3857
|
+
}, [
|
|
3858
|
+
{
|
|
3859
|
+
entitySha: 'a',
|
|
3860
|
+
name: 'RuleBuilder',
|
|
3861
|
+
scenarioCount: 5,
|
|
3862
|
+
preExisting: true,
|
|
3863
|
+
},
|
|
3864
|
+
{
|
|
3865
|
+
entitySha: 'b',
|
|
3866
|
+
name: 'NewComponent',
|
|
3867
|
+
scenarioCount: 1,
|
|
3868
|
+
preExisting: false,
|
|
3869
|
+
},
|
|
3870
|
+
])).toBe(false);
|
|
3871
|
+
});
|
|
3872
|
+
it('should return false when there are other failures besides incomplete entities', () => {
|
|
3873
|
+
expect(isOnlyPreExistingIncomplete({
|
|
3874
|
+
incompleteEntities: 2,
|
|
3875
|
+
preExistingIncompleteEntities: 2,
|
|
3876
|
+
componentsMissing: 1,
|
|
3877
|
+
}, [
|
|
3878
|
+
{
|
|
3879
|
+
entitySha: 'a',
|
|
3880
|
+
name: 'RuleBuilder',
|
|
3881
|
+
scenarioCount: 5,
|
|
3882
|
+
preExisting: true,
|
|
3883
|
+
},
|
|
3884
|
+
{
|
|
3885
|
+
entitySha: 'b',
|
|
3886
|
+
name: 'ArticleTableRow',
|
|
3887
|
+
scenarioCount: 2,
|
|
3888
|
+
preExisting: true,
|
|
3889
|
+
},
|
|
3890
|
+
])).toBe(false);
|
|
3891
|
+
});
|
|
3892
|
+
it('should return false when incomplete entities array is empty', () => {
|
|
3893
|
+
expect(isOnlyPreExistingIncomplete({ incompleteEntities: 0 }, [])).toBe(false);
|
|
3894
|
+
});
|
|
3895
|
+
it('should return false when incomplete entities array is missing', () => {
|
|
3896
|
+
expect(isOnlyPreExistingIncomplete({ incompleteEntities: 2, preExistingIncompleteEntities: 2 }, undefined)).toBe(false);
|
|
3897
|
+
});
|
|
3898
|
+
});
|
|
3899
|
+
describe('isOnlyIncompleteEntities with unassociatedScenarios', () => {
|
|
3900
|
+
it('should return true when only unassociatedScenarios present', () => {
|
|
3901
|
+
expect(isOnlyIncompleteEntities({ unassociatedScenarios: 5 })).toBe(true);
|
|
3902
|
+
});
|
|
3903
|
+
it('should return false when unassociatedScenarios present with other failures', () => {
|
|
3904
|
+
expect(isOnlyIncompleteEntities({
|
|
3905
|
+
unassociatedScenarios: 5,
|
|
3906
|
+
functionsMissing: 1,
|
|
3907
|
+
})).toBe(false);
|
|
3908
|
+
});
|
|
3909
|
+
});
|
|
3910
|
+
// ── aggregateClientErrorsByComponent ─────────────────────────────────
|
|
3911
|
+
describe('aggregateClientErrorsByComponent', () => {
|
|
3912
|
+
it('should attribute errors to component using componentName from metadata', () => {
|
|
3913
|
+
const result = aggregateClientErrorsByComponent({
|
|
3914
|
+
'scenario-1': {
|
|
3915
|
+
scenarioName: 'DrinkCard - Default',
|
|
3916
|
+
errors: ['TypeError: Cannot read property "price"'],
|
|
3917
|
+
},
|
|
3918
|
+
}, [
|
|
3919
|
+
{
|
|
3920
|
+
name: 'DrinkCard - Default',
|
|
3921
|
+
componentName: 'DrinkCard',
|
|
3922
|
+
},
|
|
3923
|
+
]);
|
|
3924
|
+
expect(result['DrinkCard']).toEqual([
|
|
3925
|
+
'TypeError: Cannot read property "price"',
|
|
3926
|
+
]);
|
|
3927
|
+
});
|
|
3928
|
+
it('should attribute app-level scenario errors using pageFilePath', () => {
|
|
3929
|
+
// App-level scenarios have componentName=null. The old approach
|
|
3930
|
+
// parsed the scenario name "Full Page with Library" and got
|
|
3931
|
+
// "Full Page with Library" as the component — which matches nothing.
|
|
3932
|
+
// With metadata, we derive the entity name from pageFilePath.
|
|
3933
|
+
const result = aggregateClientErrorsByComponent({
|
|
3934
|
+
'scenario-1': {
|
|
3935
|
+
scenarioName: 'Full Page with Library',
|
|
3936
|
+
errors: ['TypeError: Cannot read property "title"'],
|
|
3937
|
+
},
|
|
3938
|
+
}, [
|
|
3939
|
+
{
|
|
3940
|
+
name: 'Full Page with Library',
|
|
3941
|
+
componentName: null,
|
|
3942
|
+
pageFilePath: 'app/library/page.tsx',
|
|
3943
|
+
},
|
|
3944
|
+
]);
|
|
3945
|
+
// Should be keyed by the canonical entity name from scenarioEntityName()
|
|
3946
|
+
expect(result['Library']).toEqual([
|
|
3947
|
+
'TypeError: Cannot read property "title"',
|
|
3948
|
+
]);
|
|
3949
|
+
});
|
|
3950
|
+
it('should fall back to scenario name parsing when no metadata match exists', () => {
|
|
3951
|
+
// If the scenario is not in the metadata list (e.g., metadata not yet loaded),
|
|
3952
|
+
// fall back to the "ComponentName - Variant" convention
|
|
3953
|
+
const result = aggregateClientErrorsByComponent({
|
|
3954
|
+
'scenario-1': {
|
|
3955
|
+
scenarioName: 'Header - Dark Mode',
|
|
3956
|
+
errors: ['ReferenceError: theme is not defined'],
|
|
3957
|
+
},
|
|
3958
|
+
}, []);
|
|
3959
|
+
expect(result['Header']).toEqual([
|
|
3960
|
+
'ReferenceError: theme is not defined',
|
|
3961
|
+
]);
|
|
3962
|
+
});
|
|
3963
|
+
it('should handle non-route file paths via scenarioEntityName', () => {
|
|
3964
|
+
// Non-route files (src/...) are treated as app entry points by
|
|
3965
|
+
// scenarioEntityName → buildRoutePattern returns '/' → 'Home'.
|
|
3966
|
+
// When a url is available, it provides a better entity name.
|
|
3967
|
+
const result = aggregateClientErrorsByComponent({
|
|
3968
|
+
'scenario-1': {
|
|
3969
|
+
scenarioName: 'LibraryApp - Rich',
|
|
3970
|
+
errors: ['Error: fetch failed'],
|
|
3971
|
+
},
|
|
3972
|
+
}, [
|
|
3973
|
+
{
|
|
3974
|
+
name: 'LibraryApp - Rich',
|
|
3975
|
+
componentName: null,
|
|
3976
|
+
pageFilePath: 'src/library/LibraryApp.tsx',
|
|
3977
|
+
url: '/library',
|
|
3978
|
+
},
|
|
3979
|
+
]);
|
|
3980
|
+
// scenarioEntityName uses url fallback when pageFilePath is a non-route file
|
|
3981
|
+
// pageFilePath 'src/...' → buildRoutePattern → '/' → routeDisplayName → 'Home'
|
|
3982
|
+
// But componentName takes priority, and with url '/library' as final fallback
|
|
3983
|
+
// scenarioEntityName({ pageFilePath: 'src/library/LibraryApp.tsx' }) → 'Home'
|
|
3984
|
+
// because pageFilePath is checked before url
|
|
3985
|
+
expect(result['Home']).toEqual(['Error: fetch failed']);
|
|
3986
|
+
});
|
|
3987
|
+
it('should skip scenarios with no errors', () => {
|
|
3988
|
+
const result = aggregateClientErrorsByComponent({
|
|
3989
|
+
'scenario-1': {
|
|
3990
|
+
scenarioName: 'DrinkCard - Default',
|
|
3991
|
+
errors: [],
|
|
3992
|
+
},
|
|
3993
|
+
}, [
|
|
3994
|
+
{
|
|
3995
|
+
name: 'DrinkCard - Default',
|
|
3996
|
+
componentName: 'DrinkCard',
|
|
3997
|
+
},
|
|
3998
|
+
]);
|
|
3999
|
+
expect(result).toEqual({});
|
|
4000
|
+
});
|
|
4001
|
+
it('should aggregate errors from multiple scenarios for same component', () => {
|
|
4002
|
+
const result = aggregateClientErrorsByComponent({
|
|
4003
|
+
'scenario-1': {
|
|
4004
|
+
scenarioName: 'DrinkCard - Default',
|
|
4005
|
+
errors: ['Error A'],
|
|
4006
|
+
},
|
|
4007
|
+
'scenario-2': {
|
|
4008
|
+
scenarioName: 'DrinkCard - Hover',
|
|
4009
|
+
errors: ['Error B', 'Error C'],
|
|
4010
|
+
},
|
|
4011
|
+
}, [
|
|
4012
|
+
{ name: 'DrinkCard - Default', componentName: 'DrinkCard' },
|
|
4013
|
+
{ name: 'DrinkCard - Hover', componentName: 'DrinkCard' },
|
|
4014
|
+
]);
|
|
4015
|
+
expect(result['DrinkCard']).toEqual(['Error A', 'Error B', 'Error C']);
|
|
4016
|
+
});
|
|
4017
|
+
it('should use capitalized display name for page file paths, not lowercase directory', () => {
|
|
4018
|
+
// aggregateClientErrorsByComponent must produce keys matching scenarioEntityName().
|
|
4019
|
+
// scenarioEntityName({ pageFilePath: 'app/library/page.tsx' }) returns 'Library',
|
|
4020
|
+
// so the key must be 'Library' — not 'library' (the raw directory name).
|
|
4021
|
+
// computeAudit checks clientErrors[glossaryEntryName], so a lowercase key
|
|
4022
|
+
// will never match, silently dropping all client errors for page-level scenarios.
|
|
4023
|
+
const result = aggregateClientErrorsByComponent({
|
|
4024
|
+
'sc-1': {
|
|
4025
|
+
scenarioName: 'Library - Default',
|
|
4026
|
+
errors: ['TypeError: fetch failed'],
|
|
4027
|
+
},
|
|
4028
|
+
}, [
|
|
4029
|
+
{
|
|
4030
|
+
name: 'Library - Default',
|
|
4031
|
+
componentName: null,
|
|
4032
|
+
pageFilePath: 'app/library/page.tsx',
|
|
4033
|
+
},
|
|
4034
|
+
]);
|
|
4035
|
+
expect(result).toHaveProperty('Library');
|
|
4036
|
+
expect(result['Library']).toEqual(['TypeError: fetch failed']);
|
|
4037
|
+
});
|
|
4038
|
+
it('should use "Home" for root page app/page.tsx, not "app"', () => {
|
|
4039
|
+
// Root page: scenarioEntityName returns 'Home', not 'app'.
|
|
4040
|
+
// Custom path parsing incorrectly pops the parent dir ('app').
|
|
4041
|
+
const result = aggregateClientErrorsByComponent({
|
|
4042
|
+
'sc-1': {
|
|
4043
|
+
scenarioName: 'Home - Default',
|
|
4044
|
+
errors: ['ReferenceError: window is not defined'],
|
|
4045
|
+
},
|
|
4046
|
+
}, [
|
|
4047
|
+
{
|
|
4048
|
+
name: 'Home - Default',
|
|
4049
|
+
componentName: null,
|
|
4050
|
+
pageFilePath: 'app/page.tsx',
|
|
4051
|
+
},
|
|
4052
|
+
]);
|
|
4053
|
+
expect(result).toHaveProperty('Home');
|
|
4054
|
+
expect(result['Home']).toEqual(['ReferenceError: window is not defined']);
|
|
4055
|
+
});
|
|
4056
|
+
});
|
|
4057
|
+
// ── determineTargetedAnalysisPaths ──────────────────────────────────
|
|
4058
|
+
describe('determineTargetedAnalysisPaths', () => {
|
|
4059
|
+
it('should return unique file paths from unassociated scenarios', () => {
|
|
4060
|
+
const result = determineTargetedAnalysisPaths({
|
|
4061
|
+
unassociatedScenarios: [
|
|
4062
|
+
{ filePath: 'app/components/CreateFromFiltersButton.tsx' },
|
|
4063
|
+
{ filePath: 'app/components/StaleDismissedBanner.tsx' },
|
|
4064
|
+
],
|
|
4065
|
+
incompleteEntities: [],
|
|
4066
|
+
});
|
|
4067
|
+
expect(result).toEqual([
|
|
4068
|
+
'app/components/CreateFromFiltersButton.tsx',
|
|
4069
|
+
'app/components/StaleDismissedBanner.tsx',
|
|
4070
|
+
]);
|
|
4071
|
+
});
|
|
4072
|
+
it('should include file paths from incomplete entities', () => {
|
|
4073
|
+
const result = determineTargetedAnalysisPaths({
|
|
4074
|
+
unassociatedScenarios: [{ filePath: 'app/components/TaskCard.tsx' }],
|
|
4075
|
+
incompleteEntities: [{ filePath: 'app/components/Dashboard.tsx' }],
|
|
4076
|
+
});
|
|
4077
|
+
expect(result).toEqual([
|
|
4078
|
+
'app/components/TaskCard.tsx',
|
|
4079
|
+
'app/components/Dashboard.tsx',
|
|
4080
|
+
]);
|
|
4081
|
+
});
|
|
4082
|
+
it('should deduplicate file paths across both sources', () => {
|
|
4083
|
+
const result = determineTargetedAnalysisPaths({
|
|
4084
|
+
unassociatedScenarios: [{ filePath: 'app/components/TaskCard.tsx' }],
|
|
4085
|
+
incompleteEntities: [{ filePath: 'app/components/TaskCard.tsx' }],
|
|
4086
|
+
});
|
|
4087
|
+
expect(result).toEqual(['app/components/TaskCard.tsx']);
|
|
4088
|
+
});
|
|
4089
|
+
it('should return empty array when no issues', () => {
|
|
4090
|
+
const result = determineTargetedAnalysisPaths({
|
|
4091
|
+
unassociatedScenarios: [],
|
|
4092
|
+
incompleteEntities: [],
|
|
4093
|
+
});
|
|
4094
|
+
expect(result).toEqual([]);
|
|
4095
|
+
});
|
|
4096
|
+
it('should filter out entries with empty file paths', () => {
|
|
4097
|
+
const result = determineTargetedAnalysisPaths({
|
|
4098
|
+
unassociatedScenarios: [{ filePath: '' }],
|
|
4099
|
+
incompleteEntities: [],
|
|
4100
|
+
});
|
|
4101
|
+
expect(result).toEqual([]);
|
|
4102
|
+
});
|
|
4103
|
+
it('should handle incomplete entities with undefined filePath', () => {
|
|
4104
|
+
const result = determineTargetedAnalysisPaths({
|
|
4105
|
+
unassociatedScenarios: [{ filePath: 'app/components/Good.tsx' }],
|
|
4106
|
+
incompleteEntities: [
|
|
4107
|
+
{
|
|
4108
|
+
/* no filePath at all */
|
|
4109
|
+
},
|
|
4110
|
+
{ filePath: undefined },
|
|
4111
|
+
{ filePath: 'app/components/AlsoGood.tsx' },
|
|
4112
|
+
],
|
|
4113
|
+
});
|
|
4114
|
+
expect(result).toEqual([
|
|
4115
|
+
'app/components/Good.tsx',
|
|
4116
|
+
'app/components/AlsoGood.tsx',
|
|
4117
|
+
]);
|
|
4118
|
+
});
|
|
4119
|
+
});
|
|
4120
|
+
// ── shouldAutoRecapture ─────────────────────────────────────────────
|
|
4121
|
+
describe('shouldAutoRecapture', () => {
|
|
4122
|
+
it('should return true when fix flag is set and stale scenarios exist', () => {
|
|
4123
|
+
expect(shouldAutoRecapture({
|
|
4124
|
+
fix: true,
|
|
4125
|
+
scenariosNeedingRecapture: [
|
|
4126
|
+
{
|
|
4127
|
+
scenarioName: 'Default',
|
|
4128
|
+
entityName: 'TaskCard',
|
|
4129
|
+
status: { status: 'edited' },
|
|
4130
|
+
},
|
|
4131
|
+
],
|
|
4132
|
+
})).toBe(true);
|
|
4133
|
+
});
|
|
4134
|
+
it('should return false when fix flag is not set', () => {
|
|
4135
|
+
expect(shouldAutoRecapture({
|
|
4136
|
+
fix: false,
|
|
4137
|
+
scenariosNeedingRecapture: [
|
|
4138
|
+
{
|
|
4139
|
+
scenarioName: 'Default',
|
|
4140
|
+
entityName: 'TaskCard',
|
|
4141
|
+
status: { status: 'edited' },
|
|
4142
|
+
},
|
|
4143
|
+
],
|
|
4144
|
+
})).toBe(false);
|
|
4145
|
+
});
|
|
4146
|
+
it('should return false when fix is set but no stale scenarios', () => {
|
|
4147
|
+
expect(shouldAutoRecapture({
|
|
4148
|
+
fix: true,
|
|
4149
|
+
scenariosNeedingRecapture: [],
|
|
4150
|
+
})).toBe(false);
|
|
4151
|
+
});
|
|
4152
|
+
it('should return false when both are falsy', () => {
|
|
4153
|
+
expect(shouldAutoRecapture({
|
|
4154
|
+
fix: false,
|
|
4155
|
+
scenariosNeedingRecapture: [],
|
|
4156
|
+
})).toBe(false);
|
|
1876
4157
|
});
|
|
1877
4158
|
});
|
|
1878
4159
|
});
|