@codeyam/codeyam-cli 0.1.0-staging.ae0de75 → 0.1.0-staging.b147f46
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 +7 -7
- package/analyzer-template/packages/ai/package.json +1 -1
- package/analyzer-template/packages/ai/src/lib/astScopes/methodSemantics.ts +135 -0
- package/analyzer-template/packages/ai/src/lib/astScopes/nodeToSource.ts +19 -0
- package/analyzer-template/packages/ai/src/lib/astScopes/paths.ts +11 -4
- package/analyzer-template/packages/ai/src/lib/dataStructure/ScopeDataStructure.ts +36 -9
- package/analyzer-template/packages/ai/src/lib/dataStructure/equivalencyManagers/ParentScopeManager.ts +10 -3
- package/analyzer-template/packages/ai/src/lib/dataStructure/helpers/cleanKnownObjectFunctions.ts +16 -6
- package/analyzer-template/packages/ai/src/lib/generateExecutionFlows.ts +0 -33
- package/analyzer-template/packages/analyze/index.ts +4 -1
- package/analyzer-template/packages/analyze/src/lib/ProjectAnalyzer.ts +13 -7
- package/analyzer-template/packages/analyze/src/lib/asts/index.ts +7 -2
- package/analyzer-template/packages/analyze/src/lib/files/analyze/analyzeEntities/prepareDataStructures.ts +28 -2
- package/analyzer-template/packages/analyze/src/lib/files/analyze/analyzeEntities.ts +5 -36
- package/analyzer-template/packages/analyze/src/lib/files/analyze/findOrCreateEntity.ts +10 -6
- package/analyzer-template/packages/analyze/src/lib/files/analyze/gatherEntityMap.ts +9 -12
- package/analyzer-template/packages/analyze/src/lib/files/analyze/trackEntityCircularDependencies.ts +21 -0
- package/analyzer-template/packages/analyze/src/lib/files/analyze/validateDependencyAnalyses.ts +82 -10
- package/analyzer-template/packages/analyze/src/lib/files/analyzeChange.ts +4 -0
- package/analyzer-template/packages/analyze/src/lib/files/analyzeInitial.ts +4 -0
- package/analyzer-template/packages/analyze/src/lib/files/analyzeNextRoute.ts +8 -3
- package/analyzer-template/packages/analyze/src/lib/files/scenarios/generateDataStructure.ts +239 -58
- package/analyzer-template/packages/analyze/src/lib/files/scenarios/generateExecutionFlows.ts +0 -98
- package/analyzer-template/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.ts +1684 -1462
- package/analyzer-template/packages/aws/package.json +2 -2
- package/analyzer-template/packages/database/package.json +2 -2
- package/analyzer-template/packages/database/src/lib/kysely/tables/editorScenariosTable.ts +102 -0
- package/analyzer-template/packages/database/src/lib/loadAnalysis.ts +19 -15
- package/analyzer-template/packages/database/src/lib/loadEntities.ts +0 -6
- package/analyzer-template/packages/database/src/lib/loadEntity.ts +19 -8
- package/analyzer-template/packages/database/src/lib/updateCommitMetadata.ts +0 -65
- package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/editorScenariosTable.d.ts +9 -0
- package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/editorScenariosTable.d.ts.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/editorScenariosTable.js +104 -0
- package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/editorScenariosTable.js.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/loadAnalysis.d.ts.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/loadAnalysis.js +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/loadAnalysis.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/loadEntity.d.ts +4 -1
- package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.d.ts.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.js +5 -5
- package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.js.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/updateCommitMetadata.d.ts.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/updateCommitMetadata.js +0 -25
- package/analyzer-template/packages/github/dist/database/src/lib/updateCommitMetadata.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/types/src/enums/ProjectFramework.ts +2 -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/utils/src/lib/fs/rsyncCopy.d.ts +3 -1
- package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.d.ts.map +1 -1
- package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.js +22 -1
- package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.js.map +1 -1
- package/analyzer-template/packages/utils/src/lib/fs/rsyncCopy.ts +27 -0
- package/analyzer-template/project/analyzeFileEntities.ts +26 -0
- package/background/src/lib/virtualized/project/analyzeFileEntities.js +22 -0
- package/background/src/lib/virtualized/project/analyzeFileEntities.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 +24 -0
- package/codeyam-cli/src/cli.js.map +1 -1
- package/codeyam-cli/src/commands/__tests__/editor.analyzeImportsArgs.test.js +47 -0
- package/codeyam-cli/src/commands/__tests__/editor.analyzeImportsArgs.test.js.map +1 -0
- package/codeyam-cli/src/commands/__tests__/editor.auditNoAutoAnalysis.test.js +71 -0
- package/codeyam-cli/src/commands/__tests__/editor.auditNoAutoAnalysis.test.js.map +1 -0
- package/codeyam-cli/src/commands/__tests__/editor.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/default.js +3 -46
- package/codeyam-cli/src/commands/default.js.map +1 -1
- package/codeyam-cli/src/commands/editor.js +4654 -836
- package/codeyam-cli/src/commands/editor.js.map +1 -1
- package/codeyam-cli/src/commands/editorAnalyzeImportsArgs.js +23 -0
- package/codeyam-cli/src/commands/editorAnalyzeImportsArgs.js.map +1 -0
- package/codeyam-cli/src/commands/editorIsolateArgs.js +25 -0
- package/codeyam-cli/src/commands/editorIsolateArgs.js.map +1 -0
- package/codeyam-cli/src/commands/init.js +69 -34
- package/codeyam-cli/src/commands/init.js.map +1 -1
- package/codeyam-cli/src/commands/telemetry.js +37 -0
- package/codeyam-cli/src/commands/telemetry.js.map +1 -0
- package/codeyam-cli/src/data/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 +3769 -1
- package/codeyam-cli/src/utils/__tests__/editorAudit.test.js.map +1 -1
- 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__/editorCaptureScenarioSeeding.test.js +137 -0
- package/codeyam-cli/src/utils/__tests__/editorCaptureScenarioSeeding.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 +181 -3
- package/codeyam-cli/src/utils/__tests__/editorDevServer.test.js.map +1 -1
- 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 +381 -0
- package/codeyam-cli/src/utils/__tests__/editorEntityHelpers.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorGuardMiddleware.test.js +67 -0
- package/codeyam-cli/src/utils/__tests__/editorGuardMiddleware.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/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 +249 -2
- package/codeyam-cli/src/utils/__tests__/editorJournal.test.js.map +1 -1
- 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__/editorPreloadHelpers.test.js +118 -1
- package/codeyam-cli/src/utils/__tests__/editorPreloadHelpers.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/editorPreview.test.js +217 -3
- package/codeyam-cli/src/utils/__tests__/editorPreview.test.js.map +1 -1
- 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 +291 -0
- package/codeyam-cli/src/utils/__tests__/editorScenarioSwitch.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorScenarios.test.js +1694 -2
- package/codeyam-cli/src/utils/__tests__/editorScenarios.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/editorSeedAdapter.test.js +329 -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 +2121 -0
- package/codeyam-cli/src/utils/__tests__/entityChangeStatus.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/glossaryAdd.test.js +177 -0
- package/codeyam-cli/src/utils/__tests__/glossaryAdd.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/journalCaptureStabilization.test.js +122 -0
- package/codeyam-cli/src/utils/__tests__/journalCaptureStabilization.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/manualEntityAnalysis.test.js +302 -0
- package/codeyam-cli/src/utils/__tests__/manualEntityAnalysis.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/parseRegisterArg.test.js +129 -0
- package/codeyam-cli/src/utils/__tests__/parseRegisterArg.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/registerScenarioResult.test.js +127 -0
- package/codeyam-cli/src/utils/__tests__/registerScenarioResult.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/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__/scenariosManifest.test.js +672 -0
- package/codeyam-cli/src/utils/__tests__/scenariosManifest.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/screenshotHash.test.js +84 -0
- package/codeyam-cli/src/utils/__tests__/screenshotHash.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/setupClaudeCodeSettings.test.js +26 -5
- 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__/testRunner.test.js +217 -0
- package/codeyam-cli/src/utils/__tests__/testRunner.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 +39 -8
- package/codeyam-cli/src/utils/analysisRunner.js.map +1 -1
- package/codeyam-cli/src/utils/analyzer.js +19 -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 +95 -19
- package/codeyam-cli/src/utils/backgroundServer.js.map +1 -1
- package/codeyam-cli/src/utils/database.js +37 -2
- package/codeyam-cli/src/utils/database.js.map +1 -1
- 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 +753 -8
- package/codeyam-cli/src/utils/editorAudit.js.map +1 -1
- 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 +100 -1
- package/codeyam-cli/src/utils/editorDevServer.js.map +1 -1
- 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/editorGuard.js +36 -0
- package/codeyam-cli/src/utils/editorGuard.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 +92 -4
- package/codeyam-cli/src/utils/editorJournal.js.map +1 -1
- 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 +1 -1
- package/codeyam-cli/src/utils/editorPreloadHelpers.js +72 -1
- package/codeyam-cli/src/utils/editorPreloadHelpers.js.map +1 -1
- package/codeyam-cli/src/utils/editorPreview.js +74 -1
- package/codeyam-cli/src/utils/editorPreview.js.map +1 -1
- package/codeyam-cli/src/utils/editorRecapture.js +109 -0
- package/codeyam-cli/src/utils/editorRecapture.js.map +1 -0
- package/codeyam-cli/src/utils/editorScenarioSwitch.js +134 -0
- package/codeyam-cli/src/utils/editorScenarioSwitch.js.map +1 -0
- package/codeyam-cli/src/utils/editorScenarios.js +620 -0
- package/codeyam-cli/src/utils/editorScenarios.js.map +1 -1
- package/codeyam-cli/src/utils/editorSeedAdapter.js +462 -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 +394 -0
- package/codeyam-cli/src/utils/entityChangeStatus.js.map +1 -0
- package/codeyam-cli/src/utils/entityChangeStatus.server.js +212 -0
- package/codeyam-cli/src/utils/entityChangeStatus.server.js.map +1 -0
- package/codeyam-cli/src/utils/fileWatcher.js +38 -0
- package/codeyam-cli/src/utils/fileWatcher.js.map +1 -1
- package/codeyam-cli/src/utils/glossaryAdd.js +74 -0
- package/codeyam-cli/src/utils/glossaryAdd.js.map +1 -0
- package/codeyam-cli/src/utils/install-skills.js +15 -1
- package/codeyam-cli/src/utils/install-skills.js.map +1 -1
- package/codeyam-cli/src/utils/manualEntityAnalysis.js +196 -0
- package/codeyam-cli/src/utils/manualEntityAnalysis.js.map +1 -0
- package/codeyam-cli/src/utils/parseRegisterArg.js +31 -0
- package/codeyam-cli/src/utils/parseRegisterArg.js.map +1 -0
- package/codeyam-cli/src/utils/progress.js +2 -2
- package/codeyam-cli/src/utils/progress.js.map +1 -1
- package/codeyam-cli/src/utils/queue/job.js +26 -5
- package/codeyam-cli/src/utils/queue/job.js.map +1 -1
- package/codeyam-cli/src/utils/registerScenarioResult.js +52 -0
- package/codeyam-cli/src/utils/registerScenarioResult.js.map +1 -0
- package/codeyam-cli/src/utils/routePatternMatching.js +129 -0
- package/codeyam-cli/src/utils/routePatternMatching.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/scenariosManifest.js +307 -0
- package/codeyam-cli/src/utils/scenariosManifest.js.map +1 -0
- package/codeyam-cli/src/utils/screenshotHash.js +26 -0
- package/codeyam-cli/src/utils/screenshotHash.js.map +1 -0
- package/codeyam-cli/src/utils/serverState.js +30 -0
- package/codeyam-cli/src/utils/serverState.js.map +1 -1
- package/codeyam-cli/src/utils/setupClaudeCodeSettings.js +47 -16
- package/codeyam-cli/src/utils/setupClaudeCodeSettings.js.map +1 -1
- package/codeyam-cli/src/utils/simulationGateMiddleware.js +17 -1
- package/codeyam-cli/src/utils/simulationGateMiddleware.js.map +1 -1
- 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 +2 -2
- 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 +199 -1
- package/codeyam-cli/src/utils/testRunner.js.map +1 -1
- package/codeyam-cli/src/utils/webappDetection.js +21 -0
- 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 +107 -0
- package/codeyam-cli/src/webserver/__tests__/clientErrors.test.js.map +1 -0
- package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js +647 -0
- package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js.map +1 -0
- package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js +283 -0
- package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js.map +1 -0
- package/codeyam-cli/src/webserver/__tests__/stripClaudeCommand.test.js +135 -0
- package/codeyam-cli/src/webserver/__tests__/stripClaudeCommand.test.js.map +1 -0
- package/codeyam-cli/src/webserver/app/lib/clientErrors.js +86 -0
- package/codeyam-cli/src/webserver/app/lib/clientErrors.js.map +1 -0
- 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 +60 -61
- 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-C76mRRiF.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-CobE682z.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-BU_OAEMP.js → LoadingDots-By5zI316.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{LogViewer-ceAyBX-H.js → LogViewer-CM5zg40N.js} +3 -3
- package/codeyam-cli/src/webserver/build/client/assets/MiniClaudeChat-CQENLSrF.js +36 -0
- package/codeyam-cli/src/webserver/build/client/assets/{ReportIssueModal-djPLI-WV.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-B76aig_2.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-C96V0n15.js → _index-BAWd-Xjf.js} +4 -4
- package/codeyam-cli/src/webserver/build/client/assets/{activity.(_tab)-BpKzcsJz.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-Duc5hnl7.js → addon-web-links-CHx25PAe.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/addon-webgl-DI8QOUvO.js +58 -0
- package/codeyam-cli/src/webserver/build/client/assets/{agent-transcripts-D9hemwl6.js → agent-transcripts-Bg3e7q4S.js} +7 -7
- 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-project-info-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-recapture-stale-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-rename-scenario-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-save-scenario-data-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-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-prompt-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-schema-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-session-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/{book-open-D_nMCFmP.js → book-open-CL-lMgHh.js} +2 -2
- package/codeyam-cli/src/webserver/build/client/assets/{chevron-down-BH2h1Ea2.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-DyIKORY6.js → circle-check-DFcQkN5j.js} +2 -2
- package/codeyam-cli/src/webserver/build/client/assets/{copy-NDbZjXao.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/cy-logo-cli-Coe5NhbS.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/{cy-logo-cli-CCKUIm0S.svg → cy-logo-cli-DoA97ML3.svg} +2 -2
- package/codeyam-cli/src/webserver/build/client/assets/dev.empty-C8y4mmyv.js +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)-DMv5ESGo.js +96 -0
- package/codeyam-cli/src/webserver/build/client/assets/editorPreview-CluPkvXJ.js +41 -0
- package/codeyam-cli/src/webserver/build/client/assets/{entity._sha._-CrjR3zZW.js → entity._sha._-ByHz6rAQ.js} +14 -13
- package/codeyam-cli/src/webserver/build/client/assets/entity._sha.scenarios._scenarioId.dev-CmLO432x.js +6 -0
- package/codeyam-cli/src/webserver/build/client/assets/entity._sha.scenarios._scenarioId.fullscreen-Bz9sCUF_.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-BMvVHNXU.js → entity._sha_.edit._scenarioId-CAoXLsQr.js} +2 -2
- package/codeyam-cli/src/webserver/build/client/assets/{entry.client-DTvKq3TY.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-oyPmV37k.css +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/{index-BcvgDzbZ.js → index-Bp1l4hSv.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{index-10oVnAAH.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-BAXYRVEO.js → loader-circle-De-7qQ2u.js} +2 -2
- package/codeyam-cli/src/webserver/build/client/assets/manifest-1a45e154.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-DTAcYxBt.js → pause-CFxEKL1u.js} +3 -3
- package/codeyam-cli/src/webserver/build/client/assets/root-D2_tktnk.js +80 -0
- package/codeyam-cli/src/webserver/build/client/assets/{search-fKo7v0Zo.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-BG4heKCG.js → terminal-CrplD4b1.js} +3 -3
- package/codeyam-cli/src/webserver/build/client/assets/{triangle-alert-DtSmdtM4.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-C14nCb1q.js → useLastLogLine-BNd5hYuW.js} +1 -1
- 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/sound-test.html +98 -0
- package/codeyam-cli/src/webserver/build/server/assets/analysisRunner-By5slFjw.js +16 -0
- package/codeyam-cli/src/webserver/build/server/assets/index-DXaOwBnm.js +1 -0
- package/codeyam-cli/src/webserver/build/server/assets/init-CLG1LjQM.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-NZmUqQv6.js +688 -0
- package/codeyam-cli/src/webserver/build/server/index.js +1 -1
- package/codeyam-cli/src/webserver/build-info.json +5 -5
- package/codeyam-cli/src/webserver/editorProxy.js +638 -50
- package/codeyam-cli/src/webserver/editorProxy.js.map +1 -1
- package/codeyam-cli/src/webserver/idleDetector.js +121 -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 +242 -3
- package/codeyam-cli/src/webserver/scripts/journalCapture.ts +147 -4
- package/codeyam-cli/src/webserver/server.js +169 -16
- package/codeyam-cli/src/webserver/server.js.map +1 -1
- package/codeyam-cli/src/webserver/terminalServer.js +321 -49
- package/codeyam-cli/src/webserver/terminalServer.js.map +1 -1
- package/codeyam-cli/templates/__tests__/editor-step-hook.prompt-capture.test.ts +118 -0
- package/codeyam-cli/templates/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 +86 -5
- package/codeyam-cli/templates/codeyam-editor-reference.md +216 -0
- package/codeyam-cli/templates/editor-step-hook.py +262 -41
- 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/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/codeyam-isolate/layout.tsx +12 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/app/lib/prisma.ts +9 -4
- package/codeyam-cli/templates/nextjs-prisma-sqlite/env +4 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/gitignore +21 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/package.json +6 -2
- package/codeyam-cli/templates/nextjs-prisma-sqlite/prisma/seed.ts +4 -1
- package/codeyam-cli/templates/nextjs-prisma-sqlite/seed-adapter.ts +135 -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-sqlite/PRISMA_SETUP.md → nextjs-prisma-supabase/SUPABASE_SETUP.md} +37 -17
- 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/seed-adapters/supabase.ts +282 -0
- package/codeyam-cli/templates/skills/codeyam-dev-mode/SKILL.md +2 -2
- package/codeyam-cli/templates/skills/codeyam-editor/SKILL.md +177 -17
- package/codeyam-cli/templates/skills/codeyam-memory/SKILL.md +10 -10
- 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/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/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/package.json +16 -10
- package/packages/ai/src/lib/astScopes/methodSemantics.js +99 -0
- package/packages/ai/src/lib/astScopes/methodSemantics.js.map +1 -1
- package/packages/ai/src/lib/astScopes/nodeToSource.js +16 -0
- package/packages/ai/src/lib/astScopes/nodeToSource.js.map +1 -1
- package/packages/ai/src/lib/astScopes/paths.js +12 -3
- package/packages/ai/src/lib/astScopes/paths.js.map +1 -1
- package/packages/ai/src/lib/dataStructure/ScopeDataStructure.js +27 -10
- package/packages/ai/src/lib/dataStructure/ScopeDataStructure.js.map +1 -1
- package/packages/ai/src/lib/dataStructure/equivalencyManagers/ParentScopeManager.js +9 -2
- package/packages/ai/src/lib/dataStructure/equivalencyManagers/ParentScopeManager.js.map +1 -1
- package/packages/ai/src/lib/dataStructure/helpers/cleanKnownObjectFunctions.js +14 -4
- package/packages/ai/src/lib/dataStructure/helpers/cleanKnownObjectFunctions.js.map +1 -1
- package/packages/ai/src/lib/generateExecutionFlows.js +0 -11
- package/packages/ai/src/lib/generateExecutionFlows.js.map +1 -1
- package/packages/analyze/index.js +1 -1
- package/packages/analyze/index.js.map +1 -1
- package/packages/analyze/src/lib/ProjectAnalyzer.js +10 -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/files/analyze/analyzeEntities/prepareDataStructures.js +16 -2
- package/packages/analyze/src/lib/files/analyze/analyzeEntities/prepareDataStructures.js.map +1 -1
- package/packages/analyze/src/lib/files/analyze/analyzeEntities.js +6 -26
- package/packages/analyze/src/lib/files/analyze/analyzeEntities.js.map +1 -1
- package/packages/analyze/src/lib/files/analyze/findOrCreateEntity.js +3 -2
- package/packages/analyze/src/lib/files/analyze/findOrCreateEntity.js.map +1 -1
- package/packages/analyze/src/lib/files/analyze/gatherEntityMap.js +9 -7
- package/packages/analyze/src/lib/files/analyze/gatherEntityMap.js.map +1 -1
- package/packages/analyze/src/lib/files/analyze/trackEntityCircularDependencies.js +14 -0
- package/packages/analyze/src/lib/files/analyze/trackEntityCircularDependencies.js.map +1 -1
- package/packages/analyze/src/lib/files/analyze/validateDependencyAnalyses.js +44 -11
- package/packages/analyze/src/lib/files/analyze/validateDependencyAnalyses.js.map +1 -1
- package/packages/analyze/src/lib/files/analyzeChange.js +1 -0
- package/packages/analyze/src/lib/files/analyzeChange.js.map +1 -1
- package/packages/analyze/src/lib/files/analyzeInitial.js +1 -0
- package/packages/analyze/src/lib/files/analyzeInitial.js.map +1 -1
- package/packages/analyze/src/lib/files/analyzeNextRoute.js +5 -1
- package/packages/analyze/src/lib/files/analyzeNextRoute.js.map +1 -1
- package/packages/analyze/src/lib/files/scenarios/generateDataStructure.js +120 -28
- package/packages/analyze/src/lib/files/scenarios/generateDataStructure.js.map +1 -1
- package/packages/analyze/src/lib/files/scenarios/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 +1368 -1193
- package/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.js.map +1 -1
- package/packages/database/src/lib/kysely/tables/editorScenariosTable.js +104 -0
- package/packages/database/src/lib/kysely/tables/editorScenariosTable.js.map +1 -1
- package/packages/database/src/lib/loadAnalysis.js +1 -1
- package/packages/database/src/lib/loadAnalysis.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/loadEntity.js +5 -5
- package/packages/database/src/lib/loadEntity.js.map +1 -1
- package/packages/database/src/lib/updateCommitMetadata.js +0 -25
- package/packages/database/src/lib/updateCommitMetadata.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 +22 -1
- package/packages/utils/src/lib/fs/rsyncCopy.js.map +1 -1
- package/scripts/npm-post-install.cjs +22 -0
- package/codeyam-cli/src/webserver/build/client/assets/CopyButton-DmJveP3T.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/EntityTypeBadge-g3saevPb.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/InlineSpinner-Bu6c6aDe.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/InteractivePreview-DYFW3lDD.js +0 -25
- package/codeyam-cli/src/webserver/build/client/assets/LibraryFunctionPreview-DLeucoVX.js +0 -3
- package/codeyam-cli/src/webserver/build/client/assets/SafeScreenshot-BED4B6sP.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/Spinner-Bb5uFQ5V.js +0 -34
- package/codeyam-cli/src/webserver/build/client/assets/Terminal-wkqC0AQk.js +0 -41
- package/codeyam-cli/src/webserver/build/client/assets/TruncatedFilePath-C8OKAR5x.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/addon-fit-CUXOrorO.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/chunk-JZWAC4HX-C4pqxYJB.js +0 -51
- package/codeyam-cli/src/webserver/build/client/assets/createLucideIcon-CMT1jU2q.js +0 -21
- package/codeyam-cli/src/webserver/build/client/assets/cy-logo-cli-DcX-ZS3p.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/dev.empty-BiM6z3Do.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/editor-CdjF_fX6.js +0 -8
- package/codeyam-cli/src/webserver/build/client/assets/entity._sha.scenarios._scenarioId.dev-D8ILZMR0.js +0 -6
- package/codeyam-cli/src/webserver/build/client/assets/entity._sha.scenarios._scenarioId.fullscreen-C28BiQzt.js +0 -6
- package/codeyam-cli/src/webserver/build/client/assets/entity._sha_.create-scenario-p9hhkjJM.js +0 -6
- package/codeyam-cli/src/webserver/build/client/assets/fileTableUtils-cPo8LiG3.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/files-DO4CZ16O.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/git-CFCTYk9I.js +0 -15
- package/codeyam-cli/src/webserver/build/client/assets/globals-B17TBSS6.css +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/labs-Zk7ryIM1.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/manifest-b8fd6b07.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/memory-FweZHj5U.js +0 -93
- package/codeyam-cli/src/webserver/build/client/assets/root-DUKqhFlb.js +0 -67
- package/codeyam-cli/src/webserver/build/client/assets/settings-DfuTtcJP.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/simulations-B3aOzpCZ.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/useCustomSizes-ByhSyh0W.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/useReportContext-O-jkvSPx.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/useToast-9FIWuYfK.js +0 -1
- package/codeyam-cli/src/webserver/build/server/assets/index-BLhjL9Xi.js +0 -1
- package/codeyam-cli/src/webserver/build/server/assets/server-build-DyMuI5mU.js +0 -363
- package/codeyam-cli/templates/skills/codeyam-memory/scripts/holistic-analysis/detect-deprecated-patterns.sh +0 -108
- package/codeyam-cli/templates/skills/codeyam-memory/scripts/holistic-analysis/find-exports.sh +0 -69
- package/codeyam-cli/templates/skills/codeyam-memory/scripts/session-mining/cleanup.sh +0 -12
- package/codeyam-cli/templates/skills/codeyam-memory/scripts/session-mining/filter.jq +0 -45
- package/codeyam-cli/templates/skills/codeyam-memory/scripts/session-mining/preprocess.sh +0 -139
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import Database from 'better-sqlite3';
|
|
5
|
+
import { Kysely, SqliteDialect } from 'kysely';
|
|
6
|
+
import { deduplicateByName, generateScenarioSlug, convertIsoToSqliteTimestamp, determineCaptureUrl, resolvePreviewNavPath, clearEditorState, clearEditorUserPrompt, readDefaultScreenSize, readScreenSizes, resolveScenarioViewport, resolveViewportWithProjectDefault, upsertEditorScenario, cleanupScenarioFiles, readPreservedConfigProperties, validateStepTransition, slugifyDimension, isRowInFeatureSession, backfillEntityShaOnScenarios, countScenariosNeedingEntityBackfill, validateEntityLinkageForAppScenario, validateScenarioCategorization, resolveSeedFrom, } from "../editorScenarios.js";
|
|
2
7
|
describe('editorScenarios', () => {
|
|
3
8
|
describe('deduplicateByName', () => {
|
|
4
9
|
it('should keep only the last item for each key', () => {
|
|
@@ -15,6 +20,35 @@ describe('editorScenarios', () => {
|
|
|
15
20
|
it('should return empty array for empty input', () => {
|
|
16
21
|
expect(deduplicateByName([], (x) => x.name)).toEqual([]);
|
|
17
22
|
});
|
|
23
|
+
it('should not lose screenshots when re-registered scenario has null screenshot (filter before dedup)', () => {
|
|
24
|
+
// Simulates the journal screenshot lookup flow:
|
|
25
|
+
// Scenarios ordered by created_at ASC from DB, some re-registered with null screenshot
|
|
26
|
+
const scenarios = [
|
|
27
|
+
{
|
|
28
|
+
name: 'Full Catalog',
|
|
29
|
+
screenshot_path: 'screenshots/aaa.png',
|
|
30
|
+
id: 'aaa',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'API Error',
|
|
34
|
+
screenshot_path: 'screenshots/bbb.png',
|
|
35
|
+
id: 'bbb',
|
|
36
|
+
},
|
|
37
|
+
{ name: 'API Error', screenshot_path: null, id: 'ccc' }, // re-registered, capture in progress
|
|
38
|
+
];
|
|
39
|
+
// BUG: dedup-then-filter loses "API Error" entirely because dedup picks
|
|
40
|
+
// the latest row (null screenshot), then filter removes it.
|
|
41
|
+
const buggyDeduped = deduplicateByName(scenarios, (s) => s.name);
|
|
42
|
+
const buggyResult = buggyDeduped.filter((s) => s.screenshot_path);
|
|
43
|
+
// This would only contain "Full Catalog" — "API Error" is gone!
|
|
44
|
+
expect(buggyResult).toHaveLength(1); // demonstrates the bug
|
|
45
|
+
// FIX: filter-then-dedup preserves the latest row that HAS a screenshot.
|
|
46
|
+
const filtered = scenarios.filter((s) => s.screenshot_path);
|
|
47
|
+
const fixedResult = deduplicateByName(filtered, (s) => s.name);
|
|
48
|
+
expect(fixedResult).toHaveLength(2);
|
|
49
|
+
expect(fixedResult.find((s) => s.name === 'API Error')?.id).toBe('bbb');
|
|
50
|
+
expect(fixedResult.find((s) => s.name === 'Full Catalog')?.id).toBe('aaa');
|
|
51
|
+
});
|
|
18
52
|
it('should preserve order of first appearance', () => {
|
|
19
53
|
const items = [
|
|
20
54
|
{ name: 'A', v: 1 },
|
|
@@ -53,7 +87,14 @@ describe('editorScenarios', () => {
|
|
|
53
87
|
});
|
|
54
88
|
describe('determineCaptureUrl', () => {
|
|
55
89
|
it('should combine path with proxy URL when path-based and proxy available', () => {
|
|
56
|
-
expect(determineCaptureUrl('/
|
|
90
|
+
expect(determineCaptureUrl('/isolated-components/DrinkCard?s=Default', 'http://localhost:3112', 'http://localhost:3000')).toBe('http://localhost:3112/isolated-components/DrinkCard?s=Default');
|
|
91
|
+
});
|
|
92
|
+
it('should work with path field normalized to url (path alias)', () => {
|
|
93
|
+
// When Claude writes "path": "/drinks/1" instead of "url": "/drinks/1",
|
|
94
|
+
// the register endpoint normalizes path → url before calling determineCaptureUrl.
|
|
95
|
+
// This test verifies that determineCaptureUrl handles the resulting value correctly.
|
|
96
|
+
const normalizedUrl = '/drinks/1'; // after normalization: body.url = body.path
|
|
97
|
+
expect(determineCaptureUrl(normalizedUrl, 'http://localhost:3112', 'http://localhost:3000')).toBe('http://localhost:3112/drinks/1');
|
|
57
98
|
});
|
|
58
99
|
it('should use full URL directly when not path-based', () => {
|
|
59
100
|
expect(determineCaptureUrl('http://external.com/page', 'http://localhost:3112', 'http://localhost:3000')).toBe('http://external.com/page');
|
|
@@ -72,5 +113,1656 @@ describe('editorScenarios', () => {
|
|
|
72
113
|
expect(determineCaptureUrl('/some-path', null, 'http://localhost:3000')).toBe('http://localhost:3000');
|
|
73
114
|
});
|
|
74
115
|
});
|
|
116
|
+
describe('clearEditorState', () => {
|
|
117
|
+
let tmpDir;
|
|
118
|
+
let codeyamDir;
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'editor-state-'));
|
|
121
|
+
codeyamDir = path.join(tmpDir, '.codeyam');
|
|
122
|
+
fs.mkdirSync(codeyamDir, { recursive: true });
|
|
123
|
+
});
|
|
124
|
+
afterEach(() => {
|
|
125
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
126
|
+
});
|
|
127
|
+
it('should delete the editor-step.json state file', () => {
|
|
128
|
+
fs.writeFileSync(path.join(codeyamDir, 'editor-step.json'), JSON.stringify({ step: 5, label: 'Extract' }));
|
|
129
|
+
clearEditorState(tmpDir);
|
|
130
|
+
expect(fs.existsSync(path.join(codeyamDir, 'editor-step.json'))).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
it('should preserve editor-user-prompt.txt (captured by hook before step 1)', () => {
|
|
133
|
+
// The hook captures the user's feature request BEFORE step 1 calls clearEditorState.
|
|
134
|
+
// If clearEditorState deletes the prompt file, the next unrelated user message
|
|
135
|
+
// (e.g. "ok" approving a database reset) gets captured as the feature prompt.
|
|
136
|
+
fs.writeFileSync(path.join(codeyamDir, 'editor-step.json'), JSON.stringify({ step: 13, label: 'Present' }));
|
|
137
|
+
fs.writeFileSync(path.join(codeyamDir, 'editor-user-prompt.txt'), 'Could we add a page for each individual drink?');
|
|
138
|
+
clearEditorState(tmpDir);
|
|
139
|
+
expect(fs.readFileSync(path.join(codeyamDir, 'editor-user-prompt.txt'), 'utf8')).toBe('Could we add a page for each individual drink?');
|
|
140
|
+
});
|
|
141
|
+
it('should not throw if state file does not exist', () => {
|
|
142
|
+
expect(() => clearEditorState(tmpDir)).not.toThrow();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
describe('clearEditorUserPrompt', () => {
|
|
146
|
+
let tmpDir;
|
|
147
|
+
let codeyamDir;
|
|
148
|
+
beforeEach(() => {
|
|
149
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'editor-prompt-'));
|
|
150
|
+
codeyamDir = path.join(tmpDir, '.codeyam');
|
|
151
|
+
fs.mkdirSync(codeyamDir, { recursive: true });
|
|
152
|
+
});
|
|
153
|
+
afterEach(() => {
|
|
154
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
155
|
+
});
|
|
156
|
+
it('should delete the prompt file', () => {
|
|
157
|
+
fs.writeFileSync(path.join(codeyamDir, 'editor-user-prompt.txt'), 'old feature prompt');
|
|
158
|
+
clearEditorUserPrompt(tmpDir);
|
|
159
|
+
expect(fs.existsSync(path.join(codeyamDir, 'editor-user-prompt.txt'))).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
it('should not throw if file does not exist', () => {
|
|
162
|
+
expect(() => clearEditorUserPrompt(tmpDir)).not.toThrow();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
describe('resolvePreviewNavPath', () => {
|
|
166
|
+
it('should use explicit path when provided', () => {
|
|
167
|
+
expect(resolvePreviewNavPath('/explicit', '/scenario-url')).toBe('/explicit');
|
|
168
|
+
});
|
|
169
|
+
it('should fall back to scenario URL when no explicit path', () => {
|
|
170
|
+
expect(resolvePreviewNavPath(null, '/drinks/1')).toBe('/drinks/1');
|
|
171
|
+
});
|
|
172
|
+
it('should return undefined when neither path nor URL provided', () => {
|
|
173
|
+
expect(resolvePreviewNavPath(null, null)).toBeUndefined();
|
|
174
|
+
});
|
|
175
|
+
it('should return undefined for empty strings', () => {
|
|
176
|
+
expect(resolvePreviewNavPath('', '')).toBeUndefined();
|
|
177
|
+
});
|
|
178
|
+
it('should prefer explicit path over scenario URL', () => {
|
|
179
|
+
expect(resolvePreviewNavPath('/', '/drinks/1')).toBe('/');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
describe('register endpoint path→url normalization (logic verification)', () => {
|
|
183
|
+
// These tests verify the normalization logic used in api.editor-register-scenario.ts:
|
|
184
|
+
// body.url = body.url || body.path || undefined;
|
|
185
|
+
// The route can't be unit-tested directly (ESM import.meta), so we verify
|
|
186
|
+
// the normalization + downstream logic here.
|
|
187
|
+
function normalizeUrl(body) {
|
|
188
|
+
// Mirror the exact line from api.editor-register-scenario.ts line 152
|
|
189
|
+
return body.url || body.path || undefined;
|
|
190
|
+
}
|
|
191
|
+
it('should use url when provided', () => {
|
|
192
|
+
expect(normalizeUrl({ url: '/drinks/1' })).toBe('/drinks/1');
|
|
193
|
+
});
|
|
194
|
+
it('should fall back to path when url not provided', () => {
|
|
195
|
+
expect(normalizeUrl({ path: '/drinks/1' })).toBe('/drinks/1');
|
|
196
|
+
});
|
|
197
|
+
it('should prefer url over path', () => {
|
|
198
|
+
expect(normalizeUrl({ url: '/explicit', path: '/fallback' })).toBe('/explicit');
|
|
199
|
+
});
|
|
200
|
+
it('should return undefined when neither provided', () => {
|
|
201
|
+
expect(normalizeUrl({})).toBeUndefined();
|
|
202
|
+
});
|
|
203
|
+
it('should work end-to-end: path → normalize → determineCaptureUrl', () => {
|
|
204
|
+
// Full pipeline: Claude writes "path": "/drinks/1"
|
|
205
|
+
// → register normalizes to url: "/drinks/1"
|
|
206
|
+
// → determineCaptureUrl combines with proxy
|
|
207
|
+
const body = { path: '/drinks/1' };
|
|
208
|
+
const normalizedUrl = normalizeUrl(body);
|
|
209
|
+
const captureUrl = determineCaptureUrl(normalizedUrl || null, 'http://localhost:4100', 'http://localhost:3000');
|
|
210
|
+
expect(captureUrl).toBe('http://localhost:4100/drinks/1');
|
|
211
|
+
});
|
|
212
|
+
it('should work end-to-end: no url → normalize → proxy root', () => {
|
|
213
|
+
// Claude omits both url and path
|
|
214
|
+
// → normalize returns undefined
|
|
215
|
+
// → determineCaptureUrl falls back to proxy root
|
|
216
|
+
const body = {};
|
|
217
|
+
const normalizedUrl = normalizeUrl(body);
|
|
218
|
+
const captureUrl = determineCaptureUrl(normalizedUrl || null, 'http://localhost:4100', 'http://localhost:3000');
|
|
219
|
+
expect(captureUrl).toBe('http://localhost:4100');
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
describe('readDefaultScreenSize', () => {
|
|
223
|
+
let tmpDir;
|
|
224
|
+
beforeEach(() => {
|
|
225
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'screen-size-test-'));
|
|
226
|
+
fs.mkdirSync(path.join(tmpDir, '.codeyam'), { recursive: true });
|
|
227
|
+
});
|
|
228
|
+
afterEach(() => {
|
|
229
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
230
|
+
});
|
|
231
|
+
it('should return width and height from config', () => {
|
|
232
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
233
|
+
defaultScreenSize: { name: 'Mobile', width: 375, height: 667 },
|
|
234
|
+
}));
|
|
235
|
+
const result = readDefaultScreenSize(tmpDir);
|
|
236
|
+
expect(result).toEqual({ width: 375, height: 667 });
|
|
237
|
+
});
|
|
238
|
+
it('should return null when no defaultScreenSize in config', () => {
|
|
239
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({ projectTitle: 'Test' }));
|
|
240
|
+
const result = readDefaultScreenSize(tmpDir);
|
|
241
|
+
expect(result).toBeNull();
|
|
242
|
+
});
|
|
243
|
+
it('should return null when config file does not exist', () => {
|
|
244
|
+
const result = readDefaultScreenSize(tmpDir);
|
|
245
|
+
expect(result).toBeNull();
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
describe('resolveScenarioViewport', () => {
|
|
249
|
+
it('should use explicit viewport when provided', () => {
|
|
250
|
+
const result = resolveScenarioViewport({
|
|
251
|
+
bodyWidth: 800,
|
|
252
|
+
bodyHeight: 600,
|
|
253
|
+
projectDefault: { width: 375, height: 667 },
|
|
254
|
+
});
|
|
255
|
+
expect(result).toEqual({ width: 800, height: 600 });
|
|
256
|
+
});
|
|
257
|
+
it('should use project default when no explicit viewport', () => {
|
|
258
|
+
const result = resolveScenarioViewport({
|
|
259
|
+
projectDefault: { width: 375, height: 667 },
|
|
260
|
+
});
|
|
261
|
+
expect(result).toEqual({ width: 375, height: 667 });
|
|
262
|
+
});
|
|
263
|
+
it('should fall back to 1280x720 when neither is provided', () => {
|
|
264
|
+
const result = resolveScenarioViewport({});
|
|
265
|
+
expect(result).toEqual({ width: 1280, height: 720 });
|
|
266
|
+
});
|
|
267
|
+
it('should use project default for missing dimensions', () => {
|
|
268
|
+
const result = resolveScenarioViewport({
|
|
269
|
+
bodyWidth: 800,
|
|
270
|
+
projectDefault: { width: 375, height: 667 },
|
|
271
|
+
});
|
|
272
|
+
// bodyWidth provided, bodyHeight not — should use project default height
|
|
273
|
+
expect(result).toEqual({ width: 800, height: 667 });
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
describe('resolveViewportWithProjectDefault', () => {
|
|
277
|
+
let tmpDir;
|
|
278
|
+
beforeEach(() => {
|
|
279
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'viewport-project-default-'));
|
|
280
|
+
fs.mkdirSync(path.join(tmpDir, '.codeyam'), { recursive: true });
|
|
281
|
+
});
|
|
282
|
+
afterEach(() => {
|
|
283
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
284
|
+
});
|
|
285
|
+
it('should use custom default from config.json when no explicit viewport provided', () => {
|
|
286
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
287
|
+
defaultScreenSize: { name: 'iPhone 15', width: 393, height: 852 },
|
|
288
|
+
}));
|
|
289
|
+
const result = resolveViewportWithProjectDefault({
|
|
290
|
+
codeyamRoot: tmpDir,
|
|
291
|
+
});
|
|
292
|
+
expect(result).toEqual({ width: 393, height: 852 });
|
|
293
|
+
});
|
|
294
|
+
it('should prefer explicit viewport over project default', () => {
|
|
295
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
296
|
+
defaultScreenSize: { name: 'iPhone 15', width: 393, height: 852 },
|
|
297
|
+
}));
|
|
298
|
+
const result = resolveViewportWithProjectDefault({
|
|
299
|
+
bodyWidth: 1024,
|
|
300
|
+
bodyHeight: 768,
|
|
301
|
+
codeyamRoot: tmpDir,
|
|
302
|
+
});
|
|
303
|
+
expect(result).toEqual({ width: 1024, height: 768 });
|
|
304
|
+
});
|
|
305
|
+
it('should fall back to 1280x720 when no config and no explicit viewport', () => {
|
|
306
|
+
const result = resolveViewportWithProjectDefault({
|
|
307
|
+
codeyamRoot: tmpDir,
|
|
308
|
+
});
|
|
309
|
+
expect(result).toEqual({ width: 1280, height: 720 });
|
|
310
|
+
});
|
|
311
|
+
it('should fall back to 1280x720 when config has no defaultScreenSize', () => {
|
|
312
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({ projectTitle: 'Test' }));
|
|
313
|
+
const result = resolveViewportWithProjectDefault({
|
|
314
|
+
codeyamRoot: tmpDir,
|
|
315
|
+
});
|
|
316
|
+
expect(result).toEqual({ width: 1280, height: 720 });
|
|
317
|
+
});
|
|
318
|
+
it('should resolve dimension name to screenSizes entry', () => {
|
|
319
|
+
// Claude registers "Home - Mobile" with dimension: "Mobile"
|
|
320
|
+
// → resolveViewportWithProjectDefault looks up "Mobile" in screenSizes
|
|
321
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
322
|
+
screenSizes: {
|
|
323
|
+
Desktop: { width: 1440, height: 900 },
|
|
324
|
+
Mobile: { width: 375, height: 667 },
|
|
325
|
+
},
|
|
326
|
+
}));
|
|
327
|
+
const result = resolveViewportWithProjectDefault({
|
|
328
|
+
dimension: 'Mobile',
|
|
329
|
+
codeyamRoot: tmpDir,
|
|
330
|
+
});
|
|
331
|
+
expect(result).toEqual({ width: 375, height: 667 });
|
|
332
|
+
});
|
|
333
|
+
it('should prefer explicit viewport over dimension name', () => {
|
|
334
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
335
|
+
screenSizes: {
|
|
336
|
+
Mobile: { width: 375, height: 667 },
|
|
337
|
+
},
|
|
338
|
+
}));
|
|
339
|
+
const result = resolveViewportWithProjectDefault({
|
|
340
|
+
bodyWidth: 800,
|
|
341
|
+
bodyHeight: 600,
|
|
342
|
+
dimension: 'Mobile',
|
|
343
|
+
codeyamRoot: tmpDir,
|
|
344
|
+
});
|
|
345
|
+
expect(result).toEqual({ width: 800, height: 600 });
|
|
346
|
+
});
|
|
347
|
+
it('should prefer dimension name over project defaultScreenSize', () => {
|
|
348
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
349
|
+
defaultScreenSize: { name: 'Desktop', width: 1440, height: 900 },
|
|
350
|
+
screenSizes: {
|
|
351
|
+
Tablet: { width: 768, height: 1024 },
|
|
352
|
+
},
|
|
353
|
+
}));
|
|
354
|
+
const result = resolveViewportWithProjectDefault({
|
|
355
|
+
dimension: 'Tablet',
|
|
356
|
+
codeyamRoot: tmpDir,
|
|
357
|
+
});
|
|
358
|
+
expect(result).toEqual({ width: 768, height: 1024 });
|
|
359
|
+
});
|
|
360
|
+
it('should fall back to defaultScreenSize when dimension name not found in screenSizes', () => {
|
|
361
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
362
|
+
defaultScreenSize: { name: 'Desktop', width: 1440, height: 900 },
|
|
363
|
+
screenSizes: {
|
|
364
|
+
Mobile: { width: 375, height: 667 },
|
|
365
|
+
},
|
|
366
|
+
}));
|
|
367
|
+
const result = resolveViewportWithProjectDefault({
|
|
368
|
+
dimension: 'NonExistent',
|
|
369
|
+
codeyamRoot: tmpDir,
|
|
370
|
+
});
|
|
371
|
+
expect(result).toEqual({ width: 1440, height: 900 });
|
|
372
|
+
});
|
|
373
|
+
it('should use inherited viewport from existing scenario instead of chrome-extension project default', () => {
|
|
374
|
+
// BUG: When re-registering a scenario after a code change, the register
|
|
375
|
+
// endpoint creates a new DB record. If Claude doesn't pass viewport info,
|
|
376
|
+
// the resolution falls to defaultScreenSize (chrome-extension size for
|
|
377
|
+
// extension projects). But the original scenario had desktop viewport.
|
|
378
|
+
//
|
|
379
|
+
// FIX: The register endpoint looks up the existing scenario by name and
|
|
380
|
+
// passes its viewport_width/viewport_height as bodyWidth/bodyHeight,
|
|
381
|
+
// so the inherited desktop size takes precedence over the project default.
|
|
382
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
383
|
+
defaultScreenSize: { name: 'Popup', width: 400, height: 600 },
|
|
384
|
+
}));
|
|
385
|
+
// Without inherited values → falls to chrome-extension default
|
|
386
|
+
const withoutInherited = resolveViewportWithProjectDefault({
|
|
387
|
+
codeyamRoot: tmpDir,
|
|
388
|
+
});
|
|
389
|
+
expect(withoutInherited).toEqual({ width: 400, height: 600 });
|
|
390
|
+
// With inherited values from existing scenario → uses desktop size
|
|
391
|
+
const withInherited = resolveViewportWithProjectDefault({
|
|
392
|
+
bodyWidth: 1280,
|
|
393
|
+
bodyHeight: 720,
|
|
394
|
+
codeyamRoot: tmpDir,
|
|
395
|
+
});
|
|
396
|
+
expect(withInherited).toEqual({ width: 1280, height: 720 });
|
|
397
|
+
});
|
|
398
|
+
it('should use inherited dimension from existing scenario instead of project default', () => {
|
|
399
|
+
// Same bug but for dimension-based scenarios: the existing scenario had
|
|
400
|
+
// dimension "Desktop" stored, and the re-registration should inherit it.
|
|
401
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
402
|
+
defaultScreenSize: { name: 'Popup', width: 400, height: 600 },
|
|
403
|
+
screenSizes: {
|
|
404
|
+
Desktop: { width: 1440, height: 900 },
|
|
405
|
+
Popup: { width: 400, height: 600 },
|
|
406
|
+
},
|
|
407
|
+
}));
|
|
408
|
+
// Without inherited dimension → falls to project default (Popup)
|
|
409
|
+
const withoutInherited = resolveViewportWithProjectDefault({
|
|
410
|
+
codeyamRoot: tmpDir,
|
|
411
|
+
});
|
|
412
|
+
expect(withoutInherited).toEqual({ width: 400, height: 600 });
|
|
413
|
+
// With inherited dimension from existing scenario → resolves to Desktop
|
|
414
|
+
const withInherited = resolveViewportWithProjectDefault({
|
|
415
|
+
dimension: 'Desktop',
|
|
416
|
+
codeyamRoot: tmpDir,
|
|
417
|
+
});
|
|
418
|
+
expect(withInherited).toEqual({ width: 1440, height: 900 });
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
describe('readScreenSizes', () => {
|
|
422
|
+
let tmpDir;
|
|
423
|
+
beforeEach(() => {
|
|
424
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'screen-sizes-test-'));
|
|
425
|
+
fs.mkdirSync(path.join(tmpDir, '.codeyam'), { recursive: true });
|
|
426
|
+
});
|
|
427
|
+
afterEach(() => {
|
|
428
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
429
|
+
});
|
|
430
|
+
it('should return screenSizes map from config', () => {
|
|
431
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
432
|
+
screenSizes: {
|
|
433
|
+
Desktop: { width: 1440, height: 900 },
|
|
434
|
+
Mobile: { width: 375, height: 667 },
|
|
435
|
+
Tablet: { width: 768, height: 1024 },
|
|
436
|
+
},
|
|
437
|
+
}));
|
|
438
|
+
const result = readScreenSizes(tmpDir);
|
|
439
|
+
expect(result).toEqual({
|
|
440
|
+
Desktop: { width: 1440, height: 900 },
|
|
441
|
+
Mobile: { width: 375, height: 667 },
|
|
442
|
+
Tablet: { width: 768, height: 1024 },
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
it('should return empty object when no screenSizes in config', () => {
|
|
446
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({ projectTitle: 'Test' }));
|
|
447
|
+
const result = readScreenSizes(tmpDir);
|
|
448
|
+
expect(result).toEqual({});
|
|
449
|
+
});
|
|
450
|
+
it('should return empty object when config file does not exist', () => {
|
|
451
|
+
const result = readScreenSizes(tmpDir);
|
|
452
|
+
expect(result).toEqual({});
|
|
453
|
+
});
|
|
454
|
+
it('should return empty object when screenSizes is not an object', () => {
|
|
455
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({ screenSizes: 'invalid' }));
|
|
456
|
+
const result = readScreenSizes(tmpDir);
|
|
457
|
+
expect(result).toEqual({});
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
describe('upsertEditorScenario', () => {
|
|
461
|
+
let db;
|
|
462
|
+
let rawDb;
|
|
463
|
+
const projectId = 'test-project-id';
|
|
464
|
+
beforeEach(async () => {
|
|
465
|
+
rawDb = new Database(':memory:');
|
|
466
|
+
db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
|
|
467
|
+
// Create the editor_scenarios table matching the real schema
|
|
468
|
+
await db.schema
|
|
469
|
+
.createTable('editor_scenarios')
|
|
470
|
+
.addColumn('id', 'varchar', (col) => col.primaryKey())
|
|
471
|
+
.addColumn('project_id', 'varchar', (col) => col.notNull())
|
|
472
|
+
.addColumn('name', 'varchar', (col) => col.notNull())
|
|
473
|
+
.addColumn('description', 'text')
|
|
474
|
+
.addColumn('component_name', 'varchar')
|
|
475
|
+
.addColumn('component_path', 'varchar')
|
|
476
|
+
.addColumn('url', 'varchar')
|
|
477
|
+
.addColumn('type', 'varchar')
|
|
478
|
+
.addColumn('screenshot_path', 'varchar')
|
|
479
|
+
.addColumn('viewport_width', 'integer')
|
|
480
|
+
.addColumn('viewport_height', 'integer')
|
|
481
|
+
.addColumn('dimension', 'varchar')
|
|
482
|
+
.addColumn('created_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
|
|
483
|
+
.addColumn('updated_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
|
|
484
|
+
.execute();
|
|
485
|
+
});
|
|
486
|
+
afterEach(async () => {
|
|
487
|
+
await db.destroy();
|
|
488
|
+
});
|
|
489
|
+
it('should insert a new scenario when none exists with same name', async () => {
|
|
490
|
+
const result = await upsertEditorScenario(db, {
|
|
491
|
+
projectId,
|
|
492
|
+
name: 'ArticleRow - Default',
|
|
493
|
+
description: null,
|
|
494
|
+
componentName: 'ArticleRow',
|
|
495
|
+
componentPath: 'src/components/ArticleRow.tsx',
|
|
496
|
+
url: '/isolated-components/ArticleRow?s=Default',
|
|
497
|
+
type: null,
|
|
498
|
+
viewportWidth: 1280,
|
|
499
|
+
viewportHeight: 720,
|
|
500
|
+
});
|
|
501
|
+
expect(result.isNew).toBe(true);
|
|
502
|
+
expect(result.cleanedUpIds).toEqual([]);
|
|
503
|
+
// Verify exactly 1 row in DB
|
|
504
|
+
const rows = await db
|
|
505
|
+
.selectFrom('editor_scenarios')
|
|
506
|
+
.selectAll()
|
|
507
|
+
.execute();
|
|
508
|
+
expect(rows).toHaveLength(1);
|
|
509
|
+
expect(rows[0].id).toBe(result.scenarioId);
|
|
510
|
+
expect(rows[0].name).toBe('ArticleRow - Default');
|
|
511
|
+
});
|
|
512
|
+
it('should update existing scenario instead of creating a duplicate', async () => {
|
|
513
|
+
// First registration
|
|
514
|
+
const first = await upsertEditorScenario(db, {
|
|
515
|
+
projectId,
|
|
516
|
+
name: 'ArticleRow - Default',
|
|
517
|
+
description: null,
|
|
518
|
+
componentName: 'ArticleRow',
|
|
519
|
+
componentPath: 'src/components/ArticleRow.tsx',
|
|
520
|
+
url: '/isolated-components/ArticleRow?s=Default',
|
|
521
|
+
type: null,
|
|
522
|
+
viewportWidth: 1280,
|
|
523
|
+
viewportHeight: 720,
|
|
524
|
+
});
|
|
525
|
+
// Second registration with same name
|
|
526
|
+
const second = await upsertEditorScenario(db, {
|
|
527
|
+
projectId,
|
|
528
|
+
name: 'ArticleRow - Default',
|
|
529
|
+
description: 'updated description',
|
|
530
|
+
componentName: 'ArticleRow',
|
|
531
|
+
componentPath: 'src/components/ArticleRow.tsx',
|
|
532
|
+
url: '/isolated-components/ArticleRow?s=Default',
|
|
533
|
+
type: null,
|
|
534
|
+
viewportWidth: 800,
|
|
535
|
+
viewportHeight: 600,
|
|
536
|
+
});
|
|
537
|
+
// Should reuse the same ID
|
|
538
|
+
expect(second.scenarioId).toBe(first.scenarioId);
|
|
539
|
+
expect(second.isNew).toBe(false);
|
|
540
|
+
// Should have exactly 1 row, not 2
|
|
541
|
+
const rows = await db
|
|
542
|
+
.selectFrom('editor_scenarios')
|
|
543
|
+
.selectAll()
|
|
544
|
+
.execute();
|
|
545
|
+
expect(rows).toHaveLength(1);
|
|
546
|
+
expect(rows[0].description).toBe('updated description');
|
|
547
|
+
expect(rows[0].viewport_width).toBe(800);
|
|
548
|
+
});
|
|
549
|
+
it('should clean up duplicate rows from past always-insert behavior', async () => {
|
|
550
|
+
// Simulate old behavior: 3 rows with the same name (accumulated duplicates)
|
|
551
|
+
for (let i = 0; i < 3; i++) {
|
|
552
|
+
await db
|
|
553
|
+
.insertInto('editor_scenarios')
|
|
554
|
+
.values({
|
|
555
|
+
id: `old-id-${i}`,
|
|
556
|
+
project_id: projectId,
|
|
557
|
+
name: 'ArticleRow - Default',
|
|
558
|
+
url: '/isolated-components/ArticleRow?s=Default',
|
|
559
|
+
viewport_width: 1280,
|
|
560
|
+
viewport_height: 720,
|
|
561
|
+
created_at: new Date(Date.now() + i * 1000).toISOString(),
|
|
562
|
+
})
|
|
563
|
+
.execute();
|
|
564
|
+
}
|
|
565
|
+
// Now upsert — should consolidate to 1 row
|
|
566
|
+
const result = await upsertEditorScenario(db, {
|
|
567
|
+
projectId,
|
|
568
|
+
name: 'ArticleRow - Default',
|
|
569
|
+
description: null,
|
|
570
|
+
componentName: 'ArticleRow',
|
|
571
|
+
componentPath: 'src/components/ArticleRow.tsx',
|
|
572
|
+
url: '/isolated-components/ArticleRow?s=Default',
|
|
573
|
+
type: null,
|
|
574
|
+
viewportWidth: 1280,
|
|
575
|
+
viewportHeight: 720,
|
|
576
|
+
});
|
|
577
|
+
expect(result.isNew).toBe(false);
|
|
578
|
+
// Should return the IDs of cleaned-up duplicates
|
|
579
|
+
expect(result.cleanedUpIds).toHaveLength(2);
|
|
580
|
+
// Should have exactly 1 row remaining
|
|
581
|
+
const rows = await db
|
|
582
|
+
.selectFrom('editor_scenarios')
|
|
583
|
+
.selectAll()
|
|
584
|
+
.execute();
|
|
585
|
+
expect(rows).toHaveLength(1);
|
|
586
|
+
expect(rows[0].id).toBe(result.scenarioId);
|
|
587
|
+
});
|
|
588
|
+
it('should not conflict with scenarios of different names', async () => {
|
|
589
|
+
await upsertEditorScenario(db, {
|
|
590
|
+
projectId,
|
|
591
|
+
name: 'ArticleRow - Default',
|
|
592
|
+
description: null,
|
|
593
|
+
componentName: 'ArticleRow',
|
|
594
|
+
componentPath: 'src/components/ArticleRow.tsx',
|
|
595
|
+
url: '/isolated-components/ArticleRow?s=Default',
|
|
596
|
+
type: null,
|
|
597
|
+
viewportWidth: 1280,
|
|
598
|
+
viewportHeight: 720,
|
|
599
|
+
});
|
|
600
|
+
await upsertEditorScenario(db, {
|
|
601
|
+
projectId,
|
|
602
|
+
name: 'ArticleCard - Default',
|
|
603
|
+
description: null,
|
|
604
|
+
componentName: 'ArticleCard',
|
|
605
|
+
componentPath: 'src/components/ArticleCard.tsx',
|
|
606
|
+
url: '/isolated-components/ArticleCard?s=Default',
|
|
607
|
+
type: null,
|
|
608
|
+
viewportWidth: 1280,
|
|
609
|
+
viewportHeight: 720,
|
|
610
|
+
});
|
|
611
|
+
const rows = await db
|
|
612
|
+
.selectFrom('editor_scenarios')
|
|
613
|
+
.selectAll()
|
|
614
|
+
.execute();
|
|
615
|
+
expect(rows).toHaveLength(2);
|
|
616
|
+
});
|
|
617
|
+
it('should scope upsert to the same project — different projects can have same scenario name', async () => {
|
|
618
|
+
const result1 = await upsertEditorScenario(db, {
|
|
619
|
+
projectId: 'project-a',
|
|
620
|
+
name: 'Home - Default',
|
|
621
|
+
description: null,
|
|
622
|
+
componentName: null,
|
|
623
|
+
componentPath: null,
|
|
624
|
+
url: '/',
|
|
625
|
+
type: 'application',
|
|
626
|
+
viewportWidth: 1280,
|
|
627
|
+
viewportHeight: 720,
|
|
628
|
+
});
|
|
629
|
+
const result2 = await upsertEditorScenario(db, {
|
|
630
|
+
projectId: 'project-b',
|
|
631
|
+
name: 'Home - Default',
|
|
632
|
+
description: null,
|
|
633
|
+
componentName: null,
|
|
634
|
+
componentPath: null,
|
|
635
|
+
url: '/',
|
|
636
|
+
type: 'application',
|
|
637
|
+
viewportWidth: 1280,
|
|
638
|
+
viewportHeight: 720,
|
|
639
|
+
});
|
|
640
|
+
expect(result1.scenarioId).not.toBe(result2.scenarioId);
|
|
641
|
+
expect(result1.isNew).toBe(true);
|
|
642
|
+
expect(result2.isNew).toBe(true);
|
|
643
|
+
const rows = await db
|
|
644
|
+
.selectFrom('editor_scenarios')
|
|
645
|
+
.selectAll()
|
|
646
|
+
.execute();
|
|
647
|
+
expect(rows).toHaveLength(2);
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
describe('cleanupScenarioFiles', () => {
|
|
651
|
+
let tmpDir;
|
|
652
|
+
let scenariosDir;
|
|
653
|
+
let screenshotsDir;
|
|
654
|
+
beforeEach(() => {
|
|
655
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'scenario-cleanup-'));
|
|
656
|
+
scenariosDir = path.join(tmpDir, '.codeyam', 'editor-scenarios');
|
|
657
|
+
screenshotsDir = path.join(scenariosDir, 'screenshots');
|
|
658
|
+
fs.mkdirSync(screenshotsDir, { recursive: true });
|
|
659
|
+
});
|
|
660
|
+
afterEach(() => {
|
|
661
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
662
|
+
});
|
|
663
|
+
it('should delete .json, .seed.json, and screenshot files for given IDs', () => {
|
|
664
|
+
// Create files for two scenario IDs
|
|
665
|
+
fs.writeFileSync(path.join(scenariosDir, 'old-id-1.json'), '{"mock":"data"}');
|
|
666
|
+
fs.writeFileSync(path.join(scenariosDir, 'old-id-1.seed.json'), '{"seed":"data"}');
|
|
667
|
+
fs.writeFileSync(path.join(screenshotsDir, 'old-id-1.png'), 'fake-png-data');
|
|
668
|
+
fs.writeFileSync(path.join(scenariosDir, 'old-id-2.json'), '{"mock":"data2"}');
|
|
669
|
+
cleanupScenarioFiles(tmpDir, ['old-id-1', 'old-id-2']);
|
|
670
|
+
expect(fs.existsSync(path.join(scenariosDir, 'old-id-1.json'))).toBe(false);
|
|
671
|
+
expect(fs.existsSync(path.join(scenariosDir, 'old-id-1.seed.json'))).toBe(false);
|
|
672
|
+
expect(fs.existsSync(path.join(screenshotsDir, 'old-id-1.png'))).toBe(false);
|
|
673
|
+
expect(fs.existsSync(path.join(scenariosDir, 'old-id-2.json'))).toBe(false);
|
|
674
|
+
});
|
|
675
|
+
it('should not throw when files do not exist', () => {
|
|
676
|
+
expect(() => cleanupScenarioFiles(tmpDir, ['nonexistent-id'])).not.toThrow();
|
|
677
|
+
});
|
|
678
|
+
it('should not delete files for unrelated scenario IDs', () => {
|
|
679
|
+
fs.writeFileSync(path.join(scenariosDir, 'keep-me.json'), '{"keep":"this"}');
|
|
680
|
+
cleanupScenarioFiles(tmpDir, ['delete-me']);
|
|
681
|
+
expect(fs.existsSync(path.join(scenariosDir, 'keep-me.json'))).toBe(true);
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
describe('readPreservedConfigProperties', () => {
|
|
685
|
+
let tmpDir;
|
|
686
|
+
beforeEach(() => {
|
|
687
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'config-preserve-'));
|
|
688
|
+
fs.mkdirSync(path.join(tmpDir, '.codeyam'), { recursive: true });
|
|
689
|
+
});
|
|
690
|
+
afterEach(() => {
|
|
691
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
692
|
+
});
|
|
693
|
+
it('should preserve defaultScreenSize from existing config', () => {
|
|
694
|
+
const configPath = path.join(tmpDir, '.codeyam', 'config.json');
|
|
695
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
696
|
+
projectSlug: 'my-project',
|
|
697
|
+
defaultScreenSize: { name: 'Popup', width: 400, height: 600 },
|
|
698
|
+
webapps: [],
|
|
699
|
+
}));
|
|
700
|
+
const preserved = readPreservedConfigProperties(configPath);
|
|
701
|
+
expect(preserved.defaultScreenSize).toEqual({
|
|
702
|
+
name: 'Popup',
|
|
703
|
+
width: 400,
|
|
704
|
+
height: 600,
|
|
705
|
+
});
|
|
706
|
+
});
|
|
707
|
+
it('should preserve screenSizes from existing config', () => {
|
|
708
|
+
const configPath = path.join(tmpDir, '.codeyam', 'config.json');
|
|
709
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
710
|
+
projectSlug: 'my-project',
|
|
711
|
+
screenSizes: {
|
|
712
|
+
Desktop: { width: 1440, height: 900 },
|
|
713
|
+
Mobile: { width: 375, height: 667 },
|
|
714
|
+
},
|
|
715
|
+
}));
|
|
716
|
+
const preserved = readPreservedConfigProperties(configPath);
|
|
717
|
+
expect(preserved.screenSizes).toEqual({
|
|
718
|
+
Desktop: { width: 1440, height: 900 },
|
|
719
|
+
Mobile: { width: 375, height: 667 },
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
it('should preserve projectTitle and projectDescription', () => {
|
|
723
|
+
const configPath = path.join(tmpDir, '.codeyam', 'config.json');
|
|
724
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
725
|
+
projectSlug: 'my-project',
|
|
726
|
+
projectTitle: 'My Chrome Extension',
|
|
727
|
+
projectDescription: 'A cool extension',
|
|
728
|
+
}));
|
|
729
|
+
const preserved = readPreservedConfigProperties(configPath);
|
|
730
|
+
expect(preserved.projectTitle).toBe('My Chrome Extension');
|
|
731
|
+
expect(preserved.projectDescription).toBe('A cool extension');
|
|
732
|
+
});
|
|
733
|
+
it('should preserve all user-configured properties at once', () => {
|
|
734
|
+
const configPath = path.join(tmpDir, '.codeyam', 'config.json');
|
|
735
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
736
|
+
projectSlug: 'my-project',
|
|
737
|
+
packageManager: 'npm',
|
|
738
|
+
webapps: [],
|
|
739
|
+
defaultScreenSize: { name: 'Popup', width: 400, height: 600 },
|
|
740
|
+
screenSizes: { Desktop: { width: 1440, height: 900 } },
|
|
741
|
+
projectTitle: 'My Chrome Extension',
|
|
742
|
+
projectDescription: 'A cool extension',
|
|
743
|
+
}));
|
|
744
|
+
const preserved = readPreservedConfigProperties(configPath);
|
|
745
|
+
expect(preserved).toEqual({
|
|
746
|
+
defaultScreenSize: { name: 'Popup', width: 400, height: 600 },
|
|
747
|
+
screenSizes: { Desktop: { width: 1440, height: 900 } },
|
|
748
|
+
projectTitle: 'My Chrome Extension',
|
|
749
|
+
projectDescription: 'A cool extension',
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
it('should NOT preserve auto-detected properties like projectSlug and webapps', () => {
|
|
753
|
+
const configPath = path.join(tmpDir, '.codeyam', 'config.json');
|
|
754
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
755
|
+
projectSlug: 'my-project',
|
|
756
|
+
packageManager: 'npm',
|
|
757
|
+
webapps: [{ path: '.', framework: 'next' }],
|
|
758
|
+
createdAt: '2026-01-01',
|
|
759
|
+
defaultScreenSize: { name: 'Popup', width: 400, height: 600 },
|
|
760
|
+
}));
|
|
761
|
+
const preserved = readPreservedConfigProperties(configPath);
|
|
762
|
+
// Only user-configured properties should survive
|
|
763
|
+
expect(preserved).toEqual({
|
|
764
|
+
defaultScreenSize: { name: 'Popup', width: 400, height: 600 },
|
|
765
|
+
});
|
|
766
|
+
expect(preserved).not.toHaveProperty('projectSlug');
|
|
767
|
+
expect(preserved).not.toHaveProperty('packageManager');
|
|
768
|
+
expect(preserved).not.toHaveProperty('webapps');
|
|
769
|
+
expect(preserved).not.toHaveProperty('createdAt');
|
|
770
|
+
});
|
|
771
|
+
it('should return empty object when config does not exist', () => {
|
|
772
|
+
const configPath = path.join(tmpDir, '.codeyam', 'nonexistent.json');
|
|
773
|
+
const preserved = readPreservedConfigProperties(configPath);
|
|
774
|
+
expect(preserved).toEqual({});
|
|
775
|
+
});
|
|
776
|
+
it('should return empty object when config has no preservable properties', () => {
|
|
777
|
+
const configPath = path.join(tmpDir, '.codeyam', 'config.json');
|
|
778
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
779
|
+
projectSlug: 'my-project',
|
|
780
|
+
webapps: [],
|
|
781
|
+
}));
|
|
782
|
+
const preserved = readPreservedConfigProperties(configPath);
|
|
783
|
+
expect(preserved).toEqual({});
|
|
784
|
+
});
|
|
785
|
+
it('should return empty object when config is invalid JSON', () => {
|
|
786
|
+
const configPath = path.join(tmpDir, '.codeyam', 'config.json');
|
|
787
|
+
fs.writeFileSync(configPath, 'not valid json');
|
|
788
|
+
const preserved = readPreservedConfigProperties(configPath);
|
|
789
|
+
expect(preserved).toEqual({});
|
|
790
|
+
});
|
|
791
|
+
});
|
|
792
|
+
describe('slugifyDimension', () => {
|
|
793
|
+
it('should lowercase and replace spaces with hyphens', () => {
|
|
794
|
+
expect(slugifyDimension('Extension Popup')).toBe('extension-popup');
|
|
795
|
+
});
|
|
796
|
+
it('should strip non-alphanumeric characters', () => {
|
|
797
|
+
expect(slugifyDimension('Desktop (Large)')).toBe('desktop-large');
|
|
798
|
+
});
|
|
799
|
+
it('should collapse multiple hyphens', () => {
|
|
800
|
+
expect(slugifyDimension('My -- Screen')).toBe('my-screen');
|
|
801
|
+
});
|
|
802
|
+
it('should handle simple names', () => {
|
|
803
|
+
expect(slugifyDimension('Desktop')).toBe('desktop');
|
|
804
|
+
expect(slugifyDimension('Mobile')).toBe('mobile');
|
|
805
|
+
});
|
|
806
|
+
it('should handle empty string', () => {
|
|
807
|
+
expect(slugifyDimension('')).toBe('');
|
|
808
|
+
});
|
|
809
|
+
it('should trim leading/trailing hyphens', () => {
|
|
810
|
+
expect(slugifyDimension(' Mobile ')).toBe('mobile');
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
describe('upsertEditorScenario with dimensions', () => {
|
|
814
|
+
let db;
|
|
815
|
+
let rawDb;
|
|
816
|
+
const projectId = 'test-project-id';
|
|
817
|
+
beforeEach(async () => {
|
|
818
|
+
rawDb = new Database(':memory:');
|
|
819
|
+
db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
|
|
820
|
+
await db.schema
|
|
821
|
+
.createTable('editor_scenarios')
|
|
822
|
+
.addColumn('id', 'varchar', (col) => col.primaryKey())
|
|
823
|
+
.addColumn('project_id', 'varchar', (col) => col.notNull())
|
|
824
|
+
.addColumn('name', 'varchar', (col) => col.notNull())
|
|
825
|
+
.addColumn('description', 'text')
|
|
826
|
+
.addColumn('component_name', 'varchar')
|
|
827
|
+
.addColumn('component_path', 'varchar')
|
|
828
|
+
.addColumn('url', 'varchar')
|
|
829
|
+
.addColumn('type', 'varchar')
|
|
830
|
+
.addColumn('screenshot_path', 'varchar')
|
|
831
|
+
.addColumn('viewport_width', 'integer')
|
|
832
|
+
.addColumn('viewport_height', 'integer')
|
|
833
|
+
.addColumn('dimension', 'varchar')
|
|
834
|
+
.addColumn('dimensions', 'text')
|
|
835
|
+
.addColumn('screenshot_paths', 'text')
|
|
836
|
+
.addColumn('created_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
|
|
837
|
+
.addColumn('updated_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
|
|
838
|
+
.execute();
|
|
839
|
+
});
|
|
840
|
+
afterEach(async () => {
|
|
841
|
+
await db.destroy();
|
|
842
|
+
});
|
|
843
|
+
it('should store dimensions as JSON array', async () => {
|
|
844
|
+
const result = await upsertEditorScenario(db, {
|
|
845
|
+
projectId,
|
|
846
|
+
name: 'Home Page',
|
|
847
|
+
description: null,
|
|
848
|
+
componentName: null,
|
|
849
|
+
componentPath: null,
|
|
850
|
+
url: '/',
|
|
851
|
+
type: 'application',
|
|
852
|
+
viewportWidth: 1440,
|
|
853
|
+
viewportHeight: 900,
|
|
854
|
+
dimensions: ['Desktop', 'Mobile'],
|
|
855
|
+
screenshotPaths: null,
|
|
856
|
+
});
|
|
857
|
+
const row = await db
|
|
858
|
+
.selectFrom('editor_scenarios')
|
|
859
|
+
.selectAll()
|
|
860
|
+
.where('id', '=', result.scenarioId)
|
|
861
|
+
.executeTakeFirst();
|
|
862
|
+
expect(row).toBeDefined();
|
|
863
|
+
expect(JSON.parse(row.dimensions)).toEqual([
|
|
864
|
+
'Desktop',
|
|
865
|
+
'Mobile',
|
|
866
|
+
]);
|
|
867
|
+
});
|
|
868
|
+
it('should store screenshot_paths as JSON object', async () => {
|
|
869
|
+
const screenshotPaths = {
|
|
870
|
+
Desktop: 'screenshots/abc--desktop.png',
|
|
871
|
+
Mobile: 'screenshots/abc--mobile.png',
|
|
872
|
+
};
|
|
873
|
+
const result = await upsertEditorScenario(db, {
|
|
874
|
+
projectId,
|
|
875
|
+
name: 'Home Page',
|
|
876
|
+
description: null,
|
|
877
|
+
componentName: null,
|
|
878
|
+
componentPath: null,
|
|
879
|
+
url: '/',
|
|
880
|
+
type: 'application',
|
|
881
|
+
viewportWidth: 1440,
|
|
882
|
+
viewportHeight: 900,
|
|
883
|
+
dimensions: ['Desktop', 'Mobile'],
|
|
884
|
+
screenshotPaths,
|
|
885
|
+
});
|
|
886
|
+
const row = await db
|
|
887
|
+
.selectFrom('editor_scenarios')
|
|
888
|
+
.selectAll()
|
|
889
|
+
.where('id', '=', result.scenarioId)
|
|
890
|
+
.executeTakeFirst();
|
|
891
|
+
expect(JSON.parse(row.screenshot_paths)).toEqual(screenshotPaths);
|
|
892
|
+
});
|
|
893
|
+
it('should update dimensions and screenshot_paths on re-registration', async () => {
|
|
894
|
+
const first = await upsertEditorScenario(db, {
|
|
895
|
+
projectId,
|
|
896
|
+
name: 'Home Page',
|
|
897
|
+
description: null,
|
|
898
|
+
componentName: null,
|
|
899
|
+
componentPath: null,
|
|
900
|
+
url: '/',
|
|
901
|
+
type: 'application',
|
|
902
|
+
viewportWidth: 1440,
|
|
903
|
+
viewportHeight: 900,
|
|
904
|
+
dimensions: ['Desktop'],
|
|
905
|
+
screenshotPaths: { Desktop: 'screenshots/old--desktop.png' },
|
|
906
|
+
});
|
|
907
|
+
const second = await upsertEditorScenario(db, {
|
|
908
|
+
projectId,
|
|
909
|
+
name: 'Home Page',
|
|
910
|
+
description: null,
|
|
911
|
+
componentName: null,
|
|
912
|
+
componentPath: null,
|
|
913
|
+
url: '/',
|
|
914
|
+
type: 'application',
|
|
915
|
+
viewportWidth: 1440,
|
|
916
|
+
viewportHeight: 900,
|
|
917
|
+
dimensions: ['Desktop', 'Mobile'],
|
|
918
|
+
screenshotPaths: {
|
|
919
|
+
Desktop: 'screenshots/new--desktop.png',
|
|
920
|
+
Mobile: 'screenshots/new--mobile.png',
|
|
921
|
+
},
|
|
922
|
+
});
|
|
923
|
+
expect(second.scenarioId).toBe(first.scenarioId);
|
|
924
|
+
const row = await db
|
|
925
|
+
.selectFrom('editor_scenarios')
|
|
926
|
+
.selectAll()
|
|
927
|
+
.where('id', '=', second.scenarioId)
|
|
928
|
+
.executeTakeFirst();
|
|
929
|
+
expect(JSON.parse(row.dimensions)).toEqual([
|
|
930
|
+
'Desktop',
|
|
931
|
+
'Mobile',
|
|
932
|
+
]);
|
|
933
|
+
expect(JSON.parse(row.screenshot_paths)).toEqual({
|
|
934
|
+
Desktop: 'screenshots/new--desktop.png',
|
|
935
|
+
Mobile: 'screenshots/new--mobile.png',
|
|
936
|
+
});
|
|
937
|
+
});
|
|
938
|
+
it('should handle null dimensions gracefully (backward compat)', async () => {
|
|
939
|
+
const result = await upsertEditorScenario(db, {
|
|
940
|
+
projectId,
|
|
941
|
+
name: 'Legacy Scenario',
|
|
942
|
+
description: null,
|
|
943
|
+
componentName: null,
|
|
944
|
+
componentPath: null,
|
|
945
|
+
url: '/',
|
|
946
|
+
type: null,
|
|
947
|
+
viewportWidth: 1280,
|
|
948
|
+
viewportHeight: 720,
|
|
949
|
+
});
|
|
950
|
+
const row = await db
|
|
951
|
+
.selectFrom('editor_scenarios')
|
|
952
|
+
.selectAll()
|
|
953
|
+
.where('id', '=', result.scenarioId)
|
|
954
|
+
.executeTakeFirst();
|
|
955
|
+
expect(row.dimensions).toBeNull();
|
|
956
|
+
expect(row.screenshot_paths).toBeNull();
|
|
957
|
+
});
|
|
958
|
+
});
|
|
959
|
+
describe('validateStepTransition', () => {
|
|
960
|
+
it('should allow step 1 with no current state', () => {
|
|
961
|
+
expect(validateStepTransition(1, null)).toBeNull();
|
|
962
|
+
});
|
|
963
|
+
it('should allow step 1 even when on a later step (restart)', () => {
|
|
964
|
+
expect(validateStepTransition(1, 5)).toBeNull();
|
|
965
|
+
});
|
|
966
|
+
it('should allow advancing to the next step', () => {
|
|
967
|
+
expect(validateStepTransition(2, 1)).toBeNull();
|
|
968
|
+
expect(validateStepTransition(3, 2)).toBeNull();
|
|
969
|
+
expect(validateStepTransition(13, 12)).toBeNull();
|
|
970
|
+
});
|
|
971
|
+
it('should allow re-running the current step', () => {
|
|
972
|
+
expect(validateStepTransition(3, 3)).toBeNull();
|
|
973
|
+
expect(validateStepTransition(7, 7)).toBeNull();
|
|
974
|
+
});
|
|
975
|
+
it('should allow going back to a previous step', () => {
|
|
976
|
+
expect(validateStepTransition(3, 5)).toBeNull();
|
|
977
|
+
});
|
|
978
|
+
it('should reject skipping steps forward', () => {
|
|
979
|
+
const err = validateStepTransition(5, 2);
|
|
980
|
+
expect(err).not.toBeNull();
|
|
981
|
+
expect(err).toContain('step 2');
|
|
982
|
+
expect(err).toContain('step 5');
|
|
983
|
+
});
|
|
984
|
+
it('should reject jumping to step 13 from step 3', () => {
|
|
985
|
+
const err = validateStepTransition(13, 3);
|
|
986
|
+
expect(err).not.toBeNull();
|
|
987
|
+
expect(err).toContain('step 4');
|
|
988
|
+
});
|
|
989
|
+
it('should reject step 2 with no current state', () => {
|
|
990
|
+
const err = validateStepTransition(2, null);
|
|
991
|
+
expect(err).not.toBeNull();
|
|
992
|
+
expect(err).toContain('step 1');
|
|
993
|
+
});
|
|
994
|
+
it('should reject step 13 with no current state', () => {
|
|
995
|
+
const err = validateStepTransition(13, null);
|
|
996
|
+
expect(err).not.toBeNull();
|
|
997
|
+
});
|
|
998
|
+
it('should allow advancing from step 13 to step 14', () => {
|
|
999
|
+
expect(validateStepTransition(14, 13)).toBeNull();
|
|
1000
|
+
});
|
|
1001
|
+
it('should allow advancing from step 14 to step 15', () => {
|
|
1002
|
+
expect(validateStepTransition(15, 14)).toBeNull();
|
|
1003
|
+
});
|
|
1004
|
+
it('should allow advancing from step 15 to step 16', () => {
|
|
1005
|
+
expect(validateStepTransition(16, 15)).toBeNull();
|
|
1006
|
+
});
|
|
1007
|
+
it('should reject skipping from step 13 to step 15', () => {
|
|
1008
|
+
const err = validateStepTransition(15, 13);
|
|
1009
|
+
expect(err).not.toBeNull();
|
|
1010
|
+
expect(err).toContain('step 14');
|
|
1011
|
+
});
|
|
1012
|
+
it('should reject rapid-fire advance when step started < 3s ago', () => {
|
|
1013
|
+
const recentStart = new Date(Date.now() - 1000).toISOString(); // 1s ago
|
|
1014
|
+
const err = validateStepTransition(4, 3, recentStart);
|
|
1015
|
+
expect(err).not.toBeNull();
|
|
1016
|
+
expect(err).toContain('only 1s ago');
|
|
1017
|
+
expect(err).toContain('checklist');
|
|
1018
|
+
});
|
|
1019
|
+
it('should allow advance when step started > 3s ago', () => {
|
|
1020
|
+
const oldStart = new Date(Date.now() - 5000).toISOString(); // 5s ago
|
|
1021
|
+
expect(validateStepTransition(4, 3, oldStart)).toBeNull();
|
|
1022
|
+
});
|
|
1023
|
+
it('should not enforce time guard on step 1 → 2 transition', () => {
|
|
1024
|
+
const recentStart = new Date(Date.now() - 2000).toISOString();
|
|
1025
|
+
// currentStep=1, so guard only applies for currentStep >= 2
|
|
1026
|
+
expect(validateStepTransition(2, 1, recentStart)).toBeNull();
|
|
1027
|
+
});
|
|
1028
|
+
it('should not enforce time guard when re-running the same step', () => {
|
|
1029
|
+
const recentStart = new Date(Date.now() - 2000).toISOString();
|
|
1030
|
+
expect(validateStepTransition(5, 5, recentStart)).toBeNull();
|
|
1031
|
+
});
|
|
1032
|
+
});
|
|
1033
|
+
describe('isRowInFeatureSession', () => {
|
|
1034
|
+
const featureStart = '2026-03-12 14:01:31';
|
|
1035
|
+
it('should include row created after feature start', () => {
|
|
1036
|
+
expect(isRowInFeatureSession({ created_at: '2026-03-12 14:30:00' }, featureStart)).toBe(true);
|
|
1037
|
+
});
|
|
1038
|
+
it('should exclude row created before feature start with no updated_at', () => {
|
|
1039
|
+
expect(isRowInFeatureSession({ created_at: '2026-03-12 13:00:00' }, featureStart)).toBe(false);
|
|
1040
|
+
});
|
|
1041
|
+
it('should include re-registered row with old created_at but recent updated_at', () => {
|
|
1042
|
+
// This is the bug: application-level scenarios from a previous feature
|
|
1043
|
+
// are re-registered, which only updates updated_at. The session filter
|
|
1044
|
+
// must check updated_at too, otherwise these scenarios vanish from results.
|
|
1045
|
+
expect(isRowInFeatureSession({
|
|
1046
|
+
created_at: '2026-03-12 13:28:00',
|
|
1047
|
+
updated_at: '2026-03-12 14:32:00',
|
|
1048
|
+
}, featureStart)).toBe(true);
|
|
1049
|
+
});
|
|
1050
|
+
it('should exclude row with both timestamps before feature start', () => {
|
|
1051
|
+
expect(isRowInFeatureSession({
|
|
1052
|
+
created_at: '2026-03-12 13:00:00',
|
|
1053
|
+
updated_at: '2026-03-12 13:30:00',
|
|
1054
|
+
}, featureStart)).toBe(false);
|
|
1055
|
+
});
|
|
1056
|
+
it('should handle null created_at with recent updated_at', () => {
|
|
1057
|
+
expect(isRowInFeatureSession({ created_at: null, updated_at: '2026-03-12 14:32:00' }, featureStart)).toBe(true);
|
|
1058
|
+
});
|
|
1059
|
+
it('should handle null updated_at with recent created_at', () => {
|
|
1060
|
+
expect(isRowInFeatureSession({ created_at: '2026-03-12 14:30:00', updated_at: null }, featureStart)).toBe(true);
|
|
1061
|
+
});
|
|
1062
|
+
});
|
|
1063
|
+
describe('upsertEditorScenario with entitySha and displayName', () => {
|
|
1064
|
+
let db;
|
|
1065
|
+
let rawDb;
|
|
1066
|
+
const projectId = 'test-project-id';
|
|
1067
|
+
beforeEach(async () => {
|
|
1068
|
+
rawDb = new Database(':memory:');
|
|
1069
|
+
db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
|
|
1070
|
+
await db.schema
|
|
1071
|
+
.createTable('editor_scenarios')
|
|
1072
|
+
.addColumn('id', 'varchar', (col) => col.primaryKey())
|
|
1073
|
+
.addColumn('project_id', 'varchar', (col) => col.notNull())
|
|
1074
|
+
.addColumn('name', 'varchar', (col) => col.notNull())
|
|
1075
|
+
.addColumn('description', 'text')
|
|
1076
|
+
.addColumn('component_name', 'varchar')
|
|
1077
|
+
.addColumn('component_path', 'varchar')
|
|
1078
|
+
.addColumn('url', 'varchar')
|
|
1079
|
+
.addColumn('type', 'varchar')
|
|
1080
|
+
.addColumn('screenshot_path', 'varchar')
|
|
1081
|
+
.addColumn('viewport_width', 'integer')
|
|
1082
|
+
.addColumn('viewport_height', 'integer')
|
|
1083
|
+
.addColumn('dimension', 'varchar')
|
|
1084
|
+
.addColumn('dimensions', 'text')
|
|
1085
|
+
.addColumn('screenshot_paths', 'text')
|
|
1086
|
+
.addColumn('page_file_path', 'varchar')
|
|
1087
|
+
.addColumn('entity_sha', 'varchar')
|
|
1088
|
+
.addColumn('display_name', 'varchar')
|
|
1089
|
+
.addColumn('created_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
|
|
1090
|
+
.addColumn('updated_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
|
|
1091
|
+
.execute();
|
|
1092
|
+
});
|
|
1093
|
+
afterEach(async () => {
|
|
1094
|
+
await db.destroy();
|
|
1095
|
+
});
|
|
1096
|
+
it('should store entitySha and displayName on insert', async () => {
|
|
1097
|
+
const result = await upsertEditorScenario(db, {
|
|
1098
|
+
projectId,
|
|
1099
|
+
name: 'Home - Default',
|
|
1100
|
+
description: null,
|
|
1101
|
+
componentName: null,
|
|
1102
|
+
componentPath: null,
|
|
1103
|
+
url: '/',
|
|
1104
|
+
type: 'application',
|
|
1105
|
+
viewportWidth: 400,
|
|
1106
|
+
viewportHeight: 600,
|
|
1107
|
+
entitySha: 'abc123',
|
|
1108
|
+
displayName: 'Home',
|
|
1109
|
+
});
|
|
1110
|
+
expect(result.isNew).toBe(true);
|
|
1111
|
+
const row = rawDb
|
|
1112
|
+
.prepare('SELECT entity_sha, display_name FROM editor_scenarios WHERE id = ?')
|
|
1113
|
+
.get(result.scenarioId);
|
|
1114
|
+
expect(row.entity_sha).toBe('abc123');
|
|
1115
|
+
expect(row.display_name).toBe('Home');
|
|
1116
|
+
});
|
|
1117
|
+
it('should update entitySha and displayName on upsert', async () => {
|
|
1118
|
+
// Insert first
|
|
1119
|
+
await upsertEditorScenario(db, {
|
|
1120
|
+
projectId,
|
|
1121
|
+
name: 'Home - Default',
|
|
1122
|
+
description: null,
|
|
1123
|
+
componentName: null,
|
|
1124
|
+
componentPath: null,
|
|
1125
|
+
url: '/',
|
|
1126
|
+
type: 'application',
|
|
1127
|
+
viewportWidth: 400,
|
|
1128
|
+
viewportHeight: 600,
|
|
1129
|
+
entitySha: 'old-sha',
|
|
1130
|
+
displayName: 'OldName',
|
|
1131
|
+
});
|
|
1132
|
+
// Upsert with new values
|
|
1133
|
+
const result = await upsertEditorScenario(db, {
|
|
1134
|
+
projectId,
|
|
1135
|
+
name: 'Home - Default',
|
|
1136
|
+
description: null,
|
|
1137
|
+
componentName: null,
|
|
1138
|
+
componentPath: null,
|
|
1139
|
+
url: '/',
|
|
1140
|
+
type: 'application',
|
|
1141
|
+
viewportWidth: 400,
|
|
1142
|
+
viewportHeight: 600,
|
|
1143
|
+
entitySha: 'new-sha',
|
|
1144
|
+
displayName: 'Home',
|
|
1145
|
+
});
|
|
1146
|
+
expect(result.isNew).toBe(false);
|
|
1147
|
+
const row = rawDb
|
|
1148
|
+
.prepare('SELECT entity_sha, display_name FROM editor_scenarios WHERE id = ?')
|
|
1149
|
+
.get(result.scenarioId);
|
|
1150
|
+
expect(row.entity_sha).toBe('new-sha');
|
|
1151
|
+
expect(row.display_name).toBe('Home');
|
|
1152
|
+
});
|
|
1153
|
+
it('should not overwrite entitySha when not provided', async () => {
|
|
1154
|
+
// Insert with SHA
|
|
1155
|
+
const first = await upsertEditorScenario(db, {
|
|
1156
|
+
projectId,
|
|
1157
|
+
name: 'Home - Default',
|
|
1158
|
+
description: null,
|
|
1159
|
+
componentName: null,
|
|
1160
|
+
componentPath: null,
|
|
1161
|
+
url: '/',
|
|
1162
|
+
type: 'application',
|
|
1163
|
+
viewportWidth: 400,
|
|
1164
|
+
viewportHeight: 600,
|
|
1165
|
+
entitySha: 'keep-this',
|
|
1166
|
+
displayName: 'Home',
|
|
1167
|
+
});
|
|
1168
|
+
// Upsert without entitySha/displayName
|
|
1169
|
+
await upsertEditorScenario(db, {
|
|
1170
|
+
projectId,
|
|
1171
|
+
name: 'Home - Default',
|
|
1172
|
+
description: 'updated',
|
|
1173
|
+
componentName: null,
|
|
1174
|
+
componentPath: null,
|
|
1175
|
+
url: '/',
|
|
1176
|
+
type: 'application',
|
|
1177
|
+
viewportWidth: 400,
|
|
1178
|
+
viewportHeight: 600,
|
|
1179
|
+
});
|
|
1180
|
+
const row = rawDb
|
|
1181
|
+
.prepare('SELECT entity_sha, display_name FROM editor_scenarios WHERE id = ?')
|
|
1182
|
+
.get(first.scenarioId);
|
|
1183
|
+
expect(row.entity_sha).toBe('keep-this');
|
|
1184
|
+
expect(row.display_name).toBe('Home');
|
|
1185
|
+
});
|
|
1186
|
+
});
|
|
1187
|
+
describe('backfillEntityShaOnScenarios', () => {
|
|
1188
|
+
let db;
|
|
1189
|
+
let rawDb;
|
|
1190
|
+
const projectId = 'test-project-id';
|
|
1191
|
+
beforeEach(async () => {
|
|
1192
|
+
rawDb = new Database(':memory:');
|
|
1193
|
+
db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
|
|
1194
|
+
await db.schema
|
|
1195
|
+
.createTable('editor_scenarios')
|
|
1196
|
+
.addColumn('id', 'varchar', (col) => col.primaryKey())
|
|
1197
|
+
.addColumn('project_id', 'varchar', (col) => col.notNull())
|
|
1198
|
+
.addColumn('name', 'varchar', (col) => col.notNull())
|
|
1199
|
+
.addColumn('description', 'text')
|
|
1200
|
+
.addColumn('component_name', 'varchar')
|
|
1201
|
+
.addColumn('component_path', 'varchar')
|
|
1202
|
+
.addColumn('url', 'varchar')
|
|
1203
|
+
.addColumn('type', 'varchar')
|
|
1204
|
+
.addColumn('screenshot_path', 'varchar')
|
|
1205
|
+
.addColumn('viewport_width', 'integer')
|
|
1206
|
+
.addColumn('viewport_height', 'integer')
|
|
1207
|
+
.addColumn('dimension', 'varchar')
|
|
1208
|
+
.addColumn('dimensions', 'text')
|
|
1209
|
+
.addColumn('screenshot_paths', 'text')
|
|
1210
|
+
.addColumn('page_file_path', 'varchar')
|
|
1211
|
+
.addColumn('entity_sha', 'varchar')
|
|
1212
|
+
.addColumn('display_name', 'varchar')
|
|
1213
|
+
.addColumn('created_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
|
|
1214
|
+
.addColumn('updated_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
|
|
1215
|
+
.execute();
|
|
1216
|
+
});
|
|
1217
|
+
afterEach(async () => {
|
|
1218
|
+
await db.destroy();
|
|
1219
|
+
});
|
|
1220
|
+
it('should backfill component scenario by component_path match', async () => {
|
|
1221
|
+
// Insert a scenario with null entity_sha but with component_path
|
|
1222
|
+
rawDb
|
|
1223
|
+
.prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height)
|
|
1224
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1225
|
+
.run('sc-1', projectId, 'Header - Default', 'Header', 'src/components/Header.tsx', '/isolated-components/Header', 'component', 1280, 720);
|
|
1226
|
+
const result = await backfillEntityShaOnScenarios(db, [
|
|
1227
|
+
{
|
|
1228
|
+
sha: 'entity-sha-1',
|
|
1229
|
+
name: 'Header',
|
|
1230
|
+
filePath: 'src/components/Header.tsx',
|
|
1231
|
+
},
|
|
1232
|
+
]);
|
|
1233
|
+
expect(result.updated).toBe(1);
|
|
1234
|
+
const row = rawDb
|
|
1235
|
+
.prepare('SELECT entity_sha, display_name FROM editor_scenarios WHERE id = ?')
|
|
1236
|
+
.get('sc-1');
|
|
1237
|
+
expect(row.entity_sha).toBe('entity-sha-1');
|
|
1238
|
+
expect(row.display_name).toBe('Header');
|
|
1239
|
+
});
|
|
1240
|
+
it('should backfill page scenario by page_file_path match and derive display_name from route', async () => {
|
|
1241
|
+
// Insert a scenario with null entity_sha but with page_file_path
|
|
1242
|
+
rawDb
|
|
1243
|
+
.prepare(`INSERT INTO editor_scenarios (id, project_id, name, page_file_path, url, type, viewport_width, viewport_height)
|
|
1244
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1245
|
+
.run('sc-2', projectId, 'Feedback - Default', 'app/feedback/page.tsx', '/feedback', 'application', 1280, 720);
|
|
1246
|
+
const result = await backfillEntityShaOnScenarios(db, [
|
|
1247
|
+
{
|
|
1248
|
+
sha: 'entity-sha-2',
|
|
1249
|
+
name: 'Feedback',
|
|
1250
|
+
filePath: 'app/feedback/page.tsx',
|
|
1251
|
+
},
|
|
1252
|
+
]);
|
|
1253
|
+
expect(result.updated).toBe(1);
|
|
1254
|
+
const row = rawDb
|
|
1255
|
+
.prepare('SELECT entity_sha, display_name FROM editor_scenarios WHERE id = ?')
|
|
1256
|
+
.get('sc-2');
|
|
1257
|
+
expect(row.entity_sha).toBe('entity-sha-2');
|
|
1258
|
+
// display_name derived from page_file_path via routeDisplayName(buildRoutePattern())
|
|
1259
|
+
expect(row.display_name).toBe('Feedback');
|
|
1260
|
+
});
|
|
1261
|
+
it('should update scenarios with stale entity_sha to latest', async () => {
|
|
1262
|
+
// Margo bug: entity code changes create new versions with new SHAs,
|
|
1263
|
+
// but scenarios still point to the old SHA. Since journals preserve
|
|
1264
|
+
// screenshots and everything is in git, we always want scenarios
|
|
1265
|
+
// pointing to the latest entity version.
|
|
1266
|
+
rawDb
|
|
1267
|
+
.prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height, entity_sha, display_name)
|
|
1268
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1269
|
+
.run('sc-3', projectId, 'Header - Dark', 'Header', 'src/components/Header.tsx', '/isolated-components/Header', 'component', 1280, 720, 'old-sha', 'Header');
|
|
1270
|
+
const result = await backfillEntityShaOnScenarios(db, [
|
|
1271
|
+
{
|
|
1272
|
+
sha: 'latest-sha',
|
|
1273
|
+
name: 'Header',
|
|
1274
|
+
filePath: 'src/components/Header.tsx',
|
|
1275
|
+
},
|
|
1276
|
+
]);
|
|
1277
|
+
expect(result.updated).toBe(1);
|
|
1278
|
+
const row = rawDb
|
|
1279
|
+
.prepare('SELECT entity_sha FROM editor_scenarios WHERE id = ?')
|
|
1280
|
+
.get('sc-3');
|
|
1281
|
+
expect(row.entity_sha).toBe('latest-sha');
|
|
1282
|
+
});
|
|
1283
|
+
it('should not update scenarios already pointing to the latest SHA', async () => {
|
|
1284
|
+
rawDb
|
|
1285
|
+
.prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height, entity_sha, display_name)
|
|
1286
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1287
|
+
.run('sc-3b', projectId, 'Header - Dark', 'Header', 'src/components/Header.tsx', '/isolated-components/Header', 'component', 1280, 720, 'current-sha', 'Header');
|
|
1288
|
+
const result = await backfillEntityShaOnScenarios(db, [
|
|
1289
|
+
{
|
|
1290
|
+
sha: 'current-sha',
|
|
1291
|
+
name: 'Header',
|
|
1292
|
+
filePath: 'src/components/Header.tsx',
|
|
1293
|
+
},
|
|
1294
|
+
]);
|
|
1295
|
+
expect(result.updated).toBe(0);
|
|
1296
|
+
});
|
|
1297
|
+
it('should skip scenarios with no matching entity', async () => {
|
|
1298
|
+
rawDb
|
|
1299
|
+
.prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height)
|
|
1300
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1301
|
+
.run('sc-4', projectId, 'Footer - Default', 'Footer', 'src/components/Footer.tsx', '/isolated-components/Footer', 'component', 1280, 720);
|
|
1302
|
+
// Entities list doesn't include Footer.tsx
|
|
1303
|
+
const result = await backfillEntityShaOnScenarios(db, [
|
|
1304
|
+
{
|
|
1305
|
+
sha: 'entity-sha-1',
|
|
1306
|
+
name: 'Header',
|
|
1307
|
+
filePath: 'src/components/Header.tsx',
|
|
1308
|
+
},
|
|
1309
|
+
]);
|
|
1310
|
+
expect(result.updated).toBe(0);
|
|
1311
|
+
const row = rawDb
|
|
1312
|
+
.prepare('SELECT entity_sha FROM editor_scenarios WHERE id = ?')
|
|
1313
|
+
.get('sc-4');
|
|
1314
|
+
expect(row.entity_sha).toBeNull();
|
|
1315
|
+
});
|
|
1316
|
+
it('should match component scenarios by name+path, not just path (multi-entity files)', async () => {
|
|
1317
|
+
// A single file can export multiple components (e.g., FullPageLibrary.tsx
|
|
1318
|
+
// exports FullPageLibrary, FullPageEmptyState, FullPageArticleCard).
|
|
1319
|
+
// Each component scenario should get the SHA for its specific entity,
|
|
1320
|
+
// not whichever entity happens to be last in the map.
|
|
1321
|
+
rawDb
|
|
1322
|
+
.prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height, entity_sha)
|
|
1323
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1324
|
+
.run('sc-fplib', projectId, 'FullPageLibrary - Default', 'FullPageLibrary', 'src/library/FullPageLibrary.tsx', '/isolated-components/FullPageLibrary', 'component', 1280, 720, 'old-fplib-sha');
|
|
1325
|
+
rawDb
|
|
1326
|
+
.prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height, entity_sha)
|
|
1327
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1328
|
+
.run('sc-fpempty', projectId, 'FullPageEmptyState - Default', 'FullPageEmptyState', 'src/library/FullPageLibrary.tsx', '/isolated-components/FullPageEmptyState', 'component', 1280, 720, 'old-fpempty-sha');
|
|
1329
|
+
const result = await backfillEntityShaOnScenarios(db, [
|
|
1330
|
+
{
|
|
1331
|
+
sha: 'new-fplib-sha',
|
|
1332
|
+
name: 'FullPageLibrary',
|
|
1333
|
+
filePath: 'src/library/FullPageLibrary.tsx',
|
|
1334
|
+
},
|
|
1335
|
+
{
|
|
1336
|
+
sha: 'new-fpempty-sha',
|
|
1337
|
+
name: 'FullPageEmptyState',
|
|
1338
|
+
filePath: 'src/library/FullPageLibrary.tsx',
|
|
1339
|
+
},
|
|
1340
|
+
]);
|
|
1341
|
+
expect(result.updated).toBe(2);
|
|
1342
|
+
const fplib = rawDb
|
|
1343
|
+
.prepare('SELECT entity_sha FROM editor_scenarios WHERE id = ?')
|
|
1344
|
+
.get('sc-fplib');
|
|
1345
|
+
expect(fplib.entity_sha).toBe('new-fplib-sha');
|
|
1346
|
+
const fpempty = rawDb
|
|
1347
|
+
.prepare('SELECT entity_sha FROM editor_scenarios WHERE id = ?')
|
|
1348
|
+
.get('sc-fpempty');
|
|
1349
|
+
expect(fpempty.entity_sha).toBe('new-fpempty-sha');
|
|
1350
|
+
});
|
|
1351
|
+
it('should prefer default export entity for page scenarios when file has multiple entities', async () => {
|
|
1352
|
+
// Bug: src/popup/App.tsx has multiple entities (App visual, getInitialView library,
|
|
1353
|
+
// View type). Page scenarios should link to the default export (App), not whichever
|
|
1354
|
+
// entity happens to come first in the array.
|
|
1355
|
+
rawDb
|
|
1356
|
+
.prepare(`INSERT INTO editor_scenarios (id, project_id, name, page_file_path, url, type, viewport_width, viewport_height)
|
|
1357
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1358
|
+
.run('sc-page-1', projectId, 'Save View - First Time User', 'src/popup/App.tsx', '/', 'application', 400, 600);
|
|
1359
|
+
// Pass multiple entities for the same file — default export should win
|
|
1360
|
+
// regardless of order. Here the default export (App) appears first but
|
|
1361
|
+
// gets overwritten by later non-default entities in the naive set() impl.
|
|
1362
|
+
const result = await backfillEntityShaOnScenarios(db, [
|
|
1363
|
+
{
|
|
1364
|
+
sha: 'app-sha',
|
|
1365
|
+
name: 'App',
|
|
1366
|
+
filePath: 'src/popup/App.tsx',
|
|
1367
|
+
isDefaultExport: true,
|
|
1368
|
+
},
|
|
1369
|
+
{
|
|
1370
|
+
sha: 'get-initial-view-sha',
|
|
1371
|
+
name: 'getInitialView',
|
|
1372
|
+
filePath: 'src/popup/App.tsx',
|
|
1373
|
+
isDefaultExport: false,
|
|
1374
|
+
},
|
|
1375
|
+
{
|
|
1376
|
+
sha: 'view-type-sha',
|
|
1377
|
+
name: 'View',
|
|
1378
|
+
filePath: 'src/popup/App.tsx',
|
|
1379
|
+
isDefaultExport: false,
|
|
1380
|
+
},
|
|
1381
|
+
]);
|
|
1382
|
+
expect(result.updated).toBe(1);
|
|
1383
|
+
const row = rawDb
|
|
1384
|
+
.prepare('SELECT entity_sha FROM editor_scenarios WHERE id = ?')
|
|
1385
|
+
.get('sc-page-1');
|
|
1386
|
+
expect(row.entity_sha).toBe('app-sha');
|
|
1387
|
+
});
|
|
1388
|
+
it('should update page scenario to latest entity version when multiple versions exist', async () => {
|
|
1389
|
+
// Real-world bug: App.tsx entity gets versioned (code changes create new SHAs).
|
|
1390
|
+
// The scenario points to the oldest version's SHA. When sync receives all 3
|
|
1391
|
+
// versions of the same default-export entity, it should update the scenario
|
|
1392
|
+
// to the LATEST version, not keep it stuck on the oldest.
|
|
1393
|
+
rawDb
|
|
1394
|
+
.prepare(`INSERT INTO editor_scenarios (id, project_id, name, page_file_path, url, type, viewport_width, viewport_height, entity_sha, display_name)
|
|
1395
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1396
|
+
.run('sc-home', projectId, 'Welcome Screen — First Install', 'src/popup/App.tsx', '/', 'application', 400, 600, 'version-1-sha', // stale — points to oldest version
|
|
1397
|
+
'Home');
|
|
1398
|
+
// Pass all 3 versions — all default exports for the same file.
|
|
1399
|
+
// The sync should pick the LAST one (latest) since callers pass
|
|
1400
|
+
// entities in creation order.
|
|
1401
|
+
const result = await backfillEntityShaOnScenarios(db, [
|
|
1402
|
+
{
|
|
1403
|
+
sha: 'version-1-sha',
|
|
1404
|
+
name: 'App',
|
|
1405
|
+
filePath: 'src/popup/App.tsx',
|
|
1406
|
+
isDefaultExport: true,
|
|
1407
|
+
},
|
|
1408
|
+
{
|
|
1409
|
+
sha: 'version-2-sha',
|
|
1410
|
+
name: 'App',
|
|
1411
|
+
filePath: 'src/popup/App.tsx',
|
|
1412
|
+
isDefaultExport: true,
|
|
1413
|
+
},
|
|
1414
|
+
{
|
|
1415
|
+
sha: 'version-3-sha',
|
|
1416
|
+
name: 'App',
|
|
1417
|
+
filePath: 'src/popup/App.tsx',
|
|
1418
|
+
isDefaultExport: true,
|
|
1419
|
+
},
|
|
1420
|
+
]);
|
|
1421
|
+
expect(result.updated).toBe(1);
|
|
1422
|
+
const row = rawDb
|
|
1423
|
+
.prepare('SELECT entity_sha FROM editor_scenarios WHERE id = ?')
|
|
1424
|
+
.get('sc-home');
|
|
1425
|
+
expect(row.entity_sha).toBe('version-3-sha');
|
|
1426
|
+
});
|
|
1427
|
+
});
|
|
1428
|
+
describe('countScenariosNeedingEntityBackfill', () => {
|
|
1429
|
+
let db;
|
|
1430
|
+
let rawDb;
|
|
1431
|
+
const projectId = 'test-project-id';
|
|
1432
|
+
beforeEach(async () => {
|
|
1433
|
+
rawDb = new Database(':memory:');
|
|
1434
|
+
db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
|
|
1435
|
+
await db.schema
|
|
1436
|
+
.createTable('editor_scenarios')
|
|
1437
|
+
.addColumn('id', 'varchar', (col) => col.primaryKey())
|
|
1438
|
+
.addColumn('project_id', 'varchar', (col) => col.notNull())
|
|
1439
|
+
.addColumn('name', 'varchar', (col) => col.notNull())
|
|
1440
|
+
.addColumn('description', 'text')
|
|
1441
|
+
.addColumn('component_name', 'varchar')
|
|
1442
|
+
.addColumn('component_path', 'varchar')
|
|
1443
|
+
.addColumn('url', 'varchar')
|
|
1444
|
+
.addColumn('type', 'varchar')
|
|
1445
|
+
.addColumn('screenshot_path', 'varchar')
|
|
1446
|
+
.addColumn('viewport_width', 'integer')
|
|
1447
|
+
.addColumn('viewport_height', 'integer')
|
|
1448
|
+
.addColumn('dimension', 'varchar')
|
|
1449
|
+
.addColumn('dimensions', 'text')
|
|
1450
|
+
.addColumn('screenshot_paths', 'text')
|
|
1451
|
+
.addColumn('page_file_path', 'varchar')
|
|
1452
|
+
.addColumn('entity_sha', 'varchar')
|
|
1453
|
+
.addColumn('display_name', 'varchar')
|
|
1454
|
+
.addColumn('created_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
|
|
1455
|
+
.addColumn('updated_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
|
|
1456
|
+
.execute();
|
|
1457
|
+
});
|
|
1458
|
+
afterEach(async () => {
|
|
1459
|
+
await db.destroy();
|
|
1460
|
+
});
|
|
1461
|
+
it('should NOT count scenarios that already have entity_sha', async () => {
|
|
1462
|
+
rawDb
|
|
1463
|
+
.prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height, entity_sha)
|
|
1464
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1465
|
+
.run('sc-1', projectId, 'Header - Default', 'Header', 'src/components/Header.tsx', '/isolated-components/Header', 'component', 1280, 720, 'existing-sha');
|
|
1466
|
+
const count = await countScenariosNeedingEntityBackfill(db);
|
|
1467
|
+
expect(count).toBe(0);
|
|
1468
|
+
});
|
|
1469
|
+
it('should count scenarios with null entity_sha and page_file_path', async () => {
|
|
1470
|
+
rawDb
|
|
1471
|
+
.prepare(`INSERT INTO editor_scenarios (id, project_id, name, page_file_path, url, type, viewport_width, viewport_height)
|
|
1472
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1473
|
+
.run('sc-2', projectId, 'Home - Default', 'app/page.tsx', '/', 'application', 1280, 720);
|
|
1474
|
+
const count = await countScenariosNeedingEntityBackfill(db);
|
|
1475
|
+
expect(count).toBe(1);
|
|
1476
|
+
});
|
|
1477
|
+
it('should count scenarios with null entity_sha and component_path', async () => {
|
|
1478
|
+
rawDb
|
|
1479
|
+
.prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height)
|
|
1480
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1481
|
+
.run('sc-3', projectId, 'Footer - Default', 'Footer', 'src/components/Footer.tsx', '/isolated-components/Footer', 'component', 1280, 720);
|
|
1482
|
+
const count = await countScenariosNeedingEntityBackfill(db);
|
|
1483
|
+
expect(count).toBe(1);
|
|
1484
|
+
});
|
|
1485
|
+
it('should NOT count scenarios with null entity_sha and no file paths', async () => {
|
|
1486
|
+
rawDb
|
|
1487
|
+
.prepare(`INSERT INTO editor_scenarios (id, project_id, name, url, type, viewport_width, viewport_height)
|
|
1488
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
1489
|
+
.run('sc-4', projectId, 'No File Path', '/', 'application', 1280, 720);
|
|
1490
|
+
const count = await countScenariosNeedingEntityBackfill(db);
|
|
1491
|
+
expect(count).toBe(0);
|
|
1492
|
+
});
|
|
1493
|
+
});
|
|
1494
|
+
describe('validateEntityLinkageForAppScenario', () => {
|
|
1495
|
+
let tmpDir;
|
|
1496
|
+
let glossaryPath;
|
|
1497
|
+
beforeEach(() => {
|
|
1498
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'entity-linkage-'));
|
|
1499
|
+
const codeyamDir = path.join(tmpDir, '.codeyam');
|
|
1500
|
+
fs.mkdirSync(codeyamDir, { recursive: true });
|
|
1501
|
+
glossaryPath = path.join(codeyamDir, 'glossary.json');
|
|
1502
|
+
});
|
|
1503
|
+
afterEach(() => {
|
|
1504
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1505
|
+
});
|
|
1506
|
+
it('should return error when file is not in glossary', () => {
|
|
1507
|
+
// Write a glossary that does NOT include the target file
|
|
1508
|
+
fs.writeFileSync(glossaryPath, JSON.stringify([
|
|
1509
|
+
{ name: 'Header', filePath: 'src/components/Header.tsx' },
|
|
1510
|
+
]));
|
|
1511
|
+
const result = validateEntityLinkageForAppScenario({
|
|
1512
|
+
lookupFilePath: 'src/App.tsx',
|
|
1513
|
+
scenarioType: 'application',
|
|
1514
|
+
projectRoot: tmpDir,
|
|
1515
|
+
});
|
|
1516
|
+
expect(result.valid).toBe(false);
|
|
1517
|
+
expect(result.error).toContain('No glossary entry found');
|
|
1518
|
+
expect(result.error).toContain('src/App.tsx');
|
|
1519
|
+
});
|
|
1520
|
+
it('should return needsAnalysis when file IS in glossary but no entity exists', () => {
|
|
1521
|
+
// Write a glossary that includes the target file
|
|
1522
|
+
fs.writeFileSync(glossaryPath, JSON.stringify([
|
|
1523
|
+
{ name: 'App', filePath: 'src/App.tsx', description: 'Main app' },
|
|
1524
|
+
]));
|
|
1525
|
+
const result = validateEntityLinkageForAppScenario({
|
|
1526
|
+
lookupFilePath: 'src/App.tsx',
|
|
1527
|
+
scenarioType: 'application',
|
|
1528
|
+
projectRoot: tmpDir,
|
|
1529
|
+
});
|
|
1530
|
+
expect(result.valid).toBe(true);
|
|
1531
|
+
expect(result.needsAnalysis).toBe(true);
|
|
1532
|
+
});
|
|
1533
|
+
it('should skip validation for component-type scenarios', () => {
|
|
1534
|
+
// No glossary file exists at all
|
|
1535
|
+
const result = validateEntityLinkageForAppScenario({
|
|
1536
|
+
lookupFilePath: 'src/components/Header.tsx',
|
|
1537
|
+
scenarioType: 'component',
|
|
1538
|
+
projectRoot: tmpDir,
|
|
1539
|
+
});
|
|
1540
|
+
expect(result.valid).toBe(true);
|
|
1541
|
+
expect(result.needsAnalysis).toBeUndefined();
|
|
1542
|
+
});
|
|
1543
|
+
it('should skip validation when lookupFilePath is null', () => {
|
|
1544
|
+
const result = validateEntityLinkageForAppScenario({
|
|
1545
|
+
lookupFilePath: null,
|
|
1546
|
+
scenarioType: 'application',
|
|
1547
|
+
projectRoot: tmpDir,
|
|
1548
|
+
});
|
|
1549
|
+
expect(result.valid).toBe(true);
|
|
1550
|
+
expect(result.needsAnalysis).toBeUndefined();
|
|
1551
|
+
});
|
|
1552
|
+
it('should handle glossary wrapped in object format', () => {
|
|
1553
|
+
// LLMs sometimes write glossary as {"components": [...]}
|
|
1554
|
+
fs.writeFileSync(glossaryPath, JSON.stringify({
|
|
1555
|
+
components: [
|
|
1556
|
+
{ name: 'App', filePath: 'src/App.tsx', description: 'Main app' },
|
|
1557
|
+
],
|
|
1558
|
+
}));
|
|
1559
|
+
const result = validateEntityLinkageForAppScenario({
|
|
1560
|
+
lookupFilePath: 'src/App.tsx',
|
|
1561
|
+
scenarioType: 'user',
|
|
1562
|
+
projectRoot: tmpDir,
|
|
1563
|
+
});
|
|
1564
|
+
expect(result.valid).toBe(true);
|
|
1565
|
+
expect(result.needsAnalysis).toBe(true);
|
|
1566
|
+
});
|
|
1567
|
+
it('should return error when glossary file does not exist', () => {
|
|
1568
|
+
// Don't create a glossary file
|
|
1569
|
+
const result = validateEntityLinkageForAppScenario({
|
|
1570
|
+
lookupFilePath: 'src/App.tsx',
|
|
1571
|
+
scenarioType: 'application',
|
|
1572
|
+
projectRoot: tmpDir,
|
|
1573
|
+
});
|
|
1574
|
+
expect(result.valid).toBe(false);
|
|
1575
|
+
expect(result.error).toContain('No glossary entry found');
|
|
1576
|
+
});
|
|
1577
|
+
});
|
|
1578
|
+
describe('validateScenarioCategorization', () => {
|
|
1579
|
+
it('should reject componentName with non-isolation URL', () => {
|
|
1580
|
+
const result = validateScenarioCategorization({
|
|
1581
|
+
componentName: 'Library',
|
|
1582
|
+
url: '/library',
|
|
1583
|
+
});
|
|
1584
|
+
expect(result.valid).toBe(false);
|
|
1585
|
+
expect(result.error).toBeDefined();
|
|
1586
|
+
expect(result.error).toContain('componentName');
|
|
1587
|
+
});
|
|
1588
|
+
it('should accept componentName with /isolated-components/ URL', () => {
|
|
1589
|
+
const result = validateScenarioCategorization({
|
|
1590
|
+
componentName: 'Library',
|
|
1591
|
+
url: '/isolated-components/Library?s=Default',
|
|
1592
|
+
});
|
|
1593
|
+
expect(result.valid).toBe(true);
|
|
1594
|
+
});
|
|
1595
|
+
it('should accept componentName with /codeyam-isolate/ URL', () => {
|
|
1596
|
+
const result = validateScenarioCategorization({
|
|
1597
|
+
componentName: 'Library',
|
|
1598
|
+
url: '/codeyam-isolate/Library?s=Default',
|
|
1599
|
+
});
|
|
1600
|
+
expect(result.valid).toBe(true);
|
|
1601
|
+
});
|
|
1602
|
+
it('should accept application scenario without componentName', () => {
|
|
1603
|
+
const result = validateScenarioCategorization({
|
|
1604
|
+
url: '/library',
|
|
1605
|
+
type: 'application',
|
|
1606
|
+
});
|
|
1607
|
+
expect(result.valid).toBe(true);
|
|
1608
|
+
});
|
|
1609
|
+
it('should accept component with no URL', () => {
|
|
1610
|
+
const result = validateScenarioCategorization({
|
|
1611
|
+
componentName: 'Library',
|
|
1612
|
+
url: null,
|
|
1613
|
+
});
|
|
1614
|
+
expect(result.valid).toBe(true);
|
|
1615
|
+
});
|
|
1616
|
+
it('should suggest both fixes in error message', () => {
|
|
1617
|
+
const result = validateScenarioCategorization({
|
|
1618
|
+
componentName: 'Library',
|
|
1619
|
+
url: '/library',
|
|
1620
|
+
});
|
|
1621
|
+
expect(result.valid).toBe(false);
|
|
1622
|
+
// Should suggest changing URL to isolation route
|
|
1623
|
+
expect(result.error).toContain('/isolated-components/');
|
|
1624
|
+
// Should suggest removing componentName
|
|
1625
|
+
expect(result.error).toContain('remove');
|
|
1626
|
+
});
|
|
1627
|
+
});
|
|
1628
|
+
describe('resolveSeedFrom', () => {
|
|
1629
|
+
let tmpDir;
|
|
1630
|
+
let db;
|
|
1631
|
+
beforeEach(() => {
|
|
1632
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'seedfrom-'));
|
|
1633
|
+
fs.mkdirSync(path.join(tmpDir, '.codeyam', 'editor-scenarios'), {
|
|
1634
|
+
recursive: true,
|
|
1635
|
+
});
|
|
1636
|
+
const raw = new Database(':memory:');
|
|
1637
|
+
raw.exec(`
|
|
1638
|
+
CREATE TABLE editor_scenarios (
|
|
1639
|
+
id TEXT PRIMARY KEY,
|
|
1640
|
+
project_id TEXT,
|
|
1641
|
+
name TEXT,
|
|
1642
|
+
description TEXT,
|
|
1643
|
+
component_name TEXT,
|
|
1644
|
+
component_path TEXT,
|
|
1645
|
+
url TEXT,
|
|
1646
|
+
type TEXT,
|
|
1647
|
+
screenshot_path TEXT,
|
|
1648
|
+
viewport_width INTEGER,
|
|
1649
|
+
viewport_height INTEGER,
|
|
1650
|
+
dimensions TEXT,
|
|
1651
|
+
screenshot_paths TEXT,
|
|
1652
|
+
page_file_path TEXT,
|
|
1653
|
+
entity_sha TEXT,
|
|
1654
|
+
display_name TEXT,
|
|
1655
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1656
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
1657
|
+
)
|
|
1658
|
+
`);
|
|
1659
|
+
db = new Kysely({ dialect: new SqliteDialect({ database: raw }) });
|
|
1660
|
+
});
|
|
1661
|
+
afterEach(() => {
|
|
1662
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1663
|
+
});
|
|
1664
|
+
it('should resolve seed data by scenario ID', async () => {
|
|
1665
|
+
const scenarioId = 'aaaa-bbbb-cccc-dddd';
|
|
1666
|
+
const seedData = { article: [{ id: '1', title: 'Test' }] };
|
|
1667
|
+
// Write the seed file
|
|
1668
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'editor-scenarios', `${scenarioId}.seed.json`), JSON.stringify(seedData));
|
|
1669
|
+
const result = await resolveSeedFrom(tmpDir, db, scenarioId);
|
|
1670
|
+
expect(result).toEqual(seedData);
|
|
1671
|
+
});
|
|
1672
|
+
it('should resolve seed data by scenario name', async () => {
|
|
1673
|
+
const scenarioId = 'eeee-ffff-1111-2222';
|
|
1674
|
+
const seedData = { user: [{ id: '1', name: 'Alice' }] };
|
|
1675
|
+
// Write the seed file
|
|
1676
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'editor-scenarios', `${scenarioId}.seed.json`), JSON.stringify(seedData));
|
|
1677
|
+
// Insert scenario into DB so name lookup works
|
|
1678
|
+
await db
|
|
1679
|
+
.insertInto('editor_scenarios')
|
|
1680
|
+
.values({
|
|
1681
|
+
id: scenarioId,
|
|
1682
|
+
project_id: 'proj-1',
|
|
1683
|
+
name: 'Library - Rich Data',
|
|
1684
|
+
viewport_width: 1280,
|
|
1685
|
+
viewport_height: 720,
|
|
1686
|
+
})
|
|
1687
|
+
.execute();
|
|
1688
|
+
const result = await resolveSeedFrom(tmpDir, db, 'Library - Rich Data');
|
|
1689
|
+
expect(result).toEqual(seedData);
|
|
1690
|
+
});
|
|
1691
|
+
it('should fall back to seed from scenario data file when no .seed.json exists', async () => {
|
|
1692
|
+
const scenarioId = 'cccc-dddd-eeee-ffff';
|
|
1693
|
+
const seedData = { tag: [{ id: '1', name: 'AI' }] };
|
|
1694
|
+
// Write the scenario data file (not the .seed.json)
|
|
1695
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'editor-scenarios', `${scenarioId}.json`), JSON.stringify({ seed: seedData, _metadata: { name: 'Test' } }));
|
|
1696
|
+
const result = await resolveSeedFrom(tmpDir, db, scenarioId);
|
|
1697
|
+
expect(result).toEqual(seedData);
|
|
1698
|
+
});
|
|
1699
|
+
it('should return null when scenario not found', async () => {
|
|
1700
|
+
const result = await resolveSeedFrom(tmpDir, db, 'nonexistent');
|
|
1701
|
+
expect(result).toBeNull();
|
|
1702
|
+
});
|
|
1703
|
+
it('should return null when scenario exists but has no seed data', async () => {
|
|
1704
|
+
const scenarioId = 'no-seed-1111-2222';
|
|
1705
|
+
// Scenario data file with no seed
|
|
1706
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'editor-scenarios', `${scenarioId}.json`), JSON.stringify({ _metadata: { name: 'No Seed' } }));
|
|
1707
|
+
const result = await resolveSeedFrom(tmpDir, db, scenarioId);
|
|
1708
|
+
expect(result).toBeNull();
|
|
1709
|
+
});
|
|
1710
|
+
it('should return null when seed.json contains invalid JSON', async () => {
|
|
1711
|
+
const scenarioId = 'bad-json-1111';
|
|
1712
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'editor-scenarios', `${scenarioId}.seed.json`), '{ this is not valid json }}}');
|
|
1713
|
+
const result = await resolveSeedFrom(tmpDir, db, scenarioId);
|
|
1714
|
+
expect(result).toBeNull();
|
|
1715
|
+
});
|
|
1716
|
+
it('should return null when scenario data file contains invalid JSON', async () => {
|
|
1717
|
+
const scenarioId = 'bad-data-2222';
|
|
1718
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'editor-scenarios', `${scenarioId}.json`), 'not json at all');
|
|
1719
|
+
const result = await resolveSeedFrom(tmpDir, db, scenarioId);
|
|
1720
|
+
expect(result).toBeNull();
|
|
1721
|
+
});
|
|
1722
|
+
it('should pick the most recent scenario when multiple share the same name', async () => {
|
|
1723
|
+
const oldId = 'old-scenario-1111';
|
|
1724
|
+
const newId = 'new-scenario-2222';
|
|
1725
|
+
const oldSeed = { article: [{ id: '1', title: 'Old' }] };
|
|
1726
|
+
const newSeed = { article: [{ id: '2', title: 'New' }] };
|
|
1727
|
+
// Write seed files for both
|
|
1728
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'editor-scenarios', `${oldId}.seed.json`), JSON.stringify(oldSeed));
|
|
1729
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'editor-scenarios', `${newId}.seed.json`), JSON.stringify(newSeed));
|
|
1730
|
+
// Insert both with the same name, different created_at
|
|
1731
|
+
await db
|
|
1732
|
+
.insertInto('editor_scenarios')
|
|
1733
|
+
.values({
|
|
1734
|
+
id: oldId,
|
|
1735
|
+
project_id: 'proj-1',
|
|
1736
|
+
name: 'Shared Name',
|
|
1737
|
+
viewport_width: 1280,
|
|
1738
|
+
viewport_height: 720,
|
|
1739
|
+
created_at: '2026-01-01 00:00:00',
|
|
1740
|
+
})
|
|
1741
|
+
.execute();
|
|
1742
|
+
await db
|
|
1743
|
+
.insertInto('editor_scenarios')
|
|
1744
|
+
.values({
|
|
1745
|
+
id: newId,
|
|
1746
|
+
project_id: 'proj-1',
|
|
1747
|
+
name: 'Shared Name',
|
|
1748
|
+
viewport_width: 1280,
|
|
1749
|
+
viewport_height: 720,
|
|
1750
|
+
created_at: '2026-03-01 00:00:00',
|
|
1751
|
+
})
|
|
1752
|
+
.execute();
|
|
1753
|
+
const result = await resolveSeedFrom(tmpDir, db, 'Shared Name');
|
|
1754
|
+
expect(result).toEqual(newSeed);
|
|
1755
|
+
});
|
|
1756
|
+
it('should prefer .seed.json over inline seed in data file', async () => {
|
|
1757
|
+
const scenarioId = 'prefer-seed-file';
|
|
1758
|
+
const seedFileData = { article: [{ id: '1', title: 'From seed file' }] };
|
|
1759
|
+
const inlineData = { article: [{ id: '2', title: 'From inline' }] };
|
|
1760
|
+
// Write both files
|
|
1761
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'editor-scenarios', `${scenarioId}.seed.json`), JSON.stringify(seedFileData));
|
|
1762
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'editor-scenarios', `${scenarioId}.json`), JSON.stringify({ seed: inlineData }));
|
|
1763
|
+
const result = await resolveSeedFrom(tmpDir, db, scenarioId);
|
|
1764
|
+
expect(result).toEqual(seedFileData);
|
|
1765
|
+
});
|
|
1766
|
+
});
|
|
75
1767
|
});
|
|
76
1768
|
//# sourceMappingURL=editorScenarios.test.js.map
|