@codeyam/codeyam-cli 0.1.0-staging.d3e886e → 0.1.0-staging.d4f25c3
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 +23 -23
- package/analyzer-template/packages/ai/package.json +3 -3
- package/analyzer-template/packages/ai/src/lib/astScopes/astScopeAnalyzer.ts +34 -3
- package/analyzer-template/packages/ai/src/lib/completionCall.ts +114 -113
- package/analyzer-template/packages/ai/src/lib/dataStructure/ScopeDataStructure.ts +259 -5
- package/analyzer-template/packages/ai/src/lib/dataStructure/helpers/coercePrimitivesToArraysBySchema.ts +62 -0
- package/analyzer-template/packages/ai/src/lib/dataStructure/helpers/stripNullableMarkers.ts +35 -0
- package/analyzer-template/packages/ai/src/lib/generateEntityScenarioData.ts +78 -2
- package/analyzer-template/packages/ai/src/lib/generateExecutionFlows.ts +0 -33
- package/analyzer-template/packages/analyze/src/lib/ProjectAnalyzer.ts +19 -7
- package/analyzer-template/packages/analyze/src/lib/asts/index.ts +7 -2
- package/analyzer-template/packages/analyze/src/lib/asts/nodes/getNodeType.ts +1 -0
- package/analyzer-template/packages/analyze/src/lib/files/analyze/analyzeEntities.ts +9 -1
- package/analyzer-template/packages/analyze/src/lib/files/analyze/dependencyResolver.ts +0 -6
- package/analyzer-template/packages/analyze/src/lib/files/analyze/findOrCreateEntity.ts +12 -0
- package/analyzer-template/packages/analyze/src/lib/files/scenarios/TransformationTracer.ts +65 -28
- package/analyzer-template/packages/analyze/src/lib/files/scenarios/generateDataStructure.ts +83 -0
- package/analyzer-template/packages/analyze/src/lib/files/scenarios/generateExecutionFlows.ts +0 -98
- package/analyzer-template/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.ts +23 -4
- package/analyzer-template/packages/aws/package.json +10 -10
- package/analyzer-template/packages/database/index.ts +1 -0
- package/analyzer-template/packages/database/package.json +3 -3
- package/analyzer-template/packages/database/src/lib/kysely/db.ts +8 -0
- package/analyzer-template/packages/database/src/lib/kysely/tables/editorScenariosTable.ts +164 -0
- package/analyzer-template/packages/database/src/lib/loadCommits.ts +31 -20
- package/analyzer-template/packages/database/src/lib/loadEntities.ts +0 -6
- package/analyzer-template/packages/database/src/lib/loadReadyToBeCapturedAnalyses.ts +0 -5
- package/analyzer-template/packages/database/src/lib/updateCommitMetadata.ts +94 -143
- package/analyzer-template/packages/database/src/lib/updateFreshAnalysisStatus.ts +58 -42
- package/analyzer-template/packages/database/src/lib/updateFreshAnalysisStatusWithScenarios.ts +81 -65
- package/analyzer-template/packages/generate/src/lib/componentScenarioPage/generateScenarioClientWrapper.ts +29 -1
- package/analyzer-template/packages/generate/src/lib/componentScenarioPage/getIFrameMessageListenerCode.ts +33 -5
- package/analyzer-template/packages/github/dist/database/index.d.ts +1 -0
- package/analyzer-template/packages/github/dist/database/index.d.ts.map +1 -1
- package/analyzer-template/packages/github/dist/database/index.js +1 -0
- package/analyzer-template/packages/github/dist/database/index.js.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/kysely/db.d.ts +2 -0
- package/analyzer-template/packages/github/dist/database/src/lib/kysely/db.d.ts.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/kysely/db.js +5 -0
- package/analyzer-template/packages/github/dist/database/src/lib/kysely/db.js.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/editorScenariosTable.d.ts +29 -0
- package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/editorScenariosTable.d.ts.map +1 -0
- package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/editorScenariosTable.js +149 -0
- package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/editorScenariosTable.js.map +1 -0
- package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/scenariosTable.d.ts +5 -0
- package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/scenariosTable.d.ts.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/loadCommits.d.ts.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/loadCommits.js +23 -13
- package/analyzer-template/packages/github/dist/database/src/lib/loadCommits.js.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/loadEntities.d.ts.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/loadEntities.js +0 -6
- package/analyzer-template/packages/github/dist/database/src/lib/loadEntities.js.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/loadReadyToBeCapturedAnalyses.d.ts.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/loadReadyToBeCapturedAnalyses.js +1 -4
- package/analyzer-template/packages/github/dist/database/src/lib/loadReadyToBeCapturedAnalyses.js.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/updateCommitMetadata.d.ts.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/updateCommitMetadata.js +76 -90
- package/analyzer-template/packages/github/dist/database/src/lib/updateCommitMetadata.js.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/updateFreshAnalysisStatus.d.ts.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/updateFreshAnalysisStatus.js +41 -30
- package/analyzer-template/packages/github/dist/database/src/lib/updateFreshAnalysisStatus.js.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/updateFreshAnalysisStatusWithScenarios.d.ts.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/updateFreshAnalysisStatusWithScenarios.js +68 -57
- package/analyzer-template/packages/github/dist/database/src/lib/updateFreshAnalysisStatusWithScenarios.js.map +1 -1
- package/analyzer-template/packages/github/dist/generate/src/lib/componentScenarioPage/generateScenarioClientWrapper.d.ts.map +1 -1
- package/analyzer-template/packages/github/dist/generate/src/lib/componentScenarioPage/generateScenarioClientWrapper.js +29 -1
- package/analyzer-template/packages/github/dist/generate/src/lib/componentScenarioPage/generateScenarioClientWrapper.js.map +1 -1
- package/analyzer-template/packages/github/dist/generate/src/lib/componentScenarioPage/getIFrameMessageListenerCode.d.ts.map +1 -1
- package/analyzer-template/packages/github/dist/generate/src/lib/componentScenarioPage/getIFrameMessageListenerCode.js +33 -5
- package/analyzer-template/packages/github/dist/generate/src/lib/componentScenarioPage/getIFrameMessageListenerCode.js.map +1 -1
- package/analyzer-template/packages/github/dist/types/src/enums/ProjectFramework.d.ts +2 -0
- package/analyzer-template/packages/github/dist/types/src/enums/ProjectFramework.d.ts.map +1 -1
- package/analyzer-template/packages/github/dist/types/src/enums/ProjectFramework.js +2 -0
- package/analyzer-template/packages/github/dist/types/src/enums/ProjectFramework.js.map +1 -1
- package/analyzer-template/packages/github/dist/types/src/types/ProjectMetadata.d.ts +1 -0
- package/analyzer-template/packages/github/dist/types/src/types/ProjectMetadata.d.ts.map +1 -1
- package/analyzer-template/packages/github/dist/types/src/types/Scenario.d.ts +10 -0
- package/analyzer-template/packages/github/dist/types/src/types/Scenario.d.ts.map +1 -1
- package/analyzer-template/packages/github/package.json +1 -1
- package/analyzer-template/packages/types/src/enums/ProjectFramework.ts +2 -0
- package/analyzer-template/packages/types/src/types/ProjectMetadata.ts +1 -0
- package/analyzer-template/packages/types/src/types/Scenario.ts +10 -0
- package/analyzer-template/packages/ui-components/package.json +1 -1
- package/analyzer-template/packages/utils/dist/types/src/enums/ProjectFramework.d.ts +2 -0
- package/analyzer-template/packages/utils/dist/types/src/enums/ProjectFramework.d.ts.map +1 -1
- package/analyzer-template/packages/utils/dist/types/src/enums/ProjectFramework.js +2 -0
- package/analyzer-template/packages/utils/dist/types/src/enums/ProjectFramework.js.map +1 -1
- package/analyzer-template/packages/utils/dist/types/src/types/ProjectMetadata.d.ts +1 -0
- package/analyzer-template/packages/utils/dist/types/src/types/ProjectMetadata.d.ts.map +1 -1
- package/analyzer-template/packages/utils/dist/types/src/types/Scenario.d.ts +10 -0
- package/analyzer-template/packages/utils/dist/types/src/types/Scenario.d.ts.map +1 -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 +6 -2
- 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 +14 -2
- package/analyzer-template/playwright/captureFromUrl.ts +89 -82
- package/analyzer-template/project/constructMockCode.ts +168 -48
- package/analyzer-template/project/orchestrateCapture.ts +4 -1
- package/analyzer-template/project/reconcileMockDataKeys.ts +19 -14
- package/analyzer-template/project/start.ts +3 -0
- package/analyzer-template/project/startScenarioCapture.ts +9 -0
- package/analyzer-template/project/writeClientLogRoute.ts +125 -0
- package/analyzer-template/project/writeMockDataTsx.ts +17 -0
- package/analyzer-template/project/writeScenarioComponents.ts +96 -17
- package/analyzer-template/tsconfig.json +13 -1
- package/background/src/lib/virtualized/project/constructMockCode.js +143 -39
- package/background/src/lib/virtualized/project/constructMockCode.js.map +1 -1
- package/background/src/lib/virtualized/project/orchestrateCapture.js +4 -1
- package/background/src/lib/virtualized/project/orchestrateCapture.js.map +1 -1
- package/background/src/lib/virtualized/project/reconcileMockDataKeys.js +17 -11
- package/background/src/lib/virtualized/project/reconcileMockDataKeys.js.map +1 -1
- package/background/src/lib/virtualized/project/start.js +2 -0
- package/background/src/lib/virtualized/project/start.js.map +1 -1
- package/background/src/lib/virtualized/project/startScenarioCapture.js +5 -0
- package/background/src/lib/virtualized/project/startScenarioCapture.js.map +1 -1
- package/background/src/lib/virtualized/project/writeClientLogRoute.js +110 -0
- package/background/src/lib/virtualized/project/writeClientLogRoute.js.map +1 -0
- package/background/src/lib/virtualized/project/writeMockDataTsx.js +12 -0
- package/background/src/lib/virtualized/project/writeMockDataTsx.js.map +1 -1
- package/background/src/lib/virtualized/project/writeScenarioComponents.js +73 -12
- package/background/src/lib/virtualized/project/writeScenarioComponents.js.map +1 -1
- package/codeyam-cli/scripts/apply-setup.js +208 -11
- package/codeyam-cli/scripts/apply-setup.js.map +1 -1
- package/codeyam-cli/src/__tests__/memory-scripts/filter-session.test.js +196 -0
- package/codeyam-cli/src/__tests__/memory-scripts/filter-session.test.js.map +1 -0
- package/codeyam-cli/src/__tests__/memory-scripts/read-json-field.test.js +114 -0
- package/codeyam-cli/src/__tests__/memory-scripts/read-json-field.test.js.map +1 -0
- package/codeyam-cli/src/__tests__/memory-scripts/ripgrep-fallback.test.js +149 -0
- package/codeyam-cli/src/__tests__/memory-scripts/ripgrep-fallback.test.js.map +1 -0
- package/codeyam-cli/src/cli.js +41 -25
- package/codeyam-cli/src/cli.js.map +1 -1
- package/codeyam-cli/src/commands/__tests__/editor.isolateArgs.test.js +51 -0
- package/codeyam-cli/src/commands/__tests__/editor.isolateArgs.test.js.map +1 -0
- package/codeyam-cli/src/commands/__tests__/editor.stepDispatch.test.js +56 -0
- package/codeyam-cli/src/commands/__tests__/editor.stepDispatch.test.js.map +1 -0
- package/codeyam-cli/src/commands/__tests__/init.gitignore.test.js +101 -47
- package/codeyam-cli/src/commands/__tests__/init.gitignore.test.js.map +1 -1
- package/codeyam-cli/src/commands/analyze.js +17 -7
- package/codeyam-cli/src/commands/analyze.js.map +1 -1
- package/codeyam-cli/src/commands/default.js +14 -2
- package/codeyam-cli/src/commands/default.js.map +1 -1
- package/codeyam-cli/src/commands/editor.js +4630 -0
- package/codeyam-cli/src/commands/editor.js.map +1 -0
- package/codeyam-cli/src/commands/editorIsolateArgs.js +25 -0
- package/codeyam-cli/src/commands/editorIsolateArgs.js.map +1 -0
- package/codeyam-cli/src/commands/init.js +109 -45
- package/codeyam-cli/src/commands/init.js.map +1 -1
- package/codeyam-cli/src/commands/memory.js +89 -75
- package/codeyam-cli/src/commands/memory.js.map +1 -1
- package/codeyam-cli/src/commands/telemetry.js +37 -0
- package/codeyam-cli/src/commands/telemetry.js.map +1 -0
- package/codeyam-cli/src/data/techStacks.js +77 -0
- package/codeyam-cli/src/data/techStacks.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/analyzerFinalization.test.js +173 -0
- package/codeyam-cli/src/utils/__tests__/analyzerFinalization.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/backgroundServer.test.js +46 -0
- package/codeyam-cli/src/utils/__tests__/backgroundServer.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/devServerState.test.js +134 -0
- package/codeyam-cli/src/utils/__tests__/devServerState.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorApi.test.js +137 -0
- package/codeyam-cli/src/utils/__tests__/editorApi.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorAudit.test.js +2379 -0
- package/codeyam-cli/src/utils/__tests__/editorAudit.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorBroadcastViewport.test.js +76 -0
- package/codeyam-cli/src/utils/__tests__/editorBroadcastViewport.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorCapture.test.js +93 -0
- package/codeyam-cli/src/utils/__tests__/editorCapture.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorDeleteScenario.test.js +100 -0
- package/codeyam-cli/src/utils/__tests__/editorDeleteScenario.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorDevServer.test.js +304 -0
- package/codeyam-cli/src/utils/__tests__/editorDevServer.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorEntityChangeStatus.test.js +194 -0
- package/codeyam-cli/src/utils/__tests__/editorEntityChangeStatus.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorEntityHelpers.test.js +315 -0
- package/codeyam-cli/src/utils/__tests__/editorEntityHelpers.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorImageVerifier.test.js +294 -0
- package/codeyam-cli/src/utils/__tests__/editorImageVerifier.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorJournal.test.js +542 -0
- package/codeyam-cli/src/utils/__tests__/editorJournal.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorLoaderHelpers.test.js +594 -0
- package/codeyam-cli/src/utils/__tests__/editorLoaderHelpers.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorMigration.test.js +435 -0
- package/codeyam-cli/src/utils/__tests__/editorMigration.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorMockState.test.js +270 -0
- package/codeyam-cli/src/utils/__tests__/editorMockState.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorPreloadHelpers.test.js +217 -0
- package/codeyam-cli/src/utils/__tests__/editorPreloadHelpers.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorPreview.test.js +353 -0
- package/codeyam-cli/src/utils/__tests__/editorPreview.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorProxySession.test.js +153 -0
- package/codeyam-cli/src/utils/__tests__/editorProxySession.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorScenarioLookup.test.js +139 -0
- package/codeyam-cli/src/utils/__tests__/editorScenarioLookup.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorScenarioSwitch.test.js +221 -0
- package/codeyam-cli/src/utils/__tests__/editorScenarioSwitch.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorScenarios.test.js +1559 -0
- package/codeyam-cli/src/utils/__tests__/editorScenarios.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorSeedAdapter.test.js +280 -0
- package/codeyam-cli/src/utils/__tests__/editorSeedAdapter.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorSeedAdapterPrismaValidation.test.js +143 -0
- package/codeyam-cli/src/utils/__tests__/editorSeedAdapterPrismaValidation.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorSessionFilter.test.js +66 -0
- package/codeyam-cli/src/utils/__tests__/editorSessionFilter.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorShouldRevalidate.test.js +53 -0
- package/codeyam-cli/src/utils/__tests__/editorShouldRevalidate.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/entityChangeStatus.test.js +1857 -0
- package/codeyam-cli/src/utils/__tests__/entityChangeStatus.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/git.editor.test.js +134 -0
- package/codeyam-cli/src/utils/__tests__/git.editor.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/journalCaptureStabilization.test.js +107 -0
- package/codeyam-cli/src/utils/__tests__/journalCaptureStabilization.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/npmVersionCheck.test.js +26 -20
- package/codeyam-cli/src/utils/__tests__/npmVersionCheck.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/parseRegisterArg.test.js +129 -0
- package/codeyam-cli/src/utils/__tests__/parseRegisterArg.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/pathIgnoring.test.js +9 -0
- package/codeyam-cli/src/utils/__tests__/pathIgnoring.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/project.test.js +65 -0
- package/codeyam-cli/src/utils/__tests__/project.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/routePatternMatching.test.js +118 -0
- package/codeyam-cli/src/utils/__tests__/routePatternMatching.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/scenarioCoverage.test.js +284 -0
- package/codeyam-cli/src/utils/__tests__/scenarioCoverage.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/scenarioMarkers.test.js +121 -0
- package/codeyam-cli/src/utils/__tests__/scenarioMarkers.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/scenariosManifest.test.js +672 -0
- package/codeyam-cli/src/utils/__tests__/scenariosManifest.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/setupClaudeCodeSettings.test.js +51 -4
- package/codeyam-cli/src/utils/__tests__/setupClaudeCodeSettings.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/telemetry.test.js +159 -0
- package/codeyam-cli/src/utils/__tests__/telemetry.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/templateConsistency.test.js +51 -0
- package/codeyam-cli/src/utils/__tests__/templateConsistency.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/webappDetection.test.js +142 -0
- package/codeyam-cli/src/utils/__tests__/webappDetection.test.js.map +1 -0
- package/codeyam-cli/src/utils/analysisRunner.js +3 -1
- package/codeyam-cli/src/utils/analysisRunner.js.map +1 -1
- package/codeyam-cli/src/utils/analyzer.js +9 -0
- package/codeyam-cli/src/utils/analyzer.js.map +1 -1
- package/codeyam-cli/src/utils/analyzerFinalization.js +100 -0
- package/codeyam-cli/src/utils/analyzerFinalization.js.map +1 -0
- package/codeyam-cli/src/utils/backgroundServer.js +105 -13
- package/codeyam-cli/src/utils/backgroundServer.js.map +1 -1
- package/codeyam-cli/src/utils/buildFlags.js +4 -0
- package/codeyam-cli/src/utils/buildFlags.js.map +1 -0
- package/codeyam-cli/src/utils/database.js +37 -2
- package/codeyam-cli/src/utils/database.js.map +1 -1
- package/codeyam-cli/src/utils/devModeEvents.js +40 -0
- package/codeyam-cli/src/utils/devModeEvents.js.map +1 -0
- package/codeyam-cli/src/utils/devServerState.js +71 -0
- package/codeyam-cli/src/utils/devServerState.js.map +1 -0
- package/codeyam-cli/src/utils/editorApi.js +79 -0
- package/codeyam-cli/src/utils/editorApi.js.map +1 -0
- package/codeyam-cli/src/utils/editorAudit.js +480 -0
- package/codeyam-cli/src/utils/editorAudit.js.map +1 -0
- package/codeyam-cli/src/utils/editorBroadcastViewport.js +26 -0
- package/codeyam-cli/src/utils/editorBroadcastViewport.js.map +1 -0
- package/codeyam-cli/src/utils/editorCapture.js +102 -0
- package/codeyam-cli/src/utils/editorCapture.js.map +1 -0
- package/codeyam-cli/src/utils/editorDeleteScenario.js +67 -0
- package/codeyam-cli/src/utils/editorDeleteScenario.js.map +1 -0
- package/codeyam-cli/src/utils/editorDevServer.js +197 -0
- package/codeyam-cli/src/utils/editorDevServer.js.map +1 -0
- package/codeyam-cli/src/utils/editorEntityChangeStatus.js +50 -0
- package/codeyam-cli/src/utils/editorEntityChangeStatus.js.map +1 -0
- package/codeyam-cli/src/utils/editorEntityHelpers.js +144 -0
- package/codeyam-cli/src/utils/editorEntityHelpers.js.map +1 -0
- package/codeyam-cli/src/utils/editorImageVerifier.js +155 -0
- package/codeyam-cli/src/utils/editorImageVerifier.js.map +1 -0
- package/codeyam-cli/src/utils/editorJournal.js +225 -0
- package/codeyam-cli/src/utils/editorJournal.js.map +1 -0
- package/codeyam-cli/src/utils/editorLoaderHelpers.js +152 -0
- package/codeyam-cli/src/utils/editorLoaderHelpers.js.map +1 -0
- package/codeyam-cli/src/utils/editorMigration.js +224 -0
- package/codeyam-cli/src/utils/editorMigration.js.map +1 -0
- package/codeyam-cli/src/utils/editorMockState.js +248 -0
- package/codeyam-cli/src/utils/editorMockState.js.map +1 -0
- package/codeyam-cli/src/utils/editorPreloadHelpers.js +135 -0
- package/codeyam-cli/src/utils/editorPreloadHelpers.js.map +1 -0
- package/codeyam-cli/src/utils/editorPreview.js +137 -0
- package/codeyam-cli/src/utils/editorPreview.js.map +1 -0
- package/codeyam-cli/src/utils/editorScenarioSwitch.js +112 -0
- package/codeyam-cli/src/utils/editorScenarioSwitch.js.map +1 -0
- package/codeyam-cli/src/utils/editorScenarios.js +557 -0
- package/codeyam-cli/src/utils/editorScenarios.js.map +1 -0
- package/codeyam-cli/src/utils/editorSeedAdapter.js +422 -0
- package/codeyam-cli/src/utils/editorSeedAdapter.js.map +1 -0
- package/codeyam-cli/src/utils/editorShouldRevalidate.js +21 -0
- package/codeyam-cli/src/utils/editorShouldRevalidate.js.map +1 -0
- package/codeyam-cli/src/utils/entityChangeStatus.js +366 -0
- package/codeyam-cli/src/utils/entityChangeStatus.js.map +1 -0
- package/codeyam-cli/src/utils/entityChangeStatus.server.js +196 -0
- package/codeyam-cli/src/utils/entityChangeStatus.server.js.map +1 -0
- package/codeyam-cli/src/utils/fileMetadata.js +5 -0
- package/codeyam-cli/src/utils/fileMetadata.js.map +1 -1
- package/codeyam-cli/src/utils/fileWatcher.js +63 -9
- package/codeyam-cli/src/utils/fileWatcher.js.map +1 -1
- package/codeyam-cli/src/utils/git.js +103 -0
- package/codeyam-cli/src/utils/git.js.map +1 -1
- package/codeyam-cli/src/utils/install-skills.js +71 -15
- package/codeyam-cli/src/utils/install-skills.js.map +1 -1
- package/codeyam-cli/src/utils/interactiveSyncWatcher.js +126 -0
- package/codeyam-cli/src/utils/interactiveSyncWatcher.js.map +1 -0
- package/codeyam-cli/src/utils/npmVersionCheck.js +2 -2
- package/codeyam-cli/src/utils/npmVersionCheck.js.map +1 -1
- package/codeyam-cli/src/utils/parseRegisterArg.js +31 -0
- package/codeyam-cli/src/utils/parseRegisterArg.js.map +1 -0
- package/codeyam-cli/src/utils/pathIgnoring.js +19 -7
- package/codeyam-cli/src/utils/pathIgnoring.js.map +1 -1
- package/codeyam-cli/src/utils/progress.js +2 -2
- package/codeyam-cli/src/utils/progress.js.map +1 -1
- package/codeyam-cli/src/utils/project.js +15 -5
- package/codeyam-cli/src/utils/project.js.map +1 -1
- package/codeyam-cli/src/utils/queue/__tests__/heartbeat.test.js +11 -11
- package/codeyam-cli/src/utils/queue/__tests__/heartbeat.test.js.map +1 -1
- package/codeyam-cli/src/utils/queue/__tests__/manager.test.js +22 -0
- package/codeyam-cli/src/utils/queue/__tests__/manager.test.js.map +1 -1
- package/codeyam-cli/src/utils/queue/heartbeat.js +13 -5
- package/codeyam-cli/src/utils/queue/heartbeat.js.map +1 -1
- package/codeyam-cli/src/utils/queue/job.js +70 -1
- package/codeyam-cli/src/utils/queue/job.js.map +1 -1
- package/codeyam-cli/src/utils/queue/manager.js +7 -6
- package/codeyam-cli/src/utils/queue/manager.js.map +1 -1
- package/codeyam-cli/src/utils/requireSimulations.js +1 -1
- package/codeyam-cli/src/utils/requireSimulations.js.map +1 -1
- package/codeyam-cli/src/utils/routePatternMatching.js +129 -0
- package/codeyam-cli/src/utils/routePatternMatching.js.map +1 -0
- package/codeyam-cli/src/utils/ruleReflection/__tests__/contextBuilder.test.js +5 -6
- package/codeyam-cli/src/utils/ruleReflection/__tests__/contextBuilder.test.js.map +1 -1
- package/codeyam-cli/src/utils/ruleReflection/__tests__/integration/helpers/assertRules.js +1 -1
- package/codeyam-cli/src/utils/ruleReflection/__tests__/integration/helpers/assertRules.js.map +1 -1
- package/codeyam-cli/src/utils/ruleReflection/__tests__/integration/helpers/setupTempProject.js +0 -1
- package/codeyam-cli/src/utils/ruleReflection/__tests__/integration/helpers/setupTempProject.js.map +1 -1
- package/codeyam-cli/src/utils/ruleReflection/__tests__/integration/ruleReflectionE2E.test.js +2 -4
- package/codeyam-cli/src/utils/ruleReflection/__tests__/integration/ruleReflectionE2E.test.js.map +1 -1
- package/codeyam-cli/src/utils/ruleReflection/__tests__/promptBuilder.test.js +4 -6
- package/codeyam-cli/src/utils/ruleReflection/__tests__/promptBuilder.test.js.map +1 -1
- package/codeyam-cli/src/utils/ruleReflection/contextBuilder.js +1 -1
- package/codeyam-cli/src/utils/ruleReflection/contextBuilder.js.map +1 -1
- package/codeyam-cli/src/utils/rules/__tests__/parser.test.js +83 -0
- package/codeyam-cli/src/utils/rules/__tests__/parser.test.js.map +1 -0
- package/codeyam-cli/src/utils/rules/__tests__/pathMatcher.test.js +118 -0
- package/codeyam-cli/src/utils/rules/__tests__/pathMatcher.test.js.map +1 -0
- package/codeyam-cli/src/utils/rules/__tests__/rulePlacement.test.js +72 -0
- package/codeyam-cli/src/utils/rules/__tests__/rulePlacement.test.js.map +1 -0
- package/codeyam-cli/src/utils/rules/__tests__/sourceFiles.test.js +76 -0
- package/codeyam-cli/src/utils/rules/__tests__/sourceFiles.test.js.map +1 -0
- package/codeyam-cli/src/utils/rules/index.js +1 -0
- package/codeyam-cli/src/utils/rules/index.js.map +1 -1
- package/codeyam-cli/src/utils/rules/parser.js +14 -4
- package/codeyam-cli/src/utils/rules/parser.js.map +1 -1
- package/codeyam-cli/src/utils/rules/pathMatcher.js +34 -3
- package/codeyam-cli/src/utils/rules/pathMatcher.js.map +1 -1
- package/codeyam-cli/src/utils/rules/rulePlacement.js +65 -0
- package/codeyam-cli/src/utils/rules/rulePlacement.js.map +1 -0
- package/codeyam-cli/src/utils/rules/sourceFiles.js +43 -0
- package/codeyam-cli/src/utils/rules/sourceFiles.js.map +1 -0
- package/codeyam-cli/src/utils/scenarioCoverage.js +77 -0
- package/codeyam-cli/src/utils/scenarioCoverage.js.map +1 -0
- package/codeyam-cli/src/utils/scenarioMarkers.js +134 -0
- package/codeyam-cli/src/utils/scenarioMarkers.js.map +1 -0
- package/codeyam-cli/src/utils/scenariosManifest.js +285 -0
- package/codeyam-cli/src/utils/scenariosManifest.js.map +1 -0
- package/codeyam-cli/src/utils/serverState.js +57 -2
- package/codeyam-cli/src/utils/serverState.js.map +1 -1
- package/codeyam-cli/src/utils/setupClaudeCodeSettings.js +83 -11
- package/codeyam-cli/src/utils/setupClaudeCodeSettings.js.map +1 -1
- package/codeyam-cli/src/utils/simulationGateMiddleware.js +166 -0
- package/codeyam-cli/src/utils/simulationGateMiddleware.js.map +1 -0
- package/codeyam-cli/src/utils/slugUtils.js +25 -0
- package/codeyam-cli/src/utils/slugUtils.js.map +1 -0
- package/codeyam-cli/src/utils/syncMocksMiddleware.js +7 -26
- package/codeyam-cli/src/utils/syncMocksMiddleware.js.map +1 -1
- package/codeyam-cli/src/utils/telemetry.js +106 -0
- package/codeyam-cli/src/utils/telemetry.js.map +1 -0
- package/codeyam-cli/src/utils/telemetryMiddleware.js +22 -0
- package/codeyam-cli/src/utils/telemetryMiddleware.js.map +1 -0
- package/codeyam-cli/src/utils/testRunner.js +158 -0
- package/codeyam-cli/src/utils/testRunner.js.map +1 -0
- package/codeyam-cli/src/utils/transcriptPruning.js +67 -0
- package/codeyam-cli/src/utils/transcriptPruning.js.map +1 -0
- package/codeyam-cli/src/utils/versionInfo.js +46 -0
- package/codeyam-cli/src/utils/versionInfo.js.map +1 -1
- package/codeyam-cli/src/utils/webappDetection.js +35 -2
- package/codeyam-cli/src/utils/webappDetection.js.map +1 -1
- package/codeyam-cli/src/webserver/__tests__/buildPtyEnv.test.js +35 -0
- package/codeyam-cli/src/webserver/__tests__/buildPtyEnv.test.js.map +1 -0
- package/codeyam-cli/src/webserver/__tests__/clientErrors.test.js +80 -0
- package/codeyam-cli/src/webserver/__tests__/clientErrors.test.js.map +1 -0
- package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js +628 -0
- package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js.map +1 -0
- package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js +217 -0
- package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js.map +1 -0
- package/codeyam-cli/src/webserver/app/lib/clientErrors.js +71 -0
- package/codeyam-cli/src/webserver/app/lib/clientErrors.js.map +1 -0
- package/codeyam-cli/src/webserver/app/lib/database.js +41 -27
- package/codeyam-cli/src/webserver/app/lib/database.js.map +1 -1
- package/codeyam-cli/src/webserver/app/lib/dbNotifier.js.map +1 -1
- package/codeyam-cli/src/webserver/app/lib/git.js +397 -0
- package/codeyam-cli/src/webserver/app/lib/git.js.map +1 -0
- package/codeyam-cli/src/webserver/app/types/editor.js +8 -0
- package/codeyam-cli/src/webserver/app/types/editor.js.map +1 -0
- package/codeyam-cli/src/webserver/backgroundServer.js +141 -42
- package/codeyam-cli/src/webserver/backgroundServer.js.map +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/CopyButton-CLe80MMu.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/{EntityItem-bwuHPyTa.js → EntityItem-Crt_KN_U.js} +5 -5
- package/codeyam-cli/src/webserver/build/client/assets/EntityTypeBadge-CQgyEGV-.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/{EntityTypeIcon-BH0XDim7.js → EntityTypeIcon-CD7lGABo.js} +9 -9
- package/codeyam-cli/src/webserver/build/client/assets/InlineSpinner-CgTNOhnu.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/InteractivePreview-CKeQT5Ty.js +25 -0
- package/codeyam-cli/src/webserver/build/client/assets/LibraryFunctionPreview-D3s1MFkb.js +3 -0
- package/codeyam-cli/src/webserver/build/client/assets/{LoadingDots-BvMu2i-g.js → LoadingDots-By5zI316.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{LogViewer-kgBTLoJD.js → LogViewer-CM5zg40N.js} +3 -3
- package/codeyam-cli/src/webserver/build/client/assets/{ReportIssueModal-BzPgx-xO.js → ReportIssueModal-C2PLkej3.js} +4 -4
- package/codeyam-cli/src/webserver/build/client/assets/SafeScreenshot-DanvyBPb.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/{ScenarioViewer-BX2Ny2Qj.js → ScenarioViewer-DUMfcNVK.js} +3 -3
- package/codeyam-cli/src/webserver/build/client/assets/Spinner-D0LgAaSa.js +34 -0
- package/codeyam-cli/src/webserver/build/client/assets/TruncatedFilePath-CK7-NaPZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/ViewportInspectBar-BA_Ry-rs.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/{_index-BRx8ZGZo.js → _index-BAWd-Xjf.js} +4 -4
- package/codeyam-cli/src/webserver/build/client/assets/{activity.(_tab)-4S4yPfFw.js → activity.(_tab)-BOARiB-g.js} +8 -8
- package/codeyam-cli/src/webserver/build/client/assets/addon-canvas-DpzMmAy5.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/addon-fit-YJmn1quW.js +12 -0
- package/codeyam-cli/src/webserver/build/client/assets/addon-web-links-CHx25PAe.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/addon-webgl-DI8QOUvO.js +58 -0
- package/codeyam-cli/src/webserver/build/client/assets/agent-transcripts-Bg3e7q4S.js +22 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.dev-mode-events-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-audit-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-capture-scenario-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-client-errors-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-commit-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-dev-server-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-entity-status-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-file-diff-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-file-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-journal-entry-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-journal-image._-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-journal-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-journal-screenshot-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-journal-update-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-load-commit-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-project-info-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-refresh-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-register-scenario-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-rename-scenario-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-save-seed-state-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-scenario-coverage-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-scenario-data-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-scenario-image._-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-scenario-prompt-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-scenarios-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-session-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-switch-scenario-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-test-results-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.rule-path-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/{book-open-D4IPYH_y.js → book-open-CL-lMgHh.js} +2 -2
- package/codeyam-cli/src/webserver/build/client/assets/{chevron-down-CG65viiV.js → chevron-down-GmAjGS9-.js} +2 -2
- package/codeyam-cli/src/webserver/build/client/assets/chunk-JZWAC4HX-BAdwhyCx.js +43 -0
- package/codeyam-cli/src/webserver/build/client/assets/{circle-check-igfMr5DY.js → circle-check-DFcQkN5j.js} +2 -2
- package/codeyam-cli/src/webserver/build/client/assets/{copy-Coc4o_8c.js → copy-C6iF61Xs.js} +3 -3
- package/codeyam-cli/src/webserver/build/client/assets/createLucideIcon-4ImjHTVC.js +41 -0
- package/codeyam-cli/src/webserver/build/client/assets/dev.empty-C8y4mmyv.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/editor._tab-Gbk_i5Js.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-DN5ouXAl.js +58 -0
- package/codeyam-cli/src/webserver/build/client/assets/editorPreview-oepecPae.js +41 -0
- package/codeyam-cli/src/webserver/build/client/assets/{entity._sha._-B0h9AqE6.js → entity._sha._-Blfy9UlN.js} +11 -11
- package/codeyam-cli/src/webserver/build/client/assets/entity._sha.scenarios._scenarioId.dev-KTQuL0aj.js +6 -0
- package/codeyam-cli/src/webserver/build/client/assets/entity._sha.scenarios._scenarioId.fullscreen-C6eeL24i.js +6 -0
- package/codeyam-cli/src/webserver/build/client/assets/entity._sha_.create-scenario-DQM8E7L4.js +6 -0
- package/codeyam-cli/src/webserver/build/client/assets/{entity._sha_.edit._scenarioId-PePWg17F.js → entity._sha_.edit._scenarioId-CAoXLsQr.js} +2 -2
- package/codeyam-cli/src/webserver/build/client/assets/{entry.client-I-Wo99C_.js → entry.client-SuW9syRS.js} +6 -6
- package/codeyam-cli/src/webserver/build/client/assets/fileTableUtils-Daa96Fr1.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/files-D-xGrg29.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/git-Bq_fbXP5.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/globals-fAqOD9ex.css +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/{index-_417gcQW.js → index-Bp1l4hSv.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{index-CUM5iXwc.js → index-CWV9XZiG.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/index-DE3jI_dv.js +15 -0
- package/codeyam-cli/src/webserver/build/client/assets/jsx-runtime-D_zvdyIk.js +9 -0
- package/codeyam-cli/src/webserver/build/client/assets/labs-B_IX45ih.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/{loader-circle-TzRHMVog.js → loader-circle-De-7qQ2u.js} +2 -2
- package/codeyam-cli/src/webserver/build/client/assets/manifest-389033be.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/memory-Cx2xEx7s.js +101 -0
- package/codeyam-cli/src/webserver/build/client/assets/{pause-hjzB7t2z.js → pause-CFxEKL1u.js} +3 -3
- package/codeyam-cli/src/webserver/build/client/assets/root-DB3O9_9j.js +67 -0
- package/codeyam-cli/src/webserver/build/client/assets/{search-DcAwD_Ln.js → search-BdBb5aqc.js} +2 -2
- package/codeyam-cli/src/webserver/build/client/assets/settings-DdE-Untf.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/simulations-DSCdE99u.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/{terminal-DbEAHMbA.js → terminal-CrplD4b1.js} +3 -3
- package/codeyam-cli/src/webserver/build/client/assets/{triangle-alert-CAD5b1o_.js → triangle-alert-DqJ0j69l.js} +2 -2
- package/codeyam-cli/src/webserver/build/client/assets/useCustomSizes-DhXHbEjP.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/useLastLogLine-BNd5hYuW.js +2 -0
- package/codeyam-cli/src/webserver/build/client/assets/useReportContext-Cy5Qg_UR.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/useToast-5HR2j9ZE.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/xterm-BqvuqXEL.js +27 -0
- package/codeyam-cli/src/webserver/build/client/sound-test.html +98 -0
- package/codeyam-cli/src/webserver/build/server/assets/analysisRunner-D_1MSYeW.js +13 -0
- package/codeyam-cli/src/webserver/build/server/assets/index-ckWaCf_v.js +1 -0
- package/codeyam-cli/src/webserver/build/server/assets/init-ld124R4Z.js +10 -0
- package/codeyam-cli/src/webserver/build/server/assets/progress-CHTtrxFG.js +1 -0
- package/codeyam-cli/src/webserver/build/server/assets/server-build-DzzNZGv_.js +551 -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/devServer.js +39 -5
- package/codeyam-cli/src/webserver/devServer.js.map +1 -1
- package/codeyam-cli/src/webserver/editorProxy.js +976 -0
- package/codeyam-cli/src/webserver/editorProxy.js.map +1 -0
- package/codeyam-cli/src/webserver/idleDetector.js +106 -0
- package/codeyam-cli/src/webserver/idleDetector.js.map +1 -0
- package/codeyam-cli/src/webserver/mockStateEvents.js +28 -0
- package/codeyam-cli/src/webserver/mockStateEvents.js.map +1 -0
- package/codeyam-cli/src/webserver/public/sound-test.html +98 -0
- package/codeyam-cli/src/webserver/scripts/codeyam-preload.mjs +414 -0
- package/codeyam-cli/src/webserver/scripts/journalCapture.ts +266 -0
- package/codeyam-cli/src/webserver/server.js +341 -1
- package/codeyam-cli/src/webserver/server.js.map +1 -1
- package/codeyam-cli/src/webserver/terminalServer.js +831 -0
- package/codeyam-cli/src/webserver/terminalServer.js.map +1 -0
- package/codeyam-cli/templates/chrome-extension-react/EXTENSION_SETUP.md +75 -0
- package/codeyam-cli/templates/chrome-extension-react/README.md +46 -0
- package/codeyam-cli/templates/chrome-extension-react/gitignore +15 -0
- package/codeyam-cli/templates/chrome-extension-react/index.html +12 -0
- package/codeyam-cli/templates/chrome-extension-react/package.json +27 -0
- package/codeyam-cli/templates/chrome-extension-react/popup.html +12 -0
- package/codeyam-cli/templates/chrome-extension-react/public/manifest.json +15 -0
- package/codeyam-cli/templates/chrome-extension-react/src/background/service-worker.ts +7 -0
- package/codeyam-cli/templates/chrome-extension-react/src/globals.css +6 -0
- package/codeyam-cli/templates/chrome-extension-react/src/lib/storage.ts +37 -0
- package/codeyam-cli/templates/chrome-extension-react/src/popup/App.tsx +12 -0
- package/codeyam-cli/templates/chrome-extension-react/src/popup/main.tsx +10 -0
- package/codeyam-cli/templates/chrome-extension-react/tsconfig.json +24 -0
- package/codeyam-cli/templates/chrome-extension-react/vite.config.ts +41 -0
- package/codeyam-cli/templates/codeyam-editor-claude.md +147 -0
- package/codeyam-cli/templates/codeyam-editor-reference.md +214 -0
- package/codeyam-cli/templates/editor-step-hook.py +321 -0
- package/codeyam-cli/templates/expo-react-native/MOBILE_SETUP.md +89 -0
- package/codeyam-cli/templates/expo-react-native/README.md +41 -0
- package/codeyam-cli/templates/expo-react-native/app/(tabs)/_layout.tsx +33 -0
- package/codeyam-cli/templates/expo-react-native/app/(tabs)/index.tsx +12 -0
- package/codeyam-cli/templates/expo-react-native/app/(tabs)/settings.tsx +12 -0
- package/codeyam-cli/templates/expo-react-native/app/_layout.tsx +12 -0
- package/codeyam-cli/templates/expo-react-native/app.json +18 -0
- package/codeyam-cli/templates/expo-react-native/babel.config.js +9 -0
- package/codeyam-cli/templates/expo-react-native/gitignore +12 -0
- package/codeyam-cli/templates/expo-react-native/global.css +3 -0
- package/codeyam-cli/templates/expo-react-native/lib/storage.ts +32 -0
- package/codeyam-cli/templates/expo-react-native/metro.config.js +6 -0
- package/codeyam-cli/templates/expo-react-native/nativewind-env.d.ts +1 -0
- package/codeyam-cli/templates/expo-react-native/package.json +38 -0
- package/codeyam-cli/templates/expo-react-native/tailwind.config.js +10 -0
- package/codeyam-cli/templates/expo-react-native/tsconfig.json +10 -0
- package/codeyam-cli/templates/hooks/staleness-check.sh +43 -0
- package/codeyam-cli/templates/isolation-route/next-app.tsx.template +80 -0
- package/codeyam-cli/templates/isolation-route/next-pages.tsx.template +79 -0
- package/codeyam-cli/templates/isolation-route/vite-react.tsx.template +78 -0
- package/codeyam-cli/templates/msw/browser-setup.ts.template +47 -0
- package/codeyam-cli/templates/msw/handler-router.ts.template +47 -0
- package/codeyam-cli/templates/msw/server-setup.ts.template +52 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/AUTH_PATTERNS.md +308 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/AUTH_UPGRADE.md +304 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/DATABASE.md +126 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/FEATURE_PATTERNS.md +37 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/README.md +53 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/app/api/todos/route.ts +17 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/app/codeyam-isolate/layout.tsx +12 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/app/globals.css +26 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/app/layout.tsx +34 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/app/lib/prisma.ts +24 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/app/page.tsx +10 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/env +4 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/eslint.config.mjs +11 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/gitignore +64 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/next.config.ts +14 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/package.json +39 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/postcss.config.mjs +7 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/prisma/schema.prisma +27 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/prisma/seed.ts +40 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/prisma.config.ts +12 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/seed-adapter.ts +127 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/tsconfig.json +34 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/vitest.config.ts +13 -0
- package/codeyam-cli/templates/nextjs-prisma-supabase/README.md +52 -0
- package/codeyam-cli/templates/nextjs-prisma-supabase/SUPABASE_SETUP.md +104 -0
- package/codeyam-cli/templates/nextjs-prisma-supabase/app/api/todos/route.ts +17 -0
- package/codeyam-cli/templates/nextjs-prisma-supabase/app/globals.css +26 -0
- package/codeyam-cli/templates/nextjs-prisma-supabase/app/layout.tsx +34 -0
- package/codeyam-cli/templates/nextjs-prisma-supabase/app/lib/prisma.ts +20 -0
- package/codeyam-cli/templates/nextjs-prisma-supabase/app/lib/supabase.ts +12 -0
- package/codeyam-cli/templates/nextjs-prisma-supabase/app/page.tsx +10 -0
- package/codeyam-cli/templates/nextjs-prisma-supabase/env +9 -0
- package/codeyam-cli/templates/nextjs-prisma-supabase/eslint.config.mjs +11 -0
- package/codeyam-cli/templates/nextjs-prisma-supabase/gitignore +40 -0
- package/codeyam-cli/templates/nextjs-prisma-supabase/next.config.ts +11 -0
- package/codeyam-cli/templates/nextjs-prisma-supabase/package.json +37 -0
- package/codeyam-cli/templates/nextjs-prisma-supabase/postcss.config.mjs +7 -0
- package/codeyam-cli/templates/nextjs-prisma-supabase/prisma/schema.prisma +27 -0
- package/codeyam-cli/templates/nextjs-prisma-supabase/prisma/seed.ts +39 -0
- package/codeyam-cli/templates/nextjs-prisma-supabase/prisma.config.ts +12 -0
- package/codeyam-cli/templates/nextjs-prisma-supabase/tsconfig.json +34 -0
- package/codeyam-cli/templates/prompts/conversation-guidance.txt +44 -0
- package/codeyam-cli/templates/prompts/conversation-prompt.txt +28 -0
- package/codeyam-cli/templates/prompts/interruption-prompt.txt +31 -0
- package/codeyam-cli/templates/prompts/stale-rules-prompt.txt +24 -0
- package/codeyam-cli/templates/rule-notification-hook.py +44 -17
- package/codeyam-cli/templates/rule-reflection-hook.py +25 -5
- package/codeyam-cli/templates/rules-instructions.md +34 -88
- package/codeyam-cli/templates/seed-adapters/supabase.ts +282 -0
- package/codeyam-cli/templates/skills/codeyam-dev-mode/SKILL.md +237 -0
- package/codeyam-cli/templates/skills/codeyam-editor/SKILL.md +211 -0
- package/codeyam-cli/templates/{codeyam-memory.md → skills/codeyam-memory/SKILL.md} +215 -0
- package/codeyam-cli/templates/skills/codeyam-memory/scripts/holistic-analysis/deprecated-prompt.md +100 -0
- package/codeyam-cli/templates/skills/codeyam-memory/scripts/holistic-analysis/detect-deprecated-patterns.mjs +139 -0
- package/codeyam-cli/templates/skills/codeyam-memory/scripts/holistic-analysis/find-exports.mjs +52 -0
- package/codeyam-cli/templates/skills/codeyam-memory/scripts/holistic-analysis/misleading-api-prompt.md +117 -0
- package/codeyam-cli/templates/skills/codeyam-memory/scripts/lib/read-json-field.mjs +61 -0
- package/codeyam-cli/templates/skills/codeyam-memory/scripts/lib/ripgrep-fallback.mjs +155 -0
- package/codeyam-cli/templates/skills/codeyam-memory/scripts/session-mining/analyze-prompt.md +46 -0
- package/codeyam-cli/templates/skills/codeyam-memory/scripts/session-mining/cleanup.mjs +13 -0
- package/codeyam-cli/templates/skills/codeyam-memory/scripts/session-mining/filter-session.mjs +95 -0
- package/codeyam-cli/templates/skills/codeyam-memory/scripts/session-mining/preprocess.mjs +160 -0
- package/codeyam-cli/templates/{codeyam-new-rule.md → skills/codeyam-new-rule/SKILL.md} +0 -2
- package/package.json +22 -14
- package/packages/ai/src/lib/astScopes/astScopeAnalyzer.js +22 -4
- package/packages/ai/src/lib/astScopes/astScopeAnalyzer.js.map +1 -1
- package/packages/ai/src/lib/completionCall.js +10 -7
- package/packages/ai/src/lib/completionCall.js.map +1 -1
- package/packages/ai/src/lib/dataStructure/ScopeDataStructure.js +234 -3
- package/packages/ai/src/lib/dataStructure/ScopeDataStructure.js.map +1 -1
- package/packages/ai/src/lib/dataStructure/helpers/coercePrimitivesToArraysBySchema.js +54 -0
- package/packages/ai/src/lib/dataStructure/helpers/coercePrimitivesToArraysBySchema.js.map +1 -0
- package/packages/ai/src/lib/dataStructure/helpers/stripNullableMarkers.js +34 -0
- package/packages/ai/src/lib/dataStructure/helpers/stripNullableMarkers.js.map +1 -0
- package/packages/ai/src/lib/generateEntityScenarioData.js +57 -2
- package/packages/ai/src/lib/generateEntityScenarioData.js.map +1 -1
- package/packages/ai/src/lib/generateExecutionFlows.js +0 -11
- package/packages/ai/src/lib/generateExecutionFlows.js.map +1 -1
- package/packages/analyze/src/lib/ProjectAnalyzer.js +13 -4
- package/packages/analyze/src/lib/ProjectAnalyzer.js.map +1 -1
- package/packages/analyze/src/lib/asts/index.js +4 -2
- package/packages/analyze/src/lib/asts/index.js.map +1 -1
- package/packages/analyze/src/lib/asts/nodes/getNodeType.js +1 -0
- package/packages/analyze/src/lib/asts/nodes/getNodeType.js.map +1 -1
- package/packages/analyze/src/lib/files/analyze/analyzeEntities.js +8 -1
- package/packages/analyze/src/lib/files/analyze/analyzeEntities.js.map +1 -1
- package/packages/analyze/src/lib/files/analyze/dependencyResolver.js +0 -5
- package/packages/analyze/src/lib/files/analyze/dependencyResolver.js.map +1 -1
- package/packages/analyze/src/lib/files/analyze/findOrCreateEntity.js +9 -0
- package/packages/analyze/src/lib/files/analyze/findOrCreateEntity.js.map +1 -1
- package/packages/analyze/src/lib/files/scenarios/TransformationTracer.js +54 -27
- package/packages/analyze/src/lib/files/scenarios/TransformationTracer.js.map +1 -1
- package/packages/analyze/src/lib/files/scenarios/generateDataStructure.js +65 -0
- package/packages/analyze/src/lib/files/scenarios/generateDataStructure.js.map +1 -1
- package/packages/analyze/src/lib/files/scenarios/generateExecutionFlows.js +0 -40
- package/packages/analyze/src/lib/files/scenarios/generateExecutionFlows.js.map +1 -1
- package/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.js +18 -4
- package/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.js.map +1 -1
- package/packages/database/index.js +1 -0
- package/packages/database/index.js.map +1 -1
- package/packages/database/src/lib/kysely/db.js +5 -0
- package/packages/database/src/lib/kysely/db.js.map +1 -1
- package/packages/database/src/lib/kysely/tables/editorScenariosTable.js +149 -0
- package/packages/database/src/lib/kysely/tables/editorScenariosTable.js.map +1 -0
- package/packages/database/src/lib/loadCommits.js +23 -13
- package/packages/database/src/lib/loadCommits.js.map +1 -1
- package/packages/database/src/lib/loadEntities.js +0 -6
- package/packages/database/src/lib/loadEntities.js.map +1 -1
- package/packages/database/src/lib/loadReadyToBeCapturedAnalyses.js +1 -4
- package/packages/database/src/lib/loadReadyToBeCapturedAnalyses.js.map +1 -1
- package/packages/database/src/lib/updateCommitMetadata.js +76 -90
- package/packages/database/src/lib/updateCommitMetadata.js.map +1 -1
- package/packages/database/src/lib/updateFreshAnalysisStatus.js +41 -30
- package/packages/database/src/lib/updateFreshAnalysisStatus.js.map +1 -1
- package/packages/database/src/lib/updateFreshAnalysisStatusWithScenarios.js +68 -57
- package/packages/database/src/lib/updateFreshAnalysisStatusWithScenarios.js.map +1 -1
- package/packages/generate/src/lib/componentScenarioPage/generateScenarioClientWrapper.js +29 -1
- package/packages/generate/src/lib/componentScenarioPage/generateScenarioClientWrapper.js.map +1 -1
- package/packages/generate/src/lib/componentScenarioPage/getIFrameMessageListenerCode.js +33 -5
- package/packages/generate/src/lib/componentScenarioPage/getIFrameMessageListenerCode.js.map +1 -1
- package/packages/types/src/enums/ProjectFramework.js +2 -0
- package/packages/types/src/enums/ProjectFramework.js.map +1 -1
- package/packages/utils/src/lib/fs/rsyncCopy.js +6 -2
- package/packages/utils/src/lib/fs/rsyncCopy.js.map +1 -1
- package/scripts/npm-post-install.cjs +34 -0
- package/codeyam-cli/src/commands/detect-universal-mocks.js +0 -120
- package/codeyam-cli/src/commands/detect-universal-mocks.js.map +0 -1
- package/codeyam-cli/src/commands/list.js +0 -31
- package/codeyam-cli/src/commands/list.js.map +0 -1
- package/codeyam-cli/src/commands/webapp-info.js +0 -146
- package/codeyam-cli/src/commands/webapp-info.js.map +0 -1
- package/codeyam-cli/src/utils/universal-mocks.js +0 -152
- package/codeyam-cli/src/utils/universal-mocks.js.map +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/CopyButton-jNYXRRNI.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/EntityTypeBadge-CvzqMxcu.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/InlineSpinner-EhOseatT.js +0 -34
- package/codeyam-cli/src/webserver/build/client/assets/InteractivePreview-yjIHlOGa.js +0 -25
- package/codeyam-cli/src/webserver/build/client/assets/LibraryFunctionPreview-Cq5o8jL4.js +0 -3
- package/codeyam-cli/src/webserver/build/client/assets/SafeScreenshot-CwZrv-Ok.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/TruncatedFilePath-CDpEprKa.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/agent-transcripts-DHKuQSmR.js +0 -17
- package/codeyam-cli/src/webserver/build/client/assets/chunk-JZWAC4HX-DB3aFuEO.js +0 -51
- package/codeyam-cli/src/webserver/build/client/assets/createLucideIcon-D1zB-pYc.js +0 -21
- package/codeyam-cli/src/webserver/build/client/assets/dev.empty-JTAjQ54M.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/entity._sha.scenarios._scenarioId.fullscreen-DjLxr2JB.js +0 -6
- package/codeyam-cli/src/webserver/build/client/assets/entity._sha_.create-scenario-CtYowLOt.js +0 -6
- package/codeyam-cli/src/webserver/build/client/assets/fileTableUtils-9sMMAiWJ.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/files-Co65J0s3.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/git-BdHOxVfg.js +0 -15
- package/codeyam-cli/src/webserver/build/client/assets/globals-CCgBKWy4.css +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/labs-BK0C1H1T.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/manifest-390cb8fa.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/memory-CzZySbBE.js +0 -78
- package/codeyam-cli/src/webserver/build/client/assets/root-DnbDhvTU.js +0 -62
- package/codeyam-cli/src/webserver/build/client/assets/settings-CclxrcPK.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/simulations-DVNJVQgD.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/useCustomSizes-BqgrAzs3.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/useLastLogLine-DAFqfEDH.js +0 -2
- package/codeyam-cli/src/webserver/build/client/assets/useReportContext-DZlYx2c4.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/useToast-ihdMtlf6.js +0 -1
- package/codeyam-cli/src/webserver/build/server/assets/index-CxaRxKVt.js +0 -1
- package/codeyam-cli/src/webserver/build/server/assets/server-build-D4DT0nM_.js +0 -259
- package/codeyam-cli/templates/codeyam-stop-hook.sh +0 -284
- package/scripts/finalize-analyzer.cjs +0 -13
- /package/codeyam-cli/templates/{codeyam-diagnose.md → commands/codeyam-diagnose.md} +0 -0
- /package/codeyam-cli/templates/{codeyam-debug.md → skills/codeyam-debug/SKILL.md} +0 -0
- /package/codeyam-cli/templates/{codeyam-setup.md → skills/codeyam-setup/SKILL.md} +0 -0
- /package/codeyam-cli/templates/{codeyam-sim.md → skills/codeyam-sim/SKILL.md} +0 -0
- /package/codeyam-cli/templates/{codeyam-test.md → skills/codeyam-test/SKILL.md} +0 -0
- /package/codeyam-cli/templates/{codeyam-verify.md → skills/codeyam-verify/SKILL.md} +0 -0
|
@@ -0,0 +1,2379 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { Kysely, SqliteDialect } from 'kysely';
|
|
3
|
+
import { isComponent, classifyGlossaryEntries, computeAudit, filterGlossaryByChangeStatus, resolveAuditSessionScope, queryScenarioCounts, queryPageScenarioCounts, queryIncompleteEntities, queryMiscategorizedScenarios, isOnlyIncompleteEntities, isAutoRemediable, identifyScenariosNeedingRecapture, detectDuplicateNames, } from "../editorAudit.js";
|
|
4
|
+
describe('editorAudit', () => {
|
|
5
|
+
describe('isComponent', () => {
|
|
6
|
+
it('should return true for JSX.Element return type', () => {
|
|
7
|
+
expect(isComponent({ returnType: 'JSX.Element' })).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
it('should return true for React.ReactElement return type', () => {
|
|
10
|
+
expect(isComponent({ returnType: 'React.ReactElement' })).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
it('should return true for ReactNode return type', () => {
|
|
13
|
+
expect(isComponent({ returnType: 'ReactNode' })).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
it('should return true for Element return type', () => {
|
|
16
|
+
expect(isComponent({ returnType: 'Element' })).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
it('should return true when JSX appears anywhere in the return type', () => {
|
|
19
|
+
expect(isComponent({ returnType: 'Promise<JSX.Element>' })).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
it('should return false for non-component return types', () => {
|
|
22
|
+
expect(isComponent({ returnType: 'number' })).toBe(false);
|
|
23
|
+
expect(isComponent({ returnType: 'string' })).toBe(false);
|
|
24
|
+
expect(isComponent({ returnType: 'void' })).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
it('should return false when returnType is undefined', () => {
|
|
27
|
+
expect(isComponent({})).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
it('should return false when returnType is empty string', () => {
|
|
30
|
+
expect(isComponent({ returnType: '' })).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe('classifyGlossaryEntries', () => {
|
|
34
|
+
it('should classify entries with JSX return types as components', () => {
|
|
35
|
+
const entries = [
|
|
36
|
+
{
|
|
37
|
+
name: 'DrinkCard',
|
|
38
|
+
filePath: 'app/components/DrinkCard.tsx',
|
|
39
|
+
returnType: 'JSX.Element',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'calculatePrice',
|
|
43
|
+
filePath: 'app/lib/pricing.ts',
|
|
44
|
+
returnType: 'number',
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
const result = classifyGlossaryEntries(entries);
|
|
48
|
+
expect(result.components).toHaveLength(1);
|
|
49
|
+
expect(result.components[0].name).toBe('DrinkCard');
|
|
50
|
+
expect(result.functions).toHaveLength(1);
|
|
51
|
+
expect(result.functions[0].name).toBe('calculatePrice');
|
|
52
|
+
});
|
|
53
|
+
it('should return empty arrays for empty input', () => {
|
|
54
|
+
const result = classifyGlossaryEntries([]);
|
|
55
|
+
expect(result.components).toEqual([]);
|
|
56
|
+
expect(result.functions).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
it('should treat entries without returnType as functions', () => {
|
|
59
|
+
const entries = [{ name: 'helper', filePath: 'app/lib/helper.ts' }];
|
|
60
|
+
const result = classifyGlossaryEntries(entries);
|
|
61
|
+
expect(result.components).toHaveLength(0);
|
|
62
|
+
expect(result.functions).toHaveLength(1);
|
|
63
|
+
});
|
|
64
|
+
it('should handle all components', () => {
|
|
65
|
+
const entries = [
|
|
66
|
+
{
|
|
67
|
+
name: 'Header',
|
|
68
|
+
filePath: 'app/components/Header.tsx',
|
|
69
|
+
returnType: 'ReactNode',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'Footer',
|
|
73
|
+
filePath: 'app/components/Footer.tsx',
|
|
74
|
+
returnType: 'JSX.Element',
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
const result = classifyGlossaryEntries(entries);
|
|
78
|
+
expect(result.components).toHaveLength(2);
|
|
79
|
+
expect(result.functions).toHaveLength(0);
|
|
80
|
+
});
|
|
81
|
+
it('should handle all functions', () => {
|
|
82
|
+
const entries = [
|
|
83
|
+
{ name: 'add', filePath: 'app/lib/math.ts', returnType: 'number' },
|
|
84
|
+
{ name: 'format', filePath: 'app/lib/format.ts', returnType: 'string' },
|
|
85
|
+
];
|
|
86
|
+
const result = classifyGlossaryEntries(entries);
|
|
87
|
+
expect(result.components).toHaveLength(0);
|
|
88
|
+
expect(result.functions).toHaveLength(2);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe('computeAudit', () => {
|
|
92
|
+
it('should mark components with scenarios as ok', () => {
|
|
93
|
+
const result = computeAudit({
|
|
94
|
+
components: [
|
|
95
|
+
{ name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
|
|
96
|
+
],
|
|
97
|
+
functions: [],
|
|
98
|
+
scenarioCounts: { DrinkCard: 2 },
|
|
99
|
+
testFileExistence: {},
|
|
100
|
+
});
|
|
101
|
+
expect(result.components[0].status).toBe('ok');
|
|
102
|
+
expect(result.components[0].scenarioCount).toBe(2);
|
|
103
|
+
expect(result.summary.componentsOk).toBe(1);
|
|
104
|
+
expect(result.summary.componentsMissing).toBe(0);
|
|
105
|
+
});
|
|
106
|
+
it('should mark components without scenarios as missing', () => {
|
|
107
|
+
const result = computeAudit({
|
|
108
|
+
components: [
|
|
109
|
+
{ name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
|
|
110
|
+
],
|
|
111
|
+
functions: [],
|
|
112
|
+
scenarioCounts: {},
|
|
113
|
+
testFileExistence: {},
|
|
114
|
+
});
|
|
115
|
+
expect(result.components[0].status).toBe('missing');
|
|
116
|
+
expect(result.components[0].scenarioCount).toBe(0);
|
|
117
|
+
expect(result.summary.componentsMissing).toBe(1);
|
|
118
|
+
});
|
|
119
|
+
it('should mark functions with existing test files as ok', () => {
|
|
120
|
+
const result = computeAudit({
|
|
121
|
+
components: [],
|
|
122
|
+
functions: [
|
|
123
|
+
{
|
|
124
|
+
name: 'calculatePrice',
|
|
125
|
+
filePath: 'app/lib/pricing.ts',
|
|
126
|
+
testFile: 'app/lib/pricing.test.ts',
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
scenarioCounts: {},
|
|
130
|
+
testFileExistence: { 'app/lib/pricing.test.ts': true },
|
|
131
|
+
});
|
|
132
|
+
expect(result.functions[0].status).toBe('ok');
|
|
133
|
+
expect(result.functions[0].testFileExists).toBe(true);
|
|
134
|
+
expect(result.summary.functionsOk).toBe(1);
|
|
135
|
+
expect(result.summary.functionsMissing).toBe(0);
|
|
136
|
+
});
|
|
137
|
+
it('should mark functions with missing test files as missing', () => {
|
|
138
|
+
const result = computeAudit({
|
|
139
|
+
components: [],
|
|
140
|
+
functions: [
|
|
141
|
+
{
|
|
142
|
+
name: 'calculatePrice',
|
|
143
|
+
filePath: 'app/lib/pricing.ts',
|
|
144
|
+
testFile: 'app/lib/pricing.test.ts',
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
scenarioCounts: {},
|
|
148
|
+
testFileExistence: { 'app/lib/pricing.test.ts': false },
|
|
149
|
+
});
|
|
150
|
+
expect(result.functions[0].status).toBe('missing');
|
|
151
|
+
expect(result.functions[0].testFileExists).toBe(false);
|
|
152
|
+
expect(result.summary.functionsMissing).toBe(1);
|
|
153
|
+
});
|
|
154
|
+
it('should mark functions without testFile field as missing', () => {
|
|
155
|
+
const result = computeAudit({
|
|
156
|
+
components: [],
|
|
157
|
+
functions: [{ name: 'calculatePrice', filePath: 'app/lib/pricing.ts' }],
|
|
158
|
+
scenarioCounts: {},
|
|
159
|
+
testFileExistence: {},
|
|
160
|
+
});
|
|
161
|
+
expect(result.functions[0].status).toBe('missing');
|
|
162
|
+
expect(result.functions[0].testFile).toBeUndefined();
|
|
163
|
+
expect(result.functions[0].testFileExists).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
it('should set allPassing to true when everything is ok', () => {
|
|
166
|
+
const result = computeAudit({
|
|
167
|
+
components: [
|
|
168
|
+
{ name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
|
|
169
|
+
],
|
|
170
|
+
functions: [
|
|
171
|
+
{
|
|
172
|
+
name: 'calculatePrice',
|
|
173
|
+
filePath: 'app/lib/pricing.ts',
|
|
174
|
+
testFile: 'app/lib/pricing.test.ts',
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
scenarioCounts: { DrinkCard: 1 },
|
|
178
|
+
testFileExistence: { 'app/lib/pricing.test.ts': true },
|
|
179
|
+
});
|
|
180
|
+
expect(result.summary.allPassing).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
it('should set allPassing to false when any component is missing', () => {
|
|
183
|
+
const result = computeAudit({
|
|
184
|
+
components: [
|
|
185
|
+
{ name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
|
|
186
|
+
],
|
|
187
|
+
functions: [],
|
|
188
|
+
scenarioCounts: {},
|
|
189
|
+
testFileExistence: {},
|
|
190
|
+
});
|
|
191
|
+
expect(result.summary.allPassing).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
it('should set allPassing to false when any function is missing', () => {
|
|
194
|
+
const result = computeAudit({
|
|
195
|
+
components: [],
|
|
196
|
+
functions: [{ name: 'calculatePrice', filePath: 'app/lib/pricing.ts' }],
|
|
197
|
+
scenarioCounts: {},
|
|
198
|
+
testFileExistence: {},
|
|
199
|
+
});
|
|
200
|
+
expect(result.summary.allPassing).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
it('should set allPassing to true for empty inputs', () => {
|
|
203
|
+
const result = computeAudit({
|
|
204
|
+
components: [],
|
|
205
|
+
functions: [],
|
|
206
|
+
scenarioCounts: {},
|
|
207
|
+
testFileExistence: {},
|
|
208
|
+
});
|
|
209
|
+
expect(result.summary.allPassing).toBe(true);
|
|
210
|
+
expect(result.summary.totalComponents).toBe(0);
|
|
211
|
+
expect(result.summary.totalFunctions).toBe(0);
|
|
212
|
+
});
|
|
213
|
+
it('should compute correct summary totals', () => {
|
|
214
|
+
const result = computeAudit({
|
|
215
|
+
components: [
|
|
216
|
+
{ name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
|
|
217
|
+
{ name: 'Header', filePath: 'app/components/Header.tsx' },
|
|
218
|
+
{ name: 'Footer', filePath: 'app/components/Footer.tsx' },
|
|
219
|
+
],
|
|
220
|
+
functions: [
|
|
221
|
+
{
|
|
222
|
+
name: 'calculatePrice',
|
|
223
|
+
filePath: 'app/lib/pricing.ts',
|
|
224
|
+
testFile: 'app/lib/pricing.test.ts',
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: 'formatDate',
|
|
228
|
+
filePath: 'app/lib/format.ts',
|
|
229
|
+
testFile: 'app/lib/format.test.ts',
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
scenarioCounts: { DrinkCard: 3, Header: 1 },
|
|
233
|
+
testFileExistence: {
|
|
234
|
+
'app/lib/pricing.test.ts': true,
|
|
235
|
+
'app/lib/format.test.ts': false,
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
expect(result.summary.totalComponents).toBe(3);
|
|
239
|
+
expect(result.summary.componentsOk).toBe(2);
|
|
240
|
+
expect(result.summary.componentsMissing).toBe(1);
|
|
241
|
+
expect(result.summary.totalFunctions).toBe(2);
|
|
242
|
+
expect(result.summary.functionsOk).toBe(1);
|
|
243
|
+
expect(result.summary.functionsMissing).toBe(1);
|
|
244
|
+
expect(result.summary.allPassing).toBe(false);
|
|
245
|
+
});
|
|
246
|
+
// ── Test results integration ────────────────────────────────────
|
|
247
|
+
it('should mark function as ok when tests pass and describe matches entity name', () => {
|
|
248
|
+
const result = computeAudit({
|
|
249
|
+
components: [],
|
|
250
|
+
functions: [
|
|
251
|
+
{
|
|
252
|
+
name: 'calculatePrice',
|
|
253
|
+
filePath: 'app/lib/pricing.ts',
|
|
254
|
+
testFile: 'app/lib/pricing.test.ts',
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
scenarioCounts: {},
|
|
258
|
+
testFileExistence: { 'app/lib/pricing.test.ts': true },
|
|
259
|
+
testResults: {
|
|
260
|
+
'app/lib/pricing.test.ts': {
|
|
261
|
+
passing: true,
|
|
262
|
+
hasEntityNameDescribe: true,
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
expect(result.functions[0].status).toBe('ok');
|
|
267
|
+
expect(result.functions[0].testsPassing).toBe(true);
|
|
268
|
+
expect(result.functions[0].testsVisibleInUi).toBe(true);
|
|
269
|
+
});
|
|
270
|
+
it('should mark function as failing when tests fail', () => {
|
|
271
|
+
const result = computeAudit({
|
|
272
|
+
components: [],
|
|
273
|
+
functions: [
|
|
274
|
+
{
|
|
275
|
+
name: 'calculatePrice',
|
|
276
|
+
filePath: 'app/lib/pricing.ts',
|
|
277
|
+
testFile: 'app/lib/pricing.test.ts',
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
scenarioCounts: {},
|
|
281
|
+
testFileExistence: { 'app/lib/pricing.test.ts': true },
|
|
282
|
+
testResults: {
|
|
283
|
+
'app/lib/pricing.test.ts': {
|
|
284
|
+
passing: false,
|
|
285
|
+
hasEntityNameDescribe: true,
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
expect(result.functions[0].status).toBe('failing');
|
|
290
|
+
expect(result.functions[0].testsPassing).toBe(false);
|
|
291
|
+
expect(result.functions[0].testsVisibleInUi).toBe(true);
|
|
292
|
+
});
|
|
293
|
+
it('should distinguish runner errors from test failures and include error message', () => {
|
|
294
|
+
const result = computeAudit({
|
|
295
|
+
components: [],
|
|
296
|
+
functions: [
|
|
297
|
+
{
|
|
298
|
+
name: 'getTimeAgo',
|
|
299
|
+
filePath: 'src/lib/format.ts',
|
|
300
|
+
testFile: 'src/lib/format.test.ts',
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
scenarioCounts: {},
|
|
304
|
+
testFileExistence: { 'src/lib/format.test.ts': true },
|
|
305
|
+
testResults: {
|
|
306
|
+
'src/lib/format.test.ts': {
|
|
307
|
+
passing: false,
|
|
308
|
+
hasEntityNameDescribe: false,
|
|
309
|
+
errorMessage: 'Error: Cannot find module "@/lib/format" from "src/lib/format.test.ts"',
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
expect(result.functions[0].status).toBe('runner_error');
|
|
314
|
+
expect(result.functions[0].errorMessage).toBe('Error: Cannot find module "@/lib/format" from "src/lib/format.test.ts"');
|
|
315
|
+
});
|
|
316
|
+
it('should mark function as name_mismatch when tests pass but no describe matches', () => {
|
|
317
|
+
const result = computeAudit({
|
|
318
|
+
components: [],
|
|
319
|
+
functions: [
|
|
320
|
+
{
|
|
321
|
+
name: 'useDrinks',
|
|
322
|
+
filePath: 'app/hooks/useDrinks.ts',
|
|
323
|
+
testFile: 'app/hooks/useDrinks.test.ts',
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
scenarioCounts: {},
|
|
327
|
+
testFileExistence: { 'app/hooks/useDrinks.test.ts': true },
|
|
328
|
+
testResults: {
|
|
329
|
+
'app/hooks/useDrinks.test.ts': {
|
|
330
|
+
passing: true,
|
|
331
|
+
hasEntityNameDescribe: false,
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
expect(result.functions[0].status).toBe('name_mismatch');
|
|
336
|
+
expect(result.functions[0].testsPassing).toBe(true);
|
|
337
|
+
expect(result.functions[0].testsVisibleInUi).toBe(false);
|
|
338
|
+
});
|
|
339
|
+
it('should still mark function as missing when test file does not exist even with testResults', () => {
|
|
340
|
+
const result = computeAudit({
|
|
341
|
+
components: [],
|
|
342
|
+
functions: [
|
|
343
|
+
{
|
|
344
|
+
name: 'calculatePrice',
|
|
345
|
+
filePath: 'app/lib/pricing.ts',
|
|
346
|
+
testFile: 'app/lib/pricing.test.ts',
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
scenarioCounts: {},
|
|
350
|
+
testFileExistence: { 'app/lib/pricing.test.ts': false },
|
|
351
|
+
testResults: {},
|
|
352
|
+
});
|
|
353
|
+
expect(result.functions[0].status).toBe('missing');
|
|
354
|
+
expect(result.functions[0].testsPassing).toBeUndefined();
|
|
355
|
+
expect(result.functions[0].testsVisibleInUi).toBeUndefined();
|
|
356
|
+
});
|
|
357
|
+
it('should include functionsFailing and functionsNameMismatch in summary', () => {
|
|
358
|
+
const result = computeAudit({
|
|
359
|
+
components: [],
|
|
360
|
+
functions: [
|
|
361
|
+
{
|
|
362
|
+
name: 'fnOk',
|
|
363
|
+
filePath: 'a.ts',
|
|
364
|
+
testFile: 'a.test.ts',
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
name: 'fnFailing',
|
|
368
|
+
filePath: 'b.ts',
|
|
369
|
+
testFile: 'b.test.ts',
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: 'fnMismatch',
|
|
373
|
+
filePath: 'c.ts',
|
|
374
|
+
testFile: 'c.test.ts',
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
name: 'fnMissing',
|
|
378
|
+
filePath: 'd.ts',
|
|
379
|
+
testFile: 'd.test.ts',
|
|
380
|
+
},
|
|
381
|
+
],
|
|
382
|
+
scenarioCounts: {},
|
|
383
|
+
testFileExistence: {
|
|
384
|
+
'a.test.ts': true,
|
|
385
|
+
'b.test.ts': true,
|
|
386
|
+
'c.test.ts': true,
|
|
387
|
+
'd.test.ts': false,
|
|
388
|
+
},
|
|
389
|
+
testResults: {
|
|
390
|
+
'a.test.ts': { passing: true, hasEntityNameDescribe: true },
|
|
391
|
+
'b.test.ts': { passing: false, hasEntityNameDescribe: true },
|
|
392
|
+
'c.test.ts': { passing: true, hasEntityNameDescribe: false },
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
expect(result.summary.functionsOk).toBe(1);
|
|
396
|
+
expect(result.summary.functionsFailing).toBe(1);
|
|
397
|
+
expect(result.summary.functionsNameMismatch).toBe(1);
|
|
398
|
+
expect(result.summary.functionsMissing).toBe(1);
|
|
399
|
+
expect(result.summary.allPassing).toBe(false);
|
|
400
|
+
});
|
|
401
|
+
it('should set allPassing to true only when all functions are ok', () => {
|
|
402
|
+
const result = computeAudit({
|
|
403
|
+
components: [
|
|
404
|
+
{ name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
|
|
405
|
+
],
|
|
406
|
+
functions: [
|
|
407
|
+
{
|
|
408
|
+
name: 'calculatePrice',
|
|
409
|
+
filePath: 'app/lib/pricing.ts',
|
|
410
|
+
testFile: 'app/lib/pricing.test.ts',
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
scenarioCounts: { DrinkCard: 1 },
|
|
414
|
+
testFileExistence: { 'app/lib/pricing.test.ts': true },
|
|
415
|
+
testResults: {
|
|
416
|
+
'app/lib/pricing.test.ts': {
|
|
417
|
+
passing: true,
|
|
418
|
+
hasEntityNameDescribe: true,
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
expect(result.summary.allPassing).toBe(true);
|
|
423
|
+
});
|
|
424
|
+
it('should set allPassing to false when any function is failing', () => {
|
|
425
|
+
const result = computeAudit({
|
|
426
|
+
components: [],
|
|
427
|
+
functions: [
|
|
428
|
+
{
|
|
429
|
+
name: 'calculatePrice',
|
|
430
|
+
filePath: 'app/lib/pricing.ts',
|
|
431
|
+
testFile: 'app/lib/pricing.test.ts',
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
scenarioCounts: {},
|
|
435
|
+
testFileExistence: { 'app/lib/pricing.test.ts': true },
|
|
436
|
+
testResults: {
|
|
437
|
+
'app/lib/pricing.test.ts': {
|
|
438
|
+
passing: false,
|
|
439
|
+
hasEntityNameDescribe: true,
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
expect(result.summary.allPassing).toBe(false);
|
|
444
|
+
});
|
|
445
|
+
it('should set allPassing to false when any function has name_mismatch', () => {
|
|
446
|
+
const result = computeAudit({
|
|
447
|
+
components: [],
|
|
448
|
+
functions: [
|
|
449
|
+
{
|
|
450
|
+
name: 'useDrinks',
|
|
451
|
+
filePath: 'app/hooks/useDrinks.ts',
|
|
452
|
+
testFile: 'app/hooks/useDrinks.test.ts',
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
scenarioCounts: {},
|
|
456
|
+
testFileExistence: { 'app/hooks/useDrinks.test.ts': true },
|
|
457
|
+
testResults: {
|
|
458
|
+
'app/hooks/useDrinks.test.ts': {
|
|
459
|
+
passing: true,
|
|
460
|
+
hasEntityNameDescribe: false,
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
expect(result.summary.allPassing).toBe(false);
|
|
465
|
+
});
|
|
466
|
+
it('should fall back to file-existence-only behavior when testResults is not provided', () => {
|
|
467
|
+
// Backward compat: no testResults → existing behavior
|
|
468
|
+
const result = computeAudit({
|
|
469
|
+
components: [],
|
|
470
|
+
functions: [
|
|
471
|
+
{
|
|
472
|
+
name: 'calculatePrice',
|
|
473
|
+
filePath: 'app/lib/pricing.ts',
|
|
474
|
+
testFile: 'app/lib/pricing.test.ts',
|
|
475
|
+
},
|
|
476
|
+
],
|
|
477
|
+
scenarioCounts: {},
|
|
478
|
+
testFileExistence: { 'app/lib/pricing.test.ts': true },
|
|
479
|
+
});
|
|
480
|
+
expect(result.functions[0].status).toBe('ok');
|
|
481
|
+
expect(result.functions[0].testsPassing).toBeUndefined();
|
|
482
|
+
expect(result.functions[0].testsVisibleInUi).toBeUndefined();
|
|
483
|
+
});
|
|
484
|
+
it('should fall back to file-existence-only when testResults is empty for a given file', () => {
|
|
485
|
+
const result = computeAudit({
|
|
486
|
+
components: [],
|
|
487
|
+
functions: [
|
|
488
|
+
{
|
|
489
|
+
name: 'calculatePrice',
|
|
490
|
+
filePath: 'app/lib/pricing.ts',
|
|
491
|
+
testFile: 'app/lib/pricing.test.ts',
|
|
492
|
+
},
|
|
493
|
+
],
|
|
494
|
+
scenarioCounts: {},
|
|
495
|
+
testFileExistence: { 'app/lib/pricing.test.ts': true },
|
|
496
|
+
testResults: {},
|
|
497
|
+
});
|
|
498
|
+
// No test result for this file → fall back to existence check
|
|
499
|
+
expect(result.functions[0].status).toBe('ok');
|
|
500
|
+
expect(result.functions[0].testsPassing).toBeUndefined();
|
|
501
|
+
expect(result.functions[0].testsVisibleInUi).toBeUndefined();
|
|
502
|
+
});
|
|
503
|
+
// ── Client errors integration ─────────────────────────────────────
|
|
504
|
+
it('should mark component as has_errors when clientErrors reports errors', () => {
|
|
505
|
+
const result = computeAudit({
|
|
506
|
+
components: [
|
|
507
|
+
{ name: 'DrinkInfo', filePath: 'app/components/DrinkInfo.tsx' },
|
|
508
|
+
],
|
|
509
|
+
functions: [],
|
|
510
|
+
scenarioCounts: { DrinkInfo: 3 },
|
|
511
|
+
testFileExistence: {},
|
|
512
|
+
clientErrors: {
|
|
513
|
+
DrinkInfo: ['Cannot read properties of undefined'],
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
expect(result.components[0].status).toBe('has_errors');
|
|
517
|
+
expect(result.components[0].clientErrors).toEqual([
|
|
518
|
+
'Cannot read properties of undefined',
|
|
519
|
+
]);
|
|
520
|
+
expect(result.summary.componentsWithErrors).toBe(1);
|
|
521
|
+
expect(result.summary.allPassing).toBe(false);
|
|
522
|
+
});
|
|
523
|
+
it('should not count client errors for components with no scenarios', () => {
|
|
524
|
+
// If a component has no scenarios AND errors, it's still "missing"
|
|
525
|
+
// (errors come from scenarios, so if there are none, errors are stale)
|
|
526
|
+
const result = computeAudit({
|
|
527
|
+
components: [
|
|
528
|
+
{ name: 'DrinkInfo', filePath: 'app/components/DrinkInfo.tsx' },
|
|
529
|
+
],
|
|
530
|
+
functions: [],
|
|
531
|
+
scenarioCounts: {},
|
|
532
|
+
testFileExistence: {},
|
|
533
|
+
clientErrors: {
|
|
534
|
+
DrinkInfo: ['Cannot read properties of undefined'],
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
expect(result.components[0].status).toBe('missing');
|
|
538
|
+
});
|
|
539
|
+
it('should set allPassing false when any component has errors', () => {
|
|
540
|
+
const result = computeAudit({
|
|
541
|
+
components: [
|
|
542
|
+
{ name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
|
|
543
|
+
{ name: 'DrinkInfo', filePath: 'app/components/DrinkInfo.tsx' },
|
|
544
|
+
],
|
|
545
|
+
functions: [],
|
|
546
|
+
scenarioCounts: { DrinkCard: 2, DrinkInfo: 3 },
|
|
547
|
+
testFileExistence: {},
|
|
548
|
+
clientErrors: {
|
|
549
|
+
DrinkInfo: ['TypeError: something broke'],
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
expect(result.summary.allPassing).toBe(false);
|
|
553
|
+
expect(result.summary.componentsOk).toBe(1);
|
|
554
|
+
expect(result.summary.componentsWithErrors).toBe(1);
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
// ── filterGlossaryByChangeStatus ──────────────────────────────────
|
|
558
|
+
describe('filterGlossaryByChangeStatus', () => {
|
|
559
|
+
const allEntries = [
|
|
560
|
+
// Feature 1 components (unchanged)
|
|
561
|
+
{
|
|
562
|
+
name: 'StarRating',
|
|
563
|
+
filePath: 'app/components/StarRating.tsx',
|
|
564
|
+
returnType: 'JSX.Element',
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
name: 'CategoryBadge',
|
|
568
|
+
filePath: 'app/components/CategoryBadge.tsx',
|
|
569
|
+
returnType: 'JSX.Element',
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
name: 'DrinkCard',
|
|
573
|
+
filePath: 'app/components/DrinkCard.tsx',
|
|
574
|
+
returnType: 'JSX.Element',
|
|
575
|
+
},
|
|
576
|
+
// Feature 2 components (new)
|
|
577
|
+
{
|
|
578
|
+
name: 'HeroImage',
|
|
579
|
+
filePath: 'app/components/HeroImage.tsx',
|
|
580
|
+
returnType: 'JSX.Element',
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
name: 'ReviewCard',
|
|
584
|
+
filePath: 'app/components/ReviewCard.tsx',
|
|
585
|
+
returnType: 'JSX.Element',
|
|
586
|
+
},
|
|
587
|
+
// Feature 1 function (unchanged)
|
|
588
|
+
{
|
|
589
|
+
name: 'computeAvgRating',
|
|
590
|
+
filePath: 'app/lib/ratings.ts',
|
|
591
|
+
returnType: 'number',
|
|
592
|
+
testFile: 'app/lib/ratings.test.ts',
|
|
593
|
+
},
|
|
594
|
+
// Feature 2 function (new)
|
|
595
|
+
{
|
|
596
|
+
name: 'formatReviewCount',
|
|
597
|
+
filePath: 'app/lib/formatting.ts',
|
|
598
|
+
returnType: 'string',
|
|
599
|
+
testFile: 'app/lib/formatting.test.ts',
|
|
600
|
+
},
|
|
601
|
+
];
|
|
602
|
+
it('should only include entries whose files are new, edited, or impacted', () => {
|
|
603
|
+
const entityChangeStatus = {
|
|
604
|
+
HeroImage: { status: 'new' },
|
|
605
|
+
ReviewCard: { status: 'new' },
|
|
606
|
+
formatReviewCount: { status: 'new' },
|
|
607
|
+
// DrinkCard edited because it now links to detail page
|
|
608
|
+
DrinkCard: { status: 'edited' },
|
|
609
|
+
// StarRating impacted because DrinkInfo uses it
|
|
610
|
+
StarRating: {
|
|
611
|
+
status: 'impacted',
|
|
612
|
+
impactedBy: [
|
|
613
|
+
{
|
|
614
|
+
name: 'DrinkInfo',
|
|
615
|
+
filePath: 'app/components/DrinkInfo.tsx',
|
|
616
|
+
changeType: 'new',
|
|
617
|
+
},
|
|
618
|
+
],
|
|
619
|
+
},
|
|
620
|
+
};
|
|
621
|
+
const filtered = filterGlossaryByChangeStatus(allEntries, entityChangeStatus);
|
|
622
|
+
const names = filtered.map((e) => e.name).sort();
|
|
623
|
+
expect(names).toEqual([
|
|
624
|
+
'DrinkCard',
|
|
625
|
+
'HeroImage',
|
|
626
|
+
'ReviewCard',
|
|
627
|
+
'StarRating',
|
|
628
|
+
'formatReviewCount',
|
|
629
|
+
]);
|
|
630
|
+
});
|
|
631
|
+
it('should return all entries when entityChangeStatus is empty', () => {
|
|
632
|
+
// When there's no change status data (e.g., no git changes), fall back to auditing everything
|
|
633
|
+
const filtered = filterGlossaryByChangeStatus(allEntries, {});
|
|
634
|
+
expect(filtered).toEqual(allEntries);
|
|
635
|
+
});
|
|
636
|
+
it('should return all entries when entityChangeStatus is undefined', () => {
|
|
637
|
+
const filtered = filterGlossaryByChangeStatus(allEntries, undefined);
|
|
638
|
+
expect(filtered).toEqual(allEntries);
|
|
639
|
+
});
|
|
640
|
+
it('should exclude unchanged entries that have no status', () => {
|
|
641
|
+
const entityChangeStatus = {
|
|
642
|
+
HeroImage: { status: 'new' },
|
|
643
|
+
};
|
|
644
|
+
const filtered = filterGlossaryByChangeStatus(allEntries, entityChangeStatus);
|
|
645
|
+
expect(filtered).toHaveLength(1);
|
|
646
|
+
expect(filtered[0].name).toBe('HeroImage');
|
|
647
|
+
});
|
|
648
|
+
it('should match entries by filePath when name does not match any status key', () => {
|
|
649
|
+
// Sometimes entity names in entityChangeStatus are derived from file paths
|
|
650
|
+
// and may not exactly match glossary names. Fall back to filePath matching.
|
|
651
|
+
const entityChangeStatus = {
|
|
652
|
+
// Entity name derived differently, but filePath matches
|
|
653
|
+
'app/components/DrinkCard.tsx': { status: 'edited' },
|
|
654
|
+
};
|
|
655
|
+
const filtered = filterGlossaryByChangeStatus(allEntries, entityChangeStatus);
|
|
656
|
+
expect(filtered.map((e) => e.name)).toContain('DrinkCard');
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
// ── resolveAuditSessionScope ──────────────────────────────────────
|
|
660
|
+
describe('resolveAuditSessionScope', () => {
|
|
661
|
+
it('should scope to session when entityChangeStatus has entries and featureStartedAt exists', () => {
|
|
662
|
+
const result = resolveAuditSessionScope({
|
|
663
|
+
featureStartedAt: '2026-03-12T14:01:31.291Z',
|
|
664
|
+
entityChangeStatus: {
|
|
665
|
+
ArticleCard: { status: 'new' },
|
|
666
|
+
LibraryHeader: { status: 'new' },
|
|
667
|
+
},
|
|
668
|
+
});
|
|
669
|
+
expect(result.featureStartedAt).toBe('2026-03-12T14:01:31.291Z');
|
|
670
|
+
expect(result.entityChangeStatus).toEqual({
|
|
671
|
+
ArticleCard: { status: 'new' },
|
|
672
|
+
LibraryHeader: { status: 'new' },
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
it('should NOT scope scenario counts to session when entityChangeStatus is empty', () => {
|
|
676
|
+
// This is the testapp bug: entityChangeStatus computation returned empty,
|
|
677
|
+
// so the glossary filter falls back to "audit all components", but the
|
|
678
|
+
// scenario count query still filters by featureStartedAt — causing
|
|
679
|
+
// Feature 1 components to appear as "no scenarios" even though they have them.
|
|
680
|
+
const result = resolveAuditSessionScope({
|
|
681
|
+
featureStartedAt: '2026-03-12T14:01:31.291Z',
|
|
682
|
+
entityChangeStatus: undefined,
|
|
683
|
+
});
|
|
684
|
+
// When we can't scope the glossary, we shouldn't scope scenario counts either
|
|
685
|
+
expect(result.featureStartedAt).toBeNull();
|
|
686
|
+
});
|
|
687
|
+
it('should NOT scope scenario counts to session when entityChangeStatus is an empty object', () => {
|
|
688
|
+
const result = resolveAuditSessionScope({
|
|
689
|
+
featureStartedAt: '2026-03-12T14:01:31.291Z',
|
|
690
|
+
entityChangeStatus: {},
|
|
691
|
+
});
|
|
692
|
+
expect(result.featureStartedAt).toBeNull();
|
|
693
|
+
});
|
|
694
|
+
it('should return null featureStartedAt when none was provided', () => {
|
|
695
|
+
const result = resolveAuditSessionScope({
|
|
696
|
+
featureStartedAt: null,
|
|
697
|
+
entityChangeStatus: { Header: { status: 'new' } },
|
|
698
|
+
});
|
|
699
|
+
expect(result.featureStartedAt).toBeNull();
|
|
700
|
+
});
|
|
701
|
+
it('should pass through entityChangeStatus unchanged when it has entries', () => {
|
|
702
|
+
const ecs = {
|
|
703
|
+
ArticleCard: { status: 'new' },
|
|
704
|
+
Header: { status: 'edited' },
|
|
705
|
+
};
|
|
706
|
+
const result = resolveAuditSessionScope({
|
|
707
|
+
featureStartedAt: '2026-03-12T14:01:31.291Z',
|
|
708
|
+
entityChangeStatus: ecs,
|
|
709
|
+
});
|
|
710
|
+
expect(result.entityChangeStatus).toBe(ecs);
|
|
711
|
+
});
|
|
712
|
+
describe('multi-feature audit scenario (testapp reproduction)', () => {
|
|
713
|
+
// Reproduces the exact testapp bug:
|
|
714
|
+
// - Feature 1 built 8 components with scenarios at 13:28-13:30
|
|
715
|
+
// - Feature 2 starts at 14:01, builds 4 new components with scenarios at 14:14-14:16
|
|
716
|
+
// - entityChangeStatus computation returns empty (fails/returns {})
|
|
717
|
+
// - Glossary includes ALL 12 components
|
|
718
|
+
// - Scenario counts only include Feature 2 scenarios (after 14:01)
|
|
719
|
+
// - Result: Feature 1's 8 components reported as "no scenarios"
|
|
720
|
+
const allComponents = [
|
|
721
|
+
// Feature 1 components
|
|
722
|
+
{
|
|
723
|
+
name: 'Header',
|
|
724
|
+
filePath: 'src/components/Header.tsx',
|
|
725
|
+
returnType: 'JSX.Element',
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
name: 'Logo',
|
|
729
|
+
filePath: 'src/components/Logo.tsx',
|
|
730
|
+
returnType: 'JSX.Element',
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
name: 'TabBar',
|
|
734
|
+
filePath: 'src/components/TabBar.tsx',
|
|
735
|
+
returnType: 'JSX.Element',
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
name: 'ArticlePreview',
|
|
739
|
+
filePath: 'src/components/ArticlePreview.tsx',
|
|
740
|
+
returnType: 'JSX.Element',
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
name: 'SaveButton',
|
|
744
|
+
filePath: 'src/components/SaveButton.tsx',
|
|
745
|
+
returnType: 'JSX.Element',
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
name: 'StatusBanner',
|
|
749
|
+
filePath: 'src/components/StatusBanner.tsx',
|
|
750
|
+
returnType: 'JSX.Element',
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
name: 'EmptyState',
|
|
754
|
+
filePath: 'src/components/EmptyState.tsx',
|
|
755
|
+
returnType: 'JSX.Element',
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
name: 'ArticleRow',
|
|
759
|
+
filePath: 'src/components/ArticleRow.tsx',
|
|
760
|
+
returnType: 'JSX.Element',
|
|
761
|
+
},
|
|
762
|
+
// Feature 2 components
|
|
763
|
+
{
|
|
764
|
+
name: 'ArticleCard',
|
|
765
|
+
filePath: 'src/components/ArticleCard.tsx',
|
|
766
|
+
returnType: 'JSX.Element',
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
name: 'LibraryHeader',
|
|
770
|
+
filePath: 'src/components/LibraryHeader.tsx',
|
|
771
|
+
returnType: 'JSX.Element',
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
name: 'ArticleCardGrid',
|
|
775
|
+
filePath: 'src/components/ArticleCardGrid.tsx',
|
|
776
|
+
returnType: 'JSX.Element',
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
name: 'OpenLibraryButton',
|
|
780
|
+
filePath: 'src/components/OpenLibraryButton.tsx',
|
|
781
|
+
returnType: 'JSX.Element',
|
|
782
|
+
},
|
|
783
|
+
];
|
|
784
|
+
// All scenarios that exist in the DB (both features)
|
|
785
|
+
const allScenarioCounts = {
|
|
786
|
+
Header: 2,
|
|
787
|
+
Logo: 1,
|
|
788
|
+
TabBar: 3,
|
|
789
|
+
ArticlePreview: 4,
|
|
790
|
+
SaveButton: 2,
|
|
791
|
+
StatusBanner: 2,
|
|
792
|
+
EmptyState: 2,
|
|
793
|
+
ArticleRow: 3,
|
|
794
|
+
ArticleCard: 4,
|
|
795
|
+
LibraryHeader: 3,
|
|
796
|
+
ArticleCardGrid: 2,
|
|
797
|
+
OpenLibraryButton: 1,
|
|
798
|
+
};
|
|
799
|
+
// Only Feature 2 scenarios (created after featureStartedAt)
|
|
800
|
+
const sessionScopedCounts = {
|
|
801
|
+
ArticleCard: 4,
|
|
802
|
+
LibraryHeader: 3,
|
|
803
|
+
ArticleCardGrid: 2,
|
|
804
|
+
OpenLibraryButton: 1,
|
|
805
|
+
};
|
|
806
|
+
it('should pass audit when entityChangeStatus is empty and all scenarios are counted', () => {
|
|
807
|
+
// With the fix: resolveAuditSessionScope nullifies featureStartedAt
|
|
808
|
+
// when entityChangeStatus is empty, so ALL scenarios are counted
|
|
809
|
+
const scope = resolveAuditSessionScope({
|
|
810
|
+
featureStartedAt: '2026-03-12T14:01:31.291Z',
|
|
811
|
+
entityChangeStatus: undefined,
|
|
812
|
+
});
|
|
813
|
+
// Glossary filter: returns all (no change status to filter by)
|
|
814
|
+
const filtered = filterGlossaryByChangeStatus(allComponents, scope.entityChangeStatus);
|
|
815
|
+
expect(filtered).toHaveLength(12);
|
|
816
|
+
// Since featureStartedAt is now null, the route would query ALL scenarios
|
|
817
|
+
// (not session-scoped), giving us allScenarioCounts
|
|
818
|
+
const countsToUse = scope.featureStartedAt
|
|
819
|
+
? sessionScopedCounts
|
|
820
|
+
: allScenarioCounts;
|
|
821
|
+
const { components } = classifyGlossaryEntries(filtered);
|
|
822
|
+
const result = computeAudit({
|
|
823
|
+
components,
|
|
824
|
+
functions: [],
|
|
825
|
+
scenarioCounts: countsToUse,
|
|
826
|
+
testFileExistence: {},
|
|
827
|
+
});
|
|
828
|
+
// Every component should have scenarios
|
|
829
|
+
expect(result.summary.componentsMissing).toBe(0);
|
|
830
|
+
expect(result.summary.componentsOk).toBe(12);
|
|
831
|
+
expect(result.summary.allPassing).toBe(true);
|
|
832
|
+
});
|
|
833
|
+
it('demonstrates the bug: session-scoped counts with unscoped glossary fails Feature 1', () => {
|
|
834
|
+
// WITHOUT the fix: entityChangeStatus is empty so all 12 components are audited,
|
|
835
|
+
// but scenario counts are session-scoped so Feature 1 components get 0
|
|
836
|
+
const filtered = filterGlossaryByChangeStatus(allComponents, undefined);
|
|
837
|
+
expect(filtered).toHaveLength(12);
|
|
838
|
+
const { components } = classifyGlossaryEntries(filtered);
|
|
839
|
+
const result = computeAudit({
|
|
840
|
+
components,
|
|
841
|
+
functions: [],
|
|
842
|
+
scenarioCounts: sessionScopedCounts, // BUG: only Feature 2 scenarios
|
|
843
|
+
testFileExistence: {},
|
|
844
|
+
});
|
|
845
|
+
// This is the broken behavior — 8 components incorrectly flagged
|
|
846
|
+
expect(result.summary.componentsMissing).toBe(8);
|
|
847
|
+
expect(result.summary.allPassing).toBe(false);
|
|
848
|
+
});
|
|
849
|
+
it('should correctly scope audit when entityChangeStatus is available', () => {
|
|
850
|
+
// When entityChangeStatus works, only Feature 2 components are audited
|
|
851
|
+
// and only Feature 2 scenarios are counted — both filters agree
|
|
852
|
+
const ecs = {
|
|
853
|
+
ArticleCard: { status: 'new' },
|
|
854
|
+
LibraryHeader: { status: 'new' },
|
|
855
|
+
ArticleCardGrid: { status: 'new' },
|
|
856
|
+
OpenLibraryButton: { status: 'new' },
|
|
857
|
+
};
|
|
858
|
+
const scope = resolveAuditSessionScope({
|
|
859
|
+
featureStartedAt: '2026-03-12T14:01:31.291Z',
|
|
860
|
+
entityChangeStatus: ecs,
|
|
861
|
+
});
|
|
862
|
+
// featureStartedAt preserved — session scoping is valid
|
|
863
|
+
expect(scope.featureStartedAt).toBe('2026-03-12T14:01:31.291Z');
|
|
864
|
+
const filtered = filterGlossaryByChangeStatus(allComponents, scope.entityChangeStatus);
|
|
865
|
+
expect(filtered).toHaveLength(4); // Only Feature 2 components
|
|
866
|
+
const { components } = classifyGlossaryEntries(filtered);
|
|
867
|
+
const result = computeAudit({
|
|
868
|
+
components,
|
|
869
|
+
functions: [],
|
|
870
|
+
scenarioCounts: sessionScopedCounts,
|
|
871
|
+
testFileExistence: {},
|
|
872
|
+
});
|
|
873
|
+
expect(result.summary.componentsMissing).toBe(0);
|
|
874
|
+
expect(result.summary.componentsOk).toBe(4);
|
|
875
|
+
expect(result.summary.allPassing).toBe(true);
|
|
876
|
+
});
|
|
877
|
+
});
|
|
878
|
+
});
|
|
879
|
+
// ── queryScenarioCounts ─────────────────────────────────────────────
|
|
880
|
+
describe('queryScenarioCounts', () => {
|
|
881
|
+
let db;
|
|
882
|
+
let rawDb;
|
|
883
|
+
const projectId = 'test-project-id';
|
|
884
|
+
beforeEach(async () => {
|
|
885
|
+
rawDb = new Database(':memory:');
|
|
886
|
+
db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
|
|
887
|
+
await db.schema
|
|
888
|
+
.createTable('editor_scenarios')
|
|
889
|
+
.addColumn('id', 'varchar', (col) => col.primaryKey())
|
|
890
|
+
.addColumn('project_id', 'varchar', (col) => col.notNull())
|
|
891
|
+
.addColumn('name', 'varchar', (col) => col.notNull())
|
|
892
|
+
.addColumn('description', 'text')
|
|
893
|
+
.addColumn('component_name', 'varchar')
|
|
894
|
+
.addColumn('component_path', 'varchar')
|
|
895
|
+
.addColumn('url', 'varchar')
|
|
896
|
+
.addColumn('type', 'varchar')
|
|
897
|
+
.addColumn('screenshot_path', 'varchar')
|
|
898
|
+
.addColumn('viewport_width', 'integer')
|
|
899
|
+
.addColumn('viewport_height', 'integer')
|
|
900
|
+
.addColumn('dimension', 'varchar')
|
|
901
|
+
.addColumn('created_at', 'datetime')
|
|
902
|
+
.addColumn('updated_at', 'datetime')
|
|
903
|
+
.execute();
|
|
904
|
+
});
|
|
905
|
+
afterEach(async () => {
|
|
906
|
+
await db.destroy();
|
|
907
|
+
});
|
|
908
|
+
it('should count all scenarios when featureStartedAt is null', async () => {
|
|
909
|
+
await db
|
|
910
|
+
.insertInto('editor_scenarios')
|
|
911
|
+
.values({
|
|
912
|
+
id: 'sc-1',
|
|
913
|
+
project_id: projectId,
|
|
914
|
+
name: 'ArticleRow - Default',
|
|
915
|
+
component_name: 'ArticleRow',
|
|
916
|
+
viewport_width: 1280,
|
|
917
|
+
viewport_height: 720,
|
|
918
|
+
created_at: '2026-03-12 13:00:00',
|
|
919
|
+
})
|
|
920
|
+
.execute();
|
|
921
|
+
const counts = await queryScenarioCounts(db, projectId, null);
|
|
922
|
+
expect(counts).toEqual({ ArticleRow: 1 });
|
|
923
|
+
});
|
|
924
|
+
it('should count scenarios created after featureStartedAt', async () => {
|
|
925
|
+
await db
|
|
926
|
+
.insertInto('editor_scenarios')
|
|
927
|
+
.values({
|
|
928
|
+
id: 'sc-1',
|
|
929
|
+
project_id: projectId,
|
|
930
|
+
name: 'ArticleRow - Default',
|
|
931
|
+
component_name: 'ArticleRow',
|
|
932
|
+
viewport_width: 1280,
|
|
933
|
+
viewport_height: 720,
|
|
934
|
+
created_at: '2026-03-12 14:30:00',
|
|
935
|
+
})
|
|
936
|
+
.execute();
|
|
937
|
+
const counts = await queryScenarioCounts(db, projectId, '2026-03-12T14:01:31.291Z');
|
|
938
|
+
expect(counts).toEqual({ ArticleRow: 1 });
|
|
939
|
+
});
|
|
940
|
+
it('should exclude scenarios created before featureStartedAt', async () => {
|
|
941
|
+
await db
|
|
942
|
+
.insertInto('editor_scenarios')
|
|
943
|
+
.values({
|
|
944
|
+
id: 'sc-1',
|
|
945
|
+
project_id: projectId,
|
|
946
|
+
name: 'ArticleRow - Default',
|
|
947
|
+
component_name: 'ArticleRow',
|
|
948
|
+
viewport_width: 1280,
|
|
949
|
+
viewport_height: 720,
|
|
950
|
+
created_at: '2026-03-12 13:00:00',
|
|
951
|
+
})
|
|
952
|
+
.execute();
|
|
953
|
+
const counts = await queryScenarioCounts(db, projectId, '2026-03-12T14:01:31.291Z');
|
|
954
|
+
expect(counts).toEqual({});
|
|
955
|
+
});
|
|
956
|
+
it('should count re-registered scenarios whose updated_at is after featureStartedAt', async () => {
|
|
957
|
+
// This is the bug: a scenario from Feature 1 (created_at before featureStartedAt)
|
|
958
|
+
// is re-registered in Feature 2 (updated_at after featureStartedAt).
|
|
959
|
+
// The audit should count it because it was actively re-registered in this session.
|
|
960
|
+
await db
|
|
961
|
+
.insertInto('editor_scenarios')
|
|
962
|
+
.values({
|
|
963
|
+
id: 'sc-1',
|
|
964
|
+
project_id: projectId,
|
|
965
|
+
name: 'ArticleRow - Default',
|
|
966
|
+
component_name: 'ArticleRow',
|
|
967
|
+
viewport_width: 400,
|
|
968
|
+
viewport_height: 600,
|
|
969
|
+
created_at: '2026-03-12 13:28:00', // Feature 1
|
|
970
|
+
updated_at: '2026-03-12 14:32:00', // Re-registered in Feature 2
|
|
971
|
+
})
|
|
972
|
+
.execute();
|
|
973
|
+
const counts = await queryScenarioCounts(db, projectId, '2026-03-12T14:01:31.291Z');
|
|
974
|
+
// Should count because updated_at is after featureStartedAt
|
|
975
|
+
expect(counts).toEqual({ ArticleRow: 1 });
|
|
976
|
+
});
|
|
977
|
+
it('should handle mixed scenarios: some new, some re-registered', async () => {
|
|
978
|
+
// ArticleCard: new in Feature 2 (created_at after featureStartedAt)
|
|
979
|
+
await db
|
|
980
|
+
.insertInto('editor_scenarios')
|
|
981
|
+
.values({
|
|
982
|
+
id: 'sc-1',
|
|
983
|
+
project_id: projectId,
|
|
984
|
+
name: 'ArticleCard - Default',
|
|
985
|
+
component_name: 'ArticleCard',
|
|
986
|
+
viewport_width: 1280,
|
|
987
|
+
viewport_height: 720,
|
|
988
|
+
created_at: '2026-03-12 14:14:00',
|
|
989
|
+
})
|
|
990
|
+
.execute();
|
|
991
|
+
// ArticleRow: from Feature 1, re-registered in Feature 2
|
|
992
|
+
await db
|
|
993
|
+
.insertInto('editor_scenarios')
|
|
994
|
+
.values({
|
|
995
|
+
id: 'sc-2',
|
|
996
|
+
project_id: projectId,
|
|
997
|
+
name: 'ArticleRow - Default',
|
|
998
|
+
component_name: 'ArticleRow',
|
|
999
|
+
viewport_width: 400,
|
|
1000
|
+
viewport_height: 600,
|
|
1001
|
+
created_at: '2026-03-12 13:28:00', // Feature 1
|
|
1002
|
+
updated_at: '2026-03-12 14:32:00', // Re-registered in Feature 2
|
|
1003
|
+
})
|
|
1004
|
+
.execute();
|
|
1005
|
+
const counts = await queryScenarioCounts(db, projectId, '2026-03-12T14:01:31.291Z');
|
|
1006
|
+
expect(counts).toEqual({ ArticleCard: 1, ArticleRow: 1 });
|
|
1007
|
+
});
|
|
1008
|
+
});
|
|
1009
|
+
// ── queryPageScenarioCounts ──────────────────────────────────────────
|
|
1010
|
+
describe('queryPageScenarioCounts', () => {
|
|
1011
|
+
let db;
|
|
1012
|
+
let rawDb;
|
|
1013
|
+
const projectId = 'test-project-id';
|
|
1014
|
+
beforeEach(async () => {
|
|
1015
|
+
rawDb = new Database(':memory:');
|
|
1016
|
+
db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
|
|
1017
|
+
await db.schema
|
|
1018
|
+
.createTable('editor_scenarios')
|
|
1019
|
+
.addColumn('id', 'varchar', (col) => col.primaryKey())
|
|
1020
|
+
.addColumn('project_id', 'varchar', (col) => col.notNull())
|
|
1021
|
+
.addColumn('name', 'varchar', (col) => col.notNull())
|
|
1022
|
+
.addColumn('description', 'text')
|
|
1023
|
+
.addColumn('component_name', 'varchar')
|
|
1024
|
+
.addColumn('component_path', 'varchar')
|
|
1025
|
+
.addColumn('page_file_path', 'varchar')
|
|
1026
|
+
.addColumn('url', 'varchar')
|
|
1027
|
+
.addColumn('type', 'varchar')
|
|
1028
|
+
.addColumn('screenshot_path', 'varchar')
|
|
1029
|
+
.addColumn('viewport_width', 'integer')
|
|
1030
|
+
.addColumn('viewport_height', 'integer')
|
|
1031
|
+
.addColumn('dimension', 'varchar')
|
|
1032
|
+
.addColumn('created_at', 'datetime')
|
|
1033
|
+
.addColumn('updated_at', 'datetime')
|
|
1034
|
+
.execute();
|
|
1035
|
+
});
|
|
1036
|
+
afterEach(async () => {
|
|
1037
|
+
await db.destroy();
|
|
1038
|
+
});
|
|
1039
|
+
it('should count app-level scenarios by page_file_path', async () => {
|
|
1040
|
+
// App-level scenario: has page_file_path but no component_name
|
|
1041
|
+
await db
|
|
1042
|
+
.insertInto('editor_scenarios')
|
|
1043
|
+
.values({
|
|
1044
|
+
id: 'sc-1',
|
|
1045
|
+
project_id: projectId,
|
|
1046
|
+
name: 'Library - Default',
|
|
1047
|
+
component_name: null,
|
|
1048
|
+
page_file_path: 'app/library/page.tsx',
|
|
1049
|
+
created_at: '2026-03-12 13:00:00',
|
|
1050
|
+
})
|
|
1051
|
+
.execute();
|
|
1052
|
+
await db
|
|
1053
|
+
.insertInto('editor_scenarios')
|
|
1054
|
+
.values({
|
|
1055
|
+
id: 'sc-2',
|
|
1056
|
+
project_id: projectId,
|
|
1057
|
+
name: 'Library - Empty',
|
|
1058
|
+
component_name: null,
|
|
1059
|
+
page_file_path: 'app/library/page.tsx',
|
|
1060
|
+
created_at: '2026-03-12 13:05:00',
|
|
1061
|
+
})
|
|
1062
|
+
.execute();
|
|
1063
|
+
const counts = await queryPageScenarioCounts(db, projectId, null);
|
|
1064
|
+
expect(counts).toEqual({ 'app/library/page.tsx': 2 });
|
|
1065
|
+
});
|
|
1066
|
+
it('should not count component scenarios (those have component_name)', async () => {
|
|
1067
|
+
// Component scenario — should NOT appear in page counts
|
|
1068
|
+
await db
|
|
1069
|
+
.insertInto('editor_scenarios')
|
|
1070
|
+
.values({
|
|
1071
|
+
id: 'sc-1',
|
|
1072
|
+
project_id: projectId,
|
|
1073
|
+
name: 'ArticleRow - Default',
|
|
1074
|
+
component_name: 'ArticleRow',
|
|
1075
|
+
page_file_path: null,
|
|
1076
|
+
created_at: '2026-03-12 13:00:00',
|
|
1077
|
+
})
|
|
1078
|
+
.execute();
|
|
1079
|
+
const counts = await queryPageScenarioCounts(db, projectId, null);
|
|
1080
|
+
expect(counts).toEqual({});
|
|
1081
|
+
});
|
|
1082
|
+
it('should respect featureStartedAt filter', async () => {
|
|
1083
|
+
await db
|
|
1084
|
+
.insertInto('editor_scenarios')
|
|
1085
|
+
.values({
|
|
1086
|
+
id: 'sc-1',
|
|
1087
|
+
project_id: projectId,
|
|
1088
|
+
name: 'Library - Old',
|
|
1089
|
+
component_name: null,
|
|
1090
|
+
page_file_path: 'app/library/page.tsx',
|
|
1091
|
+
created_at: '2026-03-12 13:00:00',
|
|
1092
|
+
})
|
|
1093
|
+
.execute();
|
|
1094
|
+
await db
|
|
1095
|
+
.insertInto('editor_scenarios')
|
|
1096
|
+
.values({
|
|
1097
|
+
id: 'sc-2',
|
|
1098
|
+
project_id: projectId,
|
|
1099
|
+
name: 'Library - New',
|
|
1100
|
+
component_name: null,
|
|
1101
|
+
page_file_path: 'app/library/page.tsx',
|
|
1102
|
+
created_at: '2026-03-12 15:00:00',
|
|
1103
|
+
})
|
|
1104
|
+
.execute();
|
|
1105
|
+
const counts = await queryPageScenarioCounts(db, projectId, '2026-03-12T14:01:31.291Z');
|
|
1106
|
+
expect(counts).toEqual({ 'app/library/page.tsx': 1 });
|
|
1107
|
+
});
|
|
1108
|
+
it('should count re-registered page scenarios via updated_at', async () => {
|
|
1109
|
+
await db
|
|
1110
|
+
.insertInto('editor_scenarios')
|
|
1111
|
+
.values({
|
|
1112
|
+
id: 'sc-1',
|
|
1113
|
+
project_id: projectId,
|
|
1114
|
+
name: 'Library - Default',
|
|
1115
|
+
component_name: null,
|
|
1116
|
+
page_file_path: 'app/library/page.tsx',
|
|
1117
|
+
created_at: '2026-03-12 13:00:00',
|
|
1118
|
+
updated_at: '2026-03-12 15:00:00',
|
|
1119
|
+
})
|
|
1120
|
+
.execute();
|
|
1121
|
+
const counts = await queryPageScenarioCounts(db, projectId, '2026-03-12T14:01:31.291Z');
|
|
1122
|
+
expect(counts).toEqual({ 'app/library/page.tsx': 1 });
|
|
1123
|
+
});
|
|
1124
|
+
it('should group counts by page_file_path across multiple pages', async () => {
|
|
1125
|
+
await db
|
|
1126
|
+
.insertInto('editor_scenarios')
|
|
1127
|
+
.values([
|
|
1128
|
+
{
|
|
1129
|
+
id: 'sc-1',
|
|
1130
|
+
project_id: projectId,
|
|
1131
|
+
name: 'Library - Default',
|
|
1132
|
+
component_name: null,
|
|
1133
|
+
page_file_path: 'app/library/page.tsx',
|
|
1134
|
+
created_at: '2026-03-12 13:00:00',
|
|
1135
|
+
},
|
|
1136
|
+
{
|
|
1137
|
+
id: 'sc-2',
|
|
1138
|
+
project_id: projectId,
|
|
1139
|
+
name: 'Library - Rich',
|
|
1140
|
+
component_name: null,
|
|
1141
|
+
page_file_path: 'app/library/page.tsx',
|
|
1142
|
+
created_at: '2026-03-12 13:00:00',
|
|
1143
|
+
},
|
|
1144
|
+
{
|
|
1145
|
+
id: 'sc-3',
|
|
1146
|
+
project_id: projectId,
|
|
1147
|
+
name: 'Collections - Default',
|
|
1148
|
+
component_name: null,
|
|
1149
|
+
page_file_path: 'app/library/collections/page.tsx',
|
|
1150
|
+
created_at: '2026-03-12 13:00:00',
|
|
1151
|
+
},
|
|
1152
|
+
])
|
|
1153
|
+
.execute();
|
|
1154
|
+
const counts = await queryPageScenarioCounts(db, projectId, null);
|
|
1155
|
+
expect(counts).toEqual({
|
|
1156
|
+
'app/library/page.tsx': 2,
|
|
1157
|
+
'app/library/collections/page.tsx': 1,
|
|
1158
|
+
});
|
|
1159
|
+
});
|
|
1160
|
+
});
|
|
1161
|
+
// ── Audit + entity completeness integration ─────────────────────────
|
|
1162
|
+
describe('audit should catch incomplete entities (bug reproduction)', () => {
|
|
1163
|
+
let db;
|
|
1164
|
+
let rawDb;
|
|
1165
|
+
const projectId = 'test-project-id';
|
|
1166
|
+
beforeEach(async () => {
|
|
1167
|
+
rawDb = new Database(':memory:');
|
|
1168
|
+
db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
|
|
1169
|
+
await db.schema
|
|
1170
|
+
.createTable('editor_scenarios')
|
|
1171
|
+
.addColumn('id', 'varchar', (col) => col.primaryKey())
|
|
1172
|
+
.addColumn('project_id', 'varchar', (col) => col.notNull())
|
|
1173
|
+
.addColumn('name', 'varchar', (col) => col.notNull())
|
|
1174
|
+
.addColumn('component_name', 'varchar')
|
|
1175
|
+
.addColumn('component_path', 'varchar')
|
|
1176
|
+
.addColumn('entity_sha', 'varchar')
|
|
1177
|
+
.addColumn('display_name', 'varchar')
|
|
1178
|
+
.addColumn('page_file_path', 'varchar')
|
|
1179
|
+
.addColumn('url', 'varchar')
|
|
1180
|
+
.addColumn('created_at', 'datetime')
|
|
1181
|
+
.addColumn('updated_at', 'datetime')
|
|
1182
|
+
.execute();
|
|
1183
|
+
await db.schema
|
|
1184
|
+
.createTable('analyses')
|
|
1185
|
+
.addColumn('id', 'varchar', (col) => col.primaryKey())
|
|
1186
|
+
.addColumn('entity_sha', 'varchar')
|
|
1187
|
+
.addColumn('entity_name', 'varchar')
|
|
1188
|
+
.addColumn('project_id', 'varchar')
|
|
1189
|
+
.execute();
|
|
1190
|
+
await db.schema
|
|
1191
|
+
.createTable('entities')
|
|
1192
|
+
.addColumn('sha', 'varchar', (col) => col.primaryKey())
|
|
1193
|
+
.addColumn('name', 'varchar')
|
|
1194
|
+
.addColumn('entity_type', 'varchar')
|
|
1195
|
+
.addColumn('file_path', 'varchar')
|
|
1196
|
+
.execute();
|
|
1197
|
+
});
|
|
1198
|
+
afterEach(async () => {
|
|
1199
|
+
await db.destroy();
|
|
1200
|
+
});
|
|
1201
|
+
it('demonstrates the bug: computeAudit passes but entities are incomplete', async () => {
|
|
1202
|
+
// Setup: Two components in glossary, both have scenarios registered.
|
|
1203
|
+
// CollectionChips has scenarios but NO analysis records — it's "incomplete."
|
|
1204
|
+
// The glossary-based audit (computeAudit) doesn't know about entity analyses
|
|
1205
|
+
// so it says allPassing: true. This is the bug.
|
|
1206
|
+
// Entities in DB
|
|
1207
|
+
await db
|
|
1208
|
+
.insertInto('entities')
|
|
1209
|
+
.values({
|
|
1210
|
+
sha: 'sha-header',
|
|
1211
|
+
name: 'Header',
|
|
1212
|
+
entity_type: 'visual',
|
|
1213
|
+
file_path: 'src/components/Header.tsx',
|
|
1214
|
+
})
|
|
1215
|
+
.execute();
|
|
1216
|
+
await db
|
|
1217
|
+
.insertInto('entities')
|
|
1218
|
+
.values({
|
|
1219
|
+
sha: 'sha-chips',
|
|
1220
|
+
name: 'CollectionChips',
|
|
1221
|
+
entity_type: 'visual',
|
|
1222
|
+
file_path: 'src/components/CollectionChips.tsx',
|
|
1223
|
+
})
|
|
1224
|
+
.execute();
|
|
1225
|
+
// Header has an analysis — it's complete
|
|
1226
|
+
await db
|
|
1227
|
+
.insertInto('analyses')
|
|
1228
|
+
.values({
|
|
1229
|
+
id: 'a-1',
|
|
1230
|
+
entity_sha: 'sha-header',
|
|
1231
|
+
entity_name: 'Header',
|
|
1232
|
+
project_id: projectId,
|
|
1233
|
+
})
|
|
1234
|
+
.execute();
|
|
1235
|
+
// CollectionChips has NO analysis — it's incomplete
|
|
1236
|
+
// Both have scenarios
|
|
1237
|
+
await db
|
|
1238
|
+
.insertInto('editor_scenarios')
|
|
1239
|
+
.values({
|
|
1240
|
+
id: 'sc-1',
|
|
1241
|
+
project_id: projectId,
|
|
1242
|
+
name: 'Header - Default',
|
|
1243
|
+
component_name: 'Header',
|
|
1244
|
+
entity_sha: 'sha-header',
|
|
1245
|
+
created_at: '2026-03-16 23:00:00',
|
|
1246
|
+
})
|
|
1247
|
+
.execute();
|
|
1248
|
+
await db
|
|
1249
|
+
.insertInto('editor_scenarios')
|
|
1250
|
+
.values({
|
|
1251
|
+
id: 'sc-2',
|
|
1252
|
+
project_id: projectId,
|
|
1253
|
+
name: 'CollectionChips - Default',
|
|
1254
|
+
component_name: 'CollectionChips',
|
|
1255
|
+
entity_sha: 'sha-chips',
|
|
1256
|
+
created_at: '2026-03-16 23:19:00',
|
|
1257
|
+
})
|
|
1258
|
+
.execute();
|
|
1259
|
+
await db
|
|
1260
|
+
.insertInto('editor_scenarios')
|
|
1261
|
+
.values({
|
|
1262
|
+
id: 'sc-3',
|
|
1263
|
+
project_id: projectId,
|
|
1264
|
+
name: 'CollectionChips - Many',
|
|
1265
|
+
component_name: 'CollectionChips',
|
|
1266
|
+
entity_sha: 'sha-chips',
|
|
1267
|
+
created_at: '2026-03-16 23:19:05',
|
|
1268
|
+
})
|
|
1269
|
+
.execute();
|
|
1270
|
+
// The glossary says both are components with scenarios
|
|
1271
|
+
const scenarioCounts = await queryScenarioCounts(db, projectId, null);
|
|
1272
|
+
expect(scenarioCounts).toEqual({ Header: 1, CollectionChips: 2 });
|
|
1273
|
+
// computeAudit only checks glossary coverage — it passes!
|
|
1274
|
+
const auditResult = computeAudit({
|
|
1275
|
+
components: [
|
|
1276
|
+
{
|
|
1277
|
+
name: 'Header',
|
|
1278
|
+
filePath: 'src/components/Header.tsx',
|
|
1279
|
+
returnType: 'JSX.Element',
|
|
1280
|
+
},
|
|
1281
|
+
{
|
|
1282
|
+
name: 'CollectionChips',
|
|
1283
|
+
filePath: 'src/components/CollectionChips.tsx',
|
|
1284
|
+
returnType: 'JSX.Element',
|
|
1285
|
+
},
|
|
1286
|
+
],
|
|
1287
|
+
functions: [],
|
|
1288
|
+
scenarioCounts,
|
|
1289
|
+
testFileExistence: {},
|
|
1290
|
+
});
|
|
1291
|
+
// BUG: computeAudit alone says everything is fine
|
|
1292
|
+
expect(auditResult.summary.allPassing).toBe(true);
|
|
1293
|
+
expect(auditResult.summary.componentsOk).toBe(2);
|
|
1294
|
+
// But queryIncompleteEntities catches the real issue
|
|
1295
|
+
const incomplete = await queryIncompleteEntities(db, projectId, null);
|
|
1296
|
+
expect(incomplete).toHaveLength(1);
|
|
1297
|
+
expect(incomplete[0].name).toBe('CollectionChips');
|
|
1298
|
+
expect(incomplete[0].scenarioCount).toBe(2);
|
|
1299
|
+
});
|
|
1300
|
+
it('audit should fail when combining computeAudit with incomplete entity check', async () => {
|
|
1301
|
+
// Same setup as above — this test shows the FIX working:
|
|
1302
|
+
// after computeAudit, we also check queryIncompleteEntities,
|
|
1303
|
+
// and if any are found, allPassing becomes false.
|
|
1304
|
+
await db
|
|
1305
|
+
.insertInto('entities')
|
|
1306
|
+
.values({
|
|
1307
|
+
sha: 'sha-header',
|
|
1308
|
+
name: 'Header',
|
|
1309
|
+
entity_type: 'visual',
|
|
1310
|
+
file_path: 'src/components/Header.tsx',
|
|
1311
|
+
})
|
|
1312
|
+
.execute();
|
|
1313
|
+
await db
|
|
1314
|
+
.insertInto('entities')
|
|
1315
|
+
.values({
|
|
1316
|
+
sha: 'sha-chips',
|
|
1317
|
+
name: 'CollectionChips',
|
|
1318
|
+
entity_type: 'visual',
|
|
1319
|
+
file_path: 'src/components/CollectionChips.tsx',
|
|
1320
|
+
})
|
|
1321
|
+
.execute();
|
|
1322
|
+
await db
|
|
1323
|
+
.insertInto('analyses')
|
|
1324
|
+
.values({
|
|
1325
|
+
id: 'a-1',
|
|
1326
|
+
entity_sha: 'sha-header',
|
|
1327
|
+
entity_name: 'Header',
|
|
1328
|
+
project_id: projectId,
|
|
1329
|
+
})
|
|
1330
|
+
.execute();
|
|
1331
|
+
await db
|
|
1332
|
+
.insertInto('editor_scenarios')
|
|
1333
|
+
.values({
|
|
1334
|
+
id: 'sc-1',
|
|
1335
|
+
project_id: projectId,
|
|
1336
|
+
name: 'Header - Default',
|
|
1337
|
+
component_name: 'Header',
|
|
1338
|
+
entity_sha: 'sha-header',
|
|
1339
|
+
created_at: '2026-03-16 23:00:00',
|
|
1340
|
+
})
|
|
1341
|
+
.execute();
|
|
1342
|
+
await db
|
|
1343
|
+
.insertInto('editor_scenarios')
|
|
1344
|
+
.values({
|
|
1345
|
+
id: 'sc-2',
|
|
1346
|
+
project_id: projectId,
|
|
1347
|
+
name: 'CollectionChips - Default',
|
|
1348
|
+
component_name: 'CollectionChips',
|
|
1349
|
+
entity_sha: 'sha-chips',
|
|
1350
|
+
created_at: '2026-03-16 23:19:00',
|
|
1351
|
+
})
|
|
1352
|
+
.execute();
|
|
1353
|
+
const scenarioCounts = await queryScenarioCounts(db, projectId, null);
|
|
1354
|
+
const auditResult = computeAudit({
|
|
1355
|
+
components: [
|
|
1356
|
+
{
|
|
1357
|
+
name: 'Header',
|
|
1358
|
+
filePath: 'src/components/Header.tsx',
|
|
1359
|
+
returnType: 'JSX.Element',
|
|
1360
|
+
},
|
|
1361
|
+
{
|
|
1362
|
+
name: 'CollectionChips',
|
|
1363
|
+
filePath: 'src/components/CollectionChips.tsx',
|
|
1364
|
+
returnType: 'JSX.Element',
|
|
1365
|
+
},
|
|
1366
|
+
],
|
|
1367
|
+
functions: [],
|
|
1368
|
+
scenarioCounts,
|
|
1369
|
+
testFileExistence: {},
|
|
1370
|
+
});
|
|
1371
|
+
// Apply the same post-processing the audit endpoint does
|
|
1372
|
+
const incomplete = await queryIncompleteEntities(db, projectId, null);
|
|
1373
|
+
if (incomplete.length > 0) {
|
|
1374
|
+
auditResult.summary.allPassing = false;
|
|
1375
|
+
auditResult.summary.incompleteEntities = incomplete.length;
|
|
1376
|
+
}
|
|
1377
|
+
// NOW the audit correctly fails
|
|
1378
|
+
expect(auditResult.summary.allPassing).toBe(false);
|
|
1379
|
+
expect(auditResult.summary.incompleteEntities).toBe(1);
|
|
1380
|
+
});
|
|
1381
|
+
it('audit should pass when all entities have analyses', async () => {
|
|
1382
|
+
// Both entities have analyses — everything is complete
|
|
1383
|
+
await db
|
|
1384
|
+
.insertInto('entities')
|
|
1385
|
+
.values({
|
|
1386
|
+
sha: 'sha-header',
|
|
1387
|
+
name: 'Header',
|
|
1388
|
+
entity_type: 'visual',
|
|
1389
|
+
file_path: 'src/components/Header.tsx',
|
|
1390
|
+
})
|
|
1391
|
+
.execute();
|
|
1392
|
+
await db
|
|
1393
|
+
.insertInto('entities')
|
|
1394
|
+
.values({
|
|
1395
|
+
sha: 'sha-chips',
|
|
1396
|
+
name: 'CollectionChips',
|
|
1397
|
+
entity_type: 'visual',
|
|
1398
|
+
file_path: 'src/components/CollectionChips.tsx',
|
|
1399
|
+
})
|
|
1400
|
+
.execute();
|
|
1401
|
+
await db
|
|
1402
|
+
.insertInto('analyses')
|
|
1403
|
+
.values({
|
|
1404
|
+
id: 'a-1',
|
|
1405
|
+
entity_sha: 'sha-header',
|
|
1406
|
+
entity_name: 'Header',
|
|
1407
|
+
project_id: projectId,
|
|
1408
|
+
})
|
|
1409
|
+
.execute();
|
|
1410
|
+
await db
|
|
1411
|
+
.insertInto('analyses')
|
|
1412
|
+
.values({
|
|
1413
|
+
id: 'a-2',
|
|
1414
|
+
entity_sha: 'sha-chips',
|
|
1415
|
+
entity_name: 'CollectionChips',
|
|
1416
|
+
project_id: projectId,
|
|
1417
|
+
})
|
|
1418
|
+
.execute();
|
|
1419
|
+
await db
|
|
1420
|
+
.insertInto('editor_scenarios')
|
|
1421
|
+
.values({
|
|
1422
|
+
id: 'sc-1',
|
|
1423
|
+
project_id: projectId,
|
|
1424
|
+
name: 'Header - Default',
|
|
1425
|
+
component_name: 'Header',
|
|
1426
|
+
entity_sha: 'sha-header',
|
|
1427
|
+
created_at: '2026-03-16 23:00:00',
|
|
1428
|
+
})
|
|
1429
|
+
.execute();
|
|
1430
|
+
await db
|
|
1431
|
+
.insertInto('editor_scenarios')
|
|
1432
|
+
.values({
|
|
1433
|
+
id: 'sc-2',
|
|
1434
|
+
project_id: projectId,
|
|
1435
|
+
name: 'CollectionChips - Default',
|
|
1436
|
+
component_name: 'CollectionChips',
|
|
1437
|
+
entity_sha: 'sha-chips',
|
|
1438
|
+
created_at: '2026-03-16 23:19:00',
|
|
1439
|
+
})
|
|
1440
|
+
.execute();
|
|
1441
|
+
const scenarioCounts = await queryScenarioCounts(db, projectId, null);
|
|
1442
|
+
const auditResult = computeAudit({
|
|
1443
|
+
components: [
|
|
1444
|
+
{
|
|
1445
|
+
name: 'Header',
|
|
1446
|
+
filePath: 'src/components/Header.tsx',
|
|
1447
|
+
returnType: 'JSX.Element',
|
|
1448
|
+
},
|
|
1449
|
+
{
|
|
1450
|
+
name: 'CollectionChips',
|
|
1451
|
+
filePath: 'src/components/CollectionChips.tsx',
|
|
1452
|
+
returnType: 'JSX.Element',
|
|
1453
|
+
},
|
|
1454
|
+
],
|
|
1455
|
+
functions: [],
|
|
1456
|
+
scenarioCounts,
|
|
1457
|
+
testFileExistence: {},
|
|
1458
|
+
});
|
|
1459
|
+
const incomplete = await queryIncompleteEntities(db, projectId, null);
|
|
1460
|
+
if (incomplete.length > 0) {
|
|
1461
|
+
auditResult.summary.allPassing = false;
|
|
1462
|
+
auditResult.summary.incompleteEntities = incomplete.length;
|
|
1463
|
+
}
|
|
1464
|
+
// Everything complete — audit passes
|
|
1465
|
+
expect(auditResult.summary.allPassing).toBe(true);
|
|
1466
|
+
expect(auditResult.summary.incompleteEntities).toBeUndefined();
|
|
1467
|
+
});
|
|
1468
|
+
});
|
|
1469
|
+
// ── queryMiscategorizedScenarios ─────────────────────────────────────
|
|
1470
|
+
describe('queryMiscategorizedScenarios', () => {
|
|
1471
|
+
let db;
|
|
1472
|
+
let rawDb;
|
|
1473
|
+
const projectId = 'test-project-id';
|
|
1474
|
+
beforeEach(async () => {
|
|
1475
|
+
rawDb = new Database(':memory:');
|
|
1476
|
+
db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
|
|
1477
|
+
await db.schema
|
|
1478
|
+
.createTable('editor_scenarios')
|
|
1479
|
+
.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
|
+
.addColumn('entity_sha', 'varchar')
|
|
1485
|
+
.addColumn('display_name', 'varchar')
|
|
1486
|
+
.addColumn('page_file_path', 'varchar')
|
|
1487
|
+
.addColumn('url', 'varchar')
|
|
1488
|
+
.addColumn('created_at', 'datetime')
|
|
1489
|
+
.addColumn('updated_at', 'datetime')
|
|
1490
|
+
.execute();
|
|
1491
|
+
});
|
|
1492
|
+
afterEach(async () => {
|
|
1493
|
+
await db.destroy();
|
|
1494
|
+
});
|
|
1495
|
+
it('should return empty when all component scenarios use isolation routes', async () => {
|
|
1496
|
+
await db
|
|
1497
|
+
.insertInto('editor_scenarios')
|
|
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
|
|
1513
|
+
await db
|
|
1514
|
+
.insertInto('editor_scenarios')
|
|
1515
|
+
.values({
|
|
1516
|
+
id: 'sc-1',
|
|
1517
|
+
project_id: projectId,
|
|
1518
|
+
name: 'Full Library Page',
|
|
1519
|
+
component_name: 'LibraryPage',
|
|
1520
|
+
url: '/library',
|
|
1521
|
+
created_at: '2026-03-17 12:41:40',
|
|
1522
|
+
})
|
|
1523
|
+
.execute();
|
|
1524
|
+
await db
|
|
1525
|
+
.insertInto('editor_scenarios')
|
|
1526
|
+
.values({
|
|
1527
|
+
id: 'sc-2',
|
|
1528
|
+
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
|
+
})
|
|
1534
|
+
.execute();
|
|
1535
|
+
const result = await queryMiscategorizedScenarios(db, projectId, null);
|
|
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
|
|
1546
|
+
await db
|
|
1547
|
+
.insertInto('editor_scenarios')
|
|
1548
|
+
.values({
|
|
1549
|
+
id: 'sc-1',
|
|
1550
|
+
project_id: projectId,
|
|
1551
|
+
name: 'Library with Articles',
|
|
1552
|
+
url: '/',
|
|
1553
|
+
created_at: '2026-03-17 12:25:14',
|
|
1554
|
+
})
|
|
1555
|
+
.execute();
|
|
1556
|
+
const result = await queryMiscategorizedScenarios(db, projectId, null);
|
|
1557
|
+
expect(result).toEqual([]);
|
|
1558
|
+
});
|
|
1559
|
+
it('should group miscategorized scenarios by component and URL', async () => {
|
|
1560
|
+
// Two different components both misusing real URLs
|
|
1561
|
+
await db
|
|
1562
|
+
.insertInto('editor_scenarios')
|
|
1563
|
+
.values({
|
|
1564
|
+
id: 'sc-1',
|
|
1565
|
+
project_id: projectId,
|
|
1566
|
+
name: 'Full Library Page',
|
|
1567
|
+
component_name: 'LibraryPage',
|
|
1568
|
+
url: '/library',
|
|
1569
|
+
created_at: '2026-03-17 12:41:40',
|
|
1570
|
+
})
|
|
1571
|
+
.execute();
|
|
1572
|
+
await db
|
|
1573
|
+
.insertInto('editor_scenarios')
|
|
1574
|
+
.values({
|
|
1575
|
+
id: 'sc-2',
|
|
1576
|
+
project_id: projectId,
|
|
1577
|
+
name: 'Dashboard - Full',
|
|
1578
|
+
component_name: 'Dashboard',
|
|
1579
|
+
url: '/dashboard',
|
|
1580
|
+
created_at: '2026-03-17 12:50:00',
|
|
1581
|
+
})
|
|
1582
|
+
.execute();
|
|
1583
|
+
const result = await queryMiscategorizedScenarios(db, projectId, null);
|
|
1584
|
+
expect(result).toHaveLength(2);
|
|
1585
|
+
expect(result.map((r) => r.componentName).sort()).toEqual([
|
|
1586
|
+
'Dashboard',
|
|
1587
|
+
'LibraryPage',
|
|
1588
|
+
]);
|
|
1589
|
+
});
|
|
1590
|
+
it('should scope to session when featureStartedAt is provided', async () => {
|
|
1591
|
+
// Old miscategorized scenario — before session
|
|
1592
|
+
await db
|
|
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
|
|
1604
|
+
await db
|
|
1605
|
+
.insertInto('editor_scenarios')
|
|
1606
|
+
.values({
|
|
1607
|
+
id: 'sc-new',
|
|
1608
|
+
project_id: projectId,
|
|
1609
|
+
name: 'Full Library Page',
|
|
1610
|
+
component_name: 'LibraryPage',
|
|
1611
|
+
url: '/library',
|
|
1612
|
+
created_at: '2026-03-17 12:41:40',
|
|
1613
|
+
})
|
|
1614
|
+
.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
|
+
await db
|
|
1621
|
+
.insertInto('editor_scenarios')
|
|
1622
|
+
.values({
|
|
1623
|
+
id: 'sc-1',
|
|
1624
|
+
project_id: projectId,
|
|
1625
|
+
name: 'NoUrl - Default',
|
|
1626
|
+
component_name: 'NoUrl',
|
|
1627
|
+
created_at: '2026-03-17 12:00:00',
|
|
1628
|
+
})
|
|
1629
|
+
.execute();
|
|
1630
|
+
const result = await queryMiscategorizedScenarios(db, projectId, null);
|
|
1631
|
+
expect(result).toEqual([]);
|
|
1632
|
+
});
|
|
1633
|
+
});
|
|
1634
|
+
// ── isOnlyIncompleteEntities ─────────────────────────────────────────
|
|
1635
|
+
describe('isOnlyIncompleteEntities', () => {
|
|
1636
|
+
it('should return true when incompleteEntities is the only failure', () => {
|
|
1637
|
+
expect(isOnlyIncompleteEntities({
|
|
1638
|
+
componentsMissing: 0,
|
|
1639
|
+
componentsWithErrors: 0,
|
|
1640
|
+
functionsFailing: 0,
|
|
1641
|
+
functionsNameMismatch: 0,
|
|
1642
|
+
functionsMissing: 0,
|
|
1643
|
+
missingFromGlossary: 0,
|
|
1644
|
+
incompleteEntities: 3,
|
|
1645
|
+
allPassing: false,
|
|
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', () => {
|
|
1768
|
+
let db;
|
|
1769
|
+
let rawDb;
|
|
1770
|
+
const projectId = 'test-project-id';
|
|
1771
|
+
beforeEach(async () => {
|
|
1772
|
+
rawDb = new Database(':memory:');
|
|
1773
|
+
db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
|
|
1774
|
+
await db.schema
|
|
1775
|
+
.createTable('editor_scenarios')
|
|
1776
|
+
.addColumn('id', 'varchar', (col) => col.primaryKey())
|
|
1777
|
+
.addColumn('project_id', 'varchar', (col) => col.notNull())
|
|
1778
|
+
.addColumn('name', 'varchar', (col) => col.notNull())
|
|
1779
|
+
.addColumn('component_name', 'varchar')
|
|
1780
|
+
.addColumn('component_path', 'varchar')
|
|
1781
|
+
.addColumn('entity_sha', 'varchar')
|
|
1782
|
+
.addColumn('display_name', 'varchar')
|
|
1783
|
+
.addColumn('page_file_path', 'varchar')
|
|
1784
|
+
.addColumn('url', 'varchar')
|
|
1785
|
+
.addColumn('created_at', 'datetime')
|
|
1786
|
+
.addColumn('updated_at', 'datetime')
|
|
1787
|
+
.execute();
|
|
1788
|
+
await db.schema
|
|
1789
|
+
.createTable('analyses')
|
|
1790
|
+
.addColumn('id', 'varchar', (col) => col.primaryKey())
|
|
1791
|
+
.addColumn('entity_sha', 'varchar')
|
|
1792
|
+
.addColumn('entity_name', 'varchar')
|
|
1793
|
+
.addColumn('project_id', 'varchar')
|
|
1794
|
+
.execute();
|
|
1795
|
+
await db.schema
|
|
1796
|
+
.createTable('entities')
|
|
1797
|
+
.addColumn('sha', 'varchar', (col) => col.primaryKey())
|
|
1798
|
+
.addColumn('name', 'varchar')
|
|
1799
|
+
.addColumn('entity_type', 'varchar')
|
|
1800
|
+
.addColumn('file_path', 'varchar')
|
|
1801
|
+
.execute();
|
|
1802
|
+
});
|
|
1803
|
+
afterEach(async () => {
|
|
1804
|
+
await db.destroy();
|
|
1805
|
+
});
|
|
1806
|
+
it('should return empty when all scenario entity SHAs have analyses', async () => {
|
|
1807
|
+
// Entity with analysis
|
|
1808
|
+
await db
|
|
1809
|
+
.insertInto('entities')
|
|
1810
|
+
.values({
|
|
1811
|
+
sha: 'sha-header',
|
|
1812
|
+
name: 'Header',
|
|
1813
|
+
entity_type: 'visual',
|
|
1814
|
+
file_path: 'src/Header.tsx',
|
|
1815
|
+
})
|
|
1816
|
+
.execute();
|
|
1817
|
+
await db
|
|
1818
|
+
.insertInto('analyses')
|
|
1819
|
+
.values({
|
|
1820
|
+
id: 'a-1',
|
|
1821
|
+
entity_sha: 'sha-header',
|
|
1822
|
+
entity_name: 'Header',
|
|
1823
|
+
project_id: projectId,
|
|
1824
|
+
})
|
|
1825
|
+
.execute();
|
|
1826
|
+
await db
|
|
1827
|
+
.insertInto('editor_scenarios')
|
|
1828
|
+
.values({
|
|
1829
|
+
id: 'sc-1',
|
|
1830
|
+
project_id: projectId,
|
|
1831
|
+
name: 'Header - Default',
|
|
1832
|
+
component_name: 'Header',
|
|
1833
|
+
entity_sha: 'sha-header',
|
|
1834
|
+
created_at: '2026-03-16 23:00:00',
|
|
1835
|
+
})
|
|
1836
|
+
.execute();
|
|
1837
|
+
const result = await queryIncompleteEntities(db, projectId, null);
|
|
1838
|
+
expect(result).toEqual([]);
|
|
1839
|
+
});
|
|
1840
|
+
it('should return entities with scenarios but no analyses', async () => {
|
|
1841
|
+
// Entity WITHOUT analysis
|
|
1842
|
+
await db
|
|
1843
|
+
.insertInto('entities')
|
|
1844
|
+
.values({
|
|
1845
|
+
sha: 'sha-chips',
|
|
1846
|
+
name: 'CollectionChips',
|
|
1847
|
+
entity_type: 'visual',
|
|
1848
|
+
file_path: 'src/components/CollectionChips.tsx',
|
|
1849
|
+
})
|
|
1850
|
+
.execute();
|
|
1851
|
+
// Scenario referencing that entity
|
|
1852
|
+
await db
|
|
1853
|
+
.insertInto('editor_scenarios')
|
|
1854
|
+
.values({
|
|
1855
|
+
id: 'sc-1',
|
|
1856
|
+
project_id: projectId,
|
|
1857
|
+
name: 'CollectionChips - Default',
|
|
1858
|
+
component_name: 'CollectionChips',
|
|
1859
|
+
entity_sha: 'sha-chips',
|
|
1860
|
+
created_at: '2026-03-16 23:00:00',
|
|
1861
|
+
})
|
|
1862
|
+
.execute();
|
|
1863
|
+
await db
|
|
1864
|
+
.insertInto('editor_scenarios')
|
|
1865
|
+
.values({
|
|
1866
|
+
id: 'sc-2',
|
|
1867
|
+
project_id: projectId,
|
|
1868
|
+
name: 'CollectionChips - Many',
|
|
1869
|
+
component_name: 'CollectionChips',
|
|
1870
|
+
entity_sha: 'sha-chips',
|
|
1871
|
+
created_at: '2026-03-16 23:01:00',
|
|
1872
|
+
})
|
|
1873
|
+
.execute();
|
|
1874
|
+
const result = await queryIncompleteEntities(db, projectId, null);
|
|
1875
|
+
expect(result).toEqual([
|
|
1876
|
+
{ entitySha: 'sha-chips', name: 'CollectionChips', scenarioCount: 2 },
|
|
1877
|
+
]);
|
|
1878
|
+
});
|
|
1879
|
+
it('should only return entities without analyses, not those with analyses', async () => {
|
|
1880
|
+
// Entity WITH analysis (Header)
|
|
1881
|
+
await db
|
|
1882
|
+
.insertInto('entities')
|
|
1883
|
+
.values({
|
|
1884
|
+
sha: 'sha-header',
|
|
1885
|
+
name: 'Header',
|
|
1886
|
+
entity_type: 'visual',
|
|
1887
|
+
file_path: 'src/Header.tsx',
|
|
1888
|
+
})
|
|
1889
|
+
.execute();
|
|
1890
|
+
await db
|
|
1891
|
+
.insertInto('analyses')
|
|
1892
|
+
.values({
|
|
1893
|
+
id: 'a-1',
|
|
1894
|
+
entity_sha: 'sha-header',
|
|
1895
|
+
entity_name: 'Header',
|
|
1896
|
+
project_id: projectId,
|
|
1897
|
+
})
|
|
1898
|
+
.execute();
|
|
1899
|
+
await db
|
|
1900
|
+
.insertInto('editor_scenarios')
|
|
1901
|
+
.values({
|
|
1902
|
+
id: 'sc-1',
|
|
1903
|
+
project_id: projectId,
|
|
1904
|
+
name: 'Header - Default',
|
|
1905
|
+
component_name: 'Header',
|
|
1906
|
+
entity_sha: 'sha-header',
|
|
1907
|
+
created_at: '2026-03-16 23:00:00',
|
|
1908
|
+
})
|
|
1909
|
+
.execute();
|
|
1910
|
+
// Entity WITHOUT analysis (CollectionPicker)
|
|
1911
|
+
await db
|
|
1912
|
+
.insertInto('entities')
|
|
1913
|
+
.values({
|
|
1914
|
+
sha: 'sha-picker',
|
|
1915
|
+
name: 'CollectionPicker',
|
|
1916
|
+
entity_type: 'visual',
|
|
1917
|
+
file_path: 'src/components/CollectionPicker.tsx',
|
|
1918
|
+
})
|
|
1919
|
+
.execute();
|
|
1920
|
+
await db
|
|
1921
|
+
.insertInto('editor_scenarios')
|
|
1922
|
+
.values({
|
|
1923
|
+
id: 'sc-2',
|
|
1924
|
+
project_id: projectId,
|
|
1925
|
+
name: 'CollectionPicker - Default',
|
|
1926
|
+
component_name: 'CollectionPicker',
|
|
1927
|
+
entity_sha: 'sha-picker',
|
|
1928
|
+
created_at: '2026-03-16 23:00:00',
|
|
1929
|
+
})
|
|
1930
|
+
.execute();
|
|
1931
|
+
const result = await queryIncompleteEntities(db, projectId, null);
|
|
1932
|
+
expect(result).toEqual([
|
|
1933
|
+
{ entitySha: 'sha-picker', name: 'CollectionPicker', scenarioCount: 1 },
|
|
1934
|
+
]);
|
|
1935
|
+
});
|
|
1936
|
+
it('should scope to session when featureStartedAt is provided', async () => {
|
|
1937
|
+
// Entity without analysis, scenario created BEFORE session
|
|
1938
|
+
await db
|
|
1939
|
+
.insertInto('entities')
|
|
1940
|
+
.values({
|
|
1941
|
+
sha: 'sha-old',
|
|
1942
|
+
name: 'OldComponent',
|
|
1943
|
+
entity_type: 'visual',
|
|
1944
|
+
file_path: 'src/OldComponent.tsx',
|
|
1945
|
+
})
|
|
1946
|
+
.execute();
|
|
1947
|
+
await db
|
|
1948
|
+
.insertInto('editor_scenarios')
|
|
1949
|
+
.values({
|
|
1950
|
+
id: 'sc-old',
|
|
1951
|
+
project_id: projectId,
|
|
1952
|
+
name: 'OldComponent - Default',
|
|
1953
|
+
component_name: 'OldComponent',
|
|
1954
|
+
entity_sha: 'sha-old',
|
|
1955
|
+
created_at: '2026-03-16 20:00:00',
|
|
1956
|
+
})
|
|
1957
|
+
.execute();
|
|
1958
|
+
// Entity without analysis, scenario created DURING session
|
|
1959
|
+
await db
|
|
1960
|
+
.insertInto('entities')
|
|
1961
|
+
.values({
|
|
1962
|
+
sha: 'sha-new',
|
|
1963
|
+
name: 'NewComponent',
|
|
1964
|
+
entity_type: 'visual',
|
|
1965
|
+
file_path: 'src/NewComponent.tsx',
|
|
1966
|
+
})
|
|
1967
|
+
.execute();
|
|
1968
|
+
await db
|
|
1969
|
+
.insertInto('editor_scenarios')
|
|
1970
|
+
.values({
|
|
1971
|
+
id: 'sc-new',
|
|
1972
|
+
project_id: projectId,
|
|
1973
|
+
name: 'NewComponent - Default',
|
|
1974
|
+
component_name: 'NewComponent',
|
|
1975
|
+
entity_sha: 'sha-new',
|
|
1976
|
+
created_at: '2026-03-16 23:10:00',
|
|
1977
|
+
})
|
|
1978
|
+
.execute();
|
|
1979
|
+
const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
|
|
1980
|
+
// Only NewComponent should be flagged (created in session)
|
|
1981
|
+
expect(result).toEqual([
|
|
1982
|
+
{ entitySha: 'sha-new', name: 'NewComponent', scenarioCount: 1 },
|
|
1983
|
+
]);
|
|
1984
|
+
});
|
|
1985
|
+
it('should include scenarios updated in session even if created before', async () => {
|
|
1986
|
+
await db
|
|
1987
|
+
.insertInto('entities')
|
|
1988
|
+
.values({
|
|
1989
|
+
sha: 'sha-updated',
|
|
1990
|
+
name: 'UpdatedComponent',
|
|
1991
|
+
entity_type: 'visual',
|
|
1992
|
+
file_path: 'src/Updated.tsx',
|
|
1993
|
+
})
|
|
1994
|
+
.execute();
|
|
1995
|
+
await db
|
|
1996
|
+
.insertInto('editor_scenarios')
|
|
1997
|
+
.values({
|
|
1998
|
+
id: 'sc-updated',
|
|
1999
|
+
project_id: projectId,
|
|
2000
|
+
name: 'UpdatedComponent - Default',
|
|
2001
|
+
component_name: 'UpdatedComponent',
|
|
2002
|
+
entity_sha: 'sha-updated',
|
|
2003
|
+
created_at: '2026-03-16 20:00:00',
|
|
2004
|
+
updated_at: '2026-03-16 23:20:00', // Updated in session
|
|
2005
|
+
})
|
|
2006
|
+
.execute();
|
|
2007
|
+
const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
|
|
2008
|
+
expect(result).toEqual([
|
|
2009
|
+
{
|
|
2010
|
+
entitySha: 'sha-updated',
|
|
2011
|
+
name: 'UpdatedComponent',
|
|
2012
|
+
scenarioCount: 1,
|
|
2013
|
+
},
|
|
2014
|
+
]);
|
|
2015
|
+
});
|
|
2016
|
+
it('should skip scenarios with null entity_sha', async () => {
|
|
2017
|
+
await db
|
|
2018
|
+
.insertInto('editor_scenarios')
|
|
2019
|
+
.values({
|
|
2020
|
+
id: 'sc-null',
|
|
2021
|
+
project_id: projectId,
|
|
2022
|
+
name: 'Orphan Scenario',
|
|
2023
|
+
component_name: 'Orphan',
|
|
2024
|
+
created_at: '2026-03-16 23:00:00',
|
|
2025
|
+
})
|
|
2026
|
+
.execute();
|
|
2027
|
+
const result = await queryIncompleteEntities(db, projectId, null);
|
|
2028
|
+
expect(result).toEqual([]);
|
|
2029
|
+
});
|
|
2030
|
+
it('should return empty when there are no scenarios', async () => {
|
|
2031
|
+
const result = await queryIncompleteEntities(db, projectId, null);
|
|
2032
|
+
expect(result).toEqual([]);
|
|
2033
|
+
});
|
|
2034
|
+
it('should not flag entities when a sibling version (same name+filePath) has analyses', async () => {
|
|
2035
|
+
// Old entity version WITH analysis
|
|
2036
|
+
await db
|
|
2037
|
+
.insertInto('entities')
|
|
2038
|
+
.values({
|
|
2039
|
+
sha: 'sha-btn-v1',
|
|
2040
|
+
name: 'OpenLibraryButton',
|
|
2041
|
+
entity_type: 'visual',
|
|
2042
|
+
file_path: 'src/components/OpenLibraryButton.tsx',
|
|
2043
|
+
})
|
|
2044
|
+
.execute();
|
|
2045
|
+
await db
|
|
2046
|
+
.insertInto('analyses')
|
|
2047
|
+
.values({
|
|
2048
|
+
id: 'a-btn-v1',
|
|
2049
|
+
entity_sha: 'sha-btn-v1',
|
|
2050
|
+
entity_name: 'OpenLibraryButton',
|
|
2051
|
+
project_id: projectId,
|
|
2052
|
+
})
|
|
2053
|
+
.execute();
|
|
2054
|
+
// New entity version WITHOUT analysis (created by file watcher)
|
|
2055
|
+
await db
|
|
2056
|
+
.insertInto('entities')
|
|
2057
|
+
.values({
|
|
2058
|
+
sha: 'sha-btn-v2',
|
|
2059
|
+
name: 'OpenLibraryButton',
|
|
2060
|
+
entity_type: 'visual',
|
|
2061
|
+
file_path: 'src/components/OpenLibraryButton.tsx',
|
|
2062
|
+
})
|
|
2063
|
+
.execute();
|
|
2064
|
+
// Scenario points to the NEW version (backfilled after file watcher)
|
|
2065
|
+
await db
|
|
2066
|
+
.insertInto('editor_scenarios')
|
|
2067
|
+
.values({
|
|
2068
|
+
id: 'sc-btn',
|
|
2069
|
+
project_id: projectId,
|
|
2070
|
+
name: 'OpenLibraryButton - Default',
|
|
2071
|
+
component_name: 'OpenLibraryButton',
|
|
2072
|
+
entity_sha: 'sha-btn-v2',
|
|
2073
|
+
created_at: '2026-03-16 23:00:00',
|
|
2074
|
+
})
|
|
2075
|
+
.execute();
|
|
2076
|
+
// Should NOT flag as incomplete — sibling version has analyses
|
|
2077
|
+
const result = await queryIncompleteEntities(db, projectId, null);
|
|
2078
|
+
expect(result).toEqual([]);
|
|
2079
|
+
});
|
|
2080
|
+
it('should still flag entities when no sibling version has analyses', async () => {
|
|
2081
|
+
// Only one version, no analyses
|
|
2082
|
+
await db
|
|
2083
|
+
.insertInto('entities')
|
|
2084
|
+
.values({
|
|
2085
|
+
sha: 'sha-icon',
|
|
2086
|
+
name: 'ExternalLinkIcon',
|
|
2087
|
+
entity_type: 'visual',
|
|
2088
|
+
file_path: 'src/components/ExternalLinkIcon.tsx',
|
|
2089
|
+
})
|
|
2090
|
+
.execute();
|
|
2091
|
+
await db
|
|
2092
|
+
.insertInto('editor_scenarios')
|
|
2093
|
+
.values({
|
|
2094
|
+
id: 'sc-icon',
|
|
2095
|
+
project_id: projectId,
|
|
2096
|
+
name: 'ExternalLinkIcon - Default',
|
|
2097
|
+
component_name: 'ExternalLinkIcon',
|
|
2098
|
+
entity_sha: 'sha-icon',
|
|
2099
|
+
created_at: '2026-03-16 23:00:00',
|
|
2100
|
+
})
|
|
2101
|
+
.execute();
|
|
2102
|
+
// Should flag as incomplete — no version has analyses
|
|
2103
|
+
const result = await queryIncompleteEntities(db, projectId, null);
|
|
2104
|
+
expect(result).toEqual([
|
|
2105
|
+
{
|
|
2106
|
+
entitySha: 'sha-icon',
|
|
2107
|
+
name: 'ExternalLinkIcon',
|
|
2108
|
+
scenarioCount: 1,
|
|
2109
|
+
},
|
|
2110
|
+
]);
|
|
2111
|
+
});
|
|
2112
|
+
it('should use entity name from entities table, falling back to component_name', async () => {
|
|
2113
|
+
// Scenario has entity_sha but entity record doesn't exist
|
|
2114
|
+
await db
|
|
2115
|
+
.insertInto('editor_scenarios')
|
|
2116
|
+
.values({
|
|
2117
|
+
id: 'sc-1',
|
|
2118
|
+
project_id: projectId,
|
|
2119
|
+
name: 'Ghost - Default',
|
|
2120
|
+
component_name: 'GhostComponent',
|
|
2121
|
+
entity_sha: 'sha-ghost',
|
|
2122
|
+
created_at: '2026-03-16 23:00:00',
|
|
2123
|
+
})
|
|
2124
|
+
.execute();
|
|
2125
|
+
const result = await queryIncompleteEntities(db, projectId, null);
|
|
2126
|
+
expect(result).toEqual([
|
|
2127
|
+
{ entitySha: 'sha-ghost', name: 'GhostComponent', scenarioCount: 1 },
|
|
2128
|
+
]);
|
|
2129
|
+
});
|
|
2130
|
+
});
|
|
2131
|
+
// ── identifyScenariosNeedingRecapture ──────────────────────────────
|
|
2132
|
+
describe('identifyScenariosNeedingRecapture', () => {
|
|
2133
|
+
// Reproduces the Margo bug: Feature 1 built app-level popup scenarios,
|
|
2134
|
+
// Feature 2 edited LibraryView (used by App), but app-level scenarios
|
|
2135
|
+
// were never flagged for recapture because the audit only checked
|
|
2136
|
+
// component scenario existence — not whether app-level scenarios are stale.
|
|
2137
|
+
//
|
|
2138
|
+
// Each scenario's entityName is resolved by the caller via
|
|
2139
|
+
// entity_sha → entities.name (the default export for app-level scenarios).
|
|
2140
|
+
it('should flag app-level scenario when its entity is impacted by transitive dependency change', () => {
|
|
2141
|
+
// LibraryView was edited → App is impacted (imports LibraryView)
|
|
2142
|
+
// App-level scenario "Library - Rich Library" has entity_sha pointing to App
|
|
2143
|
+
// It was NOT recaptured during Feature 2 → should be flagged
|
|
2144
|
+
const entityChangeStatus = {
|
|
2145
|
+
LibraryView: { status: 'edited' },
|
|
2146
|
+
App: {
|
|
2147
|
+
status: 'impacted',
|
|
2148
|
+
impactedBy: [
|
|
2149
|
+
{
|
|
2150
|
+
name: 'LibraryView',
|
|
2151
|
+
filePath: 'src/components/LibraryView.tsx',
|
|
2152
|
+
changeType: 'edited',
|
|
2153
|
+
},
|
|
2154
|
+
],
|
|
2155
|
+
},
|
|
2156
|
+
};
|
|
2157
|
+
const result = identifyScenariosNeedingRecapture({
|
|
2158
|
+
scenarios: [
|
|
2159
|
+
{
|
|
2160
|
+
name: 'Library - Rich Library',
|
|
2161
|
+
entityName: 'App', // resolved from entity_sha → entities.name
|
|
2162
|
+
updatedInSession: false,
|
|
2163
|
+
},
|
|
2164
|
+
],
|
|
2165
|
+
entityChangeStatus,
|
|
2166
|
+
});
|
|
2167
|
+
expect(result).toHaveLength(1);
|
|
2168
|
+
expect(result[0].scenarioName).toBe('Library - Rich Library');
|
|
2169
|
+
expect(result[0].entityName).toBe('App');
|
|
2170
|
+
expect(result[0].status.status).toBe('impacted');
|
|
2171
|
+
});
|
|
2172
|
+
it('should flag component scenario when its entity is directly edited and not recaptured', () => {
|
|
2173
|
+
const entityChangeStatus = {
|
|
2174
|
+
LibraryView: { status: 'edited' },
|
|
2175
|
+
};
|
|
2176
|
+
const result = identifyScenariosNeedingRecapture({
|
|
2177
|
+
scenarios: [
|
|
2178
|
+
{
|
|
2179
|
+
name: 'LibraryView - Empty',
|
|
2180
|
+
entityName: 'LibraryView',
|
|
2181
|
+
updatedInSession: false,
|
|
2182
|
+
},
|
|
2183
|
+
],
|
|
2184
|
+
entityChangeStatus,
|
|
2185
|
+
});
|
|
2186
|
+
expect(result).toHaveLength(1);
|
|
2187
|
+
expect(result[0].scenarioName).toBe('LibraryView - Empty');
|
|
2188
|
+
expect(result[0].entityName).toBe('LibraryView');
|
|
2189
|
+
expect(result[0].status.status).toBe('edited');
|
|
2190
|
+
});
|
|
2191
|
+
it('should NOT flag scenario that was already recaptured in the current session', () => {
|
|
2192
|
+
const entityChangeStatus = {
|
|
2193
|
+
App: {
|
|
2194
|
+
status: 'impacted',
|
|
2195
|
+
impactedBy: [
|
|
2196
|
+
{
|
|
2197
|
+
name: 'LibraryView',
|
|
2198
|
+
filePath: 'src/components/LibraryView.tsx',
|
|
2199
|
+
changeType: 'edited',
|
|
2200
|
+
},
|
|
2201
|
+
],
|
|
2202
|
+
},
|
|
2203
|
+
};
|
|
2204
|
+
const result = identifyScenariosNeedingRecapture({
|
|
2205
|
+
scenarios: [
|
|
2206
|
+
{
|
|
2207
|
+
name: 'App - Default',
|
|
2208
|
+
entityName: 'App',
|
|
2209
|
+
updatedInSession: true, // re-registered during Feature 2
|
|
2210
|
+
},
|
|
2211
|
+
],
|
|
2212
|
+
entityChangeStatus,
|
|
2213
|
+
});
|
|
2214
|
+
expect(result).toHaveLength(0);
|
|
2215
|
+
});
|
|
2216
|
+
it('should NOT flag scenario whose entity has no change status', () => {
|
|
2217
|
+
const entityChangeStatus = {
|
|
2218
|
+
LibraryView: { status: 'edited' },
|
|
2219
|
+
};
|
|
2220
|
+
const result = identifyScenariosNeedingRecapture({
|
|
2221
|
+
scenarios: [
|
|
2222
|
+
{
|
|
2223
|
+
name: 'WelcomeScreen - Default',
|
|
2224
|
+
entityName: 'WelcomeScreen',
|
|
2225
|
+
updatedInSession: false,
|
|
2226
|
+
},
|
|
2227
|
+
],
|
|
2228
|
+
entityChangeStatus,
|
|
2229
|
+
});
|
|
2230
|
+
expect(result).toHaveLength(0);
|
|
2231
|
+
});
|
|
2232
|
+
it('should return empty array when entityChangeStatus is undefined', () => {
|
|
2233
|
+
const result = identifyScenariosNeedingRecapture({
|
|
2234
|
+
scenarios: [
|
|
2235
|
+
{
|
|
2236
|
+
name: 'Library - Rich Library',
|
|
2237
|
+
entityName: 'App',
|
|
2238
|
+
updatedInSession: false,
|
|
2239
|
+
},
|
|
2240
|
+
],
|
|
2241
|
+
entityChangeStatus: undefined,
|
|
2242
|
+
});
|
|
2243
|
+
expect(result).toHaveLength(0);
|
|
2244
|
+
});
|
|
2245
|
+
it('should return empty array when entityChangeStatus is empty', () => {
|
|
2246
|
+
const result = identifyScenariosNeedingRecapture({
|
|
2247
|
+
scenarios: [
|
|
2248
|
+
{
|
|
2249
|
+
name: 'Library - Rich Library',
|
|
2250
|
+
entityName: 'App',
|
|
2251
|
+
updatedInSession: false,
|
|
2252
|
+
},
|
|
2253
|
+
],
|
|
2254
|
+
entityChangeStatus: {},
|
|
2255
|
+
});
|
|
2256
|
+
expect(result).toHaveLength(0);
|
|
2257
|
+
});
|
|
2258
|
+
it('should flag multiple app-level scenarios sharing the same impacted entity', () => {
|
|
2259
|
+
const entityChangeStatus = {
|
|
2260
|
+
LibraryView: { status: 'edited' },
|
|
2261
|
+
App: {
|
|
2262
|
+
status: 'impacted',
|
|
2263
|
+
impactedBy: [
|
|
2264
|
+
{
|
|
2265
|
+
name: 'LibraryView',
|
|
2266
|
+
filePath: 'src/components/LibraryView.tsx',
|
|
2267
|
+
changeType: 'edited',
|
|
2268
|
+
},
|
|
2269
|
+
],
|
|
2270
|
+
},
|
|
2271
|
+
};
|
|
2272
|
+
const result = identifyScenariosNeedingRecapture({
|
|
2273
|
+
scenarios: [
|
|
2274
|
+
{
|
|
2275
|
+
name: 'Library - Empty',
|
|
2276
|
+
entityName: 'App',
|
|
2277
|
+
updatedInSession: false,
|
|
2278
|
+
},
|
|
2279
|
+
{
|
|
2280
|
+
name: 'Library - Rich Library',
|
|
2281
|
+
entityName: 'App',
|
|
2282
|
+
updatedInSession: false,
|
|
2283
|
+
},
|
|
2284
|
+
{
|
|
2285
|
+
name: 'First Article Saved',
|
|
2286
|
+
entityName: 'App',
|
|
2287
|
+
updatedInSession: false,
|
|
2288
|
+
},
|
|
2289
|
+
],
|
|
2290
|
+
entityChangeStatus,
|
|
2291
|
+
});
|
|
2292
|
+
expect(result).toHaveLength(3);
|
|
2293
|
+
expect(result.map((r) => r.scenarioName).sort()).toEqual([
|
|
2294
|
+
'First Article Saved',
|
|
2295
|
+
'Library - Empty',
|
|
2296
|
+
'Library - Rich Library',
|
|
2297
|
+
]);
|
|
2298
|
+
expect(result.every((r) => r.entityName === 'App')).toBe(true);
|
|
2299
|
+
});
|
|
2300
|
+
it('should skip scenarios with null entityName (no entity_sha set)', () => {
|
|
2301
|
+
const entityChangeStatus = {
|
|
2302
|
+
App: { status: 'edited' },
|
|
2303
|
+
};
|
|
2304
|
+
const result = identifyScenariosNeedingRecapture({
|
|
2305
|
+
scenarios: [
|
|
2306
|
+
{
|
|
2307
|
+
name: 'Mystery Scenario',
|
|
2308
|
+
entityName: null, // no entity_sha → no entity name
|
|
2309
|
+
updatedInSession: false,
|
|
2310
|
+
},
|
|
2311
|
+
],
|
|
2312
|
+
entityChangeStatus,
|
|
2313
|
+
});
|
|
2314
|
+
expect(result).toHaveLength(0);
|
|
2315
|
+
});
|
|
2316
|
+
});
|
|
2317
|
+
// ── detectDuplicateNames ──────────────────────────────────────────
|
|
2318
|
+
describe('detectDuplicateNames', () => {
|
|
2319
|
+
it('should return empty map when no duplicates exist', () => {
|
|
2320
|
+
const entries = [
|
|
2321
|
+
{ name: 'Header', filePath: 'app/components/Header.tsx' },
|
|
2322
|
+
{ name: 'Footer', filePath: 'app/components/Footer.tsx' },
|
|
2323
|
+
{ name: 'Sidebar', filePath: 'app/components/Sidebar.tsx' },
|
|
2324
|
+
];
|
|
2325
|
+
const result = detectDuplicateNames(entries);
|
|
2326
|
+
expect(result.size).toBe(0);
|
|
2327
|
+
});
|
|
2328
|
+
it('should group entries that share a name', () => {
|
|
2329
|
+
const entries = [
|
|
2330
|
+
{ name: 'Page', filePath: 'app/isolated-components/Foo/page.tsx' },
|
|
2331
|
+
{ name: 'Page', filePath: 'app/isolated-components/Bar/page.tsx' },
|
|
2332
|
+
{ name: 'Page', filePath: 'app/isolated-components/Baz/page.tsx' },
|
|
2333
|
+
{ name: 'Header', filePath: 'app/components/Header.tsx' },
|
|
2334
|
+
];
|
|
2335
|
+
const result = detectDuplicateNames(entries);
|
|
2336
|
+
expect(result.size).toBe(1);
|
|
2337
|
+
expect(result.has('Page')).toBe(true);
|
|
2338
|
+
const pageGroup = result.get('Page');
|
|
2339
|
+
expect(pageGroup).toHaveLength(3);
|
|
2340
|
+
expect(pageGroup.map((e) => e.filePath)).toEqual([
|
|
2341
|
+
'app/isolated-components/Foo/page.tsx',
|
|
2342
|
+
'app/isolated-components/Bar/page.tsx',
|
|
2343
|
+
'app/isolated-components/Baz/page.tsx',
|
|
2344
|
+
]);
|
|
2345
|
+
});
|
|
2346
|
+
it('should exclude single-occurrence names', () => {
|
|
2347
|
+
const entries = [
|
|
2348
|
+
{ name: 'Page', filePath: 'app/isolated-components/Foo/page.tsx' },
|
|
2349
|
+
{ name: 'Page', filePath: 'app/isolated-components/Bar/page.tsx' },
|
|
2350
|
+
{ name: 'Header', filePath: 'app/components/Header.tsx' },
|
|
2351
|
+
{ name: 'Footer', filePath: 'app/components/Footer.tsx' },
|
|
2352
|
+
];
|
|
2353
|
+
const result = detectDuplicateNames(entries);
|
|
2354
|
+
expect(result.size).toBe(1);
|
|
2355
|
+
expect(result.has('Header')).toBe(false);
|
|
2356
|
+
expect(result.has('Footer')).toBe(false);
|
|
2357
|
+
});
|
|
2358
|
+
it('should handle multiple duplicate groups', () => {
|
|
2359
|
+
const entries = [
|
|
2360
|
+
{ name: 'Page', filePath: 'app/isolated-components/A/page.tsx' },
|
|
2361
|
+
{ name: 'Page', filePath: 'app/isolated-components/B/page.tsx' },
|
|
2362
|
+
{ name: 'Layout', filePath: 'app/isolated-components/A/layout.tsx' },
|
|
2363
|
+
{ name: 'Layout', filePath: 'app/isolated-components/B/layout.tsx' },
|
|
2364
|
+
{ name: 'Unique', filePath: 'app/components/Unique.tsx' },
|
|
2365
|
+
];
|
|
2366
|
+
const result = detectDuplicateNames(entries);
|
|
2367
|
+
expect(result.size).toBe(2);
|
|
2368
|
+
expect(result.has('Page')).toBe(true);
|
|
2369
|
+
expect(result.has('Layout')).toBe(true);
|
|
2370
|
+
expect(result.get('Page')).toHaveLength(2);
|
|
2371
|
+
expect(result.get('Layout')).toHaveLength(2);
|
|
2372
|
+
});
|
|
2373
|
+
it('should return empty map for empty input', () => {
|
|
2374
|
+
const result = detectDuplicateNames([]);
|
|
2375
|
+
expect(result.size).toBe(0);
|
|
2376
|
+
});
|
|
2377
|
+
});
|
|
2378
|
+
});
|
|
2379
|
+
//# sourceMappingURL=editorAudit.test.js.map
|