@codeyam/codeyam-cli 0.1.0-staging.b6c4c78 → 0.1.0-staging.b8b17a5

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 (101) hide show
  1. package/analyzer-template/.build-info.json +8 -8
  2. package/analyzer-template/log.txt +3 -3
  3. package/analyzer-template/packages/database/src/lib/kysely/tables/editorScenariosTable.ts +26 -16
  4. package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/editorScenariosTable.d.ts +0 -1
  5. package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/editorScenariosTable.d.ts.map +1 -1
  6. package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/editorScenariosTable.js +28 -16
  7. package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/editorScenariosTable.js.map +1 -1
  8. package/codeyam-cli/src/cli.js +9 -0
  9. package/codeyam-cli/src/cli.js.map +1 -1
  10. package/codeyam-cli/src/commands/__tests__/editor.isolateArgs.test.js +51 -0
  11. package/codeyam-cli/src/commands/__tests__/editor.isolateArgs.test.js.map +1 -0
  12. package/codeyam-cli/src/commands/__tests__/editor.stepDispatch.test.js +11 -0
  13. package/codeyam-cli/src/commands/__tests__/editor.stepDispatch.test.js.map +1 -1
  14. package/codeyam-cli/src/commands/editor.js +460 -65
  15. package/codeyam-cli/src/commands/editor.js.map +1 -1
  16. package/codeyam-cli/src/commands/editorIsolateArgs.js +25 -0
  17. package/codeyam-cli/src/commands/editorIsolateArgs.js.map +1 -0
  18. package/codeyam-cli/src/commands/telemetry.js +37 -0
  19. package/codeyam-cli/src/commands/telemetry.js.map +1 -0
  20. package/codeyam-cli/src/utils/__tests__/editorAudit.test.js +994 -1
  21. package/codeyam-cli/src/utils/__tests__/editorAudit.test.js.map +1 -1
  22. package/codeyam-cli/src/utils/__tests__/editorDeleteScenario.test.js +100 -0
  23. package/codeyam-cli/src/utils/__tests__/editorDeleteScenario.test.js.map +1 -0
  24. package/codeyam-cli/src/utils/__tests__/editorEntityChangeStatus.test.js +70 -0
  25. package/codeyam-cli/src/utils/__tests__/editorEntityChangeStatus.test.js.map +1 -1
  26. package/codeyam-cli/src/utils/__tests__/editorEntityHelpers.test.js +97 -5
  27. package/codeyam-cli/src/utils/__tests__/editorEntityHelpers.test.js.map +1 -1
  28. package/codeyam-cli/src/utils/__tests__/editorLoaderHelpers.test.js +40 -1
  29. package/codeyam-cli/src/utils/__tests__/editorLoaderHelpers.test.js.map +1 -1
  30. package/codeyam-cli/src/utils/__tests__/editorMigration.test.js +5 -0
  31. package/codeyam-cli/src/utils/__tests__/editorMigration.test.js.map +1 -1
  32. package/codeyam-cli/src/utils/__tests__/editorScenarios.test.js +354 -22
  33. package/codeyam-cli/src/utils/__tests__/editorScenarios.test.js.map +1 -1
  34. package/codeyam-cli/src/utils/__tests__/editorSeedAdapterPrismaValidation.test.js +143 -0
  35. package/codeyam-cli/src/utils/__tests__/editorSeedAdapterPrismaValidation.test.js.map +1 -0
  36. package/codeyam-cli/src/utils/__tests__/entityChangeStatus.test.js +28 -0
  37. package/codeyam-cli/src/utils/__tests__/entityChangeStatus.test.js.map +1 -1
  38. package/codeyam-cli/src/utils/__tests__/scenariosManifest.test.js +40 -1
  39. package/codeyam-cli/src/utils/__tests__/scenariosManifest.test.js.map +1 -1
  40. package/codeyam-cli/src/utils/__tests__/telemetry.test.js +159 -0
  41. package/codeyam-cli/src/utils/__tests__/telemetry.test.js.map +1 -0
  42. package/codeyam-cli/src/utils/editorAudit.js +179 -1
  43. package/codeyam-cli/src/utils/editorAudit.js.map +1 -1
  44. package/codeyam-cli/src/utils/editorDeleteScenario.js +67 -0
  45. package/codeyam-cli/src/utils/editorDeleteScenario.js.map +1 -0
  46. package/codeyam-cli/src/utils/editorEntityChangeStatus.js +13 -7
  47. package/codeyam-cli/src/utils/editorEntityChangeStatus.js.map +1 -1
  48. package/codeyam-cli/src/utils/editorEntityHelpers.js +18 -3
  49. package/codeyam-cli/src/utils/editorEntityHelpers.js.map +1 -1
  50. package/codeyam-cli/src/utils/editorLoaderHelpers.js +14 -2
  51. package/codeyam-cli/src/utils/editorLoaderHelpers.js.map +1 -1
  52. package/codeyam-cli/src/utils/editorMigration.js +1 -1
  53. package/codeyam-cli/src/utils/editorMigration.js.map +1 -1
  54. package/codeyam-cli/src/utils/editorScenarios.js +150 -2
  55. package/codeyam-cli/src/utils/editorScenarios.js.map +1 -1
  56. package/codeyam-cli/src/utils/editorSeedAdapter.js +70 -0
  57. package/codeyam-cli/src/utils/editorSeedAdapter.js.map +1 -1
  58. package/codeyam-cli/src/utils/entityChangeStatus.js +8 -2
  59. package/codeyam-cli/src/utils/entityChangeStatus.js.map +1 -1
  60. package/codeyam-cli/src/utils/fileWatcher.js +38 -0
  61. package/codeyam-cli/src/utils/fileWatcher.js.map +1 -1
  62. package/codeyam-cli/src/utils/scenariosManifest.js +15 -10
  63. package/codeyam-cli/src/utils/scenariosManifest.js.map +1 -1
  64. package/codeyam-cli/src/utils/telemetry.js +106 -0
  65. package/codeyam-cli/src/utils/telemetry.js.map +1 -0
  66. package/codeyam-cli/src/utils/telemetryMiddleware.js +22 -0
  67. package/codeyam-cli/src/utils/telemetryMiddleware.js.map +1 -0
  68. package/codeyam-cli/src/webserver/__tests__/buildPtyEnv.test.js +35 -0
  69. package/codeyam-cli/src/webserver/__tests__/buildPtyEnv.test.js.map +1 -0
  70. package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js +61 -0
  71. package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js.map +1 -1
  72. package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-DYqG1D_d.js +58 -0
  73. package/codeyam-cli/src/webserver/build/client/assets/editorPreview-DggyRwOr.js +41 -0
  74. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha.scenarios._scenarioId.dev-BOi8kpwd.js → entity._sha.scenarios._scenarioId.dev-D1eikpe1.js} +1 -1
  75. package/codeyam-cli/src/webserver/build/client/assets/globals-DRvOjyO3.css +1 -0
  76. package/codeyam-cli/src/webserver/build/client/assets/{manifest-5f1c29f5.js → manifest-f4212c17.js} +1 -1
  77. package/codeyam-cli/src/webserver/build/client/assets/{root-BBCQJ_ZM.js → root-F-k2uYj5.js} +15 -15
  78. package/codeyam-cli/src/webserver/build/server/assets/analysisRunner-if8kM_1Q.js +13 -0
  79. package/codeyam-cli/src/webserver/build/server/assets/{index-BLKsJR3o.js → index-CHymws6l.js} +1 -1
  80. package/codeyam-cli/src/webserver/build/server/assets/init-D3HkMDbI.js +10 -0
  81. package/codeyam-cli/src/webserver/build/server/assets/progress-CHTtrxFG.js +1 -0
  82. package/codeyam-cli/src/webserver/build/server/assets/server-build-DTCzJQiH.js +551 -0
  83. package/codeyam-cli/src/webserver/build/server/index.js +1 -1
  84. package/codeyam-cli/src/webserver/build-info.json +5 -5
  85. package/codeyam-cli/src/webserver/editorProxy.js +78 -3
  86. package/codeyam-cli/src/webserver/editorProxy.js.map +1 -1
  87. package/codeyam-cli/src/webserver/server.js +32 -0
  88. package/codeyam-cli/src/webserver/server.js.map +1 -1
  89. package/codeyam-cli/src/webserver/terminalServer.js +7 -2
  90. package/codeyam-cli/src/webserver/terminalServer.js.map +1 -1
  91. package/codeyam-cli/templates/editor-step-hook.py +7 -0
  92. package/codeyam-cli/templates/nextjs-prisma-sqlite/package.json +1 -1
  93. package/codeyam-cli/templates/nextjs-prisma-supabase/package.json +1 -1
  94. package/package.json +2 -1
  95. package/packages/database/src/lib/kysely/tables/editorScenariosTable.js +28 -16
  96. package/packages/database/src/lib/kysely/tables/editorScenariosTable.js.map +1 -1
  97. package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-y_5LB2iU.js +0 -58
  98. package/codeyam-cli/src/webserver/build/client/assets/editorPreview-DBa7T2FK.js +0 -41
  99. package/codeyam-cli/src/webserver/build/client/assets/globals-BCTpZEY8.css +0 -1
  100. package/codeyam-cli/src/webserver/build/server/assets/init-C2iMAqYu.js +0 -10
  101. package/codeyam-cli/src/webserver/build/server/assets/server-build-DR42Xd5a.js +0 -489
