@codeyam/codeyam-cli 0.1.0-staging.9574237 → 0.1.0-staging.a77070e

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.
Files changed (153) hide show
  1. package/analyzer-template/.build-info.json +6 -6
  2. package/analyzer-template/log.txt +3 -3
  3. package/codeyam-cli/src/cli.js +9 -0
  4. package/codeyam-cli/src/cli.js.map +1 -1
  5. package/codeyam-cli/src/commands/__tests__/editor.isolateArgs.test.js +51 -0
  6. package/codeyam-cli/src/commands/__tests__/editor.isolateArgs.test.js.map +1 -0
  7. package/codeyam-cli/src/commands/editor.js +481 -72
  8. package/codeyam-cli/src/commands/editor.js.map +1 -1
  9. package/codeyam-cli/src/commands/editorIsolateArgs.js +25 -0
  10. package/codeyam-cli/src/commands/editorIsolateArgs.js.map +1 -0
  11. package/codeyam-cli/src/commands/init.js +1 -0
  12. package/codeyam-cli/src/commands/init.js.map +1 -1
  13. package/codeyam-cli/src/commands/telemetry.js +37 -0
  14. package/codeyam-cli/src/commands/telemetry.js.map +1 -0
  15. package/codeyam-cli/src/utils/__tests__/editorAudit.test.js +1534 -1
  16. package/codeyam-cli/src/utils/__tests__/editorAudit.test.js.map +1 -1
  17. package/codeyam-cli/src/utils/__tests__/editorCaptureScenarioSeeding.test.js +137 -0
  18. package/codeyam-cli/src/utils/__tests__/editorCaptureScenarioSeeding.test.js.map +1 -0
  19. package/codeyam-cli/src/utils/__tests__/editorEntityChangeStatus.test.js +70 -0
  20. package/codeyam-cli/src/utils/__tests__/editorEntityChangeStatus.test.js.map +1 -1
  21. package/codeyam-cli/src/utils/__tests__/editorEntityHelpers.test.js +97 -4
  22. package/codeyam-cli/src/utils/__tests__/editorEntityHelpers.test.js.map +1 -1
  23. package/codeyam-cli/src/utils/__tests__/editorScenarioSwitch.test.js +70 -0
  24. package/codeyam-cli/src/utils/__tests__/editorScenarioSwitch.test.js.map +1 -1
  25. package/codeyam-cli/src/utils/__tests__/editorScenarios.test.js +185 -7
  26. package/codeyam-cli/src/utils/__tests__/editorScenarios.test.js.map +1 -1
  27. package/codeyam-cli/src/utils/__tests__/entityChangeStatus.test.js +48 -1
  28. package/codeyam-cli/src/utils/__tests__/entityChangeStatus.test.js.map +1 -1
  29. package/codeyam-cli/src/utils/__tests__/glossaryAdd.test.js +177 -0
  30. package/codeyam-cli/src/utils/__tests__/glossaryAdd.test.js.map +1 -0
  31. package/codeyam-cli/src/utils/__tests__/scenarioCoverage.test.js +57 -0
  32. package/codeyam-cli/src/utils/__tests__/scenarioCoverage.test.js.map +1 -1
  33. package/codeyam-cli/src/utils/__tests__/scenariosManifest.test.js +180 -1
  34. package/codeyam-cli/src/utils/__tests__/scenariosManifest.test.js.map +1 -1
  35. package/codeyam-cli/src/utils/__tests__/telemetry.test.js +159 -0
  36. package/codeyam-cli/src/utils/__tests__/telemetry.test.js.map +1 -0
  37. package/codeyam-cli/src/utils/backgroundServer.js +1 -1
  38. package/codeyam-cli/src/utils/backgroundServer.js.map +1 -1
  39. package/codeyam-cli/src/utils/editorAudit.js +287 -1
  40. package/codeyam-cli/src/utils/editorAudit.js.map +1 -1
  41. package/codeyam-cli/src/utils/editorEntityChangeStatus.js +13 -7
  42. package/codeyam-cli/src/utils/editorEntityChangeStatus.js.map +1 -1
  43. package/codeyam-cli/src/utils/editorEntityHelpers.js +18 -3
  44. package/codeyam-cli/src/utils/editorEntityHelpers.js.map +1 -1
  45. package/codeyam-cli/src/utils/editorScenarioSwitch.js +24 -2
  46. package/codeyam-cli/src/utils/editorScenarioSwitch.js.map +1 -1
  47. package/codeyam-cli/src/utils/editorScenarios.js +73 -12
  48. package/codeyam-cli/src/utils/editorScenarios.js.map +1 -1
  49. package/codeyam-cli/src/utils/entityChangeStatus.js +9 -3
  50. package/codeyam-cli/src/utils/entityChangeStatus.js.map +1 -1
  51. package/codeyam-cli/src/utils/fileWatcher.js +38 -0
  52. package/codeyam-cli/src/utils/fileWatcher.js.map +1 -1
  53. package/codeyam-cli/src/utils/glossaryAdd.js +74 -0
  54. package/codeyam-cli/src/utils/glossaryAdd.js.map +1 -0
  55. package/codeyam-cli/src/utils/install-skills.js +5 -0
  56. package/codeyam-cli/src/utils/install-skills.js.map +1 -1
  57. package/codeyam-cli/src/utils/scenarioCoverage.js +4 -1
  58. package/codeyam-cli/src/utils/scenarioCoverage.js.map +1 -1
  59. package/codeyam-cli/src/utils/scenariosManifest.js +36 -0
  60. package/codeyam-cli/src/utils/scenariosManifest.js.map +1 -1
  61. package/codeyam-cli/src/utils/telemetry.js +106 -0
  62. package/codeyam-cli/src/utils/telemetry.js.map +1 -0
  63. package/codeyam-cli/src/utils/telemetryMiddleware.js +22 -0
  64. package/codeyam-cli/src/utils/telemetryMiddleware.js.map +1 -0
  65. package/codeyam-cli/src/webserver/__tests__/clientErrors.test.js +40 -0
  66. package/codeyam-cli/src/webserver/__tests__/clientErrors.test.js.map +1 -1
  67. package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js +61 -0
  68. package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js.map +1 -1
  69. package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js +92 -21
  70. package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js.map +1 -1
  71. package/codeyam-cli/src/webserver/app/lib/clientErrors.js +7 -1
  72. package/codeyam-cli/src/webserver/app/lib/clientErrors.js.map +1 -1
  73. package/codeyam-cli/src/webserver/backgroundServer.js +42 -57
  74. package/codeyam-cli/src/webserver/backgroundServer.js.map +1 -1
  75. package/codeyam-cli/src/webserver/build/client/assets/{CopyButton-CzTDWkF2.js → CopyButton-CLe80MMu.js} +1 -1
  76. package/codeyam-cli/src/webserver/build/client/assets/{EntityItem-BFbq6iFk.js → EntityItem-Crt_KN_U.js} +1 -1
  77. package/codeyam-cli/src/webserver/build/client/assets/{EntityTypeIcon-B6OMi58N.js → EntityTypeIcon-CD7lGABo.js} +1 -1
  78. package/codeyam-cli/src/webserver/build/client/assets/{InlineSpinner-DuYodzo1.js → InlineSpinner-CgTNOhnu.js} +1 -1
  79. package/codeyam-cli/src/webserver/build/client/assets/{InteractivePreview-CXo9EeCl.js → InteractivePreview-CKeQT5Ty.js} +2 -2
  80. package/codeyam-cli/src/webserver/build/client/assets/{LibraryFunctionPreview-DYCNb2It.js → LibraryFunctionPreview-D3s1MFkb.js} +1 -1
  81. package/codeyam-cli/src/webserver/build/client/assets/{LogViewer-CZgY3sxX.js → LogViewer-CM5zg40N.js} +1 -1
  82. package/codeyam-cli/src/webserver/build/client/assets/{ReportIssueModal-CnYYwRDw.js → ReportIssueModal-C2PLkej3.js} +1 -1
  83. package/codeyam-cli/src/webserver/build/client/assets/{SafeScreenshot-CDoF7ZpU.js → SafeScreenshot-DanvyBPb.js} +1 -1
  84. package/codeyam-cli/src/webserver/build/client/assets/{ScenarioViewer-DrnfvaLL.js → ScenarioViewer-DUMfcNVK.js} +1 -1
  85. package/codeyam-cli/src/webserver/build/client/assets/{Spinner-Df3UCi8k.js → Spinner-D0LgAaSa.js} +1 -1
  86. package/codeyam-cli/src/webserver/build/client/assets/{ViewportInspectBar-DRKR9T0U.js → ViewportInspectBar-BA_Ry-rs.js} +1 -1
  87. package/codeyam-cli/src/webserver/build/client/assets/{_index-ClR-g3tY.js → _index-BAWd-Xjf.js} +1 -1
  88. package/codeyam-cli/src/webserver/build/client/assets/{activity.(_tab)-DTH6ydEA.js → activity.(_tab)-BOARiB-g.js} +1 -1
  89. package/codeyam-cli/src/webserver/build/client/assets/{addon-web-links-74hnHF59.js → addon-web-links-CHx25PAe.js} +1 -1
  90. package/codeyam-cli/src/webserver/build/client/assets/{agent-transcripts-B8CYhCO9.js → agent-transcripts-Bg3e7q4S.js} +1 -1
  91. package/codeyam-cli/src/webserver/build/client/assets/{book-open-CLaoh4ac.js → book-open-CL-lMgHh.js} +1 -1
  92. package/codeyam-cli/src/webserver/build/client/assets/{chevron-down-BZ2DZxbW.js → chevron-down-GmAjGS9-.js} +1 -1
  93. package/codeyam-cli/src/webserver/build/client/assets/{chunk-JZWAC4HX-BBXArFPl.js → chunk-JZWAC4HX-BAdwhyCx.js} +11 -11
  94. package/codeyam-cli/src/webserver/build/client/assets/{circle-check-CT4unAk-.js → circle-check-DFcQkN5j.js} +1 -1
  95. package/codeyam-cli/src/webserver/build/client/assets/{copy-zK0B6Nu-.js → copy-C6iF61Xs.js} +1 -1
  96. package/codeyam-cli/src/webserver/build/client/assets/{createLucideIcon-DJB0YQJL.js → createLucideIcon-4ImjHTVC.js} +1 -1
  97. package/codeyam-cli/src/webserver/build/client/assets/{dev.empty-CkXFP_i-.js → dev.empty-C8y4mmyv.js} +1 -1
  98. package/codeyam-cli/src/webserver/build/client/assets/editor._tab-Gbk_i5Js.js +1 -0
  99. package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-Bnx7yUP0.js +58 -0
  100. package/codeyam-cli/src/webserver/build/client/assets/editorPreview-oepecPae.js +41 -0
  101. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha._-BqAN7hyG.js → entity._sha._-Blfy9UlN.js} +1 -1
  102. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha.scenarios._scenarioId.dev-BOi8kpwd.js → entity._sha.scenarios._scenarioId.dev-KTQuL0aj.js} +1 -1
  103. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha.scenarios._scenarioId.fullscreen-Dg1NhIms.js → entity._sha.scenarios._scenarioId.fullscreen-C6eeL24i.js} +1 -1
  104. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha_.create-scenario-CJX6kkkV.js → entity._sha_.create-scenario-DQM8E7L4.js} +1 -1
  105. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha_.edit._scenarioId-BhVjZhKg.js → entity._sha_.edit._scenarioId-CAoXLsQr.js} +1 -1
  106. package/codeyam-cli/src/webserver/build/client/assets/{entry.client-_gzKltPN.js → entry.client-SuW9syRS.js} +1 -1
  107. package/codeyam-cli/src/webserver/build/client/assets/{files-CV_17tZS.js → files-D-xGrg29.js} +1 -1
  108. package/codeyam-cli/src/webserver/build/client/assets/{git-D-YXmMbR.js → git-Bq_fbXP5.js} +1 -1
  109. package/codeyam-cli/src/webserver/build/client/assets/globals-fAqOD9ex.css +1 -0
  110. package/codeyam-cli/src/webserver/build/client/assets/{index-CCrgCshv.js → index-Bp1l4hSv.js} +1 -1
  111. package/codeyam-cli/src/webserver/build/client/assets/{index-BsX0F-9C.js → index-CWV9XZiG.js} +1 -1
  112. package/codeyam-cli/src/webserver/build/client/assets/{index-Blo6EK8G.js → index-DE3jI_dv.js} +1 -1
  113. package/codeyam-cli/src/webserver/build/client/assets/{labs-Byazq8Pv.js → labs-B_IX45ih.js} +1 -1
  114. package/codeyam-cli/src/webserver/build/client/assets/{loader-circle-DVQ0oHR7.js → loader-circle-De-7qQ2u.js} +1 -1
  115. package/codeyam-cli/src/webserver/build/client/assets/manifest-3157d6b8.js +1 -0
  116. package/codeyam-cli/src/webserver/build/client/assets/{memory-b-VmA2Vj.js → memory-Cx2xEx7s.js} +1 -1
  117. package/codeyam-cli/src/webserver/build/client/assets/{pause-DGcndCAa.js → pause-CFxEKL1u.js} +1 -1
  118. package/codeyam-cli/src/webserver/build/client/assets/root-DB3O9_9j.js +67 -0
  119. package/codeyam-cli/src/webserver/build/client/assets/{search-C0Uw0bcK.js → search-BdBb5aqc.js} +1 -1
  120. package/codeyam-cli/src/webserver/build/client/assets/{settings-OoNgHIfW.js → settings-DdE-Untf.js} +1 -1
  121. package/codeyam-cli/src/webserver/build/client/assets/{simulations-Bcemfu8a.js → simulations-DSCdE99u.js} +1 -1
  122. package/codeyam-cli/src/webserver/build/client/assets/{terminal-BgMmG7R9.js → terminal-CrplD4b1.js} +1 -1
  123. package/codeyam-cli/src/webserver/build/client/assets/{triangle-alert-Cs87hJYK.js → triangle-alert-DqJ0j69l.js} +1 -1
  124. package/codeyam-cli/src/webserver/build/client/assets/{useCustomSizes-BR3Rs7JY.js → useCustomSizes-DhXHbEjP.js} +1 -1
  125. package/codeyam-cli/src/webserver/build/client/assets/{useLastLogLine-BxxP_XF9.js → useLastLogLine-BNd5hYuW.js} +1 -1
  126. package/codeyam-cli/src/webserver/build/client/assets/{useReportContext-BermyNU5.js → useReportContext-Cy5Qg_UR.js} +1 -1
  127. package/codeyam-cli/src/webserver/build/client/assets/{useToast-a_QN_W9_.js → useToast-5HR2j9ZE.js} +1 -1
  128. package/codeyam-cli/src/webserver/build/server/assets/{analysisRunner-DXQyOV0G.js → analysisRunner-BMmkgAkg.js} +1 -1
  129. package/codeyam-cli/src/webserver/build/server/assets/{index-CHSrVJtC.js → index-DxB0pOSt.js} +1 -1
  130. package/codeyam-cli/src/webserver/build/server/assets/{init-DL8vWZ6m.js → init-DLYLaqqP.js} +2 -2
  131. package/codeyam-cli/src/webserver/build/server/assets/server-build-CcyitQLQ.js +551 -0
  132. package/codeyam-cli/src/webserver/build/server/index.js +1 -1
  133. package/codeyam-cli/src/webserver/build-info.json +5 -5
  134. package/codeyam-cli/src/webserver/editorProxy.js +78 -3
  135. package/codeyam-cli/src/webserver/editorProxy.js.map +1 -1
  136. package/codeyam-cli/src/webserver/idleDetector.js +41 -8
  137. package/codeyam-cli/src/webserver/idleDetector.js.map +1 -1
  138. package/codeyam-cli/src/webserver/scripts/journalCapture.ts +36 -0
  139. package/codeyam-cli/src/webserver/server.js +32 -0
  140. package/codeyam-cli/src/webserver/server.js.map +1 -1
  141. package/codeyam-cli/src/webserver/terminalServer.js +5 -5
  142. package/codeyam-cli/src/webserver/terminalServer.js.map +1 -1
  143. package/codeyam-cli/templates/codeyam-editor-reference.md +214 -0
  144. package/codeyam-cli/templates/nextjs-prisma-sqlite/package.json +1 -1
  145. package/codeyam-cli/templates/nextjs-prisma-supabase/package.json +1 -1
  146. package/package.json +2 -1
  147. package/codeyam-cli/src/webserver/build/client/assets/editor._tab-DPw7NZHc.js +0 -1
  148. package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-Dmg9cGK3.js +0 -58
  149. package/codeyam-cli/src/webserver/build/client/assets/editorPreview-DBa7T2FK.js +0 -41
  150. package/codeyam-cli/src/webserver/build/client/assets/globals-Bqg9V6XV.css +0 -1
  151. package/codeyam-cli/src/webserver/build/client/assets/manifest-422a3551.js +0 -1
  152. package/codeyam-cli/src/webserver/build/client/assets/root-ue8uWVRS.js +0 -67
  153. package/codeyam-cli/src/webserver/build/server/assets/server-build-BUKVjBSZ.js +0 -501
