@codeyam/codeyam-cli 0.1.0-staging.50df560 → 0.1.0-staging.5369cbb
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 +3 -3
- 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 +1 -0
- 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 +1 -1
- package/analyzer-template/packages/database/src/lib/loadAnalysis.ts +7 -1
- package/analyzer-template/packages/database/src/lib/loadEntity.ts +11 -4
- 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 +4 -4
- 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/runMultiScenarioServer.ts +26 -3
- 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/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.statePersistence.test.js +55 -0
- package/codeyam-cli/src/commands/__tests__/editor.statePersistence.test.js.map +1 -0
- 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 +1436 -205
- 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/init.js +20 -0
- package/codeyam-cli/src/commands/init.js.map +1 -1
- 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__/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 +1969 -761
- package/codeyam-cli/src/utils/__tests__/editorAudit.test.js.map +1 -1
- 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 +120 -0
- package/codeyam-cli/src/utils/__tests__/editorScenarioSwitch.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/editorScenarios.test.js +140 -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 +33 -1
- 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__/handoffContext.test.js +500 -0
- package/codeyam-cli/src/utils/__tests__/handoffContext.test.js.map +1 -0
- 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__/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 +28 -1
- 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/designSystemShowcase.js +810 -0
- package/codeyam-cli/src/utils/designSystemShowcase.js.map +1 -0
- 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 +275 -21
- package/codeyam-cli/src/utils/editorAudit.js.map +1 -1
- package/codeyam-cli/src/utils/editorPreview.js +5 -3
- package/codeyam-cli/src/utils/editorPreview.js.map +1 -1
- 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 +27 -12
- package/codeyam-cli/src/utils/editorScenarioSwitch.js.map +1 -1
- package/codeyam-cli/src/utils/editorScenarios.js +71 -0
- 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.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/handoffContext.js +257 -0
- package/codeyam-cli/src/utils/handoffContext.js.map +1 -0
- package/codeyam-cli/src/utils/install-skills.js +36 -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 +29 -3
- 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/scenariosManifest.js +8 -2
- package/codeyam-cli/src/utils/scenariosManifest.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/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 +95 -1
- package/codeyam-cli/src/webserver/__tests__/buildPtyEnv.test.js.map +1 -1
- package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js +145 -11
- package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js.map +1 -1
- package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js +67 -0
- package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js.map +1 -1
- package/codeyam-cli/src/webserver/app/lib/clientErrors.js +3 -0
- 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/build/client/assets/{CopyButton-CLe80MMu.js → CopyButton-DTBZZfSk.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{EntityItem-Crt_KN_U.js → EntityItem-BxclONWq.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{EntityTypeIcon-CD7lGABo.js → EntityTypeIcon-BsnEOJZ_.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{InlineSpinner-CgTNOhnu.js → InlineSpinner-ByaELMbv.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{InteractivePreview-CKeQT5Ty.js → InteractivePreview-6WjVfhxX.js} +2 -2
- package/codeyam-cli/src/webserver/build/client/assets/{LibraryFunctionPreview-D3s1MFkb.js → LibraryFunctionPreview-ChX-Hp7W.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{LogViewer-CM5zg40N.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-C2PLkej3.js → ReportIssueModal-DQsceHVv.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{SafeScreenshot-DanvyBPb.js → SafeScreenshot-DThcm_9M.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{ScenarioViewer-DUMfcNVK.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-BA_Ry-rs.js → ViewportInspectBar-BqkA9zyZ.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{_index-BAWd-Xjf.js → _index-DnOgyseQ.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{activity.(_tab)-BOARiB-g.js → activity.(_tab)-DqM9hbNE.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{addon-web-links-CHx25PAe.js → addon-web-links-C58dYPwR.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{agent-transcripts-Bg3e7q4S.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-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-CL-lMgHh.js → book-open-BFSIqZgO.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{chevron-down-GmAjGS9-.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-DFcQkN5j.js → circle-check-DLPObLUx.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{copy-C6iF61Xs.js → copy-DXEmO0TD.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{createLucideIcon-4ImjHTVC.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-C8y4mmyv.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)-DOXe0Qx7.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._-Blfy9UlN.js → entity._sha._-pc-vc6wO.js} +13 -12
- package/codeyam-cli/src/webserver/build/client/assets/{entity._sha.scenarios._scenarioId.dev-KTQuL0aj.js → entity._sha.scenarios._scenarioId.dev-C8AyYgYT.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{entity._sha.scenarios._scenarioId.fullscreen-C6eeL24i.js → entity._sha.scenarios._scenarioId.fullscreen-DziaVQX1.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{entity._sha_.create-scenario-DQM8E7L4.js → entity._sha_.create-scenario-BTcpgIpC.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{entity._sha_.edit._scenarioId-CAoXLsQr.js → entity._sha_.edit._scenarioId-D_O_ajfZ.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{entry.client-SuW9syRS.js → entry.client-j1Vi0bco.js} +6 -6
- package/codeyam-cli/src/webserver/build/client/assets/{files-D-xGrg29.js → files-kuny2Q_s.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{git-Bq_fbXP5.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-Bp1l4hSv.js → index-BliGSSpl.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{index-DE3jI_dv.js → index-SqjQKTdH.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{index-CWV9XZiG.js → index-vyrZD2g4.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{labs-B_IX45ih.js → labs-c3yLxSEp.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{loader-circle-De-7qQ2u.js → loader-circle-D-q28GLF.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/manifest-30c44d84.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/{memory-Cx2xEx7s.js → memory-CEWIUC4t.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{pause-CFxEKL1u.js → pause-BP6fitdh.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{root-BxUQigda.js → root-CLedrjXQ.js} +26 -13
- package/codeyam-cli/src/webserver/build/client/assets/{search-BdBb5aqc.js → search-BooqacKS.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{settings-DdE-Untf.js → settings-BM0nbryO.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{simulations-DSCdE99u.js → simulations-ovy6FjRY.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{terminal-CrplD4b1.js → terminal-DHemCJIs.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{triangle-alert-DqJ0j69l.js → triangle-alert-D87ekDl8.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{useCustomSizes-DhXHbEjP.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-Cy5Qg_UR.js → useReportContext-jkCytuYz.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{useToast-5HR2j9ZE.js → useToast-BgqkixU9.js} +1 -1
- package/codeyam-cli/src/webserver/build/server/assets/analysisRunner-CuR5TvUx.js +16 -0
- package/codeyam-cli/src/webserver/build/server/assets/{index-CjLhfz6Z.js → index-D4MWAsqb.js} +1 -1
- package/codeyam-cli/src/webserver/build/server/assets/init-JObA4lXD.js +14 -0
- package/codeyam-cli/src/webserver/build/server/assets/server-build-i8OXK4oL.js +765 -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 +132 -7
- package/codeyam-cli/src/webserver/editorProxy.js.map +1 -1
- package/codeyam-cli/src/webserver/idleDetector.js +27 -3
- package/codeyam-cli/src/webserver/idleDetector.js.map +1 -1
- package/codeyam-cli/src/webserver/server.js +67 -0
- package/codeyam-cli/src/webserver/server.js.map +1 -1
- package/codeyam-cli/src/webserver/terminalServer.js +103 -15
- package/codeyam-cli/src/webserver/terminalServer.js.map +1 -1
- 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 +8 -6
- 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 +4 -4
- 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/seed-adapter.ts +47 -34
- package/codeyam-cli/templates/seed-adapters/supabase.ts +271 -78
- package/codeyam-cli/templates/skills/codeyam-editor/SKILL.md +17 -2
- package/package.json +1 -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 +1 -0
- package/packages/analyze/src/lib/files/analyze/findOrCreateEntity.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 +4 -4
- 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-D0LgAaSa.js +0 -34
- package/codeyam-cli/src/webserver/build/client/assets/chunk-JZWAC4HX-BAdwhyCx.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-Gbk_i5Js.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-DII1pg_z.js +0 -58
- package/codeyam-cli/src/webserver/build/client/assets/editorPreview-oepecPae.js +0 -41
- package/codeyam-cli/src/webserver/build/client/assets/globals-Yn9W3zp3.css +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/manifest-cdf2c0a7.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/useLastLogLine-BNd5hYuW.js +0 -2
- package/codeyam-cli/src/webserver/build/server/assets/analysisRunner-B_PsTAb1.js +0 -13
- package/codeyam-cli/src/webserver/build/server/assets/init-BEqlbI84.js +0 -10
- package/codeyam-cli/src/webserver/build/server/assets/server-build-YI63xTu4.js +0 -553
- 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, queryPageScenarioCounts, queryIncompleteEntities, queryMiscategorizedScenarios, queryUnassociatedScenarios, isOnlyIncompleteEntities, isAutoRemediable, identifyScenariosNeedingRecapture, detectDuplicateNames, } 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', () => {
|
|
@@ -553,6 +563,142 @@ describe('editorAudit', () => {
|
|
|
553
563
|
expect(result.summary.componentsOk).toBe(1);
|
|
554
564
|
expect(result.summary.componentsWithErrors).toBe(1);
|
|
555
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
|
+
});
|
|
556
702
|
});
|
|
557
703
|
// ── filterGlossaryByChangeStatus ──────────────────────────────────
|
|
558
704
|
describe('filterGlossaryByChangeStatus', () => {
|
|
@@ -1466,8 +1612,11 @@ describe('editorAudit', () => {
|
|
|
1466
1612
|
expect(auditResult.summary.incompleteEntities).toBeUndefined();
|
|
1467
1613
|
});
|
|
1468
1614
|
});
|
|
1469
|
-
// ──
|
|
1470
|
-
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.
|
|
1471
1620
|
let db;
|
|
1472
1621
|
let rawDb;
|
|
1473
1622
|
const projectId = 'test-project-id';
|
|
@@ -1475,296 +1624,134 @@ describe('editorAudit', () => {
|
|
|
1475
1624
|
rawDb = new Database(':memory:');
|
|
1476
1625
|
db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
|
|
1477
1626
|
await db.schema
|
|
1478
|
-
.createTable('
|
|
1627
|
+
.createTable('analyses')
|
|
1479
1628
|
.addColumn('id', 'varchar', (col) => col.primaryKey())
|
|
1480
|
-
.addColumn('project_id', 'varchar', (col) => col.notNull())
|
|
1481
|
-
.addColumn('name', 'varchar', (col) => col.notNull())
|
|
1482
|
-
.addColumn('component_name', 'varchar')
|
|
1483
|
-
.addColumn('component_path', 'varchar')
|
|
1484
1629
|
.addColumn('entity_sha', 'varchar')
|
|
1485
|
-
.addColumn('
|
|
1486
|
-
.addColumn('
|
|
1487
|
-
.
|
|
1488
|
-
|
|
1489
|
-
.
|
|
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')
|
|
1490
1639
|
.execute();
|
|
1491
1640
|
});
|
|
1492
1641
|
afterEach(async () => {
|
|
1493
1642
|
await db.destroy();
|
|
1494
1643
|
});
|
|
1495
|
-
it('should
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
.values({
|
|
1499
|
-
id: 'sc-1',
|
|
1500
|
-
project_id: projectId,
|
|
1501
|
-
name: 'LibraryCard - Default',
|
|
1502
|
-
component_name: 'LibraryCard',
|
|
1503
|
-
url: '/isolated-components/LibraryCard?s=Default',
|
|
1504
|
-
created_at: '2026-03-17 12:00:00',
|
|
1505
|
-
})
|
|
1506
|
-
.execute();
|
|
1507
|
-
const result = await queryMiscategorizedScenarios(db, projectId, null);
|
|
1508
|
-
expect(result).toEqual([]);
|
|
1509
|
-
});
|
|
1510
|
-
it('should flag component scenarios that use non-isolation URLs', async () => {
|
|
1511
|
-
// This is the bug: "Full Library Page" registered as component_name=LibraryPage
|
|
1512
|
-
// but url=/library — it's pointing at the real page, not an isolation route
|
|
1644
|
+
it('should exclude files that have an entity with an analysis', async () => {
|
|
1645
|
+
const { filterToIncompleteFilePaths } = require('../editorAudit');
|
|
1646
|
+
// Entity with analysis — skip
|
|
1513
1647
|
await db
|
|
1514
|
-
.insertInto('
|
|
1648
|
+
.insertInto('entities')
|
|
1515
1649
|
.values({
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
url: '/library',
|
|
1521
|
-
created_at: '2026-03-17 12:41:40',
|
|
1650
|
+
sha: 'sha-header',
|
|
1651
|
+
name: 'Header',
|
|
1652
|
+
entity_type: 'visual',
|
|
1653
|
+
file_path: 'app/components/Header.tsx',
|
|
1522
1654
|
})
|
|
1523
1655
|
.execute();
|
|
1524
1656
|
await db
|
|
1525
|
-
.insertInto('
|
|
1657
|
+
.insertInto('analyses')
|
|
1526
1658
|
.values({
|
|
1527
|
-
id: '
|
|
1659
|
+
id: 'a-1',
|
|
1660
|
+
entity_sha: 'sha-header',
|
|
1661
|
+
entity_name: 'Header',
|
|
1528
1662
|
project_id: projectId,
|
|
1529
|
-
name: 'Empty Library Page',
|
|
1530
|
-
component_name: 'LibraryPage',
|
|
1531
|
-
url: '/library',
|
|
1532
|
-
created_at: '2026-03-17 12:41:51',
|
|
1533
1663
|
})
|
|
1534
1664
|
.execute();
|
|
1535
|
-
|
|
1536
|
-
expect(result).toEqual([
|
|
1537
|
-
{
|
|
1538
|
-
componentName: 'LibraryPage',
|
|
1539
|
-
scenarioNames: ['Full Library Page', 'Empty Library Page'],
|
|
1540
|
-
url: '/library',
|
|
1541
|
-
},
|
|
1542
|
-
]);
|
|
1543
|
-
});
|
|
1544
|
-
it('should not flag page-level scenarios (no component_name)', async () => {
|
|
1545
|
-
// App-level scenarios have no component_name — they're fine with real URLs
|
|
1665
|
+
// Entity without analysis — needs analysis
|
|
1546
1666
|
await db
|
|
1547
|
-
.insertInto('
|
|
1667
|
+
.insertInto('entities')
|
|
1548
1668
|
.values({
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
created_at: '2026-03-17 12:25:14',
|
|
1669
|
+
sha: 'sha-rule',
|
|
1670
|
+
name: 'RuleBuilder',
|
|
1671
|
+
entity_type: 'visual',
|
|
1672
|
+
file_path: 'app/components/RuleBuilder.tsx',
|
|
1554
1673
|
})
|
|
1555
1674
|
.execute();
|
|
1556
|
-
const
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
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');
|
|
1561
1693
|
await db
|
|
1562
|
-
.insertInto('
|
|
1694
|
+
.insertInto('entities')
|
|
1563
1695
|
.values({
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
url: '/library',
|
|
1569
|
-
created_at: '2026-03-17 12:41:40',
|
|
1696
|
+
sha: 'sha-a',
|
|
1697
|
+
name: 'CompA',
|
|
1698
|
+
entity_type: 'visual',
|
|
1699
|
+
file_path: 'app/CompA.tsx',
|
|
1570
1700
|
})
|
|
1571
1701
|
.execute();
|
|
1572
1702
|
await db
|
|
1573
|
-
.insertInto('
|
|
1703
|
+
.insertInto('analyses')
|
|
1574
1704
|
.values({
|
|
1575
|
-
id: '
|
|
1705
|
+
id: 'a-1',
|
|
1706
|
+
entity_sha: 'sha-a',
|
|
1707
|
+
entity_name: 'CompA',
|
|
1576
1708
|
project_id: projectId,
|
|
1577
|
-
name: 'Dashboard - Full',
|
|
1578
|
-
component_name: 'Dashboard',
|
|
1579
|
-
url: '/dashboard',
|
|
1580
|
-
created_at: '2026-03-17 12:50:00',
|
|
1581
1709
|
})
|
|
1582
1710
|
.execute();
|
|
1583
|
-
const result = await
|
|
1584
|
-
|
|
1585
|
-
expect(result.map((r) => r.componentName).sort()).toEqual([
|
|
1586
|
-
'Dashboard',
|
|
1587
|
-
'LibraryPage',
|
|
1711
|
+
const result = await filterToIncompleteFilePaths(db, projectId, [
|
|
1712
|
+
'app/CompA.tsx',
|
|
1588
1713
|
]);
|
|
1714
|
+
expect(result).toEqual([]);
|
|
1589
1715
|
});
|
|
1590
|
-
it('should
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
.insertInto('editor_scenarios')
|
|
1594
|
-
.values({
|
|
1595
|
-
id: 'sc-old',
|
|
1596
|
-
project_id: projectId,
|
|
1597
|
-
name: 'Old Page',
|
|
1598
|
-
component_name: 'OldComponent',
|
|
1599
|
-
url: '/old',
|
|
1600
|
-
created_at: '2026-03-16 10:00:00',
|
|
1601
|
-
})
|
|
1602
|
-
.execute();
|
|
1603
|
-
// New miscategorized scenario — in session
|
|
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
|
|
1604
1719
|
await db
|
|
1605
|
-
.insertInto('
|
|
1720
|
+
.insertInto('entities')
|
|
1606
1721
|
.values({
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
url: '/library',
|
|
1612
|
-
created_at: '2026-03-17 12:41:40',
|
|
1722
|
+
sha: 'sha-util',
|
|
1723
|
+
name: 'utils',
|
|
1724
|
+
entity_type: 'library',
|
|
1725
|
+
file_path: 'app/lib/utils.ts',
|
|
1613
1726
|
})
|
|
1614
1727
|
.execute();
|
|
1615
|
-
const result = await queryMiscategorizedScenarios(db, projectId, '2026-03-17T11:58:55.562Z');
|
|
1616
|
-
expect(result).toHaveLength(1);
|
|
1617
|
-
expect(result[0].componentName).toBe('LibraryPage');
|
|
1618
|
-
});
|
|
1619
|
-
it('should not flag component scenarios with null URL', async () => {
|
|
1620
1728
|
await db
|
|
1621
|
-
.insertInto('
|
|
1729
|
+
.insertInto('analyses')
|
|
1622
1730
|
.values({
|
|
1623
|
-
id: '
|
|
1731
|
+
id: 'a-1',
|
|
1732
|
+
entity_sha: 'sha-util',
|
|
1733
|
+
entity_name: 'utils',
|
|
1624
1734
|
project_id: projectId,
|
|
1625
|
-
name: 'NoUrl - Default',
|
|
1626
|
-
component_name: 'NoUrl',
|
|
1627
|
-
created_at: '2026-03-17 12:00:00',
|
|
1628
1735
|
})
|
|
1629
1736
|
.execute();
|
|
1630
|
-
const result = await
|
|
1737
|
+
const result = await filterToIncompleteFilePaths(db, projectId, [
|
|
1738
|
+
'app/lib/utils.ts',
|
|
1739
|
+
]);
|
|
1631
1740
|
expect(result).toEqual([]);
|
|
1632
1741
|
});
|
|
1633
1742
|
});
|
|
1634
|
-
// ──
|
|
1635
|
-
describe('
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
})).toBe(true);
|
|
1647
|
-
});
|
|
1648
|
-
it('should return false when there are also missing components', () => {
|
|
1649
|
-
expect(isOnlyIncompleteEntities({
|
|
1650
|
-
componentsMissing: 1,
|
|
1651
|
-
componentsWithErrors: 0,
|
|
1652
|
-
functionsFailing: 0,
|
|
1653
|
-
functionsNameMismatch: 0,
|
|
1654
|
-
functionsMissing: 0,
|
|
1655
|
-
missingFromGlossary: 0,
|
|
1656
|
-
incompleteEntities: 2,
|
|
1657
|
-
allPassing: false,
|
|
1658
|
-
})).toBe(false);
|
|
1659
|
-
});
|
|
1660
|
-
it('should return false when there are also failing tests', () => {
|
|
1661
|
-
expect(isOnlyIncompleteEntities({
|
|
1662
|
-
componentsMissing: 0,
|
|
1663
|
-
componentsWithErrors: 0,
|
|
1664
|
-
functionsFailing: 1,
|
|
1665
|
-
functionsNameMismatch: 0,
|
|
1666
|
-
functionsMissing: 0,
|
|
1667
|
-
missingFromGlossary: 0,
|
|
1668
|
-
incompleteEntities: 2,
|
|
1669
|
-
allPassing: false,
|
|
1670
|
-
})).toBe(false);
|
|
1671
|
-
});
|
|
1672
|
-
it('should return false when there are also missing glossary entries', () => {
|
|
1673
|
-
expect(isOnlyIncompleteEntities({
|
|
1674
|
-
componentsMissing: 0,
|
|
1675
|
-
componentsWithErrors: 0,
|
|
1676
|
-
functionsFailing: 0,
|
|
1677
|
-
functionsNameMismatch: 0,
|
|
1678
|
-
functionsMissing: 0,
|
|
1679
|
-
missingFromGlossary: 1,
|
|
1680
|
-
incompleteEntities: 2,
|
|
1681
|
-
allPassing: false,
|
|
1682
|
-
})).toBe(false);
|
|
1683
|
-
});
|
|
1684
|
-
it('should return true even when incompleteEntities is 0 (no failures at all)', () => {
|
|
1685
|
-
// Edge case: all zeros means nothing is failing
|
|
1686
|
-
expect(isOnlyIncompleteEntities({
|
|
1687
|
-
componentsMissing: 0,
|
|
1688
|
-
componentsWithErrors: 0,
|
|
1689
|
-
functionsFailing: 0,
|
|
1690
|
-
functionsNameMismatch: 0,
|
|
1691
|
-
functionsMissing: 0,
|
|
1692
|
-
missingFromGlossary: 0,
|
|
1693
|
-
incompleteEntities: 0,
|
|
1694
|
-
allPassing: true,
|
|
1695
|
-
})).toBe(true);
|
|
1696
|
-
});
|
|
1697
|
-
it('should return false when there are also miscategorized scenarios', () => {
|
|
1698
|
-
expect(isOnlyIncompleteEntities({
|
|
1699
|
-
componentsMissing: 0,
|
|
1700
|
-
componentsWithErrors: 0,
|
|
1701
|
-
functionsFailing: 0,
|
|
1702
|
-
functionsNameMismatch: 0,
|
|
1703
|
-
functionsMissing: 0,
|
|
1704
|
-
missingFromGlossary: 0,
|
|
1705
|
-
miscategorizedScenarios: 1,
|
|
1706
|
-
incompleteEntities: 2,
|
|
1707
|
-
allPassing: false,
|
|
1708
|
-
})).toBe(false);
|
|
1709
|
-
});
|
|
1710
|
-
it('should handle missing fields gracefully', () => {
|
|
1711
|
-
// Summary from older API version might not have all fields
|
|
1712
|
-
expect(isOnlyIncompleteEntities({
|
|
1713
|
-
incompleteEntities: 2,
|
|
1714
|
-
allPassing: false,
|
|
1715
|
-
})).toBe(true);
|
|
1716
|
-
});
|
|
1717
|
-
});
|
|
1718
|
-
// ── isAutoRemediable ─────────────────────────────────────────────────
|
|
1719
|
-
describe('isAutoRemediable', () => {
|
|
1720
|
-
it('should return true on first attempt when only incomplete entities', () => {
|
|
1721
|
-
const result = isAutoRemediable({
|
|
1722
|
-
componentsMissing: 0,
|
|
1723
|
-
componentsWithErrors: 0,
|
|
1724
|
-
functionsFailing: 0,
|
|
1725
|
-
functionsNameMismatch: 0,
|
|
1726
|
-
functionsMissing: 0,
|
|
1727
|
-
missingFromGlossary: 0,
|
|
1728
|
-
miscategorizedScenarios: 0,
|
|
1729
|
-
incompleteEntities: 3,
|
|
1730
|
-
allPassing: false,
|
|
1731
|
-
}, false);
|
|
1732
|
-
expect(result).toBe(true);
|
|
1733
|
-
});
|
|
1734
|
-
it('should return false on second attempt (already tried once)', () => {
|
|
1735
|
-
// This is the key fix: if we already tried analyze-imports and
|
|
1736
|
-
// entities are STILL incomplete, don't try again — report the failure
|
|
1737
|
-
const result = isAutoRemediable({
|
|
1738
|
-
componentsMissing: 0,
|
|
1739
|
-
componentsWithErrors: 0,
|
|
1740
|
-
functionsFailing: 0,
|
|
1741
|
-
functionsNameMismatch: 0,
|
|
1742
|
-
functionsMissing: 0,
|
|
1743
|
-
missingFromGlossary: 0,
|
|
1744
|
-
miscategorizedScenarios: 0,
|
|
1745
|
-
incompleteEntities: 3,
|
|
1746
|
-
allPassing: false,
|
|
1747
|
-
}, true);
|
|
1748
|
-
expect(result).toBe(false);
|
|
1749
|
-
});
|
|
1750
|
-
it('should return false when there are other failures besides incomplete entities', () => {
|
|
1751
|
-
const result = isAutoRemediable({
|
|
1752
|
-
componentsMissing: 1,
|
|
1753
|
-
incompleteEntities: 3,
|
|
1754
|
-
allPassing: false,
|
|
1755
|
-
}, false);
|
|
1756
|
-
expect(result).toBe(false);
|
|
1757
|
-
});
|
|
1758
|
-
it('should return false when there are no incomplete entities', () => {
|
|
1759
|
-
const result = isAutoRemediable({
|
|
1760
|
-
componentsMissing: 1,
|
|
1761
|
-
allPassing: false,
|
|
1762
|
-
}, false);
|
|
1763
|
-
expect(result).toBe(false);
|
|
1764
|
-
});
|
|
1765
|
-
});
|
|
1766
|
-
// ── queryIncompleteEntities ─────────────────────────────────────────
|
|
1767
|
-
describe('queryIncompleteEntities', () => {
|
|
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.
|
|
1768
1755
|
let db;
|
|
1769
1756
|
let rawDb;
|
|
1770
1757
|
const projectId = 'test-project-id';
|
|
@@ -1803,123 +1790,154 @@ describe('editorAudit', () => {
|
|
|
1803
1790
|
afterEach(async () => {
|
|
1804
1791
|
await db.destroy();
|
|
1805
1792
|
});
|
|
1806
|
-
it('should
|
|
1807
|
-
//
|
|
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.
|
|
1808
1795
|
await db
|
|
1809
1796
|
.insertInto('entities')
|
|
1810
1797
|
.values({
|
|
1811
|
-
sha: 'sha-
|
|
1812
|
-
name: '
|
|
1798
|
+
sha: 'sha-real',
|
|
1799
|
+
name: 'RuleBuilder',
|
|
1813
1800
|
entity_type: 'visual',
|
|
1814
|
-
file_path: '
|
|
1801
|
+
file_path: 'app/components/RuleBuilder.tsx',
|
|
1815
1802
|
})
|
|
1816
1803
|
.execute();
|
|
1817
1804
|
await db
|
|
1818
1805
|
.insertInto('analyses')
|
|
1819
1806
|
.values({
|
|
1820
1807
|
id: 'a-1',
|
|
1821
|
-
entity_sha: 'sha-
|
|
1822
|
-
entity_name: '
|
|
1808
|
+
entity_sha: 'sha-real',
|
|
1809
|
+
entity_name: 'RuleBuilder',
|
|
1823
1810
|
project_id: projectId,
|
|
1824
1811
|
})
|
|
1825
1812
|
.execute();
|
|
1826
1813
|
await db
|
|
1827
1814
|
.insertInto('editor_scenarios')
|
|
1828
1815
|
.values({
|
|
1829
|
-
id: 'sc-
|
|
1816
|
+
id: 'sc-good',
|
|
1830
1817
|
project_id: projectId,
|
|
1831
|
-
name: '
|
|
1832
|
-
component_name: '
|
|
1833
|
-
|
|
1818
|
+
name: 'RuleBuilder - Empty',
|
|
1819
|
+
component_name: 'RuleBuilder',
|
|
1820
|
+
component_path: 'app/components/RuleBuilder.tsx',
|
|
1821
|
+
entity_sha: 'sha-real',
|
|
1834
1822
|
created_at: '2026-03-16 23:00:00',
|
|
1835
1823
|
})
|
|
1836
1824
|
.execute();
|
|
1837
|
-
|
|
1838
|
-
|
|
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);
|
|
1839
1844
|
});
|
|
1840
|
-
it('should
|
|
1841
|
-
//
|
|
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
|
|
1842
1847
|
await db
|
|
1843
1848
|
.insertInto('entities')
|
|
1844
1849
|
.values({
|
|
1845
|
-
sha: 'sha-
|
|
1846
|
-
name: '
|
|
1850
|
+
sha: 'sha-noanalysis',
|
|
1851
|
+
name: 'Footer',
|
|
1847
1852
|
entity_type: 'visual',
|
|
1848
|
-
file_path: '
|
|
1853
|
+
file_path: 'app/components/Footer.tsx',
|
|
1849
1854
|
})
|
|
1850
1855
|
.execute();
|
|
1851
|
-
// Scenario referencing that entity
|
|
1852
1856
|
await db
|
|
1853
1857
|
.insertInto('editor_scenarios')
|
|
1854
1858
|
.values({
|
|
1855
1859
|
id: 'sc-1',
|
|
1856
1860
|
project_id: projectId,
|
|
1857
|
-
name: '
|
|
1858
|
-
component_name: '
|
|
1859
|
-
|
|
1861
|
+
name: 'Footer - Default',
|
|
1862
|
+
component_name: 'Footer',
|
|
1863
|
+
component_path: 'app/components/Footer.tsx',
|
|
1864
|
+
entity_sha: 'sha-noanalysis',
|
|
1860
1865
|
created_at: '2026-03-16 23:00:00',
|
|
1861
1866
|
})
|
|
1862
1867
|
.execute();
|
|
1868
|
+
// Phantom scenario (shouldn't affect results)
|
|
1863
1869
|
await db
|
|
1864
1870
|
.insertInto('editor_scenarios')
|
|
1865
1871
|
.values({
|
|
1866
|
-
id: 'sc-
|
|
1872
|
+
id: 'sc-phantom',
|
|
1867
1873
|
project_id: projectId,
|
|
1868
|
-
name: '
|
|
1869
|
-
component_name: '
|
|
1870
|
-
|
|
1871
|
-
|
|
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',
|
|
1872
1879
|
})
|
|
1873
1880
|
.execute();
|
|
1874
|
-
const
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
scenarioCount: 2,
|
|
1880
|
-
preExisting: false,
|
|
1881
|
-
},
|
|
1882
|
-
]);
|
|
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');
|
|
1883
1886
|
});
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
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')
|
|
1894
1909
|
.execute();
|
|
1910
|
+
});
|
|
1911
|
+
afterEach(async () => {
|
|
1912
|
+
await db.destroy();
|
|
1913
|
+
});
|
|
1914
|
+
it('should return empty when all component scenarios use isolation routes', async () => {
|
|
1895
1915
|
await db
|
|
1896
|
-
.insertInto('
|
|
1916
|
+
.insertInto('editor_scenarios')
|
|
1897
1917
|
.values({
|
|
1898
|
-
id: '
|
|
1899
|
-
entity_sha: 'sha-header',
|
|
1900
|
-
entity_name: 'Header',
|
|
1918
|
+
id: 'sc-1',
|
|
1901
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',
|
|
1902
1924
|
})
|
|
1903
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 () => {
|
|
1930
|
+
// This is the bug: "Full Library Page" registered as component_name=LibraryPage
|
|
1931
|
+
// but url=/library — it's pointing at the real page, not an isolation route
|
|
1904
1932
|
await db
|
|
1905
1933
|
.insertInto('editor_scenarios')
|
|
1906
1934
|
.values({
|
|
1907
1935
|
id: 'sc-1',
|
|
1908
1936
|
project_id: projectId,
|
|
1909
|
-
name: '
|
|
1910
|
-
component_name: '
|
|
1911
|
-
|
|
1912
|
-
created_at: '2026-03-
|
|
1913
|
-
})
|
|
1914
|
-
.execute();
|
|
1915
|
-
// Entity WITHOUT analysis (CollectionPicker)
|
|
1916
|
-
await db
|
|
1917
|
-
.insertInto('entities')
|
|
1918
|
-
.values({
|
|
1919
|
-
sha: 'sha-picker',
|
|
1920
|
-
name: 'CollectionPicker',
|
|
1921
|
-
entity_type: 'visual',
|
|
1922
|
-
file_path: 'src/components/CollectionPicker.tsx',
|
|
1937
|
+
name: 'Full Library Page',
|
|
1938
|
+
component_name: 'LibraryPage',
|
|
1939
|
+
url: '/library',
|
|
1940
|
+
created_at: '2026-03-17 12:41:40',
|
|
1923
1941
|
})
|
|
1924
1942
|
.execute();
|
|
1925
1943
|
await db
|
|
@@ -1927,490 +1945,907 @@ describe('editorAudit', () => {
|
|
|
1927
1945
|
.values({
|
|
1928
1946
|
id: 'sc-2',
|
|
1929
1947
|
project_id: projectId,
|
|
1930
|
-
name: '
|
|
1931
|
-
component_name: '
|
|
1932
|
-
|
|
1933
|
-
created_at: '2026-03-
|
|
1948
|
+
name: 'Empty Library Page',
|
|
1949
|
+
component_name: 'LibraryPage',
|
|
1950
|
+
url: '/library',
|
|
1951
|
+
created_at: '2026-03-17 12:41:51',
|
|
1934
1952
|
})
|
|
1935
1953
|
.execute();
|
|
1936
|
-
const result = await
|
|
1954
|
+
const result = await queryMiscategorizedScenarios(db, projectId, null);
|
|
1937
1955
|
expect(result).toEqual([
|
|
1938
1956
|
{
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
preExisting: false,
|
|
1957
|
+
componentName: 'LibraryPage',
|
|
1958
|
+
scenarioNames: ['Full Library Page', 'Empty Library Page'],
|
|
1959
|
+
url: '/library',
|
|
1943
1960
|
},
|
|
1944
1961
|
]);
|
|
1945
1962
|
});
|
|
1946
|
-
it('should
|
|
1947
|
-
//
|
|
1948
|
-
await db
|
|
1949
|
-
.insertInto('entities')
|
|
1950
|
-
.values({
|
|
1951
|
-
sha: 'sha-old',
|
|
1952
|
-
name: 'OldComponent',
|
|
1953
|
-
entity_type: 'visual',
|
|
1954
|
-
file_path: 'src/OldComponent.tsx',
|
|
1955
|
-
})
|
|
1956
|
-
.execute();
|
|
1963
|
+
it('should not flag page-level scenarios (no component_name)', async () => {
|
|
1964
|
+
// App-level scenarios have no component_name — they're fine with real URLs
|
|
1957
1965
|
await db
|
|
1958
1966
|
.insertInto('editor_scenarios')
|
|
1959
1967
|
.values({
|
|
1960
|
-
id: 'sc-
|
|
1968
|
+
id: 'sc-1',
|
|
1961
1969
|
project_id: projectId,
|
|
1962
|
-
name: '
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
created_at: '2026-03-16 20:00:00',
|
|
1966
|
-
})
|
|
1967
|
-
.execute();
|
|
1968
|
-
// Entity without analysis, scenario created DURING session
|
|
1969
|
-
await db
|
|
1970
|
-
.insertInto('entities')
|
|
1971
|
-
.values({
|
|
1972
|
-
sha: 'sha-new',
|
|
1973
|
-
name: 'NewComponent',
|
|
1974
|
-
entity_type: 'visual',
|
|
1975
|
-
file_path: 'src/NewComponent.tsx',
|
|
1970
|
+
name: 'Library with Articles',
|
|
1971
|
+
url: '/',
|
|
1972
|
+
created_at: '2026-03-17 12:25:14',
|
|
1976
1973
|
})
|
|
1977
1974
|
.execute();
|
|
1975
|
+
const result = await queryMiscategorizedScenarios(db, projectId, null);
|
|
1976
|
+
expect(result).toEqual([]);
|
|
1977
|
+
});
|
|
1978
|
+
it('should group miscategorized scenarios by component and URL', async () => {
|
|
1979
|
+
// Two different components both misusing real URLs
|
|
1978
1980
|
await db
|
|
1979
1981
|
.insertInto('editor_scenarios')
|
|
1980
1982
|
.values({
|
|
1981
|
-
id: 'sc-
|
|
1983
|
+
id: 'sc-1',
|
|
1982
1984
|
project_id: projectId,
|
|
1983
|
-
name: '
|
|
1984
|
-
component_name: '
|
|
1985
|
-
|
|
1986
|
-
created_at: '2026-03-
|
|
1987
|
-
})
|
|
1988
|
-
.execute();
|
|
1989
|
-
const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
|
|
1990
|
-
// Both should be returned — OldComponent is preExisting, NewComponent is not
|
|
1991
|
-
expect(result).toEqual(expect.arrayContaining([
|
|
1992
|
-
{
|
|
1993
|
-
entitySha: 'sha-old',
|
|
1994
|
-
name: 'OldComponent',
|
|
1995
|
-
scenarioCount: 1,
|
|
1996
|
-
preExisting: true,
|
|
1997
|
-
},
|
|
1998
|
-
{
|
|
1999
|
-
entitySha: 'sha-new',
|
|
2000
|
-
name: 'NewComponent',
|
|
2001
|
-
scenarioCount: 1,
|
|
2002
|
-
preExisting: false,
|
|
2003
|
-
},
|
|
2004
|
-
]));
|
|
2005
|
-
expect(result).toHaveLength(2);
|
|
2006
|
-
});
|
|
2007
|
-
it('should flag preExisting: false when scenario was updated in session even if created before', async () => {
|
|
2008
|
-
await db
|
|
2009
|
-
.insertInto('entities')
|
|
2010
|
-
.values({
|
|
2011
|
-
sha: 'sha-updated',
|
|
2012
|
-
name: 'UpdatedComponent',
|
|
2013
|
-
entity_type: 'visual',
|
|
2014
|
-
file_path: 'src/Updated.tsx',
|
|
1985
|
+
name: 'Full Library Page',
|
|
1986
|
+
component_name: 'LibraryPage',
|
|
1987
|
+
url: '/library',
|
|
1988
|
+
created_at: '2026-03-17 12:41:40',
|
|
2015
1989
|
})
|
|
2016
1990
|
.execute();
|
|
2017
1991
|
await db
|
|
2018
1992
|
.insertInto('editor_scenarios')
|
|
2019
1993
|
.values({
|
|
2020
|
-
id: 'sc-
|
|
1994
|
+
id: 'sc-2',
|
|
2021
1995
|
project_id: projectId,
|
|
2022
|
-
name: '
|
|
2023
|
-
component_name: '
|
|
2024
|
-
|
|
2025
|
-
created_at: '2026-03-
|
|
2026
|
-
updated_at: '2026-03-16 23:20:00', // Updated in session
|
|
1996
|
+
name: 'Dashboard - Full',
|
|
1997
|
+
component_name: 'Dashboard',
|
|
1998
|
+
url: '/dashboard',
|
|
1999
|
+
created_at: '2026-03-17 12:50:00',
|
|
2027
2000
|
})
|
|
2028
2001
|
.execute();
|
|
2029
|
-
const result = await
|
|
2030
|
-
expect(result).
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
scenarioCount: 1,
|
|
2035
|
-
preExisting: false,
|
|
2036
|
-
},
|
|
2002
|
+
const result = await queryMiscategorizedScenarios(db, projectId, null);
|
|
2003
|
+
expect(result).toHaveLength(2);
|
|
2004
|
+
expect(result.map((r) => r.componentName).sort()).toEqual([
|
|
2005
|
+
'Dashboard',
|
|
2006
|
+
'LibraryPage',
|
|
2037
2007
|
]);
|
|
2038
2008
|
});
|
|
2039
|
-
it('should
|
|
2009
|
+
it('should scope to session when featureStartedAt is provided', async () => {
|
|
2010
|
+
// Old miscategorized scenario — before session
|
|
2040
2011
|
await db
|
|
2041
2012
|
.insertInto('editor_scenarios')
|
|
2042
2013
|
.values({
|
|
2043
|
-
id: 'sc-
|
|
2044
|
-
project_id: projectId,
|
|
2045
|
-
name: 'Orphan Scenario',
|
|
2046
|
-
component_name: 'Orphan',
|
|
2047
|
-
created_at: '2026-03-16 23:00:00',
|
|
2048
|
-
})
|
|
2049
|
-
.execute();
|
|
2050
|
-
const result = await queryIncompleteEntities(db, projectId, null);
|
|
2051
|
-
expect(result).toEqual([]);
|
|
2052
|
-
});
|
|
2053
|
-
it('should return empty when there are no scenarios', async () => {
|
|
2054
|
-
const result = await queryIncompleteEntities(db, projectId, null);
|
|
2055
|
-
expect(result).toEqual([]);
|
|
2056
|
-
});
|
|
2057
|
-
it('should not flag entities when a sibling version (same name+filePath) has analyses', async () => {
|
|
2058
|
-
// Old entity version WITH analysis
|
|
2059
|
-
await db
|
|
2060
|
-
.insertInto('entities')
|
|
2061
|
-
.values({
|
|
2062
|
-
sha: 'sha-btn-v1',
|
|
2063
|
-
name: 'OpenLibraryButton',
|
|
2064
|
-
entity_type: 'visual',
|
|
2065
|
-
file_path: 'src/components/OpenLibraryButton.tsx',
|
|
2066
|
-
})
|
|
2067
|
-
.execute();
|
|
2068
|
-
await db
|
|
2069
|
-
.insertInto('analyses')
|
|
2070
|
-
.values({
|
|
2071
|
-
id: 'a-btn-v1',
|
|
2072
|
-
entity_sha: 'sha-btn-v1',
|
|
2073
|
-
entity_name: 'OpenLibraryButton',
|
|
2014
|
+
id: 'sc-old',
|
|
2074
2015
|
project_id: projectId,
|
|
2016
|
+
name: 'Old Page',
|
|
2017
|
+
component_name: 'OldComponent',
|
|
2018
|
+
url: '/old',
|
|
2019
|
+
created_at: '2026-03-16 10:00:00',
|
|
2075
2020
|
})
|
|
2076
2021
|
.execute();
|
|
2077
|
-
// New
|
|
2078
|
-
await db
|
|
2079
|
-
.insertInto('entities')
|
|
2080
|
-
.values({
|
|
2081
|
-
sha: 'sha-btn-v2',
|
|
2082
|
-
name: 'OpenLibraryButton',
|
|
2083
|
-
entity_type: 'visual',
|
|
2084
|
-
file_path: 'src/components/OpenLibraryButton.tsx',
|
|
2085
|
-
})
|
|
2086
|
-
.execute();
|
|
2087
|
-
// Scenario points to the NEW version (backfilled after file watcher)
|
|
2022
|
+
// New miscategorized scenario — in session
|
|
2088
2023
|
await db
|
|
2089
2024
|
.insertInto('editor_scenarios')
|
|
2090
2025
|
.values({
|
|
2091
|
-
id: 'sc-
|
|
2026
|
+
id: 'sc-new',
|
|
2092
2027
|
project_id: projectId,
|
|
2093
|
-
name: '
|
|
2094
|
-
component_name: '
|
|
2095
|
-
|
|
2096
|
-
created_at: '2026-03-
|
|
2028
|
+
name: 'Full Library Page',
|
|
2029
|
+
component_name: 'LibraryPage',
|
|
2030
|
+
url: '/library',
|
|
2031
|
+
created_at: '2026-03-17 12:41:40',
|
|
2097
2032
|
})
|
|
2098
2033
|
.execute();
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
expect(result).
|
|
2034
|
+
const result = await queryMiscategorizedScenarios(db, projectId, '2026-03-17T11:58:55.562Z');
|
|
2035
|
+
expect(result).toHaveLength(1);
|
|
2036
|
+
expect(result[0].componentName).toBe('LibraryPage');
|
|
2102
2037
|
});
|
|
2103
|
-
it('should flag
|
|
2104
|
-
// Old entity version WITH analysis at ORIGINAL file path
|
|
2105
|
-
await db
|
|
2106
|
-
.insertInto('entities')
|
|
2107
|
-
.values({
|
|
2108
|
-
sha: 'sha-card-v1',
|
|
2109
|
-
name: 'TaskCard',
|
|
2110
|
-
entity_type: 'visual',
|
|
2111
|
-
file_path: 'app/page.tsx',
|
|
2112
|
-
})
|
|
2113
|
-
.execute();
|
|
2114
|
-
await db
|
|
2115
|
-
.insertInto('analyses')
|
|
2116
|
-
.values({
|
|
2117
|
-
id: 'a-card-v1',
|
|
2118
|
-
entity_sha: 'sha-card-v1',
|
|
2119
|
-
entity_name: 'TaskCard',
|
|
2120
|
-
project_id: projectId,
|
|
2121
|
-
})
|
|
2122
|
-
.execute();
|
|
2123
|
-
// New entity version WITHOUT analysis at EXTRACTED file path
|
|
2124
|
-
await db
|
|
2125
|
-
.insertInto('entities')
|
|
2126
|
-
.values({
|
|
2127
|
-
sha: 'sha-card-v2',
|
|
2128
|
-
name: 'TaskCard',
|
|
2129
|
-
entity_type: 'visual',
|
|
2130
|
-
file_path: 'app/components/TaskCard.tsx',
|
|
2131
|
-
})
|
|
2132
|
-
.execute();
|
|
2133
|
-
// Scenario points to the new version (synced by syncScenarioEntityShas)
|
|
2038
|
+
it('should not flag component scenarios with null URL', async () => {
|
|
2134
2039
|
await db
|
|
2135
2040
|
.insertInto('editor_scenarios')
|
|
2136
2041
|
.values({
|
|
2137
|
-
id: 'sc-
|
|
2042
|
+
id: 'sc-1',
|
|
2138
2043
|
project_id: projectId,
|
|
2139
|
-
name: '
|
|
2140
|
-
component_name: '
|
|
2141
|
-
|
|
2142
|
-
created_at: '2026-03-16 23:00:00',
|
|
2044
|
+
name: 'NoUrl - Default',
|
|
2045
|
+
component_name: 'NoUrl',
|
|
2046
|
+
created_at: '2026-03-17 12:00:00',
|
|
2143
2047
|
})
|
|
2144
2048
|
.execute();
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
const result = await queryIncompleteEntities(db, projectId, null);
|
|
2148
|
-
expect(result).toEqual([
|
|
2149
|
-
{
|
|
2150
|
-
entitySha: 'sha-card-v2',
|
|
2151
|
-
name: 'TaskCard',
|
|
2152
|
-
scenarioCount: 1,
|
|
2153
|
-
preExisting: false,
|
|
2154
|
-
},
|
|
2155
|
-
]);
|
|
2049
|
+
const result = await queryMiscategorizedScenarios(db, projectId, null);
|
|
2050
|
+
expect(result).toEqual([]);
|
|
2156
2051
|
});
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
id: 'sc-icon',
|
|
2172
|
-
project_id: projectId,
|
|
2173
|
-
name: 'ExternalLinkIcon - Default',
|
|
2174
|
-
component_name: 'ExternalLinkIcon',
|
|
2175
|
-
entity_sha: 'sha-icon',
|
|
2176
|
-
created_at: '2026-03-16 23:00:00',
|
|
2177
|
-
})
|
|
2178
|
-
.execute();
|
|
2179
|
-
// Should flag as incomplete — no version has analyses
|
|
2180
|
-
const result = await queryIncompleteEntities(db, projectId, null);
|
|
2181
|
-
expect(result).toEqual([
|
|
2182
|
-
{
|
|
2183
|
-
entitySha: 'sha-icon',
|
|
2184
|
-
name: 'ExternalLinkIcon',
|
|
2185
|
-
scenarioCount: 1,
|
|
2186
|
-
preExisting: false,
|
|
2187
|
-
},
|
|
2188
|
-
]);
|
|
2052
|
+
});
|
|
2053
|
+
// ── isOnlyIncompleteEntities ─────────────────────────────────────────
|
|
2054
|
+
describe('isOnlyIncompleteEntities', () => {
|
|
2055
|
+
it('should return true when incompleteEntities is the only failure', () => {
|
|
2056
|
+
expect(isOnlyIncompleteEntities({
|
|
2057
|
+
componentsMissing: 0,
|
|
2058
|
+
componentsWithErrors: 0,
|
|
2059
|
+
functionsFailing: 0,
|
|
2060
|
+
functionsNameMismatch: 0,
|
|
2061
|
+
functionsMissing: 0,
|
|
2062
|
+
missingFromGlossary: 0,
|
|
2063
|
+
incompleteEntities: 3,
|
|
2064
|
+
allPassing: false,
|
|
2065
|
+
})).toBe(true);
|
|
2189
2066
|
});
|
|
2190
|
-
it('should
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2067
|
+
it('should return false when there are also missing components', () => {
|
|
2068
|
+
expect(isOnlyIncompleteEntities({
|
|
2069
|
+
componentsMissing: 1,
|
|
2070
|
+
componentsWithErrors: 0,
|
|
2071
|
+
functionsFailing: 0,
|
|
2072
|
+
functionsNameMismatch: 0,
|
|
2073
|
+
functionsMissing: 0,
|
|
2074
|
+
missingFromGlossary: 0,
|
|
2075
|
+
incompleteEntities: 2,
|
|
2076
|
+
allPassing: false,
|
|
2077
|
+
})).toBe(false);
|
|
2078
|
+
});
|
|
2079
|
+
it('should return false when there are also failing tests', () => {
|
|
2080
|
+
expect(isOnlyIncompleteEntities({
|
|
2081
|
+
componentsMissing: 0,
|
|
2082
|
+
componentsWithErrors: 0,
|
|
2083
|
+
functionsFailing: 1,
|
|
2084
|
+
functionsNameMismatch: 0,
|
|
2085
|
+
functionsMissing: 0,
|
|
2086
|
+
missingFromGlossary: 0,
|
|
2087
|
+
incompleteEntities: 2,
|
|
2088
|
+
allPassing: false,
|
|
2089
|
+
})).toBe(false);
|
|
2090
|
+
});
|
|
2091
|
+
it('should return false when there are also missing glossary entries', () => {
|
|
2092
|
+
expect(isOnlyIncompleteEntities({
|
|
2093
|
+
componentsMissing: 0,
|
|
2094
|
+
componentsWithErrors: 0,
|
|
2095
|
+
functionsFailing: 0,
|
|
2096
|
+
functionsNameMismatch: 0,
|
|
2097
|
+
functionsMissing: 0,
|
|
2098
|
+
missingFromGlossary: 1,
|
|
2099
|
+
incompleteEntities: 2,
|
|
2100
|
+
allPassing: false,
|
|
2101
|
+
})).toBe(false);
|
|
2102
|
+
});
|
|
2103
|
+
it('should return true even when incompleteEntities is 0 (no failures at all)', () => {
|
|
2104
|
+
// Edge case: all zeros means nothing is failing
|
|
2105
|
+
expect(isOnlyIncompleteEntities({
|
|
2106
|
+
componentsMissing: 0,
|
|
2107
|
+
componentsWithErrors: 0,
|
|
2108
|
+
functionsFailing: 0,
|
|
2109
|
+
functionsNameMismatch: 0,
|
|
2110
|
+
functionsMissing: 0,
|
|
2111
|
+
missingFromGlossary: 0,
|
|
2112
|
+
incompleteEntities: 0,
|
|
2113
|
+
allPassing: true,
|
|
2114
|
+
})).toBe(true);
|
|
2115
|
+
});
|
|
2116
|
+
it('should return false when there are also miscategorized scenarios', () => {
|
|
2117
|
+
expect(isOnlyIncompleteEntities({
|
|
2118
|
+
componentsMissing: 0,
|
|
2119
|
+
componentsWithErrors: 0,
|
|
2120
|
+
functionsFailing: 0,
|
|
2121
|
+
functionsNameMismatch: 0,
|
|
2122
|
+
functionsMissing: 0,
|
|
2123
|
+
missingFromGlossary: 0,
|
|
2124
|
+
miscategorizedScenarios: 1,
|
|
2125
|
+
incompleteEntities: 2,
|
|
2126
|
+
allPassing: false,
|
|
2127
|
+
})).toBe(false);
|
|
2128
|
+
});
|
|
2129
|
+
it('should handle missing fields gracefully', () => {
|
|
2130
|
+
// Summary from older API version might not have all fields
|
|
2131
|
+
expect(isOnlyIncompleteEntities({
|
|
2132
|
+
incompleteEntities: 2,
|
|
2133
|
+
allPassing: false,
|
|
2134
|
+
})).toBe(true);
|
|
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
|
+
});
|
|
2154
|
+
});
|
|
2155
|
+
// ── isAutoRemediable ─────────────────────────────────────────────────
|
|
2156
|
+
describe('isAutoRemediable', () => {
|
|
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', () => {
|
|
2161
|
+
const result = isAutoRemediable({
|
|
2162
|
+
componentsMissing: 0,
|
|
2163
|
+
componentsWithErrors: 0,
|
|
2164
|
+
functionsFailing: 0,
|
|
2165
|
+
functionsNameMismatch: 0,
|
|
2166
|
+
functionsMissing: 0,
|
|
2167
|
+
missingFromGlossary: 0,
|
|
2168
|
+
miscategorizedScenarios: 0,
|
|
2169
|
+
incompleteEntities: 3,
|
|
2170
|
+
allPassing: false,
|
|
2171
|
+
}, false);
|
|
2172
|
+
expect(result).toBe(false);
|
|
2173
|
+
});
|
|
2174
|
+
it('should return false on second attempt', () => {
|
|
2175
|
+
const result = isAutoRemediable({
|
|
2176
|
+
componentsMissing: 0,
|
|
2177
|
+
componentsWithErrors: 0,
|
|
2178
|
+
functionsFailing: 0,
|
|
2179
|
+
functionsNameMismatch: 0,
|
|
2180
|
+
functionsMissing: 0,
|
|
2181
|
+
missingFromGlossary: 0,
|
|
2182
|
+
miscategorizedScenarios: 0,
|
|
2183
|
+
incompleteEntities: 3,
|
|
2184
|
+
allPassing: false,
|
|
2185
|
+
}, true);
|
|
2186
|
+
expect(result).toBe(false);
|
|
2187
|
+
});
|
|
2188
|
+
it('should return false when there are other failures besides incomplete entities', () => {
|
|
2189
|
+
const result = isAutoRemediable({
|
|
2190
|
+
componentsMissing: 1,
|
|
2191
|
+
incompleteEntities: 3,
|
|
2192
|
+
allPassing: false,
|
|
2193
|
+
}, false);
|
|
2194
|
+
expect(result).toBe(false);
|
|
2195
|
+
});
|
|
2196
|
+
it('should return false when there are no incomplete entities', () => {
|
|
2197
|
+
const result = isAutoRemediable({
|
|
2198
|
+
componentsMissing: 1,
|
|
2199
|
+
allPassing: false,
|
|
2200
|
+
}, false);
|
|
2201
|
+
expect(result).toBe(false);
|
|
2202
|
+
});
|
|
2203
|
+
});
|
|
2204
|
+
// ── queryIncompleteEntities ─────────────────────────────────────────
|
|
2205
|
+
describe('queryIncompleteEntities', () => {
|
|
2206
|
+
let db;
|
|
2207
|
+
let rawDb;
|
|
2208
|
+
const projectId = 'test-project-id';
|
|
2209
|
+
beforeEach(async () => {
|
|
2210
|
+
rawDb = new Database(':memory:');
|
|
2211
|
+
db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
|
|
2212
|
+
await db.schema
|
|
2213
|
+
.createTable('editor_scenarios')
|
|
2214
|
+
.addColumn('id', 'varchar', (col) => col.primaryKey())
|
|
2215
|
+
.addColumn('project_id', 'varchar', (col) => col.notNull())
|
|
2216
|
+
.addColumn('name', 'varchar', (col) => col.notNull())
|
|
2217
|
+
.addColumn('component_name', 'varchar')
|
|
2218
|
+
.addColumn('component_path', 'varchar')
|
|
2219
|
+
.addColumn('entity_sha', 'varchar')
|
|
2220
|
+
.addColumn('display_name', 'varchar')
|
|
2221
|
+
.addColumn('page_file_path', 'varchar')
|
|
2222
|
+
.addColumn('url', 'varchar')
|
|
2223
|
+
.addColumn('created_at', 'datetime')
|
|
2224
|
+
.addColumn('updated_at', 'datetime')
|
|
2225
|
+
.execute();
|
|
2226
|
+
await db.schema
|
|
2227
|
+
.createTable('analyses')
|
|
2228
|
+
.addColumn('id', 'varchar', (col) => col.primaryKey())
|
|
2229
|
+
.addColumn('entity_sha', 'varchar')
|
|
2230
|
+
.addColumn('entity_name', 'varchar')
|
|
2231
|
+
.addColumn('project_id', 'varchar')
|
|
2232
|
+
.execute();
|
|
2233
|
+
await db.schema
|
|
2234
|
+
.createTable('entities')
|
|
2235
|
+
.addColumn('sha', 'varchar', (col) => col.primaryKey())
|
|
2236
|
+
.addColumn('name', 'varchar')
|
|
2237
|
+
.addColumn('entity_type', 'varchar')
|
|
2238
|
+
.addColumn('file_path', 'varchar')
|
|
2202
2239
|
.execute();
|
|
2203
|
-
const result = await queryIncompleteEntities(db, projectId, null);
|
|
2204
|
-
expect(result).toEqual([
|
|
2205
|
-
{
|
|
2206
|
-
entitySha: 'sha-ghost',
|
|
2207
|
-
name: 'GhostComponent',
|
|
2208
|
-
scenarioCount: 1,
|
|
2209
|
-
preExisting: false,
|
|
2210
|
-
},
|
|
2211
|
-
]);
|
|
2212
2240
|
});
|
|
2213
|
-
|
|
2214
|
-
|
|
2241
|
+
afterEach(async () => {
|
|
2242
|
+
await db.destroy();
|
|
2243
|
+
});
|
|
2244
|
+
it('should return empty when all scenario entity SHAs have analyses', async () => {
|
|
2245
|
+
// Entity with analysis
|
|
2215
2246
|
await db
|
|
2216
2247
|
.insertInto('entities')
|
|
2217
2248
|
.values({
|
|
2218
|
-
sha: 'sha-
|
|
2219
|
-
name: '
|
|
2249
|
+
sha: 'sha-header',
|
|
2250
|
+
name: 'Header',
|
|
2220
2251
|
entity_type: 'visual',
|
|
2221
|
-
file_path: 'src/
|
|
2252
|
+
file_path: 'src/Header.tsx',
|
|
2253
|
+
})
|
|
2254
|
+
.execute();
|
|
2255
|
+
await db
|
|
2256
|
+
.insertInto('analyses')
|
|
2257
|
+
.values({
|
|
2258
|
+
id: 'a-1',
|
|
2259
|
+
entity_sha: 'sha-header',
|
|
2260
|
+
entity_name: 'Header',
|
|
2261
|
+
project_id: projectId,
|
|
2222
2262
|
})
|
|
2223
2263
|
.execute();
|
|
2224
2264
|
await db
|
|
2225
2265
|
.insertInto('editor_scenarios')
|
|
2226
2266
|
.values({
|
|
2227
|
-
id: 'sc-
|
|
2267
|
+
id: 'sc-1',
|
|
2228
2268
|
project_id: projectId,
|
|
2229
|
-
name: '
|
|
2230
|
-
component_name: '
|
|
2231
|
-
entity_sha: 'sha-
|
|
2232
|
-
created_at: '2026-03-16
|
|
2233
|
-
updated_at: '2026-03-16 20:00:00',
|
|
2269
|
+
name: 'Header - Default',
|
|
2270
|
+
component_name: 'Header',
|
|
2271
|
+
entity_sha: 'sha-header',
|
|
2272
|
+
created_at: '2026-03-16 23:00:00',
|
|
2234
2273
|
})
|
|
2235
2274
|
.execute();
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
// Should still be detected — the old time filter would have excluded it
|
|
2239
|
-
expect(result).toEqual([
|
|
2240
|
-
{
|
|
2241
|
-
entitySha: 'sha-preexisting',
|
|
2242
|
-
name: 'PreExistingComponent',
|
|
2243
|
-
scenarioCount: 1,
|
|
2244
|
-
preExisting: true,
|
|
2245
|
-
},
|
|
2246
|
-
]);
|
|
2275
|
+
const result = await queryIncompleteEntities(db, projectId, null);
|
|
2276
|
+
expect(result).toEqual([]);
|
|
2247
2277
|
});
|
|
2248
|
-
it('should
|
|
2278
|
+
it('should return entities with scenarios but no analyses', async () => {
|
|
2279
|
+
// Entity WITHOUT analysis
|
|
2249
2280
|
await db
|
|
2250
2281
|
.insertInto('entities')
|
|
2251
2282
|
.values({
|
|
2252
|
-
sha: 'sha-
|
|
2253
|
-
name: '
|
|
2283
|
+
sha: 'sha-chips',
|
|
2284
|
+
name: 'CollectionChips',
|
|
2254
2285
|
entity_type: 'visual',
|
|
2255
|
-
file_path: 'src/
|
|
2286
|
+
file_path: 'src/components/CollectionChips.tsx',
|
|
2256
2287
|
})
|
|
2257
2288
|
.execute();
|
|
2258
|
-
//
|
|
2289
|
+
// Scenario referencing that entity
|
|
2259
2290
|
await db
|
|
2260
2291
|
.insertInto('editor_scenarios')
|
|
2261
2292
|
.values({
|
|
2262
|
-
id: 'sc-
|
|
2293
|
+
id: 'sc-1',
|
|
2263
2294
|
project_id: projectId,
|
|
2264
|
-
name: '
|
|
2265
|
-
component_name: '
|
|
2266
|
-
entity_sha: 'sha-
|
|
2267
|
-
created_at: '2026-03-16
|
|
2268
|
-
updated_at: '2026-03-16 19:00:00',
|
|
2295
|
+
name: 'CollectionChips - Default',
|
|
2296
|
+
component_name: 'CollectionChips',
|
|
2297
|
+
entity_sha: 'sha-chips',
|
|
2298
|
+
created_at: '2026-03-16 23:00:00',
|
|
2269
2299
|
})
|
|
2270
2300
|
.execute();
|
|
2271
2301
|
await db
|
|
2272
2302
|
.insertInto('editor_scenarios')
|
|
2273
2303
|
.values({
|
|
2274
|
-
id: 'sc-
|
|
2304
|
+
id: 'sc-2',
|
|
2275
2305
|
project_id: projectId,
|
|
2276
|
-
name: '
|
|
2277
|
-
component_name: '
|
|
2278
|
-
entity_sha: 'sha-
|
|
2279
|
-
created_at: '2026-03-16
|
|
2280
|
-
updated_at: '2026-03-16 19:30:00',
|
|
2306
|
+
name: 'CollectionChips - Many',
|
|
2307
|
+
component_name: 'CollectionChips',
|
|
2308
|
+
entity_sha: 'sha-chips',
|
|
2309
|
+
created_at: '2026-03-16 23:01:00',
|
|
2281
2310
|
})
|
|
2282
2311
|
.execute();
|
|
2283
|
-
const result = await queryIncompleteEntities(db, projectId,
|
|
2312
|
+
const result = await queryIncompleteEntities(db, projectId, null);
|
|
2284
2313
|
expect(result).toEqual([
|
|
2285
2314
|
{
|
|
2286
|
-
entitySha: 'sha-
|
|
2287
|
-
name: '
|
|
2315
|
+
entitySha: 'sha-chips',
|
|
2316
|
+
name: 'CollectionChips',
|
|
2288
2317
|
scenarioCount: 2,
|
|
2289
|
-
preExisting:
|
|
2318
|
+
preExisting: false,
|
|
2290
2319
|
},
|
|
2291
2320
|
]);
|
|
2292
2321
|
});
|
|
2293
|
-
it('should
|
|
2322
|
+
it('should only return entities without analyses, not those with analyses', async () => {
|
|
2323
|
+
// Entity WITH analysis (Header)
|
|
2294
2324
|
await db
|
|
2295
2325
|
.insertInto('entities')
|
|
2296
2326
|
.values({
|
|
2297
|
-
sha: 'sha-
|
|
2298
|
-
name: '
|
|
2327
|
+
sha: 'sha-header',
|
|
2328
|
+
name: 'Header',
|
|
2299
2329
|
entity_type: 'visual',
|
|
2300
|
-
file_path: 'src/
|
|
2330
|
+
file_path: 'src/Header.tsx',
|
|
2331
|
+
})
|
|
2332
|
+
.execute();
|
|
2333
|
+
await db
|
|
2334
|
+
.insertInto('analyses')
|
|
2335
|
+
.values({
|
|
2336
|
+
id: 'a-1',
|
|
2337
|
+
entity_sha: 'sha-header',
|
|
2338
|
+
entity_name: 'Header',
|
|
2339
|
+
project_id: projectId,
|
|
2301
2340
|
})
|
|
2302
2341
|
.execute();
|
|
2303
2342
|
await db
|
|
2304
2343
|
.insertInto('editor_scenarios')
|
|
2305
2344
|
.values({
|
|
2306
|
-
id: 'sc-
|
|
2345
|
+
id: 'sc-1',
|
|
2307
2346
|
project_id: projectId,
|
|
2308
|
-
name: '
|
|
2309
|
-
component_name: '
|
|
2310
|
-
entity_sha: 'sha-
|
|
2311
|
-
created_at: '2026-03-16 23:
|
|
2312
|
-
updated_at: '2026-03-16 23:30:00',
|
|
2347
|
+
name: 'Header - Default',
|
|
2348
|
+
component_name: 'Header',
|
|
2349
|
+
entity_sha: 'sha-header',
|
|
2350
|
+
created_at: '2026-03-16 23:00:00',
|
|
2313
2351
|
})
|
|
2314
2352
|
.execute();
|
|
2315
|
-
|
|
2353
|
+
// Entity WITHOUT analysis (CollectionPicker)
|
|
2354
|
+
await db
|
|
2355
|
+
.insertInto('entities')
|
|
2356
|
+
.values({
|
|
2357
|
+
sha: 'sha-picker',
|
|
2358
|
+
name: 'CollectionPicker',
|
|
2359
|
+
entity_type: 'visual',
|
|
2360
|
+
file_path: 'src/components/CollectionPicker.tsx',
|
|
2361
|
+
})
|
|
2362
|
+
.execute();
|
|
2363
|
+
await db
|
|
2364
|
+
.insertInto('editor_scenarios')
|
|
2365
|
+
.values({
|
|
2366
|
+
id: 'sc-2',
|
|
2367
|
+
project_id: projectId,
|
|
2368
|
+
name: 'CollectionPicker - Default',
|
|
2369
|
+
component_name: 'CollectionPicker',
|
|
2370
|
+
entity_sha: 'sha-picker',
|
|
2371
|
+
created_at: '2026-03-16 23:00:00',
|
|
2372
|
+
})
|
|
2373
|
+
.execute();
|
|
2374
|
+
const result = await queryIncompleteEntities(db, projectId, null);
|
|
2316
2375
|
expect(result).toEqual([
|
|
2317
2376
|
{
|
|
2318
|
-
entitySha: 'sha-
|
|
2319
|
-
name: '
|
|
2377
|
+
entitySha: 'sha-picker',
|
|
2378
|
+
name: 'CollectionPicker',
|
|
2320
2379
|
scenarioCount: 1,
|
|
2321
2380
|
preExisting: false,
|
|
2322
2381
|
},
|
|
2323
2382
|
]);
|
|
2324
2383
|
});
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2384
|
+
it('should return both pre-session and in-session entities with preExisting flags', async () => {
|
|
2385
|
+
// Entity without analysis, scenario created BEFORE session
|
|
2386
|
+
await db
|
|
2387
|
+
.insertInto('entities')
|
|
2388
|
+
.values({
|
|
2389
|
+
sha: 'sha-old',
|
|
2390
|
+
name: 'OldComponent',
|
|
2391
|
+
entity_type: 'visual',
|
|
2392
|
+
file_path: 'src/OldComponent.tsx',
|
|
2393
|
+
})
|
|
2394
|
+
.execute();
|
|
2395
|
+
await db
|
|
2396
|
+
.insertInto('editor_scenarios')
|
|
2397
|
+
.values({
|
|
2398
|
+
id: 'sc-old',
|
|
2399
|
+
project_id: projectId,
|
|
2400
|
+
name: 'OldComponent - Default',
|
|
2401
|
+
component_name: 'OldComponent',
|
|
2402
|
+
entity_sha: 'sha-old',
|
|
2403
|
+
created_at: '2026-03-16 20:00:00',
|
|
2404
|
+
})
|
|
2405
|
+
.execute();
|
|
2406
|
+
// Entity without analysis, scenario created DURING session
|
|
2407
|
+
await db
|
|
2408
|
+
.insertInto('entities')
|
|
2409
|
+
.values({
|
|
2410
|
+
sha: 'sha-new',
|
|
2411
|
+
name: 'NewComponent',
|
|
2412
|
+
entity_type: 'visual',
|
|
2413
|
+
file_path: 'src/NewComponent.tsx',
|
|
2414
|
+
})
|
|
2415
|
+
.execute();
|
|
2416
|
+
await db
|
|
2417
|
+
.insertInto('editor_scenarios')
|
|
2418
|
+
.values({
|
|
2419
|
+
id: 'sc-new',
|
|
2420
|
+
project_id: projectId,
|
|
2421
|
+
name: 'NewComponent - Default',
|
|
2422
|
+
component_name: 'NewComponent',
|
|
2423
|
+
entity_sha: 'sha-new',
|
|
2424
|
+
created_at: '2026-03-16 23:10:00',
|
|
2425
|
+
})
|
|
2426
|
+
.execute();
|
|
2427
|
+
const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
|
|
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,
|
|
2350
2435
|
},
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
],
|
|
2360
|
-
entityChangeStatus,
|
|
2361
|
-
});
|
|
2362
|
-
expect(result).toHaveLength(1);
|
|
2363
|
-
expect(result[0].scenarioName).toBe('Library - Rich Library');
|
|
2364
|
-
expect(result[0].entityName).toBe('App');
|
|
2365
|
-
expect(result[0].status.status).toBe('impacted');
|
|
2366
|
-
});
|
|
2367
|
-
it('should flag component scenario when its entity is directly edited and not recaptured', () => {
|
|
2368
|
-
const entityChangeStatus = {
|
|
2369
|
-
LibraryView: { status: 'edited' },
|
|
2370
|
-
};
|
|
2371
|
-
const result = identifyScenariosNeedingRecapture({
|
|
2372
|
-
scenarios: [
|
|
2373
|
-
{
|
|
2374
|
-
name: 'LibraryView - Empty',
|
|
2375
|
-
entityName: 'LibraryView',
|
|
2376
|
-
updatedInSession: false,
|
|
2377
|
-
},
|
|
2378
|
-
],
|
|
2379
|
-
entityChangeStatus,
|
|
2380
|
-
});
|
|
2381
|
-
expect(result).toHaveLength(1);
|
|
2382
|
-
expect(result[0].scenarioName).toBe('LibraryView - Empty');
|
|
2383
|
-
expect(result[0].entityName).toBe('LibraryView');
|
|
2384
|
-
expect(result[0].status.status).toBe('edited');
|
|
2436
|
+
{
|
|
2437
|
+
entitySha: 'sha-new',
|
|
2438
|
+
name: 'NewComponent',
|
|
2439
|
+
scenarioCount: 1,
|
|
2440
|
+
preExisting: false,
|
|
2441
|
+
},
|
|
2442
|
+
]));
|
|
2443
|
+
expect(result).toHaveLength(2);
|
|
2385
2444
|
});
|
|
2386
|
-
it('should
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2445
|
+
it('should flag preExisting: false when scenario was updated in session even if created before', async () => {
|
|
2446
|
+
await db
|
|
2447
|
+
.insertInto('entities')
|
|
2448
|
+
.values({
|
|
2449
|
+
sha: 'sha-updated',
|
|
2450
|
+
name: 'UpdatedComponent',
|
|
2451
|
+
entity_type: 'visual',
|
|
2452
|
+
file_path: 'src/Updated.tsx',
|
|
2453
|
+
})
|
|
2454
|
+
.execute();
|
|
2455
|
+
await db
|
|
2456
|
+
.insertInto('editor_scenarios')
|
|
2457
|
+
.values({
|
|
2458
|
+
id: 'sc-updated',
|
|
2459
|
+
project_id: projectId,
|
|
2460
|
+
name: 'UpdatedComponent - Default',
|
|
2461
|
+
component_name: 'UpdatedComponent',
|
|
2462
|
+
entity_sha: 'sha-updated',
|
|
2463
|
+
created_at: '2026-03-16 20:00:00',
|
|
2464
|
+
updated_at: '2026-03-16 23:20:00', // Updated in session
|
|
2465
|
+
})
|
|
2466
|
+
.execute();
|
|
2467
|
+
const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
|
|
2468
|
+
expect(result).toEqual([
|
|
2469
|
+
{
|
|
2470
|
+
entitySha: 'sha-updated',
|
|
2471
|
+
name: 'UpdatedComponent',
|
|
2472
|
+
scenarioCount: 1,
|
|
2473
|
+
preExisting: false,
|
|
2397
2474
|
},
|
|
2398
|
-
|
|
2399
|
-
const result = identifyScenariosNeedingRecapture({
|
|
2400
|
-
scenarios: [
|
|
2401
|
-
{
|
|
2402
|
-
name: 'App - Default',
|
|
2403
|
-
entityName: 'App',
|
|
2404
|
-
updatedInSession: true, // re-registered during Feature 2
|
|
2405
|
-
},
|
|
2406
|
-
],
|
|
2407
|
-
entityChangeStatus,
|
|
2408
|
-
});
|
|
2409
|
-
expect(result).toHaveLength(0);
|
|
2475
|
+
]);
|
|
2410
2476
|
});
|
|
2411
|
-
it('should
|
|
2412
|
-
|
|
2413
|
-
|
|
2477
|
+
it('should skip scenarios with null entity_sha', async () => {
|
|
2478
|
+
await db
|
|
2479
|
+
.insertInto('editor_scenarios')
|
|
2480
|
+
.values({
|
|
2481
|
+
id: 'sc-null',
|
|
2482
|
+
project_id: projectId,
|
|
2483
|
+
name: 'Orphan Scenario',
|
|
2484
|
+
component_name: 'Orphan',
|
|
2485
|
+
created_at: '2026-03-16 23:00:00',
|
|
2486
|
+
})
|
|
2487
|
+
.execute();
|
|
2488
|
+
const result = await queryIncompleteEntities(db, projectId, null);
|
|
2489
|
+
expect(result).toEqual([]);
|
|
2490
|
+
});
|
|
2491
|
+
it('should return empty when there are no scenarios', async () => {
|
|
2492
|
+
const result = await queryIncompleteEntities(db, projectId, null);
|
|
2493
|
+
expect(result).toEqual([]);
|
|
2494
|
+
});
|
|
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)
|
|
2526
|
+
await db
|
|
2527
|
+
.insertInto('editor_scenarios')
|
|
2528
|
+
.values({
|
|
2529
|
+
id: 'sc-btn',
|
|
2530
|
+
project_id: projectId,
|
|
2531
|
+
name: 'OpenLibraryButton - Default',
|
|
2532
|
+
component_name: 'OpenLibraryButton',
|
|
2533
|
+
entity_sha: 'sha-btn-v2',
|
|
2534
|
+
created_at: '2026-03-16 23:00:00',
|
|
2535
|
+
})
|
|
2536
|
+
.execute();
|
|
2537
|
+
// Should NOT flag as incomplete — sibling version has analyses
|
|
2538
|
+
const result = await queryIncompleteEntities(db, projectId, null);
|
|
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' },
|
|
2414
2849
|
};
|
|
2415
2850
|
const result = identifyScenariosNeedingRecapture({
|
|
2416
2851
|
scenarios: [
|
|
@@ -2508,6 +2943,25 @@ describe('editorAudit', () => {
|
|
|
2508
2943
|
});
|
|
2509
2944
|
expect(result).toHaveLength(0);
|
|
2510
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
|
+
});
|
|
2511
2965
|
});
|
|
2512
2966
|
// ── detectDuplicateNames ──────────────────────────────────────────
|
|
2513
2967
|
describe('detectDuplicateNames', () => {
|
|
@@ -2675,6 +3129,28 @@ describe('editorAudit', () => {
|
|
|
2675
3129
|
expect(result.components[0].status).toBe('missing');
|
|
2676
3130
|
expect(result.summary.componentsMissing).toBe(1);
|
|
2677
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
|
+
});
|
|
2678
3154
|
});
|
|
2679
3155
|
// ── queryUnassociatedScenarios ──────────────────────────────────────
|
|
2680
3156
|
describe('queryUnassociatedScenarios', () => {
|
|
@@ -2850,91 +3326,574 @@ describe('editorAudit', () => {
|
|
|
2850
3326
|
const result = await queryUnassociatedScenarios(db, projectId, null);
|
|
2851
3327
|
expect(result).toEqual([]);
|
|
2852
3328
|
});
|
|
2853
|
-
it('should scope to feature session when featureStartedAt is provided', async () => {
|
|
2854
|
-
// Pre-existing unassociated scenario (before session)
|
|
2855
|
-
await db
|
|
2856
|
-
.insertInto('editor_scenarios')
|
|
2857
|
-
.values({
|
|
2858
|
-
id: 'sc-old',
|
|
2859
|
-
project_id: projectId,
|
|
2860
|
-
name: 'OldComponent - Default',
|
|
2861
|
-
component_name: 'OldComponent',
|
|
2862
|
-
component_path: 'src/components/OldComponent.tsx',
|
|
2863
|
-
entity_sha: null,
|
|
2864
|
-
created_at: '2026-03-19 10:00:00',
|
|
2865
|
-
updated_at: '2026-03-19 10:00:00',
|
|
2866
|
-
})
|
|
2867
|
-
.execute();
|
|
2868
|
-
// New unassociated scenario (during session)
|
|
2869
|
-
await db
|
|
2870
|
-
.insertInto('editor_scenarios')
|
|
2871
|
-
.values({
|
|
2872
|
-
id: 'sc-new',
|
|
2873
|
-
project_id: projectId,
|
|
2874
|
-
name: 'NewComponent - Default',
|
|
2875
|
-
component_name: 'NewComponent',
|
|
2876
|
-
component_path: 'src/components/NewComponent.tsx',
|
|
2877
|
-
entity_sha: null,
|
|
2878
|
-
created_at: '2026-03-20 18:45:00',
|
|
2879
|
-
updated_at: '2026-03-20 18:45:00',
|
|
2880
|
-
})
|
|
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')
|
|
2881
3688
|
.execute();
|
|
2882
|
-
const result = await queryUnassociatedScenarios(db, projectId, '2026-03-20T18:00:00.000Z');
|
|
2883
|
-
// Should only find the session-scoped one
|
|
2884
|
-
expect(result).toHaveLength(1);
|
|
2885
|
-
expect(result[0].name).toBe('NewComponent');
|
|
2886
3689
|
});
|
|
2887
|
-
|
|
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');
|
|
2888
3695
|
await db
|
|
2889
|
-
.insertInto('
|
|
2890
|
-
.values(
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
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
|
+
])
|
|
2900
3713
|
.execute();
|
|
2901
|
-
const result = await
|
|
2902
|
-
|
|
2903
|
-
|
|
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);
|
|
2904
3731
|
});
|
|
2905
|
-
it('should
|
|
2906
|
-
|
|
2907
|
-
|
|
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');
|
|
2908
3746
|
await db
|
|
2909
|
-
.insertInto('
|
|
2910
|
-
.values(
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
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
|
+
])
|
|
2920
3764
|
.execute();
|
|
2921
|
-
const result = await
|
|
2922
|
-
|
|
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']);
|
|
2923
3780
|
});
|
|
2924
3781
|
});
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
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);
|
|
2929
3871
|
});
|
|
2930
|
-
it('should return
|
|
2931
|
-
expect(
|
|
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);
|
|
2932
3891
|
});
|
|
2933
|
-
it('should return false when
|
|
2934
|
-
expect(
|
|
3892
|
+
it('should return false when incomplete entities array is empty', () => {
|
|
3893
|
+
expect(isOnlyPreExistingIncomplete({ incompleteEntities: 0 }, [])).toBe(false);
|
|
2935
3894
|
});
|
|
2936
|
-
it('should return false when
|
|
2937
|
-
expect(
|
|
3895
|
+
it('should return false when incomplete entities array is missing', () => {
|
|
3896
|
+
expect(isOnlyPreExistingIncomplete({ incompleteEntities: 2, preExistingIncompleteEntities: 2 }, undefined)).toBe(false);
|
|
2938
3897
|
});
|
|
2939
3898
|
});
|
|
2940
3899
|
describe('isOnlyIncompleteEntities with unassociatedScenarios', () => {
|
|
@@ -2948,5 +3907,254 @@ describe('editorAudit', () => {
|
|
|
2948
3907
|
})).toBe(false);
|
|
2949
3908
|
});
|
|
2950
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);
|
|
4157
|
+
});
|
|
4158
|
+
});
|
|
2951
4159
|
});
|
|
2952
4160
|
//# sourceMappingURL=editorAudit.test.js.map
|