@@ -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, queryIncompleteEntities, queryMiscategorizedScenarios, isOnlyIncompleteEntities, isAutoRemediable, } 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,975 @@ describe('editorAudit', () => {
983
1006
  expect(counts).toEqual({ ArticleCard: 1, ArticleRow: 1 });
984
1007
  });
985
1008
  });
1009
+ // ── Audit + entity completeness integration ─────────────────────────
1010
+ describe('audit should catch incomplete entities (bug reproduction)', () => {
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('component_name', 'varchar')
1023
+ .addColumn('component_path', 'varchar')
1024
+ .addColumn('entity_sha', 'varchar')
1025
+ .addColumn('display_name', 'varchar')
1026
+ .addColumn('page_file_path', 'varchar')
1027
+ .addColumn('url', 'varchar')
1028
+ .addColumn('created_at', 'datetime')
1029
+ .addColumn('updated_at', 'datetime')
1030
+ .execute();
1031
+ await db.schema
1032
+ .createTable('analyses')
1033
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1034
+ .addColumn('entity_sha', 'varchar')
1035
+ .addColumn('entity_name', 'varchar')
1036
+ .addColumn('project_id', 'varchar')
1037
+ .execute();
1038
+ await db.schema
1039
+ .createTable('entities')
1040
+ .addColumn('sha', 'varchar', (col) => col.primaryKey())
1041
+ .addColumn('name', 'varchar')
1042
+ .addColumn('entity_type', 'varchar')
1043
+ .addColumn('file_path', 'varchar')
1044
+ .execute();
1045
+ });
1046
+ afterEach(async () => {
1047
+ await db.destroy();
1048
+ });
1049
+ it('demonstrates the bug: computeAudit passes but entities are incomplete', async () => {
1050
+ // Setup: Two components in glossary, both have scenarios registered.
1051
+ // CollectionChips has scenarios but NO analysis records — it's "incomplete."
1052
+ // The glossary-based audit (computeAudit) doesn't know about entity analyses
1053
+ // so it says allPassing: true. This is the bug.
1054
+ // Entities in DB
1055
+ await db
1056
+ .insertInto('entities')
1057
+ .values({
1058
+ sha: 'sha-header',
1059
+ name: 'Header',
1060
+ entity_type: 'visual',
1061
+ file_path: 'src/components/Header.tsx',
1062
+ })
1063
+ .execute();
1064
+ await db
1065
+ .insertInto('entities')
1066
+ .values({
1067
+ sha: 'sha-chips',
1068
+ name: 'CollectionChips',
1069
+ entity_type: 'visual',
1070
+ file_path: 'src/components/CollectionChips.tsx',
1071
+ })
1072
+ .execute();
1073
+ // Header has an analysis — it's complete
1074
+ await db
1075
+ .insertInto('analyses')
1076
+ .values({
1077
+ id: 'a-1',
1078
+ entity_sha: 'sha-header',
1079
+ entity_name: 'Header',
1080
+ project_id: projectId,
1081
+ })
1082
+ .execute();
1083
+ // CollectionChips has NO analysis — it's incomplete
1084
+ // Both have scenarios
1085
+ await db
1086
+ .insertInto('editor_scenarios')
1087
+ .values({
1088
+ id: 'sc-1',
1089
+ project_id: projectId,
1090
+ name: 'Header - Default',
1091
+ component_name: 'Header',
1092
+ entity_sha: 'sha-header',
1093
+ created_at: '2026-03-16 23:00:00',
1094
+ })
1095
+ .execute();
1096
+ await db
1097
+ .insertInto('editor_scenarios')
1098
+ .values({
1099
+ id: 'sc-2',
1100
+ project_id: projectId,
1101
+ name: 'CollectionChips - Default',
1102
+ component_name: 'CollectionChips',
1103
+ entity_sha: 'sha-chips',
1104
+ created_at: '2026-03-16 23:19:00',
1105
+ })
1106
+ .execute();
1107
+ await db
1108
+ .insertInto('editor_scenarios')
1109
+ .values({
1110
+ id: 'sc-3',
1111
+ project_id: projectId,
1112
+ name: 'CollectionChips - Many',
1113
+ component_name: 'CollectionChips',
1114
+ entity_sha: 'sha-chips',
1115
+ created_at: '2026-03-16 23:19:05',
1116
+ })
1117
+ .execute();
1118
+ // The glossary says both are components with scenarios
1119
+ const scenarioCounts = await queryScenarioCounts(db, projectId, null);
1120
+ expect(scenarioCounts).toEqual({ Header: 1, CollectionChips: 2 });
1121
+ // computeAudit only checks glossary coverage — it passes!
1122
+ const auditResult = computeAudit({
1123
+ components: [
1124
+ {
1125
+ name: 'Header',
1126
+ filePath: 'src/components/Header.tsx',
1127
+ returnType: 'JSX.Element',
1128
+ },
1129
+ {
1130
+ name: 'CollectionChips',
1131
+ filePath: 'src/components/CollectionChips.tsx',
1132
+ returnType: 'JSX.Element',
1133
+ },
1134
+ ],
1135
+ functions: [],
1136
+ scenarioCounts,
1137
+ testFileExistence: {},
1138
+ });
1139
+ // BUG: computeAudit alone says everything is fine
1140
+ expect(auditResult.summary.allPassing).toBe(true);
1141
+ expect(auditResult.summary.componentsOk).toBe(2);
1142
+ // But queryIncompleteEntities catches the real issue
1143
+ const incomplete = await queryIncompleteEntities(db, projectId, null);
1144
+ expect(incomplete).toHaveLength(1);
1145
+ expect(incomplete[0].name).toBe('CollectionChips');
1146
+ expect(incomplete[0].scenarioCount).toBe(2);
1147
+ });
1148
+ it('audit should fail when combining computeAudit with incomplete entity check', async () => {
1149
+ // Same setup as above — this test shows the FIX working:
1150
+ // after computeAudit, we also check queryIncompleteEntities,
1151
+ // and if any are found, allPassing becomes false.
1152
+ await db
1153
+ .insertInto('entities')
1154
+ .values({
1155
+ sha: 'sha-header',
1156
+ name: 'Header',
1157
+ entity_type: 'visual',
1158
+ file_path: 'src/components/Header.tsx',
1159
+ })
1160
+ .execute();
1161
+ await db
1162
+ .insertInto('entities')
1163
+ .values({
1164
+ sha: 'sha-chips',
1165
+ name: 'CollectionChips',
1166
+ entity_type: 'visual',
1167
+ file_path: 'src/components/CollectionChips.tsx',
1168
+ })
1169
+ .execute();
1170
+ await db
1171
+ .insertInto('analyses')
1172
+ .values({
1173
+ id: 'a-1',
1174
+ entity_sha: 'sha-header',
1175
+ entity_name: 'Header',
1176
+ project_id: projectId,
1177
+ })
1178
+ .execute();
1179
+ await db
1180
+ .insertInto('editor_scenarios')
1181
+ .values({
1182
+ id: 'sc-1',
1183
+ project_id: projectId,
1184
+ name: 'Header - Default',
1185
+ component_name: 'Header',
1186
+ entity_sha: 'sha-header',
1187
+ created_at: '2026-03-16 23:00:00',
1188
+ })
1189
+ .execute();
1190
+ await db
1191
+ .insertInto('editor_scenarios')
1192
+ .values({
1193
+ id: 'sc-2',
1194
+ project_id: projectId,
1195
+ name: 'CollectionChips - Default',
1196
+ component_name: 'CollectionChips',
1197
+ entity_sha: 'sha-chips',
1198
+ created_at: '2026-03-16 23:19:00',
1199
+ })
1200
+ .execute();
1201
+ const scenarioCounts = await queryScenarioCounts(db, projectId, null);
1202
+ const auditResult = computeAudit({
1203
+ components: [
1204
+ {
1205
+ name: 'Header',
1206
+ filePath: 'src/components/Header.tsx',
1207
+ returnType: 'JSX.Element',
1208
+ },
1209
+ {
1210
+ name: 'CollectionChips',
1211
+ filePath: 'src/components/CollectionChips.tsx',
1212
+ returnType: 'JSX.Element',
1213
+ },
1214
+ ],
1215
+ functions: [],
1216
+ scenarioCounts,
1217
+ testFileExistence: {},
1218
+ });
1219
+ // Apply the same post-processing the audit endpoint does
1220
+ const incomplete = await queryIncompleteEntities(db, projectId, null);
1221
+ if (incomplete.length > 0) {
1222
+ auditResult.summary.allPassing = false;
1223
+ auditResult.summary.incompleteEntities = incomplete.length;
1224
+ }
1225
+ // NOW the audit correctly fails
1226
+ expect(auditResult.summary.allPassing).toBe(false);
1227
+ expect(auditResult.summary.incompleteEntities).toBe(1);
1228
+ });
1229
+ it('audit should pass when all entities have analyses', async () => {
1230
+ // Both entities have analyses — everything is complete
1231
+ await db
1232
+ .insertInto('entities')
1233
+ .values({
1234
+ sha: 'sha-header',
1235
+ name: 'Header',
1236
+ entity_type: 'visual',
1237
+ file_path: 'src/components/Header.tsx',
1238
+ })
1239
+ .execute();
1240
+ await db
1241
+ .insertInto('entities')
1242
+ .values({
1243
+ sha: 'sha-chips',
1244
+ name: 'CollectionChips',
1245
+ entity_type: 'visual',
1246
+ file_path: 'src/components/CollectionChips.tsx',
1247
+ })
1248
+ .execute();
1249
+ await db
1250
+ .insertInto('analyses')
1251
+ .values({
1252
+ id: 'a-1',
1253
+ entity_sha: 'sha-header',
1254
+ entity_name: 'Header',
1255
+ project_id: projectId,
1256
+ })
1257
+ .execute();
1258
+ await db
1259
+ .insertInto('analyses')
1260
+ .values({
1261
+ id: 'a-2',
1262
+ entity_sha: 'sha-chips',
1263
+ entity_name: 'CollectionChips',
1264
+ project_id: projectId,
1265
+ })
1266
+ .execute();
1267
+ await db
1268
+ .insertInto('editor_scenarios')
1269
+ .values({
1270
+ id: 'sc-1',
1271
+ project_id: projectId,
1272
+ name: 'Header - Default',
1273
+ component_name: 'Header',
1274
+ entity_sha: 'sha-header',
1275
+ created_at: '2026-03-16 23:00:00',
1276
+ })
1277
+ .execute();
1278
+ await db
1279
+ .insertInto('editor_scenarios')
1280
+ .values({
1281
+ id: 'sc-2',
1282
+ project_id: projectId,
1283
+ name: 'CollectionChips - Default',
1284
+ component_name: 'CollectionChips',
1285
+ entity_sha: 'sha-chips',
1286
+ created_at: '2026-03-16 23:19:00',
1287
+ })
1288
+ .execute();
1289
+ const scenarioCounts = await queryScenarioCounts(db, projectId, null);
1290
+ const auditResult = computeAudit({
1291
+ components: [
1292
+ {
1293
+ name: 'Header',
1294
+ filePath: 'src/components/Header.tsx',
1295
+ returnType: 'JSX.Element',
1296
+ },
1297
+ {
1298
+ name: 'CollectionChips',
1299
+ filePath: 'src/components/CollectionChips.tsx',
1300
+ returnType: 'JSX.Element',
1301
+ },
1302
+ ],
1303
+ functions: [],
1304
+ scenarioCounts,
1305
+ testFileExistence: {},
1306
+ });
1307
+ const incomplete = await queryIncompleteEntities(db, projectId, null);
1308
+ if (incomplete.length > 0) {
1309
+ auditResult.summary.allPassing = false;
1310
+ auditResult.summary.incompleteEntities = incomplete.length;
1311
+ }
1312
+ // Everything complete — audit passes
1313
+ expect(auditResult.summary.allPassing).toBe(true);
1314
+ expect(auditResult.summary.incompleteEntities).toBeUndefined();
1315
+ });
1316
+ });
1317
+ // ── queryMiscategorizedScenarios ─────────────────────────────────────
1318
+ describe('queryMiscategorizedScenarios', () => {
1319
+ let db;
1320
+ let rawDb;
1321
+ const projectId = 'test-project-id';
1322
+ beforeEach(async () => {
1323
+ rawDb = new Database(':memory:');
1324
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
1325
+ await db.schema
1326
+ .createTable('editor_scenarios')
1327
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1328
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
1329
+ .addColumn('name', 'varchar', (col) => col.notNull())
1330
+ .addColumn('component_name', 'varchar')
1331
+ .addColumn('component_path', 'varchar')
1332
+ .addColumn('entity_sha', 'varchar')
1333
+ .addColumn('display_name', 'varchar')
1334
+ .addColumn('page_file_path', 'varchar')
1335
+ .addColumn('url', 'varchar')
1336
+ .addColumn('created_at', 'datetime')
1337
+ .addColumn('updated_at', 'datetime')
1338
+ .execute();
1339
+ });
1340
+ afterEach(async () => {
1341
+ await db.destroy();
1342
+ });
1343
+ it('should return empty when all component scenarios use isolation routes', async () => {
1344
+ await db
1345
+ .insertInto('editor_scenarios')
1346
+ .values({
1347
+ id: 'sc-1',
1348
+ project_id: projectId,
1349
+ name: 'LibraryCard - Default',
1350
+ component_name: 'LibraryCard',
1351
+ url: '/isolated-components/LibraryCard?s=Default',
1352
+ created_at: '2026-03-17 12:00:00',
1353
+ })
1354
+ .execute();
1355
+ const result = await queryMiscategorizedScenarios(db, projectId, null);
1356
+ expect(result).toEqual([]);
1357
+ });
1358
+ it('should flag component scenarios that use non-isolation URLs', async () => {
1359
+ // This is the bug: "Full Library Page" registered as component_name=LibraryPage
1360
+ // but url=/library — it's pointing at the real page, not an isolation route
1361
+ await db
1362
+ .insertInto('editor_scenarios')
1363
+ .values({
1364
+ id: 'sc-1',
1365
+ project_id: projectId,
1366
+ name: 'Full Library Page',
1367
+ component_name: 'LibraryPage',
1368
+ url: '/library',
1369
+ created_at: '2026-03-17 12:41:40',
1370
+ })
1371
+ .execute();
1372
+ await db
1373
+ .insertInto('editor_scenarios')
1374
+ .values({
1375
+ id: 'sc-2',
1376
+ project_id: projectId,
1377
+ name: 'Empty Library Page',
1378
+ component_name: 'LibraryPage',
1379
+ url: '/library',
1380
+ created_at: '2026-03-17 12:41:51',
1381
+ })
1382
+ .execute();
1383
+ const result = await queryMiscategorizedScenarios(db, projectId, null);
1384
+ expect(result).toEqual([
1385
+ {
1386
+ componentName: 'LibraryPage',
1387
+ scenarioNames: ['Full Library Page', 'Empty Library Page'],
1388
+ url: '/library',
1389
+ },
1390
+ ]);
1391
+ });
1392
+ it('should not flag page-level scenarios (no component_name)', async () => {
1393
+ // App-level scenarios have no component_name — they're fine with real URLs
1394
+ await db
1395
+ .insertInto('editor_scenarios')
1396
+ .values({
1397
+ id: 'sc-1',
1398
+ project_id: projectId,
1399
+ name: 'Library with Articles',
1400
+ url: '/',
1401
+ created_at: '2026-03-17 12:25:14',
1402
+ })
1403
+ .execute();
1404
+ const result = await queryMiscategorizedScenarios(db, projectId, null);
1405
+ expect(result).toEqual([]);
1406
+ });
1407
+ it('should group miscategorized scenarios by component and URL', async () => {
1408
+ // Two different components both misusing real URLs
1409
+ await db
1410
+ .insertInto('editor_scenarios')
1411
+ .values({
1412
+ id: 'sc-1',
1413
+ project_id: projectId,
1414
+ name: 'Full Library Page',
1415
+ component_name: 'LibraryPage',
1416
+ url: '/library',
1417
+ created_at: '2026-03-17 12:41:40',
1418
+ })
1419
+ .execute();
1420
+ await db
1421
+ .insertInto('editor_scenarios')
1422
+ .values({
1423
+ id: 'sc-2',
1424
+ project_id: projectId,
1425
+ name: 'Dashboard - Full',
1426
+ component_name: 'Dashboard',
1427
+ url: '/dashboard',
1428
+ created_at: '2026-03-17 12:50:00',
1429
+ })
1430
+ .execute();
1431
+ const result = await queryMiscategorizedScenarios(db, projectId, null);
1432
+ expect(result).toHaveLength(2);
1433
+ expect(result.map((r) => r.componentName).sort()).toEqual([
1434
+ 'Dashboard',
1435
+ 'LibraryPage',
1436
+ ]);
1437
+ });
1438
+ it('should scope to session when featureStartedAt is provided', async () => {
1439
+ // Old miscategorized scenario — before session
1440
+ await db
1441
+ .insertInto('editor_scenarios')
1442
+ .values({
1443
+ id: 'sc-old',
1444
+ project_id: projectId,
1445
+ name: 'Old Page',
1446
+ component_name: 'OldComponent',
1447
+ url: '/old',
1448
+ created_at: '2026-03-16 10:00:00',
1449
+ })
1450
+ .execute();
1451
+ // New miscategorized scenario — in session
1452
+ await db
1453
+ .insertInto('editor_scenarios')
1454
+ .values({
1455
+ id: 'sc-new',
1456
+ project_id: projectId,
1457
+ name: 'Full Library Page',
1458
+ component_name: 'LibraryPage',
1459
+ url: '/library',
1460
+ created_at: '2026-03-17 12:41:40',
1461
+ })
1462
+ .execute();
1463
+ const result = await queryMiscategorizedScenarios(db, projectId, '2026-03-17T11:58:55.562Z');
1464
+ expect(result).toHaveLength(1);
1465
+ expect(result[0].componentName).toBe('LibraryPage');
1466
+ });
1467
+ it('should not flag component scenarios with null URL', async () => {
1468
+ await db
1469
+ .insertInto('editor_scenarios')
1470
+ .values({
1471
+ id: 'sc-1',
1472
+ project_id: projectId,
1473
+ name: 'NoUrl - Default',
1474
+ component_name: 'NoUrl',
1475
+ created_at: '2026-03-17 12:00:00',
1476
+ })
1477
+ .execute();
1478
+ const result = await queryMiscategorizedScenarios(db, projectId, null);
1479
+ expect(result).toEqual([]);
1480
+ });
1481
+ });
1482
+ // ── isOnlyIncompleteEntities ─────────────────────────────────────────
1483
+ describe('isOnlyIncompleteEntities', () => {
1484
+ it('should return true when incompleteEntities is the only failure', () => {
1485
+ expect(isOnlyIncompleteEntities({
1486
+ componentsMissing: 0,
1487
+ componentsWithErrors: 0,
1488
+ functionsFailing: 0,
1489
+ functionsNameMismatch: 0,
1490
+ functionsMissing: 0,
1491
+ missingFromGlossary: 0,
1492
+ incompleteEntities: 3,
1493
+ allPassing: false,
1494
+ })).toBe(true);
1495
+ });
1496
+ it('should return false when there are also missing components', () => {
1497
+ expect(isOnlyIncompleteEntities({
1498
+ componentsMissing: 1,
1499
+ componentsWithErrors: 0,
1500
+ functionsFailing: 0,
1501
+ functionsNameMismatch: 0,
1502
+ functionsMissing: 0,
1503
+ missingFromGlossary: 0,
1504
+ incompleteEntities: 2,
1505
+ allPassing: false,
1506
+ })).toBe(false);
1507
+ });
1508
+ it('should return false when there are also failing tests', () => {
1509
+ expect(isOnlyIncompleteEntities({
1510
+ componentsMissing: 0,
1511
+ componentsWithErrors: 0,
1512
+ functionsFailing: 1,
1513
+ functionsNameMismatch: 0,
1514
+ functionsMissing: 0,
1515
+ missingFromGlossary: 0,
1516
+ incompleteEntities: 2,
1517
+ allPassing: false,
1518
+ })).toBe(false);
1519
+ });
1520
+ it('should return false when there are also missing glossary entries', () => {
1521
+ expect(isOnlyIncompleteEntities({
1522
+ componentsMissing: 0,
1523
+ componentsWithErrors: 0,
1524
+ functionsFailing: 0,
1525
+ functionsNameMismatch: 0,
1526
+ functionsMissing: 0,
1527
+ missingFromGlossary: 1,
1528
+ incompleteEntities: 2,
1529
+ allPassing: false,
1530
+ })).toBe(false);
1531
+ });
1532
+ it('should return true even when incompleteEntities is 0 (no failures at all)', () => {
1533
+ // Edge case: all zeros means nothing is failing
1534
+ expect(isOnlyIncompleteEntities({
1535
+ componentsMissing: 0,
1536
+ componentsWithErrors: 0,
1537
+ functionsFailing: 0,
1538
+ functionsNameMismatch: 0,
1539
+ functionsMissing: 0,
1540
+ missingFromGlossary: 0,
1541
+ incompleteEntities: 0,
1542
+ allPassing: true,
1543
+ })).toBe(true);
1544
+ });
1545
+ it('should return false when there are also miscategorized scenarios', () => {
1546
+ expect(isOnlyIncompleteEntities({
1547
+ componentsMissing: 0,
1548
+ componentsWithErrors: 0,
1549
+ functionsFailing: 0,
1550
+ functionsNameMismatch: 0,
1551
+ functionsMissing: 0,
1552
+ missingFromGlossary: 0,
1553
+ miscategorizedScenarios: 1,
1554
+ incompleteEntities: 2,
1555
+ allPassing: false,
1556
+ })).toBe(false);
1557
+ });
1558
+ it('should handle missing fields gracefully', () => {
1559
+ // Summary from older API version might not have all fields
1560
+ expect(isOnlyIncompleteEntities({
1561
+ incompleteEntities: 2,
1562
+ allPassing: false,
1563
+ })).toBe(true);
1564
+ });
1565
+ });
1566
+ // ── isAutoRemediable ─────────────────────────────────────────────────
1567
+ describe('isAutoRemediable', () => {
1568
+ it('should return true on first attempt when only incomplete entities', () => {
1569
+ const result = isAutoRemediable({
1570
+ componentsMissing: 0,
1571
+ componentsWithErrors: 0,
1572
+ functionsFailing: 0,
1573
+ functionsNameMismatch: 0,
1574
+ functionsMissing: 0,
1575
+ missingFromGlossary: 0,
1576
+ miscategorizedScenarios: 0,
1577
+ incompleteEntities: 3,
1578
+ allPassing: false,
1579
+ }, false);
1580
+ expect(result).toBe(true);
1581
+ });
1582
+ it('should return false on second attempt (already tried once)', () => {
1583
+ // This is the key fix: if we already tried analyze-imports and
1584
+ // entities are STILL incomplete, don't try again — report the failure
1585
+ const result = isAutoRemediable({
1586
+ componentsMissing: 0,
1587
+ componentsWithErrors: 0,
1588
+ functionsFailing: 0,
1589
+ functionsNameMismatch: 0,
1590
+ functionsMissing: 0,
1591
+ missingFromGlossary: 0,
1592
+ miscategorizedScenarios: 0,
1593
+ incompleteEntities: 3,
1594
+ allPassing: false,
1595
+ }, true);
1596
+ expect(result).toBe(false);
1597
+ });
1598
+ it('should return false when there are other failures besides incomplete entities', () => {
1599
+ const result = isAutoRemediable({
1600
+ componentsMissing: 1,
1601
+ incompleteEntities: 3,
1602
+ allPassing: false,
1603
+ }, false);
1604
+ expect(result).toBe(false);
1605
+ });
1606
+ it('should return false when there are no incomplete entities', () => {
1607
+ const result = isAutoRemediable({
1608
+ componentsMissing: 1,
1609
+ allPassing: false,
1610
+ }, false);
1611
+ expect(result).toBe(false);
1612
+ });
1613
+ });
1614
+ // ── queryIncompleteEntities ─────────────────────────────────────────
1615
+ describe('queryIncompleteEntities', () => {
1616
+ let db;
1617
+ let rawDb;
1618
+ const projectId = 'test-project-id';
1619
+ beforeEach(async () => {
1620
+ rawDb = new Database(':memory:');
1621
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
1622
+ await db.schema
1623
+ .createTable('editor_scenarios')
1624
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1625
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
1626
+ .addColumn('name', 'varchar', (col) => col.notNull())
1627
+ .addColumn('component_name', 'varchar')
1628
+ .addColumn('component_path', 'varchar')
1629
+ .addColumn('entity_sha', 'varchar')
1630
+ .addColumn('display_name', 'varchar')
1631
+ .addColumn('page_file_path', 'varchar')
1632
+ .addColumn('url', 'varchar')
1633
+ .addColumn('created_at', 'datetime')
1634
+ .addColumn('updated_at', 'datetime')
1635
+ .execute();
1636
+ await db.schema
1637
+ .createTable('analyses')
1638
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1639
+ .addColumn('entity_sha', 'varchar')
1640
+ .addColumn('entity_name', 'varchar')
1641
+ .addColumn('project_id', 'varchar')
1642
+ .execute();
1643
+ await db.schema
1644
+ .createTable('entities')
1645
+ .addColumn('sha', 'varchar', (col) => col.primaryKey())
1646
+ .addColumn('name', 'varchar')
1647
+ .addColumn('entity_type', 'varchar')
1648
+ .addColumn('file_path', 'varchar')
1649
+ .execute();
1650
+ });
1651
+ afterEach(async () => {
1652
+ await db.destroy();
1653
+ });
1654
+ it('should return empty when all scenario entity SHAs have analyses', async () => {
1655
+ // Entity with analysis
1656
+ await db
1657
+ .insertInto('entities')
1658
+ .values({
1659
+ sha: 'sha-header',
1660
+ name: 'Header',
1661
+ entity_type: 'visual',
1662
+ file_path: 'src/Header.tsx',
1663
+ })
1664
+ .execute();
1665
+ await db
1666
+ .insertInto('analyses')
1667
+ .values({
1668
+ id: 'a-1',
1669
+ entity_sha: 'sha-header',
1670
+ entity_name: 'Header',
1671
+ project_id: projectId,
1672
+ })
1673
+ .execute();
1674
+ await db
1675
+ .insertInto('editor_scenarios')
1676
+ .values({
1677
+ id: 'sc-1',
1678
+ project_id: projectId,
1679
+ name: 'Header - Default',
1680
+ component_name: 'Header',
1681
+ entity_sha: 'sha-header',
1682
+ created_at: '2026-03-16 23:00:00',
1683
+ })
1684
+ .execute();
1685
+ const result = await queryIncompleteEntities(db, projectId, null);
1686
+ expect(result).toEqual([]);
1687
+ });
1688
+ it('should return entities with scenarios but no analyses', async () => {
1689
+ // Entity WITHOUT analysis
1690
+ await db
1691
+ .insertInto('entities')
1692
+ .values({
1693
+ sha: 'sha-chips',
1694
+ name: 'CollectionChips',
1695
+ entity_type: 'visual',
1696
+ file_path: 'src/components/CollectionChips.tsx',
1697
+ })
1698
+ .execute();
1699
+ // Scenario referencing that entity
1700
+ await db
1701
+ .insertInto('editor_scenarios')
1702
+ .values({
1703
+ id: 'sc-1',
1704
+ project_id: projectId,
1705
+ name: 'CollectionChips - Default',
1706
+ component_name: 'CollectionChips',
1707
+ entity_sha: 'sha-chips',
1708
+ created_at: '2026-03-16 23:00:00',
1709
+ })
1710
+ .execute();
1711
+ await db
1712
+ .insertInto('editor_scenarios')
1713
+ .values({
1714
+ id: 'sc-2',
1715
+ project_id: projectId,
1716
+ name: 'CollectionChips - Many',
1717
+ component_name: 'CollectionChips',
1718
+ entity_sha: 'sha-chips',
1719
+ created_at: '2026-03-16 23:01:00',
1720
+ })
1721
+ .execute();
1722
+ const result = await queryIncompleteEntities(db, projectId, null);
1723
+ expect(result).toEqual([
1724
+ { entitySha: 'sha-chips', name: 'CollectionChips', scenarioCount: 2 },
1725
+ ]);
1726
+ });
1727
+ it('should only return entities without analyses, not those with analyses', async () => {
1728
+ // Entity WITH analysis (Header)
1729
+ await db
1730
+ .insertInto('entities')
1731
+ .values({
1732
+ sha: 'sha-header',
1733
+ name: 'Header',
1734
+ entity_type: 'visual',
1735
+ file_path: 'src/Header.tsx',
1736
+ })
1737
+ .execute();
1738
+ await db
1739
+ .insertInto('analyses')
1740
+ .values({
1741
+ id: 'a-1',
1742
+ entity_sha: 'sha-header',
1743
+ entity_name: 'Header',
1744
+ project_id: projectId,
1745
+ })
1746
+ .execute();
1747
+ await db
1748
+ .insertInto('editor_scenarios')
1749
+ .values({
1750
+ id: 'sc-1',
1751
+ project_id: projectId,
1752
+ name: 'Header - Default',
1753
+ component_name: 'Header',
1754
+ entity_sha: 'sha-header',
1755
+ created_at: '2026-03-16 23:00:00',
1756
+ })
1757
+ .execute();
1758
+ // Entity WITHOUT analysis (CollectionPicker)
1759
+ await db
1760
+ .insertInto('entities')
1761
+ .values({
1762
+ sha: 'sha-picker',
1763
+ name: 'CollectionPicker',
1764
+ entity_type: 'visual',
1765
+ file_path: 'src/components/CollectionPicker.tsx',
1766
+ })
1767
+ .execute();
1768
+ await db
1769
+ .insertInto('editor_scenarios')
1770
+ .values({
1771
+ id: 'sc-2',
1772
+ project_id: projectId,
1773
+ name: 'CollectionPicker - Default',
1774
+ component_name: 'CollectionPicker',
1775
+ entity_sha: 'sha-picker',
1776
+ created_at: '2026-03-16 23:00:00',
1777
+ })
1778
+ .execute();
1779
+ const result = await queryIncompleteEntities(db, projectId, null);
1780
+ expect(result).toEqual([
1781
+ { entitySha: 'sha-picker', name: 'CollectionPicker', scenarioCount: 1 },
1782
+ ]);
1783
+ });
1784
+ it('should scope to session when featureStartedAt is provided', async () => {
1785
+ // Entity without analysis, scenario created BEFORE session
1786
+ await db
1787
+ .insertInto('entities')
1788
+ .values({
1789
+ sha: 'sha-old',
1790
+ name: 'OldComponent',
1791
+ entity_type: 'visual',
1792
+ file_path: 'src/OldComponent.tsx',
1793
+ })
1794
+ .execute();
1795
+ await db
1796
+ .insertInto('editor_scenarios')
1797
+ .values({
1798
+ id: 'sc-old',
1799
+ project_id: projectId,
1800
+ name: 'OldComponent - Default',
1801
+ component_name: 'OldComponent',
1802
+ entity_sha: 'sha-old',
1803
+ created_at: '2026-03-16 20:00:00',
1804
+ })
1805
+ .execute();
1806
+ // Entity without analysis, scenario created DURING session
1807
+ await db
1808
+ .insertInto('entities')
1809
+ .values({
1810
+ sha: 'sha-new',
1811
+ name: 'NewComponent',
1812
+ entity_type: 'visual',
1813
+ file_path: 'src/NewComponent.tsx',
1814
+ })
1815
+ .execute();
1816
+ await db
1817
+ .insertInto('editor_scenarios')
1818
+ .values({
1819
+ id: 'sc-new',
1820
+ project_id: projectId,
1821
+ name: 'NewComponent - Default',
1822
+ component_name: 'NewComponent',
1823
+ entity_sha: 'sha-new',
1824
+ created_at: '2026-03-16 23:10:00',
1825
+ })
1826
+ .execute();
1827
+ const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
1828
+ // Only NewComponent should be flagged (created in session)
1829
+ expect(result).toEqual([
1830
+ { entitySha: 'sha-new', name: 'NewComponent', scenarioCount: 1 },
1831
+ ]);
1832
+ });
1833
+ it('should include scenarios updated in session even if created before', async () => {
1834
+ await db
1835
+ .insertInto('entities')
1836
+ .values({
1837
+ sha: 'sha-updated',
1838
+ name: 'UpdatedComponent',
1839
+ entity_type: 'visual',
1840
+ file_path: 'src/Updated.tsx',
1841
+ })
1842
+ .execute();
1843
+ await db
1844
+ .insertInto('editor_scenarios')
1845
+ .values({
1846
+ id: 'sc-updated',
1847
+ project_id: projectId,
1848
+ name: 'UpdatedComponent - Default',
1849
+ component_name: 'UpdatedComponent',
1850
+ entity_sha: 'sha-updated',
1851
+ created_at: '2026-03-16 20:00:00',
1852
+ updated_at: '2026-03-16 23:20:00', // Updated in session
1853
+ })
1854
+ .execute();
1855
+ const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
1856
+ expect(result).toEqual([
1857
+ {
1858
+ entitySha: 'sha-updated',
1859
+ name: 'UpdatedComponent',
1860
+ scenarioCount: 1,
1861
+ },
1862
+ ]);
1863
+ });
1864
+ it('should skip scenarios with null entity_sha', async () => {
1865
+ await db
1866
+ .insertInto('editor_scenarios')
1867
+ .values({
1868
+ id: 'sc-null',
1869
+ project_id: projectId,
1870
+ name: 'Orphan Scenario',
1871
+ component_name: 'Orphan',
1872
+ created_at: '2026-03-16 23:00:00',
1873
+ })
1874
+ .execute();
1875
+ const result = await queryIncompleteEntities(db, projectId, null);
1876
+ expect(result).toEqual([]);
1877
+ });
1878
+ it('should return empty when there are no scenarios', async () => {
1879
+ const result = await queryIncompleteEntities(db, projectId, null);
1880
+ expect(result).toEqual([]);
1881
+ });
1882
+ it('should not flag entities when a sibling version (same name+filePath) has analyses', async () => {
1883
+ // Old entity version WITH analysis
1884
+ await db
1885
+ .insertInto('entities')
1886
+ .values({
1887
+ sha: 'sha-btn-v1',
1888
+ name: 'OpenLibraryButton',
1889
+ entity_type: 'visual',
1890
+ file_path: 'src/components/OpenLibraryButton.tsx',
1891
+ })
1892
+ .execute();
1893
+ await db
1894
+ .insertInto('analyses')
1895
+ .values({
1896
+ id: 'a-btn-v1',
1897
+ entity_sha: 'sha-btn-v1',
1898
+ entity_name: 'OpenLibraryButton',
1899
+ project_id: projectId,
1900
+ })
1901
+ .execute();
1902
+ // New entity version WITHOUT analysis (created by file watcher)
1903
+ await db
1904
+ .insertInto('entities')
1905
+ .values({
1906
+ sha: 'sha-btn-v2',
1907
+ name: 'OpenLibraryButton',
1908
+ entity_type: 'visual',
1909
+ file_path: 'src/components/OpenLibraryButton.tsx',
1910
+ })
1911
+ .execute();
1912
+ // Scenario points to the NEW version (backfilled after file watcher)
1913
+ await db
1914
+ .insertInto('editor_scenarios')
1915
+ .values({
1916
+ id: 'sc-btn',
1917
+ project_id: projectId,
1918
+ name: 'OpenLibraryButton - Default',
1919
+ component_name: 'OpenLibraryButton',
1920
+ entity_sha: 'sha-btn-v2',
1921
+ created_at: '2026-03-16 23:00:00',
1922
+ })
1923
+ .execute();
1924
+ // Should NOT flag as incomplete — sibling version has analyses
1925
+ const result = await queryIncompleteEntities(db, projectId, null);
1926
+ expect(result).toEqual([]);
1927
+ });
1928
+ it('should still flag entities when no sibling version has analyses', async () => {
1929
+ // Only one version, no analyses
1930
+ await db
1931
+ .insertInto('entities')
1932
+ .values({
1933
+ sha: 'sha-icon',
1934
+ name: 'ExternalLinkIcon',
1935
+ entity_type: 'visual',
1936
+ file_path: 'src/components/ExternalLinkIcon.tsx',
1937
+ })
1938
+ .execute();
1939
+ await db
1940
+ .insertInto('editor_scenarios')
1941
+ .values({
1942
+ id: 'sc-icon',
1943
+ project_id: projectId,
1944
+ name: 'ExternalLinkIcon - Default',
1945
+ component_name: 'ExternalLinkIcon',
1946
+ entity_sha: 'sha-icon',
1947
+ created_at: '2026-03-16 23:00:00',
1948
+ })
1949
+ .execute();
1950
+ // Should flag as incomplete — no version has analyses
1951
+ const result = await queryIncompleteEntities(db, projectId, null);
1952
+ expect(result).toEqual([
1953
+ {
1954
+ entitySha: 'sha-icon',
1955
+ name: 'ExternalLinkIcon',
1956
+ scenarioCount: 1,
1957
+ },
1958
+ ]);
1959
+ });
1960
+ it('should use entity name from entities table, falling back to component_name', async () => {
1961
+ // Scenario has entity_sha but entity record doesn't exist
1962
+ await db
1963
+ .insertInto('editor_scenarios')
1964
+ .values({
1965
+ id: 'sc-1',
1966
+ project_id: projectId,
1967
+ name: 'Ghost - Default',
1968
+ component_name: 'GhostComponent',
1969
+ entity_sha: 'sha-ghost',
1970
+ created_at: '2026-03-16 23:00:00',
1971
+ })
1972
+ .execute();
1973
+ const result = await queryIncompleteEntities(db, projectId, null);
1974
+ expect(result).toEqual([
1975
+ { entitySha: 'sha-ghost', name: 'GhostComponent', scenarioCount: 1 },
1976
+ ]);
1977
+ });
1978
+ });
986
1979
  });
987
1980
  //# sourceMappingURL=editorAudit.test.js.map