@@ -1,6 +1,6 @@
1
1
  import Database from 'better-sqlite3';
2
2
  import { Kysely, SqliteDialect } from 'kysely';
3
- import { isComponent, classifyGlossaryEntries, computeAudit, filterGlossaryByChangeStatus, resolveAuditSessionScope, queryScenarioCounts, } from "../editorAudit.js";
3
+ import { isComponent, classifyGlossaryEntries, computeAudit, filterGlossaryByChangeStatus, resolveAuditSessionScope, queryScenarioCounts, queryPageScenarioCounts, queryIncompleteEntities, queryMiscategorizedScenarios, isOnlyIncompleteEntities, isAutoRemediable, identifyScenariosNeedingRecapture, detectDuplicateNames, } from "../editorAudit.js";
4
4
  describe('editorAudit', () => {
5
5
  describe('isComponent', () => {
6
6
  it('should return true for JSX.Element return type', () => {
@@ -290,6 +290,29 @@ describe('editorAudit', () => {
290
290
  expect(result.functions[0].testsPassing).toBe(false);
291
291
  expect(result.functions[0].testsVisibleInUi).toBe(true);
292
292
  });
293
+ it('should distinguish runner errors from test failures and include error message', () => {
294
+ const result = computeAudit({
295
+ components: [],
296
+ functions: [
297
+ {
298
+ name: 'getTimeAgo',
299
+ filePath: 'src/lib/format.ts',
300
+ testFile: 'src/lib/format.test.ts',
301
+ },
302
+ ],
303
+ scenarioCounts: {},
304
+ testFileExistence: { 'src/lib/format.test.ts': true },
305
+ testResults: {
306
+ 'src/lib/format.test.ts': {
307
+ passing: false,
308
+ hasEntityNameDescribe: false,
309
+ errorMessage: 'Error: Cannot find module "@/lib/format" from "src/lib/format.test.ts"',
310
+ },
311
+ },
312
+ });
313
+ expect(result.functions[0].status).toBe('runner_error');
314
+ expect(result.functions[0].errorMessage).toBe('Error: Cannot find module "@/lib/format" from "src/lib/format.test.ts"');
315
+ });
293
316
  it('should mark function as name_mismatch when tests pass but no describe matches', () => {
294
317
  const result = computeAudit({
295
318
  components: [],
@@ -983,5 +1006,1515 @@ describe('editorAudit', () => {
983
1006
  expect(counts).toEqual({ ArticleCard: 1, ArticleRow: 1 });
984
1007
  });
985
1008
  });
1009
+ // ── queryPageScenarioCounts ──────────────────────────────────────────
1010
+ describe('queryPageScenarioCounts', () => {
1011
+ let db;
1012
+ let rawDb;
1013
+ const projectId = 'test-project-id';
1014
+ beforeEach(async () => {
1015
+ rawDb = new Database(':memory:');
1016
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
1017
+ await db.schema
1018
+ .createTable('editor_scenarios')
1019
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1020
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
1021
+ .addColumn('name', 'varchar', (col) => col.notNull())
1022
+ .addColumn('description', 'text')
1023
+ .addColumn('component_name', 'varchar')
1024
+ .addColumn('component_path', 'varchar')
1025
+ .addColumn('page_file_path', 'varchar')
1026
+ .addColumn('url', 'varchar')
1027
+ .addColumn('type', 'varchar')
1028
+ .addColumn('screenshot_path', 'varchar')
1029
+ .addColumn('viewport_width', 'integer')
1030
+ .addColumn('viewport_height', 'integer')
1031
+ .addColumn('dimension', 'varchar')
1032
+ .addColumn('created_at', 'datetime')
1033
+ .addColumn('updated_at', 'datetime')
1034
+ .execute();
1035
+ });
1036
+ afterEach(async () => {
1037
+ await db.destroy();
1038
+ });
1039
+ it('should count app-level scenarios by page_file_path', async () => {
1040
+ // App-level scenario: has page_file_path but no component_name
1041
+ await db
1042
+ .insertInto('editor_scenarios')
1043
+ .values({
1044
+ id: 'sc-1',
1045
+ project_id: projectId,
1046
+ name: 'Library - Default',
1047
+ component_name: null,
1048
+ page_file_path: 'app/library/page.tsx',
1049
+ created_at: '2026-03-12 13:00:00',
1050
+ })
1051
+ .execute();
1052
+ await db
1053
+ .insertInto('editor_scenarios')
1054
+ .values({
1055
+ id: 'sc-2',
1056
+ project_id: projectId,
1057
+ name: 'Library - Empty',
1058
+ component_name: null,
1059
+ page_file_path: 'app/library/page.tsx',
1060
+ created_at: '2026-03-12 13:05:00',
1061
+ })
1062
+ .execute();
1063
+ const counts = await queryPageScenarioCounts(db, projectId, null);
1064
+ expect(counts).toEqual({ 'app/library/page.tsx': 2 });
1065
+ });
1066
+ it('should not count component scenarios (those have component_name)', async () => {
1067
+ // Component scenario — should NOT appear in page counts
1068
+ await db
1069
+ .insertInto('editor_scenarios')
1070
+ .values({
1071
+ id: 'sc-1',
1072
+ project_id: projectId,
1073
+ name: 'ArticleRow - Default',
1074
+ component_name: 'ArticleRow',
1075
+ page_file_path: null,
1076
+ created_at: '2026-03-12 13:00:00',
1077
+ })
1078
+ .execute();
1079
+ const counts = await queryPageScenarioCounts(db, projectId, null);
1080
+ expect(counts).toEqual({});
1081
+ });
1082
+ it('should respect featureStartedAt filter', async () => {
1083
+ await db
1084
+ .insertInto('editor_scenarios')
1085
+ .values({
1086
+ id: 'sc-1',
1087
+ project_id: projectId,
1088
+ name: 'Library - Old',
1089
+ component_name: null,
1090
+ page_file_path: 'app/library/page.tsx',
1091
+ created_at: '2026-03-12 13:00:00',
1092
+ })
1093
+ .execute();
1094
+ await db
1095
+ .insertInto('editor_scenarios')
1096
+ .values({
1097
+ id: 'sc-2',
1098
+ project_id: projectId,
1099
+ name: 'Library - New',
1100
+ component_name: null,
1101
+ page_file_path: 'app/library/page.tsx',
1102
+ created_at: '2026-03-12 15:00:00',
1103
+ })
1104
+ .execute();
1105
+ const counts = await queryPageScenarioCounts(db, projectId, '2026-03-12T14:01:31.291Z');
1106
+ expect(counts).toEqual({ 'app/library/page.tsx': 1 });
1107
+ });
1108
+ it('should count re-registered page scenarios via updated_at', async () => {
1109
+ await db
1110
+ .insertInto('editor_scenarios')
1111
+ .values({
1112
+ id: 'sc-1',
1113
+ project_id: projectId,
1114
+ name: 'Library - Default',
1115
+ component_name: null,
1116
+ page_file_path: 'app/library/page.tsx',
1117
+ created_at: '2026-03-12 13:00:00',
1118
+ updated_at: '2026-03-12 15:00:00',
1119
+ })
1120
+ .execute();
1121
+ const counts = await queryPageScenarioCounts(db, projectId, '2026-03-12T14:01:31.291Z');
1122
+ expect(counts).toEqual({ 'app/library/page.tsx': 1 });
1123
+ });
1124
+ it('should group counts by page_file_path across multiple pages', async () => {
1125
+ await db
1126
+ .insertInto('editor_scenarios')
1127
+ .values([
1128
+ {
1129
+ id: 'sc-1',
1130
+ project_id: projectId,
1131
+ name: 'Library - Default',
1132
+ component_name: null,
1133
+ page_file_path: 'app/library/page.tsx',
1134
+ created_at: '2026-03-12 13:00:00',
1135
+ },
1136
+ {
1137
+ id: 'sc-2',
1138
+ project_id: projectId,
1139
+ name: 'Library - Rich',
1140
+ component_name: null,
1141
+ page_file_path: 'app/library/page.tsx',
1142
+ created_at: '2026-03-12 13:00:00',
1143
+ },
1144
+ {
1145
+ id: 'sc-3',
1146
+ project_id: projectId,
1147
+ name: 'Collections - Default',
1148
+ component_name: null,
1149
+ page_file_path: 'app/library/collections/page.tsx',
1150
+ created_at: '2026-03-12 13:00:00',
1151
+ },
1152
+ ])
1153
+ .execute();
1154
+ const counts = await queryPageScenarioCounts(db, projectId, null);
1155
+ expect(counts).toEqual({
1156
+ 'app/library/page.tsx': 2,
1157
+ 'app/library/collections/page.tsx': 1,
1158
+ });
1159
+ });
1160
+ });
1161
+ // ── Audit + entity completeness integration ─────────────────────────
1162
+ describe('audit should catch incomplete entities (bug reproduction)', () => {
1163
+ let db;
1164
+ let rawDb;
1165
+ const projectId = 'test-project-id';
1166
+ beforeEach(async () => {
1167
+ rawDb = new Database(':memory:');
1168
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
1169
+ await db.schema
1170
+ .createTable('editor_scenarios')
1171
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1172
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
1173
+ .addColumn('name', 'varchar', (col) => col.notNull())
1174
+ .addColumn('component_name', 'varchar')
1175
+ .addColumn('component_path', 'varchar')
1176
+ .addColumn('entity_sha', 'varchar')
1177
+ .addColumn('display_name', 'varchar')
1178
+ .addColumn('page_file_path', 'varchar')
1179
+ .addColumn('url', 'varchar')
1180
+ .addColumn('created_at', 'datetime')
1181
+ .addColumn('updated_at', 'datetime')
1182
+ .execute();
1183
+ await db.schema
1184
+ .createTable('analyses')
1185
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1186
+ .addColumn('entity_sha', 'varchar')
1187
+ .addColumn('entity_name', 'varchar')
1188
+ .addColumn('project_id', 'varchar')
1189
+ .execute();
1190
+ await db.schema
1191
+ .createTable('entities')
1192
+ .addColumn('sha', 'varchar', (col) => col.primaryKey())
1193
+ .addColumn('name', 'varchar')
1194
+ .addColumn('entity_type', 'varchar')
1195
+ .addColumn('file_path', 'varchar')
1196
+ .execute();
1197
+ });
1198
+ afterEach(async () => {
1199
+ await db.destroy();
1200
+ });
1201
+ it('demonstrates the bug: computeAudit passes but entities are incomplete', async () => {
1202
+ // Setup: Two components in glossary, both have scenarios registered.
1203
+ // CollectionChips has scenarios but NO analysis records — it's "incomplete."
1204
+ // The glossary-based audit (computeAudit) doesn't know about entity analyses
1205
+ // so it says allPassing: true. This is the bug.
1206
+ // Entities in DB
1207
+ await db
1208
+ .insertInto('entities')
1209
+ .values({
1210
+ sha: 'sha-header',
1211
+ name: 'Header',
1212
+ entity_type: 'visual',
1213
+ file_path: 'src/components/Header.tsx',
1214
+ })
1215
+ .execute();
1216
+ await db
1217
+ .insertInto('entities')
1218
+ .values({
1219
+ sha: 'sha-chips',
1220
+ name: 'CollectionChips',
1221
+ entity_type: 'visual',
1222
+ file_path: 'src/components/CollectionChips.tsx',
1223
+ })
1224
+ .execute();
1225
+ // Header has an analysis — it's complete
1226
+ await db
1227
+ .insertInto('analyses')
1228
+ .values({
1229
+ id: 'a-1',
1230
+ entity_sha: 'sha-header',
1231
+ entity_name: 'Header',
1232
+ project_id: projectId,
1233
+ })
1234
+ .execute();
1235
+ // CollectionChips has NO analysis — it's incomplete
1236
+ // Both have scenarios
1237
+ await db
1238
+ .insertInto('editor_scenarios')
1239
+ .values({
1240
+ id: 'sc-1',
1241
+ project_id: projectId,
1242
+ name: 'Header - Default',
1243
+ component_name: 'Header',
1244
+ entity_sha: 'sha-header',
1245
+ created_at: '2026-03-16 23:00:00',
1246
+ })
1247
+ .execute();
1248
+ await db
1249
+ .insertInto('editor_scenarios')
1250
+ .values({
1251
+ id: 'sc-2',
1252
+ project_id: projectId,
1253
+ name: 'CollectionChips - Default',
1254
+ component_name: 'CollectionChips',
1255
+ entity_sha: 'sha-chips',
1256
+ created_at: '2026-03-16 23:19:00',
1257
+ })
1258
+ .execute();
1259
+ await db
1260
+ .insertInto('editor_scenarios')
1261
+ .values({
1262
+ id: 'sc-3',
1263
+ project_id: projectId,
1264
+ name: 'CollectionChips - Many',
1265
+ component_name: 'CollectionChips',
1266
+ entity_sha: 'sha-chips',
1267
+ created_at: '2026-03-16 23:19:05',
1268
+ })
1269
+ .execute();
1270
+ // The glossary says both are components with scenarios
1271
+ const scenarioCounts = await queryScenarioCounts(db, projectId, null);
1272
+ expect(scenarioCounts).toEqual({ Header: 1, CollectionChips: 2 });
1273
+ // computeAudit only checks glossary coverage — it passes!
1274
+ const auditResult = computeAudit({
1275
+ components: [
1276
+ {
1277
+ name: 'Header',
1278
+ filePath: 'src/components/Header.tsx',
1279
+ returnType: 'JSX.Element',
1280
+ },
1281
+ {
1282
+ name: 'CollectionChips',
1283
+ filePath: 'src/components/CollectionChips.tsx',
1284
+ returnType: 'JSX.Element',
1285
+ },
1286
+ ],
1287
+ functions: [],
1288
+ scenarioCounts,
1289
+ testFileExistence: {},
1290
+ });
1291
+ // BUG: computeAudit alone says everything is fine
1292
+ expect(auditResult.summary.allPassing).toBe(true);
1293
+ expect(auditResult.summary.componentsOk).toBe(2);
1294
+ // But queryIncompleteEntities catches the real issue
1295
+ const incomplete = await queryIncompleteEntities(db, projectId, null);
1296
+ expect(incomplete).toHaveLength(1);
1297
+ expect(incomplete[0].name).toBe('CollectionChips');
1298
+ expect(incomplete[0].scenarioCount).toBe(2);
1299
+ });
1300
+ it('audit should fail when combining computeAudit with incomplete entity check', async () => {
1301
+ // Same setup as above — this test shows the FIX working:
1302
+ // after computeAudit, we also check queryIncompleteEntities,
1303
+ // and if any are found, allPassing becomes false.
1304
+ await db
1305
+ .insertInto('entities')
1306
+ .values({
1307
+ sha: 'sha-header',
1308
+ name: 'Header',
1309
+ entity_type: 'visual',
1310
+ file_path: 'src/components/Header.tsx',
1311
+ })
1312
+ .execute();
1313
+ await db
1314
+ .insertInto('entities')
1315
+ .values({
1316
+ sha: 'sha-chips',
1317
+ name: 'CollectionChips',
1318
+ entity_type: 'visual',
1319
+ file_path: 'src/components/CollectionChips.tsx',
1320
+ })
1321
+ .execute();
1322
+ await db
1323
+ .insertInto('analyses')
1324
+ .values({
1325
+ id: 'a-1',
1326
+ entity_sha: 'sha-header',
1327
+ entity_name: 'Header',
1328
+ project_id: projectId,
1329
+ })
1330
+ .execute();
1331
+ await db
1332
+ .insertInto('editor_scenarios')
1333
+ .values({
1334
+ id: 'sc-1',
1335
+ project_id: projectId,
1336
+ name: 'Header - Default',
1337
+ component_name: 'Header',
1338
+ entity_sha: 'sha-header',
1339
+ created_at: '2026-03-16 23:00:00',
1340
+ })
1341
+ .execute();
1342
+ await db
1343
+ .insertInto('editor_scenarios')
1344
+ .values({
1345
+ id: 'sc-2',
1346
+ project_id: projectId,
1347
+ name: 'CollectionChips - Default',
1348
+ component_name: 'CollectionChips',
1349
+ entity_sha: 'sha-chips',
1350
+ created_at: '2026-03-16 23:19:00',
1351
+ })
1352
+ .execute();
1353
+ const scenarioCounts = await queryScenarioCounts(db, projectId, null);
1354
+ const auditResult = computeAudit({
1355
+ components: [
1356
+ {
1357
+ name: 'Header',
1358
+ filePath: 'src/components/Header.tsx',
1359
+ returnType: 'JSX.Element',
1360
+ },
1361
+ {
1362
+ name: 'CollectionChips',
1363
+ filePath: 'src/components/CollectionChips.tsx',
1364
+ returnType: 'JSX.Element',
1365
+ },
1366
+ ],
1367
+ functions: [],
1368
+ scenarioCounts,
1369
+ testFileExistence: {},
1370
+ });
1371
+ // Apply the same post-processing the audit endpoint does
1372
+ const incomplete = await queryIncompleteEntities(db, projectId, null);
1373
+ if (incomplete.length > 0) {
1374
+ auditResult.summary.allPassing = false;
1375
+ auditResult.summary.incompleteEntities = incomplete.length;
1376
+ }
1377
+ // NOW the audit correctly fails
1378
+ expect(auditResult.summary.allPassing).toBe(false);
1379
+ expect(auditResult.summary.incompleteEntities).toBe(1);
1380
+ });
1381
+ it('audit should pass when all entities have analyses', async () => {
1382
+ // Both entities have analyses — everything is complete
1383
+ await db
1384
+ .insertInto('entities')
1385
+ .values({
1386
+ sha: 'sha-header',
1387
+ name: 'Header',
1388
+ entity_type: 'visual',
1389
+ file_path: 'src/components/Header.tsx',
1390
+ })
1391
+ .execute();
1392
+ await db
1393
+ .insertInto('entities')
1394
+ .values({
1395
+ sha: 'sha-chips',
1396
+ name: 'CollectionChips',
1397
+ entity_type: 'visual',
1398
+ file_path: 'src/components/CollectionChips.tsx',
1399
+ })
1400
+ .execute();
1401
+ await db
1402
+ .insertInto('analyses')
1403
+ .values({
1404
+ id: 'a-1',
1405
+ entity_sha: 'sha-header',
1406
+ entity_name: 'Header',
1407
+ project_id: projectId,
1408
+ })
1409
+ .execute();
1410
+ await db
1411
+ .insertInto('analyses')
1412
+ .values({
1413
+ id: 'a-2',
1414
+ entity_sha: 'sha-chips',
1415
+ entity_name: 'CollectionChips',
1416
+ project_id: projectId,
1417
+ })
1418
+ .execute();
1419
+ await db
1420
+ .insertInto('editor_scenarios')
1421
+ .values({
1422
+ id: 'sc-1',
1423
+ project_id: projectId,
1424
+ name: 'Header - Default',
1425
+ component_name: 'Header',
1426
+ entity_sha: 'sha-header',
1427
+ created_at: '2026-03-16 23:00:00',
1428
+ })
1429
+ .execute();
1430
+ await db
1431
+ .insertInto('editor_scenarios')
1432
+ .values({
1433
+ id: 'sc-2',
1434
+ project_id: projectId,
1435
+ name: 'CollectionChips - Default',
1436
+ component_name: 'CollectionChips',
1437
+ entity_sha: 'sha-chips',
1438
+ created_at: '2026-03-16 23:19:00',
1439
+ })
1440
+ .execute();
1441
+ const scenarioCounts = await queryScenarioCounts(db, projectId, null);
1442
+ const auditResult = computeAudit({
1443
+ components: [
1444
+ {
1445
+ name: 'Header',
1446
+ filePath: 'src/components/Header.tsx',
1447
+ returnType: 'JSX.Element',
1448
+ },
1449
+ {
1450
+ name: 'CollectionChips',
1451
+ filePath: 'src/components/CollectionChips.tsx',
1452
+ returnType: 'JSX.Element',
1453
+ },
1454
+ ],
1455
+ functions: [],
1456
+ scenarioCounts,
1457
+ testFileExistence: {},
1458
+ });
1459
+ const incomplete = await queryIncompleteEntities(db, projectId, null);
1460
+ if (incomplete.length > 0) {
1461
+ auditResult.summary.allPassing = false;
1462
+ auditResult.summary.incompleteEntities = incomplete.length;
1463
+ }
1464
+ // Everything complete — audit passes
1465
+ expect(auditResult.summary.allPassing).toBe(true);
1466
+ expect(auditResult.summary.incompleteEntities).toBeUndefined();
1467
+ });
1468
+ });
1469
+ // ── queryMiscategorizedScenarios ─────────────────────────────────────
1470
+ describe('queryMiscategorizedScenarios', () => {
1471
+ let db;
1472
+ let rawDb;
1473
+ const projectId = 'test-project-id';
1474
+ beforeEach(async () => {
1475
+ rawDb = new Database(':memory:');
1476
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
1477
+ await db.schema
1478
+ .createTable('editor_scenarios')
1479
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1480
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
1481
+ .addColumn('name', 'varchar', (col) => col.notNull())
1482
+ .addColumn('component_name', 'varchar')
1483
+ .addColumn('component_path', 'varchar')
1484
+ .addColumn('entity_sha', 'varchar')
1485
+ .addColumn('display_name', 'varchar')
1486
+ .addColumn('page_file_path', 'varchar')
1487
+ .addColumn('url', 'varchar')
1488
+ .addColumn('created_at', 'datetime')
1489
+ .addColumn('updated_at', 'datetime')
1490
+ .execute();
1491
+ });
1492
+ afterEach(async () => {
1493
+ await db.destroy();
1494
+ });
1495
+ it('should return empty when all component scenarios use isolation routes', async () => {
1496
+ await db
1497
+ .insertInto('editor_scenarios')
1498
+ .values({
1499
+ id: 'sc-1',
1500
+ project_id: projectId,
1501
+ name: 'LibraryCard - Default',
1502
+ component_name: 'LibraryCard',
1503
+ url: '/isolated-components/LibraryCard?s=Default',
1504
+ created_at: '2026-03-17 12:00:00',
1505
+ })
1506
+ .execute();
1507
+ const result = await queryMiscategorizedScenarios(db, projectId, null);
1508
+ expect(result).toEqual([]);
1509
+ });
1510
+ it('should flag component scenarios that use non-isolation URLs', async () => {
1511
+ // This is the bug: "Full Library Page" registered as component_name=LibraryPage
1512
+ // but url=/library — it's pointing at the real page, not an isolation route
1513
+ await db
1514
+ .insertInto('editor_scenarios')
1515
+ .values({
1516
+ id: 'sc-1',
1517
+ project_id: projectId,
1518
+ name: 'Full Library Page',
1519
+ component_name: 'LibraryPage',
1520
+ url: '/library',
1521
+ created_at: '2026-03-17 12:41:40',
1522
+ })
1523
+ .execute();
1524
+ await db
1525
+ .insertInto('editor_scenarios')
1526
+ .values({
1527
+ id: 'sc-2',
1528
+ project_id: projectId,
1529
+ name: 'Empty Library Page',
1530
+ component_name: 'LibraryPage',
1531
+ url: '/library',
1532
+ created_at: '2026-03-17 12:41:51',
1533
+ })
1534
+ .execute();
1535
+ const result = await queryMiscategorizedScenarios(db, projectId, null);
1536
+ expect(result).toEqual([
1537
+ {
1538
+ componentName: 'LibraryPage',
1539
+ scenarioNames: ['Full Library Page', 'Empty Library Page'],
1540
+ url: '/library',
1541
+ },
1542
+ ]);
1543
+ });
1544
+ it('should not flag page-level scenarios (no component_name)', async () => {
1545
+ // App-level scenarios have no component_name — they're fine with real URLs
1546
+ await db
1547
+ .insertInto('editor_scenarios')
1548
+ .values({
1549
+ id: 'sc-1',
1550
+ project_id: projectId,
1551
+ name: 'Library with Articles',
1552
+ url: '/',
1553
+ created_at: '2026-03-17 12:25:14',
1554
+ })
1555
+ .execute();
1556
+ const result = await queryMiscategorizedScenarios(db, projectId, null);
1557
+ expect(result).toEqual([]);
1558
+ });
1559
+ it('should group miscategorized scenarios by component and URL', async () => {
1560
+ // Two different components both misusing real URLs
1561
+ await db
1562
+ .insertInto('editor_scenarios')
1563
+ .values({
1564
+ id: 'sc-1',
1565
+ project_id: projectId,
1566
+ name: 'Full Library Page',
1567
+ component_name: 'LibraryPage',
1568
+ url: '/library',
1569
+ created_at: '2026-03-17 12:41:40',
1570
+ })
1571
+ .execute();
1572
+ await db
1573
+ .insertInto('editor_scenarios')
1574
+ .values({
1575
+ id: 'sc-2',
1576
+ project_id: projectId,
1577
+ name: 'Dashboard - Full',
1578
+ component_name: 'Dashboard',
1579
+ url: '/dashboard',
1580
+ created_at: '2026-03-17 12:50:00',
1581
+ })
1582
+ .execute();
1583
+ const result = await queryMiscategorizedScenarios(db, projectId, null);
1584
+ expect(result).toHaveLength(2);
1585
+ expect(result.map((r) => r.componentName).sort()).toEqual([
1586
+ 'Dashboard',
1587
+ 'LibraryPage',
1588
+ ]);
1589
+ });
1590
+ it('should scope to session when featureStartedAt is provided', async () => {
1591
+ // Old miscategorized scenario — before session
1592
+ await db
1593
+ .insertInto('editor_scenarios')
1594
+ .values({
1595
+ id: 'sc-old',
1596
+ project_id: projectId,
1597
+ name: 'Old Page',
1598
+ component_name: 'OldComponent',
1599
+ url: '/old',
1600
+ created_at: '2026-03-16 10:00:00',
1601
+ })
1602
+ .execute();
1603
+ // New miscategorized scenario — in session
1604
+ await db
1605
+ .insertInto('editor_scenarios')
1606
+ .values({
1607
+ id: 'sc-new',
1608
+ project_id: projectId,
1609
+ name: 'Full Library Page',
1610
+ component_name: 'LibraryPage',
1611
+ url: '/library',
1612
+ created_at: '2026-03-17 12:41:40',
1613
+ })
1614
+ .execute();
1615
+ const result = await queryMiscategorizedScenarios(db, projectId, '2026-03-17T11:58:55.562Z');
1616
+ expect(result).toHaveLength(1);
1617
+ expect(result[0].componentName).toBe('LibraryPage');
1618
+ });
1619
+ it('should not flag component scenarios with null URL', async () => {
1620
+ await db
1621
+ .insertInto('editor_scenarios')
1622
+ .values({
1623
+ id: 'sc-1',
1624
+ project_id: projectId,
1625
+ name: 'NoUrl - Default',
1626
+ component_name: 'NoUrl',
1627
+ created_at: '2026-03-17 12:00:00',
1628
+ })
1629
+ .execute();
1630
+ const result = await queryMiscategorizedScenarios(db, projectId, null);
1631
+ expect(result).toEqual([]);
1632
+ });
1633
+ });
1634
+ // ── isOnlyIncompleteEntities ─────────────────────────────────────────
1635
+ describe('isOnlyIncompleteEntities', () => {
1636
+ it('should return true when incompleteEntities is the only failure', () => {
1637
+ expect(isOnlyIncompleteEntities({
1638
+ componentsMissing: 0,
1639
+ componentsWithErrors: 0,
1640
+ functionsFailing: 0,
1641
+ functionsNameMismatch: 0,
1642
+ functionsMissing: 0,
1643
+ missingFromGlossary: 0,
1644
+ incompleteEntities: 3,
1645
+ allPassing: false,
1646
+ })).toBe(true);
1647
+ });
1648
+ it('should return false when there are also missing components', () => {
1649
+ expect(isOnlyIncompleteEntities({
1650
+ componentsMissing: 1,
1651
+ componentsWithErrors: 0,
1652
+ functionsFailing: 0,
1653
+ functionsNameMismatch: 0,
1654
+ functionsMissing: 0,
1655
+ missingFromGlossary: 0,
1656
+ incompleteEntities: 2,
1657
+ allPassing: false,
1658
+ })).toBe(false);
1659
+ });
1660
+ it('should return false when there are also failing tests', () => {
1661
+ expect(isOnlyIncompleteEntities({
1662
+ componentsMissing: 0,
1663
+ componentsWithErrors: 0,
1664
+ functionsFailing: 1,
1665
+ functionsNameMismatch: 0,
1666
+ functionsMissing: 0,
1667
+ missingFromGlossary: 0,
1668
+ incompleteEntities: 2,
1669
+ allPassing: false,
1670
+ })).toBe(false);
1671
+ });
1672
+ it('should return false when there are also missing glossary entries', () => {
1673
+ expect(isOnlyIncompleteEntities({
1674
+ componentsMissing: 0,
1675
+ componentsWithErrors: 0,
1676
+ functionsFailing: 0,
1677
+ functionsNameMismatch: 0,
1678
+ functionsMissing: 0,
1679
+ missingFromGlossary: 1,
1680
+ incompleteEntities: 2,
1681
+ allPassing: false,
1682
+ })).toBe(false);
1683
+ });
1684
+ it('should return true even when incompleteEntities is 0 (no failures at all)', () => {
1685
+ // Edge case: all zeros means nothing is failing
1686
+ expect(isOnlyIncompleteEntities({
1687
+ componentsMissing: 0,
1688
+ componentsWithErrors: 0,
1689
+ functionsFailing: 0,
1690
+ functionsNameMismatch: 0,
1691
+ functionsMissing: 0,
1692
+ missingFromGlossary: 0,
1693
+ incompleteEntities: 0,
1694
+ allPassing: true,
1695
+ })).toBe(true);
1696
+ });
1697
+ it('should return false when there are also miscategorized scenarios', () => {
1698
+ expect(isOnlyIncompleteEntities({
1699
+ componentsMissing: 0,
1700
+ componentsWithErrors: 0,
1701
+ functionsFailing: 0,
1702
+ functionsNameMismatch: 0,
1703
+ functionsMissing: 0,
1704
+ missingFromGlossary: 0,
1705
+ miscategorizedScenarios: 1,
1706
+ incompleteEntities: 2,
1707
+ allPassing: false,
1708
+ })).toBe(false);
1709
+ });
1710
+ it('should handle missing fields gracefully', () => {
1711
+ // Summary from older API version might not have all fields
1712
+ expect(isOnlyIncompleteEntities({
1713
+ incompleteEntities: 2,
1714
+ allPassing: false,
1715
+ })).toBe(true);
1716
+ });
1717
+ });
1718
+ // ── isAutoRemediable ─────────────────────────────────────────────────
1719
+ describe('isAutoRemediable', () => {
1720
+ it('should return true on first attempt when only incomplete entities', () => {
1721
+ const result = isAutoRemediable({
1722
+ componentsMissing: 0,
1723
+ componentsWithErrors: 0,
1724
+ functionsFailing: 0,
1725
+ functionsNameMismatch: 0,
1726
+ functionsMissing: 0,
1727
+ missingFromGlossary: 0,
1728
+ miscategorizedScenarios: 0,
1729
+ incompleteEntities: 3,
1730
+ allPassing: false,
1731
+ }, false);
1732
+ expect(result).toBe(true);
1733
+ });
1734
+ it('should return false on second attempt (already tried once)', () => {
1735
+ // This is the key fix: if we already tried analyze-imports and
1736
+ // entities are STILL incomplete, don't try again — report the failure
1737
+ const result = isAutoRemediable({
1738
+ componentsMissing: 0,
1739
+ componentsWithErrors: 0,
1740
+ functionsFailing: 0,
1741
+ functionsNameMismatch: 0,
1742
+ functionsMissing: 0,
1743
+ missingFromGlossary: 0,
1744
+ miscategorizedScenarios: 0,
1745
+ incompleteEntities: 3,
1746
+ allPassing: false,
1747
+ }, true);
1748
+ expect(result).toBe(false);
1749
+ });
1750
+ it('should return false when there are other failures besides incomplete entities', () => {
1751
+ const result = isAutoRemediable({
1752
+ componentsMissing: 1,
1753
+ incompleteEntities: 3,
1754
+ allPassing: false,
1755
+ }, false);
1756
+ expect(result).toBe(false);
1757
+ });
1758
+ it('should return false when there are no incomplete entities', () => {
1759
+ const result = isAutoRemediable({
1760
+ componentsMissing: 1,
1761
+ allPassing: false,
1762
+ }, false);
1763
+ expect(result).toBe(false);
1764
+ });
1765
+ });
1766
+ // ── queryIncompleteEntities ─────────────────────────────────────────
1767
+ describe('queryIncompleteEntities', () => {
1768
+ let db;
1769
+ let rawDb;
1770
+ const projectId = 'test-project-id';
1771
+ beforeEach(async () => {
1772
+ rawDb = new Database(':memory:');
1773
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
1774
+ await db.schema
1775
+ .createTable('editor_scenarios')
1776
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1777
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
1778
+ .addColumn('name', 'varchar', (col) => col.notNull())
1779
+ .addColumn('component_name', 'varchar')
1780
+ .addColumn('component_path', 'varchar')
1781
+ .addColumn('entity_sha', 'varchar')
1782
+ .addColumn('display_name', 'varchar')
1783
+ .addColumn('page_file_path', 'varchar')
1784
+ .addColumn('url', 'varchar')
1785
+ .addColumn('created_at', 'datetime')
1786
+ .addColumn('updated_at', 'datetime')
1787
+ .execute();
1788
+ await db.schema
1789
+ .createTable('analyses')
1790
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1791
+ .addColumn('entity_sha', 'varchar')
1792
+ .addColumn('entity_name', 'varchar')
1793
+ .addColumn('project_id', 'varchar')
1794
+ .execute();
1795
+ await db.schema
1796
+ .createTable('entities')
1797
+ .addColumn('sha', 'varchar', (col) => col.primaryKey())
1798
+ .addColumn('name', 'varchar')
1799
+ .addColumn('entity_type', 'varchar')
1800
+ .addColumn('file_path', 'varchar')
1801
+ .execute();
1802
+ });
1803
+ afterEach(async () => {
1804
+ await db.destroy();
1805
+ });
1806
+ it('should return empty when all scenario entity SHAs have analyses', async () => {
1807
+ // Entity with analysis
1808
+ await db
1809
+ .insertInto('entities')
1810
+ .values({
1811
+ sha: 'sha-header',
1812
+ name: 'Header',
1813
+ entity_type: 'visual',
1814
+ file_path: 'src/Header.tsx',
1815
+ })
1816
+ .execute();
1817
+ await db
1818
+ .insertInto('analyses')
1819
+ .values({
1820
+ id: 'a-1',
1821
+ entity_sha: 'sha-header',
1822
+ entity_name: 'Header',
1823
+ project_id: projectId,
1824
+ })
1825
+ .execute();
1826
+ await db
1827
+ .insertInto('editor_scenarios')
1828
+ .values({
1829
+ id: 'sc-1',
1830
+ project_id: projectId,
1831
+ name: 'Header - Default',
1832
+ component_name: 'Header',
1833
+ entity_sha: 'sha-header',
1834
+ created_at: '2026-03-16 23:00:00',
1835
+ })
1836
+ .execute();
1837
+ const result = await queryIncompleteEntities(db, projectId, null);
1838
+ expect(result).toEqual([]);
1839
+ });
1840
+ it('should return entities with scenarios but no analyses', async () => {
1841
+ // Entity WITHOUT analysis
1842
+ await db
1843
+ .insertInto('entities')
1844
+ .values({
1845
+ sha: 'sha-chips',
1846
+ name: 'CollectionChips',
1847
+ entity_type: 'visual',
1848
+ file_path: 'src/components/CollectionChips.tsx',
1849
+ })
1850
+ .execute();
1851
+ // Scenario referencing that entity
1852
+ await db
1853
+ .insertInto('editor_scenarios')
1854
+ .values({
1855
+ id: 'sc-1',
1856
+ project_id: projectId,
1857
+ name: 'CollectionChips - Default',
1858
+ component_name: 'CollectionChips',
1859
+ entity_sha: 'sha-chips',
1860
+ created_at: '2026-03-16 23:00:00',
1861
+ })
1862
+ .execute();
1863
+ await db
1864
+ .insertInto('editor_scenarios')
1865
+ .values({
1866
+ id: 'sc-2',
1867
+ project_id: projectId,
1868
+ name: 'CollectionChips - Many',
1869
+ component_name: 'CollectionChips',
1870
+ entity_sha: 'sha-chips',
1871
+ created_at: '2026-03-16 23:01:00',
1872
+ })
1873
+ .execute();
1874
+ const result = await queryIncompleteEntities(db, projectId, null);
1875
+ expect(result).toEqual([
1876
+ {
1877
+ entitySha: 'sha-chips',
1878
+ name: 'CollectionChips',
1879
+ scenarioCount: 2,
1880
+ preExisting: false,
1881
+ },
1882
+ ]);
1883
+ });
1884
+ it('should only return entities without analyses, not those with analyses', async () => {
1885
+ // Entity WITH analysis (Header)
1886
+ await db
1887
+ .insertInto('entities')
1888
+ .values({
1889
+ sha: 'sha-header',
1890
+ name: 'Header',
1891
+ entity_type: 'visual',
1892
+ file_path: 'src/Header.tsx',
1893
+ })
1894
+ .execute();
1895
+ await db
1896
+ .insertInto('analyses')
1897
+ .values({
1898
+ id: 'a-1',
1899
+ entity_sha: 'sha-header',
1900
+ entity_name: 'Header',
1901
+ project_id: projectId,
1902
+ })
1903
+ .execute();
1904
+ await db
1905
+ .insertInto('editor_scenarios')
1906
+ .values({
1907
+ id: 'sc-1',
1908
+ project_id: projectId,
1909
+ name: 'Header - Default',
1910
+ component_name: 'Header',
1911
+ entity_sha: 'sha-header',
1912
+ created_at: '2026-03-16 23:00:00',
1913
+ })
1914
+ .execute();
1915
+ // Entity WITHOUT analysis (CollectionPicker)
1916
+ await db
1917
+ .insertInto('entities')
1918
+ .values({
1919
+ sha: 'sha-picker',
1920
+ name: 'CollectionPicker',
1921
+ entity_type: 'visual',
1922
+ file_path: 'src/components/CollectionPicker.tsx',
1923
+ })
1924
+ .execute();
1925
+ await db
1926
+ .insertInto('editor_scenarios')
1927
+ .values({
1928
+ id: 'sc-2',
1929
+ project_id: projectId,
1930
+ name: 'CollectionPicker - Default',
1931
+ component_name: 'CollectionPicker',
1932
+ entity_sha: 'sha-picker',
1933
+ created_at: '2026-03-16 23:00:00',
1934
+ })
1935
+ .execute();
1936
+ const result = await queryIncompleteEntities(db, projectId, null);
1937
+ expect(result).toEqual([
1938
+ {
1939
+ entitySha: 'sha-picker',
1940
+ name: 'CollectionPicker',
1941
+ scenarioCount: 1,
1942
+ preExisting: false,
1943
+ },
1944
+ ]);
1945
+ });
1946
+ it('should return both pre-session and in-session entities with preExisting flags', async () => {
1947
+ // Entity without analysis, scenario created BEFORE session
1948
+ await db
1949
+ .insertInto('entities')
1950
+ .values({
1951
+ sha: 'sha-old',
1952
+ name: 'OldComponent',
1953
+ entity_type: 'visual',
1954
+ file_path: 'src/OldComponent.tsx',
1955
+ })
1956
+ .execute();
1957
+ await db
1958
+ .insertInto('editor_scenarios')
1959
+ .values({
1960
+ id: 'sc-old',
1961
+ project_id: projectId,
1962
+ name: 'OldComponent - Default',
1963
+ component_name: 'OldComponent',
1964
+ entity_sha: 'sha-old',
1965
+ created_at: '2026-03-16 20:00:00',
1966
+ })
1967
+ .execute();
1968
+ // Entity without analysis, scenario created DURING session
1969
+ await db
1970
+ .insertInto('entities')
1971
+ .values({
1972
+ sha: 'sha-new',
1973
+ name: 'NewComponent',
1974
+ entity_type: 'visual',
1975
+ file_path: 'src/NewComponent.tsx',
1976
+ })
1977
+ .execute();
1978
+ await db
1979
+ .insertInto('editor_scenarios')
1980
+ .values({
1981
+ id: 'sc-new',
1982
+ project_id: projectId,
1983
+ name: 'NewComponent - Default',
1984
+ component_name: 'NewComponent',
1985
+ entity_sha: 'sha-new',
1986
+ created_at: '2026-03-16 23:10:00',
1987
+ })
1988
+ .execute();
1989
+ const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
1990
+ // Both should be returned — OldComponent is preExisting, NewComponent is not
1991
+ expect(result).toEqual(expect.arrayContaining([
1992
+ {
1993
+ entitySha: 'sha-old',
1994
+ name: 'OldComponent',
1995
+ scenarioCount: 1,
1996
+ preExisting: true,
1997
+ },
1998
+ {
1999
+ entitySha: 'sha-new',
2000
+ name: 'NewComponent',
2001
+ scenarioCount: 1,
2002
+ preExisting: false,
2003
+ },
2004
+ ]));
2005
+ expect(result).toHaveLength(2);
2006
+ });
2007
+ it('should flag preExisting: false when scenario was updated in session even if created before', async () => {
2008
+ await db
2009
+ .insertInto('entities')
2010
+ .values({
2011
+ sha: 'sha-updated',
2012
+ name: 'UpdatedComponent',
2013
+ entity_type: 'visual',
2014
+ file_path: 'src/Updated.tsx',
2015
+ })
2016
+ .execute();
2017
+ await db
2018
+ .insertInto('editor_scenarios')
2019
+ .values({
2020
+ id: 'sc-updated',
2021
+ project_id: projectId,
2022
+ name: 'UpdatedComponent - Default',
2023
+ component_name: 'UpdatedComponent',
2024
+ entity_sha: 'sha-updated',
2025
+ created_at: '2026-03-16 20:00:00',
2026
+ updated_at: '2026-03-16 23:20:00', // Updated in session
2027
+ })
2028
+ .execute();
2029
+ const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
2030
+ expect(result).toEqual([
2031
+ {
2032
+ entitySha: 'sha-updated',
2033
+ name: 'UpdatedComponent',
2034
+ scenarioCount: 1,
2035
+ preExisting: false,
2036
+ },
2037
+ ]);
2038
+ });
2039
+ it('should skip scenarios with null entity_sha', async () => {
2040
+ await db
2041
+ .insertInto('editor_scenarios')
2042
+ .values({
2043
+ id: 'sc-null',
2044
+ project_id: projectId,
2045
+ name: 'Orphan Scenario',
2046
+ component_name: 'Orphan',
2047
+ created_at: '2026-03-16 23:00:00',
2048
+ })
2049
+ .execute();
2050
+ const result = await queryIncompleteEntities(db, projectId, null);
2051
+ expect(result).toEqual([]);
2052
+ });
2053
+ it('should return empty when there are no scenarios', async () => {
2054
+ const result = await queryIncompleteEntities(db, projectId, null);
2055
+ expect(result).toEqual([]);
2056
+ });
2057
+ it('should not flag entities when a sibling version (same name+filePath) has analyses', async () => {
2058
+ // Old entity version WITH analysis
2059
+ await db
2060
+ .insertInto('entities')
2061
+ .values({
2062
+ sha: 'sha-btn-v1',
2063
+ name: 'OpenLibraryButton',
2064
+ entity_type: 'visual',
2065
+ file_path: 'src/components/OpenLibraryButton.tsx',
2066
+ })
2067
+ .execute();
2068
+ await db
2069
+ .insertInto('analyses')
2070
+ .values({
2071
+ id: 'a-btn-v1',
2072
+ entity_sha: 'sha-btn-v1',
2073
+ entity_name: 'OpenLibraryButton',
2074
+ project_id: projectId,
2075
+ })
2076
+ .execute();
2077
+ // New entity version WITHOUT analysis (created by file watcher)
2078
+ await db
2079
+ .insertInto('entities')
2080
+ .values({
2081
+ sha: 'sha-btn-v2',
2082
+ name: 'OpenLibraryButton',
2083
+ entity_type: 'visual',
2084
+ file_path: 'src/components/OpenLibraryButton.tsx',
2085
+ })
2086
+ .execute();
2087
+ // Scenario points to the NEW version (backfilled after file watcher)
2088
+ await db
2089
+ .insertInto('editor_scenarios')
2090
+ .values({
2091
+ id: 'sc-btn',
2092
+ project_id: projectId,
2093
+ name: 'OpenLibraryButton - Default',
2094
+ component_name: 'OpenLibraryButton',
2095
+ entity_sha: 'sha-btn-v2',
2096
+ created_at: '2026-03-16 23:00:00',
2097
+ })
2098
+ .execute();
2099
+ // Should NOT flag as incomplete — sibling version has analyses
2100
+ const result = await queryIncompleteEntities(db, projectId, null);
2101
+ expect(result).toEqual([]);
2102
+ });
2103
+ it('should still flag entities when no sibling version has analyses', async () => {
2104
+ // Only one version, no analyses
2105
+ await db
2106
+ .insertInto('entities')
2107
+ .values({
2108
+ sha: 'sha-icon',
2109
+ name: 'ExternalLinkIcon',
2110
+ entity_type: 'visual',
2111
+ file_path: 'src/components/ExternalLinkIcon.tsx',
2112
+ })
2113
+ .execute();
2114
+ await db
2115
+ .insertInto('editor_scenarios')
2116
+ .values({
2117
+ id: 'sc-icon',
2118
+ project_id: projectId,
2119
+ name: 'ExternalLinkIcon - Default',
2120
+ component_name: 'ExternalLinkIcon',
2121
+ entity_sha: 'sha-icon',
2122
+ created_at: '2026-03-16 23:00:00',
2123
+ })
2124
+ .execute();
2125
+ // Should flag as incomplete — no version has analyses
2126
+ const result = await queryIncompleteEntities(db, projectId, null);
2127
+ expect(result).toEqual([
2128
+ {
2129
+ entitySha: 'sha-icon',
2130
+ name: 'ExternalLinkIcon',
2131
+ scenarioCount: 1,
2132
+ preExisting: false,
2133
+ },
2134
+ ]);
2135
+ });
2136
+ it('should use entity name from entities table, falling back to component_name', async () => {
2137
+ // Scenario has entity_sha but entity record doesn't exist
2138
+ await db
2139
+ .insertInto('editor_scenarios')
2140
+ .values({
2141
+ id: 'sc-1',
2142
+ project_id: projectId,
2143
+ name: 'Ghost - Default',
2144
+ component_name: 'GhostComponent',
2145
+ entity_sha: 'sha-ghost',
2146
+ created_at: '2026-03-16 23:00:00',
2147
+ })
2148
+ .execute();
2149
+ const result = await queryIncompleteEntities(db, projectId, null);
2150
+ expect(result).toEqual([
2151
+ {
2152
+ entitySha: 'sha-ghost',
2153
+ name: 'GhostComponent',
2154
+ scenarioCount: 1,
2155
+ preExisting: false,
2156
+ },
2157
+ ]);
2158
+ });
2159
+ it('should detect incomplete entity whose scenario predates the session', async () => {
2160
+ // Entity with no analyses, scenario created BEFORE session
2161
+ await db
2162
+ .insertInto('entities')
2163
+ .values({
2164
+ sha: 'sha-preexisting',
2165
+ name: 'PreExistingComponent',
2166
+ entity_type: 'visual',
2167
+ file_path: 'src/PreExistingComponent.tsx',
2168
+ })
2169
+ .execute();
2170
+ await db
2171
+ .insertInto('editor_scenarios')
2172
+ .values({
2173
+ id: 'sc-preexisting',
2174
+ project_id: projectId,
2175
+ name: 'PreExistingComponent - Default',
2176
+ component_name: 'PreExistingComponent',
2177
+ entity_sha: 'sha-preexisting',
2178
+ created_at: '2026-03-16 20:00:00',
2179
+ updated_at: '2026-03-16 20:00:00',
2180
+ })
2181
+ .execute();
2182
+ // Session started well after scenario was created/updated
2183
+ const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
2184
+ // Should still be detected — the old time filter would have excluded it
2185
+ expect(result).toEqual([
2186
+ {
2187
+ entitySha: 'sha-preexisting',
2188
+ name: 'PreExistingComponent',
2189
+ scenarioCount: 1,
2190
+ preExisting: true,
2191
+ },
2192
+ ]);
2193
+ });
2194
+ it('should flag preExisting: true when all scenarios predate the session', async () => {
2195
+ await db
2196
+ .insertInto('entities')
2197
+ .values({
2198
+ sha: 'sha-old-entity',
2199
+ name: 'OldEntity',
2200
+ entity_type: 'visual',
2201
+ file_path: 'src/OldEntity.tsx',
2202
+ })
2203
+ .execute();
2204
+ // Two scenarios, both before session
2205
+ await db
2206
+ .insertInto('editor_scenarios')
2207
+ .values({
2208
+ id: 'sc-old-1',
2209
+ project_id: projectId,
2210
+ name: 'OldEntity - Default',
2211
+ component_name: 'OldEntity',
2212
+ entity_sha: 'sha-old-entity',
2213
+ created_at: '2026-03-16 19:00:00',
2214
+ updated_at: '2026-03-16 19:00:00',
2215
+ })
2216
+ .execute();
2217
+ await db
2218
+ .insertInto('editor_scenarios')
2219
+ .values({
2220
+ id: 'sc-old-2',
2221
+ project_id: projectId,
2222
+ name: 'OldEntity - Hover',
2223
+ component_name: 'OldEntity',
2224
+ entity_sha: 'sha-old-entity',
2225
+ created_at: '2026-03-16 19:30:00',
2226
+ updated_at: '2026-03-16 19:30:00',
2227
+ })
2228
+ .execute();
2229
+ const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
2230
+ expect(result).toEqual([
2231
+ {
2232
+ entitySha: 'sha-old-entity',
2233
+ name: 'OldEntity',
2234
+ scenarioCount: 2,
2235
+ preExisting: true,
2236
+ },
2237
+ ]);
2238
+ });
2239
+ it('should flag preExisting: false when scenario is from the current session', async () => {
2240
+ await db
2241
+ .insertInto('entities')
2242
+ .values({
2243
+ sha: 'sha-session-entity',
2244
+ name: 'SessionEntity',
2245
+ entity_type: 'visual',
2246
+ file_path: 'src/SessionEntity.tsx',
2247
+ })
2248
+ .execute();
2249
+ await db
2250
+ .insertInto('editor_scenarios')
2251
+ .values({
2252
+ id: 'sc-session',
2253
+ project_id: projectId,
2254
+ name: 'SessionEntity - Default',
2255
+ component_name: 'SessionEntity',
2256
+ entity_sha: 'sha-session-entity',
2257
+ created_at: '2026-03-16 23:30:00',
2258
+ updated_at: '2026-03-16 23:30:00',
2259
+ })
2260
+ .execute();
2261
+ const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
2262
+ expect(result).toEqual([
2263
+ {
2264
+ entitySha: 'sha-session-entity',
2265
+ name: 'SessionEntity',
2266
+ scenarioCount: 1,
2267
+ preExisting: false,
2268
+ },
2269
+ ]);
2270
+ });
2271
+ });
2272
+ // ── identifyScenariosNeedingRecapture ──────────────────────────────
2273
+ describe('identifyScenariosNeedingRecapture', () => {
2274
+ // Reproduces the Margo bug: Feature 1 built app-level popup scenarios,
2275
+ // Feature 2 edited LibraryView (used by App), but app-level scenarios
2276
+ // were never flagged for recapture because the audit only checked
2277
+ // component scenario existence — not whether app-level scenarios are stale.
2278
+ //
2279
+ // Each scenario's entityName is resolved by the caller via
2280
+ // entity_sha → entities.name (the default export for app-level scenarios).
2281
+ it('should flag app-level scenario when its entity is impacted by transitive dependency change', () => {
2282
+ // LibraryView was edited → App is impacted (imports LibraryView)
2283
+ // App-level scenario "Library - Rich Library" has entity_sha pointing to App
2284
+ // It was NOT recaptured during Feature 2 → should be flagged
2285
+ const entityChangeStatus = {
2286
+ LibraryView: { status: 'edited' },
2287
+ App: {
2288
+ status: 'impacted',
2289
+ impactedBy: [
2290
+ {
2291
+ name: 'LibraryView',
2292
+ filePath: 'src/components/LibraryView.tsx',
2293
+ changeType: 'edited',
2294
+ },
2295
+ ],
2296
+ },
2297
+ };
2298
+ const result = identifyScenariosNeedingRecapture({
2299
+ scenarios: [
2300
+ {
2301
+ name: 'Library - Rich Library',
2302
+ entityName: 'App', // resolved from entity_sha → entities.name
2303
+ updatedInSession: false,
2304
+ },
2305
+ ],
2306
+ entityChangeStatus,
2307
+ });
2308
+ expect(result).toHaveLength(1);
2309
+ expect(result[0].scenarioName).toBe('Library - Rich Library');
2310
+ expect(result[0].entityName).toBe('App');
2311
+ expect(result[0].status.status).toBe('impacted');
2312
+ });
2313
+ it('should flag component scenario when its entity is directly edited and not recaptured', () => {
2314
+ const entityChangeStatus = {
2315
+ LibraryView: { status: 'edited' },
2316
+ };
2317
+ const result = identifyScenariosNeedingRecapture({
2318
+ scenarios: [
2319
+ {
2320
+ name: 'LibraryView - Empty',
2321
+ entityName: 'LibraryView',
2322
+ updatedInSession: false,
2323
+ },
2324
+ ],
2325
+ entityChangeStatus,
2326
+ });
2327
+ expect(result).toHaveLength(1);
2328
+ expect(result[0].scenarioName).toBe('LibraryView - Empty');
2329
+ expect(result[0].entityName).toBe('LibraryView');
2330
+ expect(result[0].status.status).toBe('edited');
2331
+ });
2332
+ it('should NOT flag scenario that was already recaptured in the current session', () => {
2333
+ const entityChangeStatus = {
2334
+ App: {
2335
+ status: 'impacted',
2336
+ impactedBy: [
2337
+ {
2338
+ name: 'LibraryView',
2339
+ filePath: 'src/components/LibraryView.tsx',
2340
+ changeType: 'edited',
2341
+ },
2342
+ ],
2343
+ },
2344
+ };
2345
+ const result = identifyScenariosNeedingRecapture({
2346
+ scenarios: [
2347
+ {
2348
+ name: 'App - Default',
2349
+ entityName: 'App',
2350
+ updatedInSession: true, // re-registered during Feature 2
2351
+ },
2352
+ ],
2353
+ entityChangeStatus,
2354
+ });
2355
+ expect(result).toHaveLength(0);
2356
+ });
2357
+ it('should NOT flag scenario whose entity has no change status', () => {
2358
+ const entityChangeStatus = {
2359
+ LibraryView: { status: 'edited' },
2360
+ };
2361
+ const result = identifyScenariosNeedingRecapture({
2362
+ scenarios: [
2363
+ {
2364
+ name: 'WelcomeScreen - Default',
2365
+ entityName: 'WelcomeScreen',
2366
+ updatedInSession: false,
2367
+ },
2368
+ ],
2369
+ entityChangeStatus,
2370
+ });
2371
+ expect(result).toHaveLength(0);
2372
+ });
2373
+ it('should return empty array when entityChangeStatus is undefined', () => {
2374
+ const result = identifyScenariosNeedingRecapture({
2375
+ scenarios: [
2376
+ {
2377
+ name: 'Library - Rich Library',
2378
+ entityName: 'App',
2379
+ updatedInSession: false,
2380
+ },
2381
+ ],
2382
+ entityChangeStatus: undefined,
2383
+ });
2384
+ expect(result).toHaveLength(0);
2385
+ });
2386
+ it('should return empty array when entityChangeStatus is empty', () => {
2387
+ const result = identifyScenariosNeedingRecapture({
2388
+ scenarios: [
2389
+ {
2390
+ name: 'Library - Rich Library',
2391
+ entityName: 'App',
2392
+ updatedInSession: false,
2393
+ },
2394
+ ],
2395
+ entityChangeStatus: {},
2396
+ });
2397
+ expect(result).toHaveLength(0);
2398
+ });
2399
+ it('should flag multiple app-level scenarios sharing the same impacted entity', () => {
2400
+ const entityChangeStatus = {
2401
+ LibraryView: { status: 'edited' },
2402
+ App: {
2403
+ status: 'impacted',
2404
+ impactedBy: [
2405
+ {
2406
+ name: 'LibraryView',
2407
+ filePath: 'src/components/LibraryView.tsx',
2408
+ changeType: 'edited',
2409
+ },
2410
+ ],
2411
+ },
2412
+ };
2413
+ const result = identifyScenariosNeedingRecapture({
2414
+ scenarios: [
2415
+ {
2416
+ name: 'Library - Empty',
2417
+ entityName: 'App',
2418
+ updatedInSession: false,
2419
+ },
2420
+ {
2421
+ name: 'Library - Rich Library',
2422
+ entityName: 'App',
2423
+ updatedInSession: false,
2424
+ },
2425
+ {
2426
+ name: 'First Article Saved',
2427
+ entityName: 'App',
2428
+ updatedInSession: false,
2429
+ },
2430
+ ],
2431
+ entityChangeStatus,
2432
+ });
2433
+ expect(result).toHaveLength(3);
2434
+ expect(result.map((r) => r.scenarioName).sort()).toEqual([
2435
+ 'First Article Saved',
2436
+ 'Library - Empty',
2437
+ 'Library - Rich Library',
2438
+ ]);
2439
+ expect(result.every((r) => r.entityName === 'App')).toBe(true);
2440
+ });
2441
+ it('should skip scenarios with null entityName (no entity_sha set)', () => {
2442
+ const entityChangeStatus = {
2443
+ App: { status: 'edited' },
2444
+ };
2445
+ const result = identifyScenariosNeedingRecapture({
2446
+ scenarios: [
2447
+ {
2448
+ name: 'Mystery Scenario',
2449
+ entityName: null, // no entity_sha → no entity name
2450
+ updatedInSession: false,
2451
+ },
2452
+ ],
2453
+ entityChangeStatus,
2454
+ });
2455
+ expect(result).toHaveLength(0);
2456
+ });
2457
+ });
2458
+ // ── detectDuplicateNames ──────────────────────────────────────────
2459
+ describe('detectDuplicateNames', () => {
2460
+ it('should return empty map when no duplicates exist', () => {
2461
+ const entries = [
2462
+ { name: 'Header', filePath: 'app/components/Header.tsx' },
2463
+ { name: 'Footer', filePath: 'app/components/Footer.tsx' },
2464
+ { name: 'Sidebar', filePath: 'app/components/Sidebar.tsx' },
2465
+ ];
2466
+ const result = detectDuplicateNames(entries);
2467
+ expect(result.size).toBe(0);
2468
+ });
2469
+ it('should group entries that share a name', () => {
2470
+ const entries = [
2471
+ { name: 'Page', filePath: 'app/isolated-components/Foo/page.tsx' },
2472
+ { name: 'Page', filePath: 'app/isolated-components/Bar/page.tsx' },
2473
+ { name: 'Page', filePath: 'app/isolated-components/Baz/page.tsx' },
2474
+ { name: 'Header', filePath: 'app/components/Header.tsx' },
2475
+ ];
2476
+ const result = detectDuplicateNames(entries);
2477
+ expect(result.size).toBe(1);
2478
+ expect(result.has('Page')).toBe(true);
2479
+ const pageGroup = result.get('Page');
2480
+ expect(pageGroup).toHaveLength(3);
2481
+ expect(pageGroup.map((e) => e.filePath)).toEqual([
2482
+ 'app/isolated-components/Foo/page.tsx',
2483
+ 'app/isolated-components/Bar/page.tsx',
2484
+ 'app/isolated-components/Baz/page.tsx',
2485
+ ]);
2486
+ });
2487
+ it('should exclude single-occurrence names', () => {
2488
+ const entries = [
2489
+ { name: 'Page', filePath: 'app/isolated-components/Foo/page.tsx' },
2490
+ { name: 'Page', filePath: 'app/isolated-components/Bar/page.tsx' },
2491
+ { name: 'Header', filePath: 'app/components/Header.tsx' },
2492
+ { name: 'Footer', filePath: 'app/components/Footer.tsx' },
2493
+ ];
2494
+ const result = detectDuplicateNames(entries);
2495
+ expect(result.size).toBe(1);
2496
+ expect(result.has('Header')).toBe(false);
2497
+ expect(result.has('Footer')).toBe(false);
2498
+ });
2499
+ it('should handle multiple duplicate groups', () => {
2500
+ const entries = [
2501
+ { name: 'Page', filePath: 'app/isolated-components/A/page.tsx' },
2502
+ { name: 'Page', filePath: 'app/isolated-components/B/page.tsx' },
2503
+ { name: 'Layout', filePath: 'app/isolated-components/A/layout.tsx' },
2504
+ { name: 'Layout', filePath: 'app/isolated-components/B/layout.tsx' },
2505
+ { name: 'Unique', filePath: 'app/components/Unique.tsx' },
2506
+ ];
2507
+ const result = detectDuplicateNames(entries);
2508
+ expect(result.size).toBe(2);
2509
+ expect(result.has('Page')).toBe(true);
2510
+ expect(result.has('Layout')).toBe(true);
2511
+ expect(result.get('Page')).toHaveLength(2);
2512
+ expect(result.get('Layout')).toHaveLength(2);
2513
+ });
2514
+ it('should return empty map for empty input', () => {
2515
+ const result = detectDuplicateNames([]);
2516
+ expect(result.size).toBe(0);
2517
+ });
2518
+ });
986
2519
  });
987
2520
  //# sourceMappingURL=editorAudit.test.js.map