@codeyam/codeyam-cli 0.1.0-staging.25a2014 → 0.1.0-staging.26dc674
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/analyzer-template/.build-info.json +7 -7
- package/analyzer-template/log.txt +3 -3
- package/analyzer-template/package.json +2 -2
- package/analyzer-template/packages/ai/src/lib/dataStructure/ScopeDataStructure.ts +5 -1
- package/analyzer-template/packages/analyze/src/lib/files/analyze/findOrCreateEntity.ts +10 -6
- package/analyzer-template/packages/analyze/src/lib/files/analyze/gatherEntityMap.ts +9 -12
- package/analyzer-template/packages/analyze/src/lib/files/analyzeChange.ts +4 -0
- package/analyzer-template/packages/analyze/src/lib/files/analyzeInitial.ts +4 -0
- package/analyzer-template/packages/aws/package.json +1 -1
- package/analyzer-template/packages/database/package.json +1 -1
- package/analyzer-template/packages/database/src/lib/loadAnalysis.ts +19 -15
- package/analyzer-template/packages/database/src/lib/loadEntity.ts +19 -8
- package/analyzer-template/packages/github/dist/database/src/lib/loadAnalysis.d.ts.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/loadAnalysis.js +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/loadAnalysis.js.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.d.ts +4 -1
- package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.d.ts.map +1 -1
- package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.js +5 -5
- package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.js.map +1 -1
- package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.d.ts +3 -1
- package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.d.ts.map +1 -1
- package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.js +22 -1
- package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.js.map +1 -1
- package/analyzer-template/packages/utils/src/lib/fs/rsyncCopy.ts +27 -0
- package/analyzer-template/project/analyzeFileEntities.ts +26 -0
- package/background/src/lib/virtualized/project/analyzeFileEntities.js +22 -0
- package/background/src/lib/virtualized/project/analyzeFileEntities.js.map +1 -1
- package/codeyam-cli/src/cli.js +15 -0
- package/codeyam-cli/src/cli.js.map +1 -1
- package/codeyam-cli/src/commands/__tests__/editor.stepDispatch.test.js +9 -9
- package/codeyam-cli/src/commands/editor.js +981 -344
- package/codeyam-cli/src/commands/editor.js.map +1 -1
- package/codeyam-cli/src/commands/init.js +1 -0
- package/codeyam-cli/src/commands/init.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/editorAudit.test.js +1274 -33
- package/codeyam-cli/src/utils/__tests__/editorAudit.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/editorCaptureScenarioSeeding.test.js +137 -0
- package/codeyam-cli/src/utils/__tests__/editorCaptureScenarioSeeding.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorEntityHelpers.test.js +66 -0
- package/codeyam-cli/src/utils/__tests__/editorEntityHelpers.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/editorGuardMiddleware.test.js +67 -0
- package/codeyam-cli/src/utils/__tests__/editorGuardMiddleware.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/editorScenarioSwitch.test.js +70 -0
- package/codeyam-cli/src/utils/__tests__/editorScenarioSwitch.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/editorScenarios.test.js +71 -1
- package/codeyam-cli/src/utils/__tests__/editorScenarios.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/entityChangeStatus.test.js +233 -1
- package/codeyam-cli/src/utils/__tests__/entityChangeStatus.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/glossaryAdd.test.js +177 -0
- package/codeyam-cli/src/utils/__tests__/glossaryAdd.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/journalCaptureStabilization.test.js +16 -1
- package/codeyam-cli/src/utils/__tests__/journalCaptureStabilization.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/registerScenarioResult.test.js +127 -0
- package/codeyam-cli/src/utils/__tests__/registerScenarioResult.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/scenarioCoverage.test.js +57 -0
- package/codeyam-cli/src/utils/__tests__/scenarioCoverage.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/scenariosManifest.test.js +41 -0
- package/codeyam-cli/src/utils/__tests__/scenariosManifest.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/screenshotHash.test.js +84 -0
- package/codeyam-cli/src/utils/__tests__/screenshotHash.test.js.map +1 -0
- package/codeyam-cli/src/utils/analysisRunner.js +8 -6
- package/codeyam-cli/src/utils/analysisRunner.js.map +1 -1
- package/codeyam-cli/src/utils/analyzer.js +8 -0
- package/codeyam-cli/src/utils/analyzer.js.map +1 -1
- package/codeyam-cli/src/utils/editorAudit.js +277 -37
- package/codeyam-cli/src/utils/editorAudit.js.map +1 -1
- package/codeyam-cli/src/utils/editorGuard.js +36 -0
- package/codeyam-cli/src/utils/editorGuard.js.map +1 -0
- package/codeyam-cli/src/utils/editorRecapture.js +109 -0
- package/codeyam-cli/src/utils/editorRecapture.js.map +1 -0
- package/codeyam-cli/src/utils/editorScenarioSwitch.js +24 -2
- package/codeyam-cli/src/utils/editorScenarioSwitch.js.map +1 -1
- package/codeyam-cli/src/utils/editorScenarios.js +65 -6
- package/codeyam-cli/src/utils/editorScenarios.js.map +1 -1
- package/codeyam-cli/src/utils/entityChangeStatus.js +31 -3
- package/codeyam-cli/src/utils/entityChangeStatus.js.map +1 -1
- package/codeyam-cli/src/utils/glossaryAdd.js +74 -0
- package/codeyam-cli/src/utils/glossaryAdd.js.map +1 -0
- package/codeyam-cli/src/utils/install-skills.js +5 -0
- package/codeyam-cli/src/utils/install-skills.js.map +1 -1
- package/codeyam-cli/src/utils/queue/job.js +6 -3
- package/codeyam-cli/src/utils/queue/job.js.map +1 -1
- package/codeyam-cli/src/utils/registerScenarioResult.js +52 -0
- package/codeyam-cli/src/utils/registerScenarioResult.js.map +1 -0
- package/codeyam-cli/src/utils/scenarioCoverage.js +4 -1
- package/codeyam-cli/src/utils/scenarioCoverage.js.map +1 -1
- package/codeyam-cli/src/utils/scenariosManifest.js +28 -0
- package/codeyam-cli/src/utils/scenariosManifest.js.map +1 -1
- package/codeyam-cli/src/utils/screenshotHash.js +26 -0
- package/codeyam-cli/src/utils/screenshotHash.js.map +1 -0
- package/codeyam-cli/src/utils/simulationGateMiddleware.js +9 -0
- package/codeyam-cli/src/utils/simulationGateMiddleware.js.map +1 -1
- package/codeyam-cli/src/webserver/__tests__/clientErrors.test.js +68 -1
- package/codeyam-cli/src/webserver/__tests__/clientErrors.test.js.map +1 -1
- package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js +123 -21
- package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js.map +1 -1
- package/codeyam-cli/src/webserver/__tests__/stripClaudeCommand.test.js +135 -0
- package/codeyam-cli/src/webserver/__tests__/stripClaudeCommand.test.js.map +1 -0
- package/codeyam-cli/src/webserver/app/lib/clientErrors.js +22 -1
- package/codeyam-cli/src/webserver/app/lib/clientErrors.js.map +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-recapture-stale-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-save-scenario-data-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-schema-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/{cy-logo-cli-CCKUIm0S.svg → cy-logo-cli-CJzc4vOH.svg} +2 -2
- package/codeyam-cli/src/webserver/build/client/assets/cy-logo-cli-DODLxLcw.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-Dx-h1rJK.js +130 -0
- package/codeyam-cli/src/webserver/build/client/assets/editorPreview-NTuLi4Xg.js +41 -0
- package/codeyam-cli/src/webserver/build/client/assets/{entity._sha.scenarios._scenarioId.dev-CUobbQdQ.js → entity._sha.scenarios._scenarioId.dev-BA5L8bU-.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{entity._sha.scenarios._scenarioId.fullscreen-C6eeL24i.js → entity._sha.scenarios._scenarioId.fullscreen-D4dmRgvO.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/globals-BrPXT1iR.css +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/manifest-5025e428.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/{root-DB3O9_9j.js → root-BCx1S8Z3.js} +26 -13
- package/codeyam-cli/src/webserver/build/server/assets/analysisRunner-C1kjC9UJ.js +13 -0
- package/codeyam-cli/src/webserver/build/server/assets/{index-CEaDhUiv.js → index-C91yWWCI.js} +1 -1
- package/codeyam-cli/src/webserver/build/server/assets/{init-DA7guOrE.js → init-Dkas-RUS.js} +2 -2
- package/codeyam-cli/src/webserver/build/server/assets/server-build-pulXLTrG.js +640 -0
- package/codeyam-cli/src/webserver/build/server/index.js +1 -1
- package/codeyam-cli/src/webserver/build-info.json +5 -5
- package/codeyam-cli/src/webserver/idleDetector.js +41 -8
- package/codeyam-cli/src/webserver/idleDetector.js.map +1 -1
- package/codeyam-cli/src/webserver/scripts/journalCapture.ts +53 -0
- package/codeyam-cli/src/webserver/server.js +52 -14
- package/codeyam-cli/src/webserver/server.js.map +1 -1
- package/codeyam-cli/src/webserver/terminalServer.js +141 -27
- package/codeyam-cli/src/webserver/terminalServer.js.map +1 -1
- package/codeyam-cli/templates/__tests__/editor-step-hook.prompt-capture.test.ts +118 -0
- package/codeyam-cli/templates/codeyam-editor-claude.md +2 -0
- package/codeyam-cli/templates/codeyam-editor-reference.md +214 -0
- package/codeyam-cli/templates/editor-step-hook.py +93 -46
- package/codeyam-cli/templates/skills/codeyam-editor/SKILL.md +19 -1
- package/package.json +1 -1
- package/packages/ai/src/lib/dataStructure/ScopeDataStructure.js +4 -1
- package/packages/ai/src/lib/dataStructure/ScopeDataStructure.js.map +1 -1
- package/packages/analyze/src/lib/files/analyze/findOrCreateEntity.js +3 -2
- package/packages/analyze/src/lib/files/analyze/findOrCreateEntity.js.map +1 -1
- package/packages/analyze/src/lib/files/analyze/gatherEntityMap.js +9 -7
- package/packages/analyze/src/lib/files/analyze/gatherEntityMap.js.map +1 -1
- package/packages/analyze/src/lib/files/analyzeChange.js +1 -0
- package/packages/analyze/src/lib/files/analyzeChange.js.map +1 -1
- package/packages/analyze/src/lib/files/analyzeInitial.js +1 -0
- package/packages/analyze/src/lib/files/analyzeInitial.js.map +1 -1
- package/packages/database/src/lib/loadAnalysis.js +1 -1
- package/packages/database/src/lib/loadAnalysis.js.map +1 -1
- package/packages/database/src/lib/loadEntity.js +5 -5
- package/packages/database/src/lib/loadEntity.js.map +1 -1
- package/packages/utils/src/lib/fs/rsyncCopy.js +22 -1
- package/packages/utils/src/lib/fs/rsyncCopy.js.map +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/cy-logo-cli-DcX-ZS3p.js +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-B7xQ9Sjy.js +0 -58
- package/codeyam-cli/src/webserver/build/client/assets/editorPreview-CxmrE6AF.js +0 -41
- package/codeyam-cli/src/webserver/build/client/assets/globals-fAqOD9ex.css +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/manifest-5d53342d.js +0 -1
- package/codeyam-cli/src/webserver/build/server/assets/analysisRunner-DcJSnBCE.js +0 -13
- package/codeyam-cli/src/webserver/build/server/assets/server-build-juyiY2m6.js +0 -551
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
2
|
import { Kysely, SqliteDialect } from 'kysely';
|
|
3
|
-
import { isComponent, classifyGlossaryEntries, computeAudit, filterGlossaryByChangeStatus, resolveAuditSessionScope, queryScenarioCounts, queryPageScenarioCounts, queryIncompleteEntities, queryMiscategorizedScenarios, isOnlyIncompleteEntities, isAutoRemediable, identifyScenariosNeedingRecapture, } from "../editorAudit.js";
|
|
3
|
+
import { isComponent, classifyGlossaryEntries, computeAudit, filterGlossaryByChangeStatus, resolveAuditSessionScope, queryScenarioCounts, queryPageScenarioCounts, queryIncompleteEntities, queryMiscategorizedScenarios, queryUnassociatedScenarios, isOnlyIncompleteEntities, isOnlyPreExistingIncomplete, 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', () => {
|
|
@@ -1466,6 +1466,279 @@ describe('editorAudit', () => {
|
|
|
1466
1466
|
expect(auditResult.summary.incompleteEntities).toBeUndefined();
|
|
1467
1467
|
});
|
|
1468
1468
|
});
|
|
1469
|
+
// ── filterToIncompleteFilePaths ──────────────────────────────────────
|
|
1470
|
+
describe('filterToIncompleteFilePaths', () => {
|
|
1471
|
+
// analyze-imports processes ALL file paths (~120 files) even when only
|
|
1472
|
+
// a few need analysis. This function filters to files that have no
|
|
1473
|
+
// entity with an analysis record.
|
|
1474
|
+
let db;
|
|
1475
|
+
let rawDb;
|
|
1476
|
+
const projectId = 'test-project-id';
|
|
1477
|
+
beforeEach(async () => {
|
|
1478
|
+
rawDb = new Database(':memory:');
|
|
1479
|
+
db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
|
|
1480
|
+
await db.schema
|
|
1481
|
+
.createTable('analyses')
|
|
1482
|
+
.addColumn('id', 'varchar', (col) => col.primaryKey())
|
|
1483
|
+
.addColumn('entity_sha', 'varchar')
|
|
1484
|
+
.addColumn('entity_name', 'varchar')
|
|
1485
|
+
.addColumn('project_id', 'varchar')
|
|
1486
|
+
.execute();
|
|
1487
|
+
await db.schema
|
|
1488
|
+
.createTable('entities')
|
|
1489
|
+
.addColumn('sha', 'varchar', (col) => col.primaryKey())
|
|
1490
|
+
.addColumn('name', 'varchar')
|
|
1491
|
+
.addColumn('entity_type', 'varchar')
|
|
1492
|
+
.addColumn('file_path', 'varchar')
|
|
1493
|
+
.execute();
|
|
1494
|
+
});
|
|
1495
|
+
afterEach(async () => {
|
|
1496
|
+
await db.destroy();
|
|
1497
|
+
});
|
|
1498
|
+
it('should exclude files that have an entity with an analysis', async () => {
|
|
1499
|
+
const { filterToIncompleteFilePaths } = require('../editorAudit');
|
|
1500
|
+
// Entity with analysis — skip
|
|
1501
|
+
await db
|
|
1502
|
+
.insertInto('entities')
|
|
1503
|
+
.values({
|
|
1504
|
+
sha: 'sha-header',
|
|
1505
|
+
name: 'Header',
|
|
1506
|
+
entity_type: 'visual',
|
|
1507
|
+
file_path: 'app/components/Header.tsx',
|
|
1508
|
+
})
|
|
1509
|
+
.execute();
|
|
1510
|
+
await db
|
|
1511
|
+
.insertInto('analyses')
|
|
1512
|
+
.values({
|
|
1513
|
+
id: 'a-1',
|
|
1514
|
+
entity_sha: 'sha-header',
|
|
1515
|
+
entity_name: 'Header',
|
|
1516
|
+
project_id: projectId,
|
|
1517
|
+
})
|
|
1518
|
+
.execute();
|
|
1519
|
+
// Entity without analysis — needs analysis
|
|
1520
|
+
await db
|
|
1521
|
+
.insertInto('entities')
|
|
1522
|
+
.values({
|
|
1523
|
+
sha: 'sha-rule',
|
|
1524
|
+
name: 'RuleBuilder',
|
|
1525
|
+
entity_type: 'visual',
|
|
1526
|
+
file_path: 'app/components/RuleBuilder.tsx',
|
|
1527
|
+
})
|
|
1528
|
+
.execute();
|
|
1529
|
+
const allFilePaths = [
|
|
1530
|
+
'app/components/Header.tsx',
|
|
1531
|
+
'app/components/RuleBuilder.tsx',
|
|
1532
|
+
'app/components/Footer.tsx', // no entity — needs analysis
|
|
1533
|
+
];
|
|
1534
|
+
const result = await filterToIncompleteFilePaths(db, projectId, allFilePaths);
|
|
1535
|
+
expect(result).toContain('app/components/RuleBuilder.tsx');
|
|
1536
|
+
expect(result).toContain('app/components/Footer.tsx');
|
|
1537
|
+
expect(result).not.toContain('app/components/Header.tsx');
|
|
1538
|
+
});
|
|
1539
|
+
it('should return all file paths when no entities exist yet', async () => {
|
|
1540
|
+
const { filterToIncompleteFilePaths } = require('../editorAudit');
|
|
1541
|
+
const filePaths = ['app/Foo.tsx', 'app/Bar.tsx'];
|
|
1542
|
+
const result = await filterToIncompleteFilePaths(db, projectId, filePaths);
|
|
1543
|
+
expect(result).toEqual(filePaths);
|
|
1544
|
+
});
|
|
1545
|
+
it('should return empty when all files have analyzed entities', async () => {
|
|
1546
|
+
const { filterToIncompleteFilePaths } = require('../editorAudit');
|
|
1547
|
+
await db
|
|
1548
|
+
.insertInto('entities')
|
|
1549
|
+
.values({
|
|
1550
|
+
sha: 'sha-a',
|
|
1551
|
+
name: 'CompA',
|
|
1552
|
+
entity_type: 'visual',
|
|
1553
|
+
file_path: 'app/CompA.tsx',
|
|
1554
|
+
})
|
|
1555
|
+
.execute();
|
|
1556
|
+
await db
|
|
1557
|
+
.insertInto('analyses')
|
|
1558
|
+
.values({
|
|
1559
|
+
id: 'a-1',
|
|
1560
|
+
entity_sha: 'sha-a',
|
|
1561
|
+
entity_name: 'CompA',
|
|
1562
|
+
project_id: projectId,
|
|
1563
|
+
})
|
|
1564
|
+
.execute();
|
|
1565
|
+
const result = await filterToIncompleteFilePaths(db, projectId, [
|
|
1566
|
+
'app/CompA.tsx',
|
|
1567
|
+
]);
|
|
1568
|
+
expect(result).toEqual([]);
|
|
1569
|
+
});
|
|
1570
|
+
it('should skip files with analysis even without scenarios', async () => {
|
|
1571
|
+
const { filterToIncompleteFilePaths } = require('../editorAudit');
|
|
1572
|
+
// Entity with analysis but NO scenarios — still complete
|
|
1573
|
+
await db
|
|
1574
|
+
.insertInto('entities')
|
|
1575
|
+
.values({
|
|
1576
|
+
sha: 'sha-util',
|
|
1577
|
+
name: 'utils',
|
|
1578
|
+
entity_type: 'library',
|
|
1579
|
+
file_path: 'app/lib/utils.ts',
|
|
1580
|
+
})
|
|
1581
|
+
.execute();
|
|
1582
|
+
await db
|
|
1583
|
+
.insertInto('analyses')
|
|
1584
|
+
.values({
|
|
1585
|
+
id: 'a-1',
|
|
1586
|
+
entity_sha: 'sha-util',
|
|
1587
|
+
entity_name: 'utils',
|
|
1588
|
+
project_id: projectId,
|
|
1589
|
+
})
|
|
1590
|
+
.execute();
|
|
1591
|
+
const result = await filterToIncompleteFilePaths(db, projectId, [
|
|
1592
|
+
'app/lib/utils.ts',
|
|
1593
|
+
]);
|
|
1594
|
+
expect(result).toEqual([]);
|
|
1595
|
+
});
|
|
1596
|
+
});
|
|
1597
|
+
// ── phantom entity SHAs ─────────────────────────────────────────────
|
|
1598
|
+
describe('queryIncompleteEntities with phantom entity SHAs', () => {
|
|
1599
|
+
// Root cause of the Margo/reader step-blocking bug:
|
|
1600
|
+
// Scenarios registered without component_path got a phantom entity_sha
|
|
1601
|
+
// computed from component_name alone. These SHAs have NO entity record
|
|
1602
|
+
// in the entities table and NO analyses. syncScenarioEntityShas can't
|
|
1603
|
+
// fix them (it skips scenarios without component_path). So they remain
|
|
1604
|
+
// "incomplete" forever, blocking step progression.
|
|
1605
|
+
//
|
|
1606
|
+
// Fix: queryIncompleteEntities should not report scenarios whose
|
|
1607
|
+
// entity_sha has no entity record — these are orphaned data, not
|
|
1608
|
+
// fixable by running analyze-imports.
|
|
1609
|
+
let db;
|
|
1610
|
+
let rawDb;
|
|
1611
|
+
const projectId = 'test-project-id';
|
|
1612
|
+
beforeEach(async () => {
|
|
1613
|
+
rawDb = new Database(':memory:');
|
|
1614
|
+
db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
|
|
1615
|
+
await db.schema
|
|
1616
|
+
.createTable('editor_scenarios')
|
|
1617
|
+
.addColumn('id', 'varchar', (col) => col.primaryKey())
|
|
1618
|
+
.addColumn('project_id', 'varchar', (col) => col.notNull())
|
|
1619
|
+
.addColumn('name', 'varchar', (col) => col.notNull())
|
|
1620
|
+
.addColumn('component_name', 'varchar')
|
|
1621
|
+
.addColumn('component_path', 'varchar')
|
|
1622
|
+
.addColumn('entity_sha', 'varchar')
|
|
1623
|
+
.addColumn('display_name', 'varchar')
|
|
1624
|
+
.addColumn('page_file_path', 'varchar')
|
|
1625
|
+
.addColumn('url', 'varchar')
|
|
1626
|
+
.addColumn('created_at', 'datetime')
|
|
1627
|
+
.addColumn('updated_at', 'datetime')
|
|
1628
|
+
.execute();
|
|
1629
|
+
await db.schema
|
|
1630
|
+
.createTable('analyses')
|
|
1631
|
+
.addColumn('id', 'varchar', (col) => col.primaryKey())
|
|
1632
|
+
.addColumn('entity_sha', 'varchar')
|
|
1633
|
+
.addColumn('entity_name', 'varchar')
|
|
1634
|
+
.addColumn('project_id', 'varchar')
|
|
1635
|
+
.execute();
|
|
1636
|
+
await db.schema
|
|
1637
|
+
.createTable('entities')
|
|
1638
|
+
.addColumn('sha', 'varchar', (col) => col.primaryKey())
|
|
1639
|
+
.addColumn('name', 'varchar')
|
|
1640
|
+
.addColumn('entity_type', 'varchar')
|
|
1641
|
+
.addColumn('file_path', 'varchar')
|
|
1642
|
+
.execute();
|
|
1643
|
+
});
|
|
1644
|
+
afterEach(async () => {
|
|
1645
|
+
await db.destroy();
|
|
1646
|
+
});
|
|
1647
|
+
it('should not report scenarios with phantom entity SHAs (no entity record exists)', async () => {
|
|
1648
|
+
// Real entity — has an entity record, analyses, and scenarios. Complete.
|
|
1649
|
+
await db
|
|
1650
|
+
.insertInto('entities')
|
|
1651
|
+
.values({
|
|
1652
|
+
sha: 'sha-real',
|
|
1653
|
+
name: 'RuleBuilder',
|
|
1654
|
+
entity_type: 'visual',
|
|
1655
|
+
file_path: 'app/components/RuleBuilder.tsx',
|
|
1656
|
+
})
|
|
1657
|
+
.execute();
|
|
1658
|
+
await db
|
|
1659
|
+
.insertInto('analyses')
|
|
1660
|
+
.values({
|
|
1661
|
+
id: 'a-1',
|
|
1662
|
+
entity_sha: 'sha-real',
|
|
1663
|
+
entity_name: 'RuleBuilder',
|
|
1664
|
+
project_id: projectId,
|
|
1665
|
+
})
|
|
1666
|
+
.execute();
|
|
1667
|
+
await db
|
|
1668
|
+
.insertInto('editor_scenarios')
|
|
1669
|
+
.values({
|
|
1670
|
+
id: 'sc-good',
|
|
1671
|
+
project_id: projectId,
|
|
1672
|
+
name: 'RuleBuilder - Empty',
|
|
1673
|
+
component_name: 'RuleBuilder',
|
|
1674
|
+
component_path: 'app/components/RuleBuilder.tsx',
|
|
1675
|
+
entity_sha: 'sha-real',
|
|
1676
|
+
created_at: '2026-03-16 23:00:00',
|
|
1677
|
+
})
|
|
1678
|
+
.execute();
|
|
1679
|
+
// Phantom entity — scenario points to a SHA that doesn't exist
|
|
1680
|
+
// in the entities table (registered without component_path).
|
|
1681
|
+
// No entity record, no analyses, unfixable by analyze-imports.
|
|
1682
|
+
await db
|
|
1683
|
+
.insertInto('editor_scenarios')
|
|
1684
|
+
.values({
|
|
1685
|
+
id: 'sc-phantom',
|
|
1686
|
+
project_id: projectId,
|
|
1687
|
+
name: 'Empty',
|
|
1688
|
+
component_name: 'RuleBuilder',
|
|
1689
|
+
component_path: null,
|
|
1690
|
+
entity_sha: 'sha-phantom-no-entity-record',
|
|
1691
|
+
created_at: '2026-03-16 22:00:00',
|
|
1692
|
+
})
|
|
1693
|
+
.execute();
|
|
1694
|
+
const incomplete = await queryIncompleteEntities(db, projectId, null);
|
|
1695
|
+
// Should NOT report phantom SHAs as incomplete — they can't be fixed
|
|
1696
|
+
// by running analyze-imports (no entity record exists to resolve).
|
|
1697
|
+
expect(incomplete).toHaveLength(0);
|
|
1698
|
+
});
|
|
1699
|
+
it('should still report real incomplete entities (entity exists but no analysis)', async () => {
|
|
1700
|
+
// Real entity without analysis — this IS a legitimate incomplete entity
|
|
1701
|
+
await db
|
|
1702
|
+
.insertInto('entities')
|
|
1703
|
+
.values({
|
|
1704
|
+
sha: 'sha-noanalysis',
|
|
1705
|
+
name: 'Footer',
|
|
1706
|
+
entity_type: 'visual',
|
|
1707
|
+
file_path: 'app/components/Footer.tsx',
|
|
1708
|
+
})
|
|
1709
|
+
.execute();
|
|
1710
|
+
await db
|
|
1711
|
+
.insertInto('editor_scenarios')
|
|
1712
|
+
.values({
|
|
1713
|
+
id: 'sc-1',
|
|
1714
|
+
project_id: projectId,
|
|
1715
|
+
name: 'Footer - Default',
|
|
1716
|
+
component_name: 'Footer',
|
|
1717
|
+
component_path: 'app/components/Footer.tsx',
|
|
1718
|
+
entity_sha: 'sha-noanalysis',
|
|
1719
|
+
created_at: '2026-03-16 23:00:00',
|
|
1720
|
+
})
|
|
1721
|
+
.execute();
|
|
1722
|
+
// Phantom scenario (shouldn't affect results)
|
|
1723
|
+
await db
|
|
1724
|
+
.insertInto('editor_scenarios')
|
|
1725
|
+
.values({
|
|
1726
|
+
id: 'sc-phantom',
|
|
1727
|
+
project_id: projectId,
|
|
1728
|
+
name: 'Footer - Alt',
|
|
1729
|
+
component_name: 'Footer',
|
|
1730
|
+
component_path: null,
|
|
1731
|
+
entity_sha: 'sha-phantom-does-not-exist',
|
|
1732
|
+
created_at: '2026-03-16 22:00:00',
|
|
1733
|
+
})
|
|
1734
|
+
.execute();
|
|
1735
|
+
const incomplete = await queryIncompleteEntities(db, projectId, null);
|
|
1736
|
+
// Should report Footer (real entity, no analysis) but NOT the phantom
|
|
1737
|
+
expect(incomplete).toHaveLength(1);
|
|
1738
|
+
expect(incomplete[0].name).toBe('Footer');
|
|
1739
|
+
expect(incomplete[0].entitySha).toBe('sha-noanalysis');
|
|
1740
|
+
});
|
|
1741
|
+
});
|
|
1469
1742
|
// ── queryMiscategorizedScenarios ─────────────────────────────────────
|
|
1470
1743
|
describe('queryMiscategorizedScenarios', () => {
|
|
1471
1744
|
let db;
|
|
@@ -1717,7 +1990,10 @@ describe('editorAudit', () => {
|
|
|
1717
1990
|
});
|
|
1718
1991
|
// ── isAutoRemediable ─────────────────────────────────────────────────
|
|
1719
1992
|
describe('isAutoRemediable', () => {
|
|
1720
|
-
|
|
1993
|
+
// isAutoRemediable always returns false — the audit never triggers
|
|
1994
|
+
// full analyze-imports inline. It takes minutes on large projects.
|
|
1995
|
+
// Only lightweight backfill is acceptable during audit.
|
|
1996
|
+
it('should return false even on first attempt with only incomplete entities', () => {
|
|
1721
1997
|
const result = isAutoRemediable({
|
|
1722
1998
|
componentsMissing: 0,
|
|
1723
1999
|
componentsWithErrors: 0,
|
|
@@ -1729,11 +2005,9 @@ describe('editorAudit', () => {
|
|
|
1729
2005
|
incompleteEntities: 3,
|
|
1730
2006
|
allPassing: false,
|
|
1731
2007
|
}, false);
|
|
1732
|
-
expect(result).toBe(
|
|
2008
|
+
expect(result).toBe(false);
|
|
1733
2009
|
});
|
|
1734
|
-
it('should return false on second attempt
|
|
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
|
|
2010
|
+
it('should return false on second attempt', () => {
|
|
1737
2011
|
const result = isAutoRemediable({
|
|
1738
2012
|
componentsMissing: 0,
|
|
1739
2013
|
componentsWithErrors: 0,
|
|
@@ -1873,7 +2147,12 @@ describe('editorAudit', () => {
|
|
|
1873
2147
|
.execute();
|
|
1874
2148
|
const result = await queryIncompleteEntities(db, projectId, null);
|
|
1875
2149
|
expect(result).toEqual([
|
|
1876
|
-
{
|
|
2150
|
+
{
|
|
2151
|
+
entitySha: 'sha-chips',
|
|
2152
|
+
name: 'CollectionChips',
|
|
2153
|
+
scenarioCount: 2,
|
|
2154
|
+
preExisting: false,
|
|
2155
|
+
},
|
|
1877
2156
|
]);
|
|
1878
2157
|
});
|
|
1879
2158
|
it('should only return entities without analyses, not those with analyses', async () => {
|
|
@@ -1930,10 +2209,15 @@ describe('editorAudit', () => {
|
|
|
1930
2209
|
.execute();
|
|
1931
2210
|
const result = await queryIncompleteEntities(db, projectId, null);
|
|
1932
2211
|
expect(result).toEqual([
|
|
1933
|
-
{
|
|
2212
|
+
{
|
|
2213
|
+
entitySha: 'sha-picker',
|
|
2214
|
+
name: 'CollectionPicker',
|
|
2215
|
+
scenarioCount: 1,
|
|
2216
|
+
preExisting: false,
|
|
2217
|
+
},
|
|
1934
2218
|
]);
|
|
1935
2219
|
});
|
|
1936
|
-
it('should
|
|
2220
|
+
it('should return both pre-session and in-session entities with preExisting flags', async () => {
|
|
1937
2221
|
// Entity without analysis, scenario created BEFORE session
|
|
1938
2222
|
await db
|
|
1939
2223
|
.insertInto('entities')
|
|
@@ -1977,12 +2261,24 @@ describe('editorAudit', () => {
|
|
|
1977
2261
|
})
|
|
1978
2262
|
.execute();
|
|
1979
2263
|
const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
|
|
1980
|
-
//
|
|
1981
|
-
expect(result).toEqual([
|
|
1982
|
-
{
|
|
1983
|
-
|
|
2264
|
+
// Both should be returned — OldComponent is preExisting, NewComponent is not
|
|
2265
|
+
expect(result).toEqual(expect.arrayContaining([
|
|
2266
|
+
{
|
|
2267
|
+
entitySha: 'sha-old',
|
|
2268
|
+
name: 'OldComponent',
|
|
2269
|
+
scenarioCount: 1,
|
|
2270
|
+
preExisting: true,
|
|
2271
|
+
},
|
|
2272
|
+
{
|
|
2273
|
+
entitySha: 'sha-new',
|
|
2274
|
+
name: 'NewComponent',
|
|
2275
|
+
scenarioCount: 1,
|
|
2276
|
+
preExisting: false,
|
|
2277
|
+
},
|
|
2278
|
+
]));
|
|
2279
|
+
expect(result).toHaveLength(2);
|
|
1984
2280
|
});
|
|
1985
|
-
it('should
|
|
2281
|
+
it('should flag preExisting: false when scenario was updated in session even if created before', async () => {
|
|
1986
2282
|
await db
|
|
1987
2283
|
.insertInto('entities')
|
|
1988
2284
|
.values({
|
|
@@ -2010,6 +2306,7 @@ describe('editorAudit', () => {
|
|
|
2010
2306
|
entitySha: 'sha-updated',
|
|
2011
2307
|
name: 'UpdatedComponent',
|
|
2012
2308
|
scenarioCount: 1,
|
|
2309
|
+
preExisting: false,
|
|
2013
2310
|
},
|
|
2014
2311
|
]);
|
|
2015
2312
|
});
|
|
@@ -2077,6 +2374,60 @@ describe('editorAudit', () => {
|
|
|
2077
2374
|
const result = await queryIncompleteEntities(db, projectId, null);
|
|
2078
2375
|
expect(result).toEqual([]);
|
|
2079
2376
|
});
|
|
2377
|
+
it('should flag entity when sibling has analyses but different filePath (extracted component)', async () => {
|
|
2378
|
+
// Old entity version WITH analysis at ORIGINAL file path
|
|
2379
|
+
await db
|
|
2380
|
+
.insertInto('entities')
|
|
2381
|
+
.values({
|
|
2382
|
+
sha: 'sha-card-v1',
|
|
2383
|
+
name: 'TaskCard',
|
|
2384
|
+
entity_type: 'visual',
|
|
2385
|
+
file_path: 'app/page.tsx',
|
|
2386
|
+
})
|
|
2387
|
+
.execute();
|
|
2388
|
+
await db
|
|
2389
|
+
.insertInto('analyses')
|
|
2390
|
+
.values({
|
|
2391
|
+
id: 'a-card-v1',
|
|
2392
|
+
entity_sha: 'sha-card-v1',
|
|
2393
|
+
entity_name: 'TaskCard',
|
|
2394
|
+
project_id: projectId,
|
|
2395
|
+
})
|
|
2396
|
+
.execute();
|
|
2397
|
+
// New entity version WITHOUT analysis at EXTRACTED file path
|
|
2398
|
+
await db
|
|
2399
|
+
.insertInto('entities')
|
|
2400
|
+
.values({
|
|
2401
|
+
sha: 'sha-card-v2',
|
|
2402
|
+
name: 'TaskCard',
|
|
2403
|
+
entity_type: 'visual',
|
|
2404
|
+
file_path: 'app/components/TaskCard.tsx',
|
|
2405
|
+
})
|
|
2406
|
+
.execute();
|
|
2407
|
+
// Scenario points to the new version (synced by syncScenarioEntityShas)
|
|
2408
|
+
await db
|
|
2409
|
+
.insertInto('editor_scenarios')
|
|
2410
|
+
.values({
|
|
2411
|
+
id: 'sc-card',
|
|
2412
|
+
project_id: projectId,
|
|
2413
|
+
name: 'TaskCard - Default',
|
|
2414
|
+
component_name: 'TaskCard',
|
|
2415
|
+
entity_sha: 'sha-card-v2',
|
|
2416
|
+
created_at: '2026-03-16 23:00:00',
|
|
2417
|
+
})
|
|
2418
|
+
.execute();
|
|
2419
|
+
// SHOULD flag as incomplete — sibling has analyses but at a different filePath,
|
|
2420
|
+
// so getAllEntities() won't inherit (it matches by name+filePath)
|
|
2421
|
+
const result = await queryIncompleteEntities(db, projectId, null);
|
|
2422
|
+
expect(result).toEqual([
|
|
2423
|
+
{
|
|
2424
|
+
entitySha: 'sha-card-v2',
|
|
2425
|
+
name: 'TaskCard',
|
|
2426
|
+
scenarioCount: 1,
|
|
2427
|
+
preExisting: false,
|
|
2428
|
+
},
|
|
2429
|
+
]);
|
|
2430
|
+
});
|
|
2080
2431
|
it('should still flag entities when no sibling version has analyses', async () => {
|
|
2081
2432
|
// Only one version, no analyses
|
|
2082
2433
|
await db
|
|
@@ -2106,11 +2457,15 @@ describe('editorAudit', () => {
|
|
|
2106
2457
|
entitySha: 'sha-icon',
|
|
2107
2458
|
name: 'ExternalLinkIcon',
|
|
2108
2459
|
scenarioCount: 1,
|
|
2460
|
+
preExisting: false,
|
|
2109
2461
|
},
|
|
2110
2462
|
]);
|
|
2111
2463
|
});
|
|
2112
|
-
it('should
|
|
2113
|
-
// Scenario has entity_sha but entity record doesn't exist
|
|
2464
|
+
it('should skip phantom SHAs (entity_sha with no entity record)', async () => {
|
|
2465
|
+
// Scenario has entity_sha but entity record doesn't exist.
|
|
2466
|
+
// These are "phantom SHAs" created when scenarios were registered
|
|
2467
|
+
// without component_path — they can never be fixed by analyze-imports
|
|
2468
|
+
// and should not block audit progression.
|
|
2114
2469
|
await db
|
|
2115
2470
|
.insertInto('editor_scenarios')
|
|
2116
2471
|
.values({
|
|
@@ -2123,26 +2478,137 @@ describe('editorAudit', () => {
|
|
|
2123
2478
|
})
|
|
2124
2479
|
.execute();
|
|
2125
2480
|
const result = await queryIncompleteEntities(db, projectId, null);
|
|
2481
|
+
// Phantom SHAs are excluded — not reportable as incomplete
|
|
2482
|
+
expect(result).toEqual([]);
|
|
2483
|
+
});
|
|
2484
|
+
it('should detect incomplete entity whose scenario predates the session', async () => {
|
|
2485
|
+
// Entity with no analyses, scenario created BEFORE session
|
|
2486
|
+
await db
|
|
2487
|
+
.insertInto('entities')
|
|
2488
|
+
.values({
|
|
2489
|
+
sha: 'sha-preexisting',
|
|
2490
|
+
name: 'PreExistingComponent',
|
|
2491
|
+
entity_type: 'visual',
|
|
2492
|
+
file_path: 'src/PreExistingComponent.tsx',
|
|
2493
|
+
})
|
|
2494
|
+
.execute();
|
|
2495
|
+
await db
|
|
2496
|
+
.insertInto('editor_scenarios')
|
|
2497
|
+
.values({
|
|
2498
|
+
id: 'sc-preexisting',
|
|
2499
|
+
project_id: projectId,
|
|
2500
|
+
name: 'PreExistingComponent - Default',
|
|
2501
|
+
component_name: 'PreExistingComponent',
|
|
2502
|
+
entity_sha: 'sha-preexisting',
|
|
2503
|
+
created_at: '2026-03-16 20:00:00',
|
|
2504
|
+
updated_at: '2026-03-16 20:00:00',
|
|
2505
|
+
})
|
|
2506
|
+
.execute();
|
|
2507
|
+
// Session started well after scenario was created/updated
|
|
2508
|
+
const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
|
|
2509
|
+
// Should still be detected — the old time filter would have excluded it
|
|
2126
2510
|
expect(result).toEqual([
|
|
2127
|
-
{
|
|
2511
|
+
{
|
|
2512
|
+
entitySha: 'sha-preexisting',
|
|
2513
|
+
name: 'PreExistingComponent',
|
|
2514
|
+
scenarioCount: 1,
|
|
2515
|
+
preExisting: true,
|
|
2516
|
+
},
|
|
2128
2517
|
]);
|
|
2129
2518
|
});
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2519
|
+
it('should flag preExisting: true when all scenarios predate the session', async () => {
|
|
2520
|
+
await db
|
|
2521
|
+
.insertInto('entities')
|
|
2522
|
+
.values({
|
|
2523
|
+
sha: 'sha-old-entity',
|
|
2524
|
+
name: 'OldEntity',
|
|
2525
|
+
entity_type: 'visual',
|
|
2526
|
+
file_path: 'src/OldEntity.tsx',
|
|
2527
|
+
})
|
|
2528
|
+
.execute();
|
|
2529
|
+
// Two scenarios, both before session
|
|
2530
|
+
await db
|
|
2531
|
+
.insertInto('editor_scenarios')
|
|
2532
|
+
.values({
|
|
2533
|
+
id: 'sc-old-1',
|
|
2534
|
+
project_id: projectId,
|
|
2535
|
+
name: 'OldEntity - Default',
|
|
2536
|
+
component_name: 'OldEntity',
|
|
2537
|
+
entity_sha: 'sha-old-entity',
|
|
2538
|
+
created_at: '2026-03-16 19:00:00',
|
|
2539
|
+
updated_at: '2026-03-16 19:00:00',
|
|
2540
|
+
})
|
|
2541
|
+
.execute();
|
|
2542
|
+
await db
|
|
2543
|
+
.insertInto('editor_scenarios')
|
|
2544
|
+
.values({
|
|
2545
|
+
id: 'sc-old-2',
|
|
2546
|
+
project_id: projectId,
|
|
2547
|
+
name: 'OldEntity - Hover',
|
|
2548
|
+
component_name: 'OldEntity',
|
|
2549
|
+
entity_sha: 'sha-old-entity',
|
|
2550
|
+
created_at: '2026-03-16 19:30:00',
|
|
2551
|
+
updated_at: '2026-03-16 19:30:00',
|
|
2552
|
+
})
|
|
2553
|
+
.execute();
|
|
2554
|
+
const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
|
|
2555
|
+
expect(result).toEqual([
|
|
2556
|
+
{
|
|
2557
|
+
entitySha: 'sha-old-entity',
|
|
2558
|
+
name: 'OldEntity',
|
|
2559
|
+
scenarioCount: 2,
|
|
2560
|
+
preExisting: true,
|
|
2561
|
+
},
|
|
2562
|
+
]);
|
|
2563
|
+
});
|
|
2564
|
+
it('should flag preExisting: false when scenario is from the current session', async () => {
|
|
2565
|
+
await db
|
|
2566
|
+
.insertInto('entities')
|
|
2567
|
+
.values({
|
|
2568
|
+
sha: 'sha-session-entity',
|
|
2569
|
+
name: 'SessionEntity',
|
|
2570
|
+
entity_type: 'visual',
|
|
2571
|
+
file_path: 'src/SessionEntity.tsx',
|
|
2572
|
+
})
|
|
2573
|
+
.execute();
|
|
2574
|
+
await db
|
|
2575
|
+
.insertInto('editor_scenarios')
|
|
2576
|
+
.values({
|
|
2577
|
+
id: 'sc-session',
|
|
2578
|
+
project_id: projectId,
|
|
2579
|
+
name: 'SessionEntity - Default',
|
|
2580
|
+
component_name: 'SessionEntity',
|
|
2581
|
+
entity_sha: 'sha-session-entity',
|
|
2582
|
+
created_at: '2026-03-16 23:30:00',
|
|
2583
|
+
updated_at: '2026-03-16 23:30:00',
|
|
2584
|
+
})
|
|
2585
|
+
.execute();
|
|
2586
|
+
const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
|
|
2587
|
+
expect(result).toEqual([
|
|
2588
|
+
{
|
|
2589
|
+
entitySha: 'sha-session-entity',
|
|
2590
|
+
name: 'SessionEntity',
|
|
2591
|
+
scenarioCount: 1,
|
|
2592
|
+
preExisting: false,
|
|
2593
|
+
},
|
|
2594
|
+
]);
|
|
2595
|
+
});
|
|
2596
|
+
});
|
|
2597
|
+
// ── identifyScenariosNeedingRecapture ──────────────────────────────
|
|
2598
|
+
describe('identifyScenariosNeedingRecapture', () => {
|
|
2599
|
+
// Reproduces the Margo bug: Feature 1 built app-level popup scenarios,
|
|
2600
|
+
// Feature 2 edited LibraryView (used by App), but app-level scenarios
|
|
2601
|
+
// were never flagged for recapture because the audit only checked
|
|
2602
|
+
// component scenario existence — not whether app-level scenarios are stale.
|
|
2603
|
+
//
|
|
2604
|
+
// Each scenario's entityName is resolved by the caller via
|
|
2605
|
+
// entity_sha → entities.name (the default export for app-level scenarios).
|
|
2606
|
+
it('should flag app-level scenario when its entity is impacted by transitive dependency change', () => {
|
|
2607
|
+
// LibraryView was edited → App is impacted (imports LibraryView)
|
|
2608
|
+
// App-level scenario "Library - Rich Library" has entity_sha pointing to App
|
|
2609
|
+
// It was NOT recaptured during Feature 2 → should be flagged
|
|
2610
|
+
const entityChangeStatus = {
|
|
2611
|
+
LibraryView: { status: 'edited' },
|
|
2146
2612
|
App: {
|
|
2147
2613
|
status: 'impacted',
|
|
2148
2614
|
impactedBy: [
|
|
@@ -2314,5 +2780,780 @@ describe('editorAudit', () => {
|
|
|
2314
2780
|
expect(result).toHaveLength(0);
|
|
2315
2781
|
});
|
|
2316
2782
|
});
|
|
2783
|
+
// ── detectDuplicateNames ──────────────────────────────────────────
|
|
2784
|
+
describe('detectDuplicateNames', () => {
|
|
2785
|
+
it('should return empty map when no duplicates exist', () => {
|
|
2786
|
+
const entries = [
|
|
2787
|
+
{ name: 'Header', filePath: 'app/components/Header.tsx' },
|
|
2788
|
+
{ name: 'Footer', filePath: 'app/components/Footer.tsx' },
|
|
2789
|
+
{ name: 'Sidebar', filePath: 'app/components/Sidebar.tsx' },
|
|
2790
|
+
];
|
|
2791
|
+
const result = detectDuplicateNames(entries);
|
|
2792
|
+
expect(result.size).toBe(0);
|
|
2793
|
+
});
|
|
2794
|
+
it('should group entries that share a name', () => {
|
|
2795
|
+
const entries = [
|
|
2796
|
+
{ name: 'Page', filePath: 'app/isolated-components/Foo/page.tsx' },
|
|
2797
|
+
{ name: 'Page', filePath: 'app/isolated-components/Bar/page.tsx' },
|
|
2798
|
+
{ name: 'Page', filePath: 'app/isolated-components/Baz/page.tsx' },
|
|
2799
|
+
{ name: 'Header', filePath: 'app/components/Header.tsx' },
|
|
2800
|
+
];
|
|
2801
|
+
const result = detectDuplicateNames(entries);
|
|
2802
|
+
expect(result.size).toBe(1);
|
|
2803
|
+
expect(result.has('Page')).toBe(true);
|
|
2804
|
+
const pageGroup = result.get('Page');
|
|
2805
|
+
expect(pageGroup).toHaveLength(3);
|
|
2806
|
+
expect(pageGroup.map((e) => e.filePath)).toEqual([
|
|
2807
|
+
'app/isolated-components/Foo/page.tsx',
|
|
2808
|
+
'app/isolated-components/Bar/page.tsx',
|
|
2809
|
+
'app/isolated-components/Baz/page.tsx',
|
|
2810
|
+
]);
|
|
2811
|
+
});
|
|
2812
|
+
it('should exclude single-occurrence names', () => {
|
|
2813
|
+
const entries = [
|
|
2814
|
+
{ name: 'Page', filePath: 'app/isolated-components/Foo/page.tsx' },
|
|
2815
|
+
{ name: 'Page', filePath: 'app/isolated-components/Bar/page.tsx' },
|
|
2816
|
+
{ name: 'Header', filePath: 'app/components/Header.tsx' },
|
|
2817
|
+
{ name: 'Footer', filePath: 'app/components/Footer.tsx' },
|
|
2818
|
+
];
|
|
2819
|
+
const result = detectDuplicateNames(entries);
|
|
2820
|
+
expect(result.size).toBe(1);
|
|
2821
|
+
expect(result.has('Header')).toBe(false);
|
|
2822
|
+
expect(result.has('Footer')).toBe(false);
|
|
2823
|
+
});
|
|
2824
|
+
it('should handle multiple duplicate groups', () => {
|
|
2825
|
+
const entries = [
|
|
2826
|
+
{ name: 'Page', filePath: 'app/isolated-components/A/page.tsx' },
|
|
2827
|
+
{ name: 'Page', filePath: 'app/isolated-components/B/page.tsx' },
|
|
2828
|
+
{ name: 'Layout', filePath: 'app/isolated-components/A/layout.tsx' },
|
|
2829
|
+
{ name: 'Layout', filePath: 'app/isolated-components/B/layout.tsx' },
|
|
2830
|
+
{ name: 'Unique', filePath: 'app/components/Unique.tsx' },
|
|
2831
|
+
];
|
|
2832
|
+
const result = detectDuplicateNames(entries);
|
|
2833
|
+
expect(result.size).toBe(2);
|
|
2834
|
+
expect(result.has('Page')).toBe(true);
|
|
2835
|
+
expect(result.has('Layout')).toBe(true);
|
|
2836
|
+
expect(result.get('Page')).toHaveLength(2);
|
|
2837
|
+
expect(result.get('Layout')).toHaveLength(2);
|
|
2838
|
+
});
|
|
2839
|
+
it('should return empty map for empty input', () => {
|
|
2840
|
+
const result = detectDuplicateNames([]);
|
|
2841
|
+
expect(result.size).toBe(0);
|
|
2842
|
+
});
|
|
2843
|
+
});
|
|
2844
|
+
// ── computeAudit: impacted components with stale scenarios ──────────
|
|
2845
|
+
describe('computeAudit — impacted components with stale scenarios', () => {
|
|
2846
|
+
it('should mark impacted component as needs_recapture when it has total scenarios but none in session', () => {
|
|
2847
|
+
// Library page has 3 scenarios from Feature 1 (totalScenarioCounts),
|
|
2848
|
+
// 0 in the current session (scenarioCounts), and is "impacted" in entityChangeStatus.
|
|
2849
|
+
// It should NOT be marked "missing" — it needs recapture, not new scenarios.
|
|
2850
|
+
const result = computeAudit({
|
|
2851
|
+
components: [
|
|
2852
|
+
{
|
|
2853
|
+
name: 'Library',
|
|
2854
|
+
filePath: 'app/library/page.tsx',
|
|
2855
|
+
returnType: 'JSX.Element',
|
|
2856
|
+
},
|
|
2857
|
+
{
|
|
2858
|
+
name: 'ArticleTable',
|
|
2859
|
+
filePath: 'app/components/ArticleTable.tsx',
|
|
2860
|
+
returnType: 'JSX.Element',
|
|
2861
|
+
},
|
|
2862
|
+
],
|
|
2863
|
+
functions: [],
|
|
2864
|
+
scenarioCounts: { ArticleTable: 2 },
|
|
2865
|
+
testFileExistence: {},
|
|
2866
|
+
totalScenarioCounts: { Library: 3 },
|
|
2867
|
+
entityChangeStatus: {
|
|
2868
|
+
Library: { status: 'impacted' },
|
|
2869
|
+
ArticleTable: { status: 'edited' },
|
|
2870
|
+
},
|
|
2871
|
+
});
|
|
2872
|
+
// Library: impacted + has total scenarios but 0 in session → needs_recapture
|
|
2873
|
+
expect(result.components[0].status).toBe('needs_recapture');
|
|
2874
|
+
expect(result.components[0].scenarioCount).toBe(3);
|
|
2875
|
+
// ArticleTable: edited + has session scenarios → ok
|
|
2876
|
+
expect(result.components[1].status).toBe('ok');
|
|
2877
|
+
// needs_recapture should NOT count as missing
|
|
2878
|
+
expect(result.summary.componentsMissing).toBe(0);
|
|
2879
|
+
expect(result.summary.componentsNeedingRecapture).toBe(1);
|
|
2880
|
+
// should NOT fail the audit (scenariosNeedingRecapture handles it)
|
|
2881
|
+
expect(result.summary.allPassing).toBe(true);
|
|
2882
|
+
});
|
|
2883
|
+
it('should still mark component as missing when impacted but has zero total scenarios', () => {
|
|
2884
|
+
// New page added to glossary but never had scenarios — truly missing
|
|
2885
|
+
const result = computeAudit({
|
|
2886
|
+
components: [
|
|
2887
|
+
{
|
|
2888
|
+
name: 'NewPage',
|
|
2889
|
+
filePath: 'app/new/page.tsx',
|
|
2890
|
+
returnType: 'JSX.Element',
|
|
2891
|
+
},
|
|
2892
|
+
],
|
|
2893
|
+
functions: [],
|
|
2894
|
+
scenarioCounts: {},
|
|
2895
|
+
testFileExistence: {},
|
|
2896
|
+
totalScenarioCounts: {},
|
|
2897
|
+
entityChangeStatus: {
|
|
2898
|
+
NewPage: { status: 'impacted' },
|
|
2899
|
+
},
|
|
2900
|
+
});
|
|
2901
|
+
expect(result.components[0].status).toBe('missing');
|
|
2902
|
+
expect(result.summary.componentsMissing).toBe(1);
|
|
2903
|
+
});
|
|
2904
|
+
it('should use needs_recapture for edited entities with existing scenarios from prior sessions', () => {
|
|
2905
|
+
// Edited entities that already have scenarios from prior sessions
|
|
2906
|
+
// need recapture, not re-registration. The code changed, but the
|
|
2907
|
+
// scenarios exist — they just need fresh screenshots.
|
|
2908
|
+
const result = computeAudit({
|
|
2909
|
+
components: [
|
|
2910
|
+
{
|
|
2911
|
+
name: 'EditedComp',
|
|
2912
|
+
filePath: 'app/components/Edited.tsx',
|
|
2913
|
+
returnType: 'JSX.Element',
|
|
2914
|
+
},
|
|
2915
|
+
],
|
|
2916
|
+
functions: [],
|
|
2917
|
+
scenarioCounts: {},
|
|
2918
|
+
testFileExistence: {},
|
|
2919
|
+
totalScenarioCounts: { EditedComp: 2 },
|
|
2920
|
+
entityChangeStatus: {
|
|
2921
|
+
EditedComp: { status: 'edited' },
|
|
2922
|
+
},
|
|
2923
|
+
});
|
|
2924
|
+
expect(result.components[0].status).toBe('needs_recapture');
|
|
2925
|
+
expect(result.summary.componentsMissing).toBe(0);
|
|
2926
|
+
expect(result.summary.componentsNeedingRecapture).toBe(1);
|
|
2927
|
+
});
|
|
2928
|
+
it('should still mark as missing when new entity has zero total scenarios', () => {
|
|
2929
|
+
// Truly new component with no scenarios ever — needs scenarios created
|
|
2930
|
+
const result = computeAudit({
|
|
2931
|
+
components: [
|
|
2932
|
+
{
|
|
2933
|
+
name: 'BrandNew',
|
|
2934
|
+
filePath: 'app/components/BrandNew.tsx',
|
|
2935
|
+
returnType: 'JSX.Element',
|
|
2936
|
+
},
|
|
2937
|
+
],
|
|
2938
|
+
functions: [],
|
|
2939
|
+
scenarioCounts: {},
|
|
2940
|
+
testFileExistence: {},
|
|
2941
|
+
totalScenarioCounts: {},
|
|
2942
|
+
entityChangeStatus: {
|
|
2943
|
+
BrandNew: { status: 'new' },
|
|
2944
|
+
},
|
|
2945
|
+
});
|
|
2946
|
+
expect(result.components[0].status).toBe('missing');
|
|
2947
|
+
expect(result.summary.componentsMissing).toBe(1);
|
|
2948
|
+
});
|
|
2949
|
+
});
|
|
2950
|
+
// ── queryUnassociatedScenarios ──────────────────────────────────────
|
|
2951
|
+
describe('queryUnassociatedScenarios', () => {
|
|
2952
|
+
let db;
|
|
2953
|
+
let rawDb;
|
|
2954
|
+
const projectId = 'test-project-id';
|
|
2955
|
+
beforeEach(async () => {
|
|
2956
|
+
rawDb = new Database(':memory:');
|
|
2957
|
+
db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
|
|
2958
|
+
await db.schema
|
|
2959
|
+
.createTable('editor_scenarios')
|
|
2960
|
+
.addColumn('id', 'varchar', (col) => col.primaryKey())
|
|
2961
|
+
.addColumn('project_id', 'varchar', (col) => col.notNull())
|
|
2962
|
+
.addColumn('name', 'varchar', (col) => col.notNull())
|
|
2963
|
+
.addColumn('component_name', 'varchar')
|
|
2964
|
+
.addColumn('component_path', 'varchar')
|
|
2965
|
+
.addColumn('entity_sha', 'varchar')
|
|
2966
|
+
.addColumn('display_name', 'varchar')
|
|
2967
|
+
.addColumn('page_file_path', 'varchar')
|
|
2968
|
+
.addColumn('url', 'varchar')
|
|
2969
|
+
.addColumn('type', 'varchar')
|
|
2970
|
+
.addColumn('created_at', 'datetime')
|
|
2971
|
+
.addColumn('updated_at', 'datetime')
|
|
2972
|
+
.execute();
|
|
2973
|
+
});
|
|
2974
|
+
afterEach(async () => {
|
|
2975
|
+
await db.destroy();
|
|
2976
|
+
});
|
|
2977
|
+
it('should return empty when all scenarios have entity_sha', async () => {
|
|
2978
|
+
await db
|
|
2979
|
+
.insertInto('editor_scenarios')
|
|
2980
|
+
.values({
|
|
2981
|
+
id: 'sc-1',
|
|
2982
|
+
project_id: projectId,
|
|
2983
|
+
name: 'Header - Default',
|
|
2984
|
+
component_name: 'Header',
|
|
2985
|
+
component_path: 'src/components/Header.tsx',
|
|
2986
|
+
entity_sha: 'sha-header',
|
|
2987
|
+
created_at: '2026-03-16 23:00:00',
|
|
2988
|
+
updated_at: '2026-03-16 23:00:00',
|
|
2989
|
+
})
|
|
2990
|
+
.execute();
|
|
2991
|
+
const result = await queryUnassociatedScenarios(db, projectId, null);
|
|
2992
|
+
expect(result).toEqual([]);
|
|
2993
|
+
});
|
|
2994
|
+
it('should find component scenarios with NULL entity_sha', async () => {
|
|
2995
|
+
// This reproduces the Margo testapp bug: subagent registered scenarios
|
|
2996
|
+
// but entity records didn't exist yet, so entity_sha was never set
|
|
2997
|
+
await db
|
|
2998
|
+
.insertInto('editor_scenarios')
|
|
2999
|
+
.values({
|
|
3000
|
+
id: 'sc-1',
|
|
3001
|
+
project_id: projectId,
|
|
3002
|
+
name: 'SearchBar - Default',
|
|
3003
|
+
component_name: 'SearchBar',
|
|
3004
|
+
component_path: 'src/components/SearchBar.tsx',
|
|
3005
|
+
entity_sha: null,
|
|
3006
|
+
created_at: '2026-03-20 18:45:00',
|
|
3007
|
+
updated_at: '2026-03-20 18:45:00',
|
|
3008
|
+
})
|
|
3009
|
+
.execute();
|
|
3010
|
+
await db
|
|
3011
|
+
.insertInto('editor_scenarios')
|
|
3012
|
+
.values({
|
|
3013
|
+
id: 'sc-2',
|
|
3014
|
+
project_id: projectId,
|
|
3015
|
+
name: 'SearchBar - With Results',
|
|
3016
|
+
component_name: 'SearchBar',
|
|
3017
|
+
component_path: 'src/components/SearchBar.tsx',
|
|
3018
|
+
entity_sha: null,
|
|
3019
|
+
created_at: '2026-03-20 18:45:05',
|
|
3020
|
+
updated_at: '2026-03-20 18:45:05',
|
|
3021
|
+
})
|
|
3022
|
+
.execute();
|
|
3023
|
+
const result = await queryUnassociatedScenarios(db, projectId, null);
|
|
3024
|
+
expect(result).toHaveLength(1);
|
|
3025
|
+
expect(result[0].name).toBe('SearchBar');
|
|
3026
|
+
expect(result[0].filePath).toBe('src/components/SearchBar.tsx');
|
|
3027
|
+
expect(result[0].scenarioCount).toBe(2);
|
|
3028
|
+
expect(result[0].scenarioNames).toEqual(expect.arrayContaining([
|
|
3029
|
+
'SearchBar - Default',
|
|
3030
|
+
'SearchBar - With Results',
|
|
3031
|
+
]));
|
|
3032
|
+
});
|
|
3033
|
+
it('should find page scenarios with NULL entity_sha', async () => {
|
|
3034
|
+
await db
|
|
3035
|
+
.insertInto('editor_scenarios')
|
|
3036
|
+
.values({
|
|
3037
|
+
id: 'sc-1',
|
|
3038
|
+
project_id: projectId,
|
|
3039
|
+
name: 'Full Page — Rich Library',
|
|
3040
|
+
component_name: null,
|
|
3041
|
+
component_path: null,
|
|
3042
|
+
page_file_path: 'src/library/LibraryApp.tsx',
|
|
3043
|
+
entity_sha: null,
|
|
3044
|
+
created_at: '2026-03-20 18:50:00',
|
|
3045
|
+
updated_at: '2026-03-20 18:50:00',
|
|
3046
|
+
})
|
|
3047
|
+
.execute();
|
|
3048
|
+
const result = await queryUnassociatedScenarios(db, projectId, null);
|
|
3049
|
+
expect(result).toHaveLength(1);
|
|
3050
|
+
expect(result[0].name).toBe('LibraryApp');
|
|
3051
|
+
expect(result[0].filePath).toBe('src/library/LibraryApp.tsx');
|
|
3052
|
+
expect(result[0].scenarioCount).toBe(1);
|
|
3053
|
+
});
|
|
3054
|
+
it('should ignore scenarios without any file path (orphans without component_path or page_file_path)', async () => {
|
|
3055
|
+
// Scenarios with no file path at all can't be associated — they're not
|
|
3056
|
+
// actionable, so don't report them as unassociated
|
|
3057
|
+
await db
|
|
3058
|
+
.insertInto('editor_scenarios')
|
|
3059
|
+
.values({
|
|
3060
|
+
id: 'sc-1',
|
|
3061
|
+
project_id: projectId,
|
|
3062
|
+
name: 'Some Orphan',
|
|
3063
|
+
component_name: null,
|
|
3064
|
+
component_path: null,
|
|
3065
|
+
page_file_path: null,
|
|
3066
|
+
entity_sha: null,
|
|
3067
|
+
created_at: '2026-03-20 18:50:00',
|
|
3068
|
+
updated_at: '2026-03-20 18:50:00',
|
|
3069
|
+
})
|
|
3070
|
+
.execute();
|
|
3071
|
+
const result = await queryUnassociatedScenarios(db, projectId, null);
|
|
3072
|
+
expect(result).toEqual([]);
|
|
3073
|
+
});
|
|
3074
|
+
it('should group multiple components separately', async () => {
|
|
3075
|
+
// Two different components both missing entity_sha
|
|
3076
|
+
await db
|
|
3077
|
+
.insertInto('editor_scenarios')
|
|
3078
|
+
.values({
|
|
3079
|
+
id: 'sc-1',
|
|
3080
|
+
project_id: projectId,
|
|
3081
|
+
name: 'FullPageHeader - Default',
|
|
3082
|
+
component_name: 'FullPageHeader',
|
|
3083
|
+
component_path: 'src/components/FullPageHeader.tsx',
|
|
3084
|
+
entity_sha: null,
|
|
3085
|
+
created_at: '2026-03-20 18:45:00',
|
|
3086
|
+
updated_at: '2026-03-20 18:45:00',
|
|
3087
|
+
})
|
|
3088
|
+
.execute();
|
|
3089
|
+
await db
|
|
3090
|
+
.insertInto('editor_scenarios')
|
|
3091
|
+
.values({
|
|
3092
|
+
id: 'sc-2',
|
|
3093
|
+
project_id: projectId,
|
|
3094
|
+
name: 'SaveConfirmation - Visible',
|
|
3095
|
+
component_name: 'SaveConfirmation',
|
|
3096
|
+
component_path: 'src/components/SaveConfirmation.tsx',
|
|
3097
|
+
entity_sha: null,
|
|
3098
|
+
created_at: '2026-03-20 19:00:00',
|
|
3099
|
+
updated_at: '2026-03-20 19:00:00',
|
|
3100
|
+
})
|
|
3101
|
+
.execute();
|
|
3102
|
+
const result = await queryUnassociatedScenarios(db, projectId, null);
|
|
3103
|
+
expect(result).toHaveLength(2);
|
|
3104
|
+
const names = result.map((r) => r.name).sort();
|
|
3105
|
+
expect(names).toEqual(['FullPageHeader', 'SaveConfirmation']);
|
|
3106
|
+
});
|
|
3107
|
+
it('should only include scenarios from the specified project', async () => {
|
|
3108
|
+
await db
|
|
3109
|
+
.insertInto('editor_scenarios')
|
|
3110
|
+
.values({
|
|
3111
|
+
id: 'sc-1',
|
|
3112
|
+
project_id: 'other-project',
|
|
3113
|
+
name: 'SearchBar - Default',
|
|
3114
|
+
component_name: 'SearchBar',
|
|
3115
|
+
component_path: 'src/components/SearchBar.tsx',
|
|
3116
|
+
entity_sha: null,
|
|
3117
|
+
created_at: '2026-03-20 18:45:00',
|
|
3118
|
+
updated_at: '2026-03-20 18:45:00',
|
|
3119
|
+
})
|
|
3120
|
+
.execute();
|
|
3121
|
+
const result = await queryUnassociatedScenarios(db, projectId, null);
|
|
3122
|
+
expect(result).toEqual([]);
|
|
3123
|
+
});
|
|
3124
|
+
it('should scope to feature session when featureStartedAt is provided', async () => {
|
|
3125
|
+
// Pre-existing unassociated scenario (before session)
|
|
3126
|
+
await db
|
|
3127
|
+
.insertInto('editor_scenarios')
|
|
3128
|
+
.values({
|
|
3129
|
+
id: 'sc-old',
|
|
3130
|
+
project_id: projectId,
|
|
3131
|
+
name: 'OldComponent - Default',
|
|
3132
|
+
component_name: 'OldComponent',
|
|
3133
|
+
component_path: 'src/components/OldComponent.tsx',
|
|
3134
|
+
entity_sha: null,
|
|
3135
|
+
created_at: '2026-03-19 10:00:00',
|
|
3136
|
+
updated_at: '2026-03-19 10:00:00',
|
|
3137
|
+
})
|
|
3138
|
+
.execute();
|
|
3139
|
+
// New unassociated scenario (during session)
|
|
3140
|
+
await db
|
|
3141
|
+
.insertInto('editor_scenarios')
|
|
3142
|
+
.values({
|
|
3143
|
+
id: 'sc-new',
|
|
3144
|
+
project_id: projectId,
|
|
3145
|
+
name: 'NewComponent - Default',
|
|
3146
|
+
component_name: 'NewComponent',
|
|
3147
|
+
component_path: 'src/components/NewComponent.tsx',
|
|
3148
|
+
entity_sha: null,
|
|
3149
|
+
created_at: '2026-03-20 18:45:00',
|
|
3150
|
+
updated_at: '2026-03-20 18:45:00',
|
|
3151
|
+
})
|
|
3152
|
+
.execute();
|
|
3153
|
+
const result = await queryUnassociatedScenarios(db, projectId, '2026-03-20T18:00:00.000Z');
|
|
3154
|
+
// Should only find the session-scoped one
|
|
3155
|
+
expect(result).toHaveLength(1);
|
|
3156
|
+
expect(result[0].name).toBe('NewComponent');
|
|
3157
|
+
});
|
|
3158
|
+
it('should include re-registered scenarios (updated_at in session) even if created before', async () => {
|
|
3159
|
+
await db
|
|
3160
|
+
.insertInto('editor_scenarios')
|
|
3161
|
+
.values({
|
|
3162
|
+
id: 'sc-1',
|
|
3163
|
+
project_id: projectId,
|
|
3164
|
+
name: 'SearchBar - Default',
|
|
3165
|
+
component_name: 'SearchBar',
|
|
3166
|
+
component_path: 'src/components/SearchBar.tsx',
|
|
3167
|
+
entity_sha: null,
|
|
3168
|
+
created_at: '2026-03-19 10:00:00',
|
|
3169
|
+
updated_at: '2026-03-20 18:45:00', // re-registered during session
|
|
3170
|
+
})
|
|
3171
|
+
.execute();
|
|
3172
|
+
const result = await queryUnassociatedScenarios(db, projectId, '2026-03-20T18:00:00.000Z');
|
|
3173
|
+
expect(result).toHaveLength(1);
|
|
3174
|
+
expect(result[0].name).toBe('SearchBar');
|
|
3175
|
+
});
|
|
3176
|
+
it('should not include scenarios with entity_sha set (even if stale)', async () => {
|
|
3177
|
+
// This scenario has an entity_sha — even if it's stale, that's a
|
|
3178
|
+
// different problem (handled by queryIncompleteEntities)
|
|
3179
|
+
await db
|
|
3180
|
+
.insertInto('editor_scenarios')
|
|
3181
|
+
.values({
|
|
3182
|
+
id: 'sc-1',
|
|
3183
|
+
project_id: projectId,
|
|
3184
|
+
name: 'Header - Default',
|
|
3185
|
+
component_name: 'Header',
|
|
3186
|
+
component_path: 'src/components/Header.tsx',
|
|
3187
|
+
entity_sha: 'sha-old-version',
|
|
3188
|
+
created_at: '2026-03-20 18:45:00',
|
|
3189
|
+
updated_at: '2026-03-20 18:45:00',
|
|
3190
|
+
})
|
|
3191
|
+
.execute();
|
|
3192
|
+
const result = await queryUnassociatedScenarios(db, projectId, null);
|
|
3193
|
+
expect(result).toEqual([]);
|
|
3194
|
+
});
|
|
3195
|
+
});
|
|
3196
|
+
// ── isAutoRemediable with unassociatedScenarios ────────────────────
|
|
3197
|
+
describe('isAutoRemediable always returns false (no inline full analysis)', () => {
|
|
3198
|
+
// Full analyze-imports takes minutes on large projects. The audit should
|
|
3199
|
+
// never trigger it — only the lightweight backfill path is acceptable.
|
|
3200
|
+
it('should return false for unassociatedScenarios only', () => {
|
|
3201
|
+
expect(isAutoRemediable({ unassociatedScenarios: 3 }, false)).toBe(false);
|
|
3202
|
+
});
|
|
3203
|
+
it('should return false for incompleteEntities + unassociatedScenarios', () => {
|
|
3204
|
+
expect(isAutoRemediable({ incompleteEntities: 1, unassociatedScenarios: 2 }, false)).toBe(false);
|
|
3205
|
+
});
|
|
3206
|
+
it('should return false even with no other failures', () => {
|
|
3207
|
+
expect(isAutoRemediable({ unassociatedScenarios: 2 }, false)).toBe(false);
|
|
3208
|
+
});
|
|
3209
|
+
it('should return false when already attempted', () => {
|
|
3210
|
+
expect(isAutoRemediable({ unassociatedScenarios: 3 }, true)).toBe(false);
|
|
3211
|
+
});
|
|
3212
|
+
});
|
|
3213
|
+
describe('suggestedTestFile for functions without testFile', () => {
|
|
3214
|
+
it('should suggest conventional .test.ts path when testFile is undefined', () => {
|
|
3215
|
+
const result = computeAudit({
|
|
3216
|
+
components: [],
|
|
3217
|
+
functions: [
|
|
3218
|
+
{ name: 'useLibraryShell', filePath: 'app/library/context.tsx' },
|
|
3219
|
+
],
|
|
3220
|
+
scenarioCounts: {},
|
|
3221
|
+
testFileExistence: {},
|
|
3222
|
+
});
|
|
3223
|
+
expect(result.functions[0].suggestedTestFile).toBe('app/library/context.test.ts');
|
|
3224
|
+
expect(result.functions[0].status).toBe('missing');
|
|
3225
|
+
});
|
|
3226
|
+
it('should suggest .test.ts for .ts files', () => {
|
|
3227
|
+
const result = computeAudit({
|
|
3228
|
+
components: [],
|
|
3229
|
+
functions: [{ name: 'calculatePrice', filePath: 'app/lib/pricing.ts' }],
|
|
3230
|
+
scenarioCounts: {},
|
|
3231
|
+
testFileExistence: {},
|
|
3232
|
+
});
|
|
3233
|
+
expect(result.functions[0].suggestedTestFile).toBe('app/lib/pricing.test.ts');
|
|
3234
|
+
});
|
|
3235
|
+
it('should not set suggestedTestFile when testFile is already specified', () => {
|
|
3236
|
+
const result = computeAudit({
|
|
3237
|
+
components: [],
|
|
3238
|
+
functions: [
|
|
3239
|
+
{
|
|
3240
|
+
name: 'calculatePrice',
|
|
3241
|
+
filePath: 'app/lib/pricing.ts',
|
|
3242
|
+
testFile: 'app/lib/pricing.test.ts',
|
|
3243
|
+
},
|
|
3244
|
+
],
|
|
3245
|
+
scenarioCounts: {},
|
|
3246
|
+
testFileExistence: { 'app/lib/pricing.test.ts': true },
|
|
3247
|
+
});
|
|
3248
|
+
expect(result.functions[0].suggestedTestFile).toBeUndefined();
|
|
3249
|
+
});
|
|
3250
|
+
});
|
|
3251
|
+
describe('hint for missing components', () => {
|
|
3252
|
+
it('should hint that layout files need app-level scenarios', () => {
|
|
3253
|
+
const result = computeAudit({
|
|
3254
|
+
components: [
|
|
3255
|
+
{ name: 'LibraryLayout', filePath: 'app/library/layout.tsx' },
|
|
3256
|
+
],
|
|
3257
|
+
functions: [],
|
|
3258
|
+
scenarioCounts: {},
|
|
3259
|
+
testFileExistence: {},
|
|
3260
|
+
});
|
|
3261
|
+
expect(result.components[0].hint).toContain('layout');
|
|
3262
|
+
expect(result.components[0].hint).toContain('pageFilePath');
|
|
3263
|
+
});
|
|
3264
|
+
it('should hint that page files need app-level scenarios', () => {
|
|
3265
|
+
const result = computeAudit({
|
|
3266
|
+
components: [
|
|
3267
|
+
{ name: 'InboxPage', filePath: 'app/library/inbox/page.tsx' },
|
|
3268
|
+
],
|
|
3269
|
+
functions: [],
|
|
3270
|
+
scenarioCounts: {},
|
|
3271
|
+
testFileExistence: {},
|
|
3272
|
+
});
|
|
3273
|
+
expect(result.components[0].hint).toContain('page');
|
|
3274
|
+
expect(result.components[0].hint).toContain('pageFilePath');
|
|
3275
|
+
});
|
|
3276
|
+
it('should hint that regular components need isolation routes', () => {
|
|
3277
|
+
const result = computeAudit({
|
|
3278
|
+
components: [
|
|
3279
|
+
{ name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
|
|
3280
|
+
],
|
|
3281
|
+
functions: [],
|
|
3282
|
+
scenarioCounts: {},
|
|
3283
|
+
testFileExistence: {},
|
|
3284
|
+
});
|
|
3285
|
+
expect(result.components[0].hint).toContain('isolated-components');
|
|
3286
|
+
});
|
|
3287
|
+
it('should not set hint when component has scenarios', () => {
|
|
3288
|
+
const result = computeAudit({
|
|
3289
|
+
components: [
|
|
3290
|
+
{ name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
|
|
3291
|
+
],
|
|
3292
|
+
functions: [],
|
|
3293
|
+
scenarioCounts: { DrinkCard: 2 },
|
|
3294
|
+
testFileExistence: {},
|
|
3295
|
+
});
|
|
3296
|
+
expect(result.components[0].hint).toBeUndefined();
|
|
3297
|
+
});
|
|
3298
|
+
});
|
|
3299
|
+
describe('formatIncompleteEntityGuidance', () => {
|
|
3300
|
+
it('should include the entity name and scenario count', () => {
|
|
3301
|
+
const { formatIncompleteEntityGuidance } = require('../editorAudit');
|
|
3302
|
+
const result = formatIncompleteEntityGuidance({
|
|
3303
|
+
entitySha: 'abc123',
|
|
3304
|
+
name: 'RuleBuilder',
|
|
3305
|
+
scenarioCount: 5,
|
|
3306
|
+
preExisting: false,
|
|
3307
|
+
});
|
|
3308
|
+
expect(result).toContain('RuleBuilder');
|
|
3309
|
+
expect(result).toContain('5');
|
|
3310
|
+
});
|
|
3311
|
+
it('should tell Claude the exact fix command', () => {
|
|
3312
|
+
const { formatIncompleteEntityGuidance } = require('../editorAudit');
|
|
3313
|
+
const result = formatIncompleteEntityGuidance({
|
|
3314
|
+
entitySha: 'abc123',
|
|
3315
|
+
name: 'RuleBuilder',
|
|
3316
|
+
scenarioCount: 5,
|
|
3317
|
+
preExisting: false,
|
|
3318
|
+
});
|
|
3319
|
+
expect(result).toContain('codeyam editor analyze-imports');
|
|
3320
|
+
});
|
|
3321
|
+
it('should flag pre-existing issues as non-blocking', () => {
|
|
3322
|
+
const { formatIncompleteEntityGuidance } = require('../editorAudit');
|
|
3323
|
+
const result = formatIncompleteEntityGuidance({
|
|
3324
|
+
entitySha: 'abc123',
|
|
3325
|
+
name: 'RuleBuilder',
|
|
3326
|
+
scenarioCount: 5,
|
|
3327
|
+
preExisting: true,
|
|
3328
|
+
});
|
|
3329
|
+
expect(result).toContain('pre-existing');
|
|
3330
|
+
});
|
|
3331
|
+
it('should explain what incomplete means', () => {
|
|
3332
|
+
const { formatIncompleteEntityGuidance } = require('../editorAudit');
|
|
3333
|
+
const result = formatIncompleteEntityGuidance({
|
|
3334
|
+
entitySha: 'abc123',
|
|
3335
|
+
name: 'RuleBuilder',
|
|
3336
|
+
scenarioCount: 5,
|
|
3337
|
+
preExisting: false,
|
|
3338
|
+
});
|
|
3339
|
+
// Should explain the root cause, not just the symptom
|
|
3340
|
+
expect(result).toMatch(/scenario.*without.*import graph|import graph.*not.*built/i);
|
|
3341
|
+
});
|
|
3342
|
+
});
|
|
3343
|
+
describe('getIncompleteEntityFilePaths', () => {
|
|
3344
|
+
// The audit should auto-fix incomplete entities by running analysis on
|
|
3345
|
+
// just their specific file paths, not all 117+ files. This function
|
|
3346
|
+
// resolves entity SHAs to file paths for targeted analysis.
|
|
3347
|
+
let db;
|
|
3348
|
+
let rawDb;
|
|
3349
|
+
beforeEach(async () => {
|
|
3350
|
+
rawDb = new Database(':memory:');
|
|
3351
|
+
db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
|
|
3352
|
+
await db.schema
|
|
3353
|
+
.createTable('entities')
|
|
3354
|
+
.addColumn('sha', 'varchar', (col) => col.primaryKey())
|
|
3355
|
+
.addColumn('name', 'varchar')
|
|
3356
|
+
.addColumn('file_path', 'varchar')
|
|
3357
|
+
.addColumn('project_id', 'varchar')
|
|
3358
|
+
.addColumn('entity_type', 'varchar')
|
|
3359
|
+
.execute();
|
|
3360
|
+
});
|
|
3361
|
+
afterEach(() => {
|
|
3362
|
+
rawDb.close();
|
|
3363
|
+
});
|
|
3364
|
+
it('should resolve entity SHAs to file paths from the entities table', async () => {
|
|
3365
|
+
const { getIncompleteEntityFilePaths } = require('../editorAudit');
|
|
3366
|
+
await db
|
|
3367
|
+
.insertInto('entities')
|
|
3368
|
+
.values([
|
|
3369
|
+
{
|
|
3370
|
+
sha: 'sha-rule',
|
|
3371
|
+
name: 'RuleBuilder',
|
|
3372
|
+
file_path: 'app/components/RuleBuilder.tsx',
|
|
3373
|
+
project_id: 'p1',
|
|
3374
|
+
entity_type: 'component',
|
|
3375
|
+
},
|
|
3376
|
+
{
|
|
3377
|
+
sha: 'sha-row',
|
|
3378
|
+
name: 'ArticleTableRow',
|
|
3379
|
+
file_path: 'app/components/ArticleTableRow.tsx',
|
|
3380
|
+
project_id: 'p1',
|
|
3381
|
+
entity_type: 'component',
|
|
3382
|
+
},
|
|
3383
|
+
])
|
|
3384
|
+
.execute();
|
|
3385
|
+
const result = await getIncompleteEntityFilePaths(db, [
|
|
3386
|
+
{
|
|
3387
|
+
entitySha: 'sha-rule',
|
|
3388
|
+
name: 'RuleBuilder',
|
|
3389
|
+
scenarioCount: 5,
|
|
3390
|
+
preExisting: false,
|
|
3391
|
+
},
|
|
3392
|
+
{
|
|
3393
|
+
entitySha: 'sha-row',
|
|
3394
|
+
name: 'ArticleTableRow',
|
|
3395
|
+
scenarioCount: 2,
|
|
3396
|
+
preExisting: false,
|
|
3397
|
+
},
|
|
3398
|
+
]);
|
|
3399
|
+
expect(result).toContain('app/components/RuleBuilder.tsx');
|
|
3400
|
+
expect(result).toContain('app/components/ArticleTableRow.tsx');
|
|
3401
|
+
expect(result).toHaveLength(2);
|
|
3402
|
+
});
|
|
3403
|
+
it('should skip entities whose SHA is not in the entities table', async () => {
|
|
3404
|
+
const { getIncompleteEntityFilePaths } = require('../editorAudit');
|
|
3405
|
+
const result = await getIncompleteEntityFilePaths(db, [
|
|
3406
|
+
{
|
|
3407
|
+
entitySha: 'nonexistent-sha',
|
|
3408
|
+
name: 'Ghost',
|
|
3409
|
+
scenarioCount: 1,
|
|
3410
|
+
preExisting: false,
|
|
3411
|
+
},
|
|
3412
|
+
]);
|
|
3413
|
+
expect(result).toHaveLength(0);
|
|
3414
|
+
});
|
|
3415
|
+
it('should deduplicate file paths', async () => {
|
|
3416
|
+
const { getIncompleteEntityFilePaths } = require('../editorAudit');
|
|
3417
|
+
await db
|
|
3418
|
+
.insertInto('entities')
|
|
3419
|
+
.values([
|
|
3420
|
+
{
|
|
3421
|
+
sha: 'sha-v1',
|
|
3422
|
+
name: 'Foo',
|
|
3423
|
+
file_path: 'app/Foo.tsx',
|
|
3424
|
+
project_id: 'p1',
|
|
3425
|
+
entity_type: 'component',
|
|
3426
|
+
},
|
|
3427
|
+
{
|
|
3428
|
+
sha: 'sha-v2',
|
|
3429
|
+
name: 'Foo',
|
|
3430
|
+
file_path: 'app/Foo.tsx',
|
|
3431
|
+
project_id: 'p1',
|
|
3432
|
+
entity_type: 'component',
|
|
3433
|
+
},
|
|
3434
|
+
])
|
|
3435
|
+
.execute();
|
|
3436
|
+
const result = await getIncompleteEntityFilePaths(db, [
|
|
3437
|
+
{
|
|
3438
|
+
entitySha: 'sha-v1',
|
|
3439
|
+
name: 'Foo',
|
|
3440
|
+
scenarioCount: 3,
|
|
3441
|
+
preExisting: false,
|
|
3442
|
+
},
|
|
3443
|
+
{
|
|
3444
|
+
entitySha: 'sha-v2',
|
|
3445
|
+
name: 'Foo',
|
|
3446
|
+
scenarioCount: 1,
|
|
3447
|
+
preExisting: false,
|
|
3448
|
+
},
|
|
3449
|
+
]);
|
|
3450
|
+
expect(result).toEqual(['app/Foo.tsx']);
|
|
3451
|
+
});
|
|
3452
|
+
});
|
|
3453
|
+
describe('isAutoRemediable never triggers full analysis', () => {
|
|
3454
|
+
// The audit must NEVER run handleAnalyzeImports inline — it takes minutes
|
|
3455
|
+
// for large projects. Auto-remediation should only do the lightweight
|
|
3456
|
+
// entity SHA backfill. isAutoRemediable is now always false; the callers
|
|
3457
|
+
// use needsBackfillOnly for the fast path instead.
|
|
3458
|
+
it('should always return false regardless of summary state', () => {
|
|
3459
|
+
expect(isAutoRemediable({ incompleteEntities: 5 }, false)).toBe(false);
|
|
3460
|
+
expect(isAutoRemediable({ unassociatedScenarios: 3 }, false)).toBe(false);
|
|
3461
|
+
expect(isAutoRemediable({ incompleteEntities: 1, unassociatedScenarios: 2 }, false)).toBe(false);
|
|
3462
|
+
});
|
|
3463
|
+
});
|
|
3464
|
+
// ── isOnlyPreExistingIncomplete ─────────────────────────────────────
|
|
3465
|
+
describe('isOnlyPreExistingIncomplete', () => {
|
|
3466
|
+
it('should return true when all incomplete entities are pre-existing and no other failures', () => {
|
|
3467
|
+
expect(isOnlyPreExistingIncomplete({
|
|
3468
|
+
incompleteEntities: 2,
|
|
3469
|
+
preExistingIncompleteEntities: 2,
|
|
3470
|
+
componentsMissing: 0,
|
|
3471
|
+
componentsWithErrors: 0,
|
|
3472
|
+
functionsFailing: 0,
|
|
3473
|
+
functionsNameMismatch: 0,
|
|
3474
|
+
functionsMissing: 0,
|
|
3475
|
+
missingFromGlossary: 0,
|
|
3476
|
+
miscategorizedScenarios: 0,
|
|
3477
|
+
scenariosNeedingRecapture: 0,
|
|
3478
|
+
}, [
|
|
3479
|
+
{
|
|
3480
|
+
entitySha: 'a',
|
|
3481
|
+
name: 'RuleBuilder',
|
|
3482
|
+
scenarioCount: 5,
|
|
3483
|
+
preExisting: true,
|
|
3484
|
+
},
|
|
3485
|
+
{
|
|
3486
|
+
entitySha: 'b',
|
|
3487
|
+
name: 'ArticleTableRow',
|
|
3488
|
+
scenarioCount: 2,
|
|
3489
|
+
preExisting: true,
|
|
3490
|
+
},
|
|
3491
|
+
])).toBe(true);
|
|
3492
|
+
});
|
|
3493
|
+
it('should return false when some incomplete entities are NOT pre-existing', () => {
|
|
3494
|
+
expect(isOnlyPreExistingIncomplete({
|
|
3495
|
+
incompleteEntities: 2,
|
|
3496
|
+
preExistingIncompleteEntities: 1,
|
|
3497
|
+
componentsMissing: 0,
|
|
3498
|
+
componentsWithErrors: 0,
|
|
3499
|
+
functionsFailing: 0,
|
|
3500
|
+
functionsNameMismatch: 0,
|
|
3501
|
+
functionsMissing: 0,
|
|
3502
|
+
missingFromGlossary: 0,
|
|
3503
|
+
miscategorizedScenarios: 0,
|
|
3504
|
+
scenariosNeedingRecapture: 0,
|
|
3505
|
+
}, [
|
|
3506
|
+
{
|
|
3507
|
+
entitySha: 'a',
|
|
3508
|
+
name: 'RuleBuilder',
|
|
3509
|
+
scenarioCount: 5,
|
|
3510
|
+
preExisting: true,
|
|
3511
|
+
},
|
|
3512
|
+
{
|
|
3513
|
+
entitySha: 'b',
|
|
3514
|
+
name: 'NewComponent',
|
|
3515
|
+
scenarioCount: 1,
|
|
3516
|
+
preExisting: false,
|
|
3517
|
+
},
|
|
3518
|
+
])).toBe(false);
|
|
3519
|
+
});
|
|
3520
|
+
it('should return false when there are other failures besides incomplete entities', () => {
|
|
3521
|
+
expect(isOnlyPreExistingIncomplete({
|
|
3522
|
+
incompleteEntities: 2,
|
|
3523
|
+
preExistingIncompleteEntities: 2,
|
|
3524
|
+
componentsMissing: 1,
|
|
3525
|
+
}, [
|
|
3526
|
+
{
|
|
3527
|
+
entitySha: 'a',
|
|
3528
|
+
name: 'RuleBuilder',
|
|
3529
|
+
scenarioCount: 5,
|
|
3530
|
+
preExisting: true,
|
|
3531
|
+
},
|
|
3532
|
+
{
|
|
3533
|
+
entitySha: 'b',
|
|
3534
|
+
name: 'ArticleTableRow',
|
|
3535
|
+
scenarioCount: 2,
|
|
3536
|
+
preExisting: true,
|
|
3537
|
+
},
|
|
3538
|
+
])).toBe(false);
|
|
3539
|
+
});
|
|
3540
|
+
it('should return false when incomplete entities array is empty', () => {
|
|
3541
|
+
expect(isOnlyPreExistingIncomplete({ incompleteEntities: 0 }, [])).toBe(false);
|
|
3542
|
+
});
|
|
3543
|
+
it('should return false when incomplete entities array is missing', () => {
|
|
3544
|
+
expect(isOnlyPreExistingIncomplete({ incompleteEntities: 2, preExistingIncompleteEntities: 2 }, undefined)).toBe(false);
|
|
3545
|
+
});
|
|
3546
|
+
});
|
|
3547
|
+
describe('isOnlyIncompleteEntities with unassociatedScenarios', () => {
|
|
3548
|
+
it('should return true when only unassociatedScenarios present', () => {
|
|
3549
|
+
expect(isOnlyIncompleteEntities({ unassociatedScenarios: 5 })).toBe(true);
|
|
3550
|
+
});
|
|
3551
|
+
it('should return false when unassociatedScenarios present with other failures', () => {
|
|
3552
|
+
expect(isOnlyIncompleteEntities({
|
|
3553
|
+
unassociatedScenarios: 5,
|
|
3554
|
+
functionsMissing: 1,
|
|
3555
|
+
})).toBe(false);
|
|
3556
|
+
});
|
|
3557
|
+
});
|
|
2317
3558
|
});
|
|
2318
3559
|
//# sourceMappingURL=editorAudit.test.js.map
|