Skip to content

Commit 8ed77e4

Browse files
committed
fix(compare): update drift detection logic to exclude added folders
- Modify comparison logic to classify only orphaned and drift folders as 'DRIFT'; added folders are now considered growth and result in a 'PASS'. - Update related tests to reflect the new logic, ensuring accurate detection of drift and growth scenarios.
1 parent c145c85 commit 8ed77e4

File tree

7 files changed

+39
-22
lines changed

7 files changed

+39
-22
lines changed

src/cli/handlers/compareHandler.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,8 @@ async function handleGitBaselineCompare(options: {
528528
};
529529

530530
// Recalculate overall status
531-
result.status = addedFolders > 0 || orphanedFolders > 0 || driftFolders > 0 ? 'DRIFT' : 'PASS';
531+
// Only orphaned folders and drift folders qualify as drift; added folders are growth, not drift
532+
result.status = orphanedFolders > 0 || driftFolders > 0 ? 'DRIFT' : 'PASS';
532533

533534
displayMultiFileCompareResult(result, stats, quiet);
534535

src/extractors/shared/propTypeNormalizer.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ export function normalizePropType(typeText: string, isOptional: boolean): PropTy
4444
return trimmed.slice(1, -1);
4545
}
4646
return trimmed;
47-
});
47+
})
48+
.sort(); // Sort literals for determinism
4849

4950
return {
5051
type: 'literal-union',

src/utils/hash.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,9 @@ export function bundleHash(
182182
*/
183183
export function stableStringify(obj: unknown): string {
184184
return JSON.stringify(obj, (k, v) => {
185-
if (v && typeof v === 'object' && !Array.isArray(v)) {
186-
// Sort object keys
185+
// Explicitly check for null (typeof null === 'object' in JavaScript)
186+
if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
187+
// Sort object keys recursively
187188
return Object.keys(v as Record<string, unknown>)
188189
.sort()
189190
.reduce((o, key) => {
@@ -192,7 +193,10 @@ export function stableStringify(obj: unknown): string {
192193
}, {} as Record<string, unknown>);
193194
}
194195
if (Array.isArray(v)) {
195-
// Sort arrays for stability
196+
// NOTE: Arrays are expected to be normalized and deterministically ordered
197+
// by the extraction/normalization layer before reaching stableStringify.
198+
// We only sort primitives here to prevent accidental ordering issues.
199+
// Arrays of objects should be pre-sorted elsewhere (e.g., via sortObject).
196200
return [...v].sort();
197201
}
198202
return v;
@@ -201,12 +205,24 @@ export function stableStringify(obj: unknown): string {
201205

202206
/**
203207
* Sort object keys for stable hashing
208+
* Recursively sorts nested objects and arrays to ensure complete determinism
204209
*/
205210
function sortObject<T extends Record<string, unknown>>(obj: T): T {
206211
const sorted = Object.keys(obj)
207212
.sort()
208213
.reduce((acc, key) => {
209-
acc[key] = obj[key];
214+
const value = obj[key];
215+
// Recursively sort nested objects
216+
if (value && typeof value === 'object' && !Array.isArray(value) && value !== null) {
217+
acc[key] = sortObject(value as Record<string, unknown>);
218+
}
219+
// Sort arrays for determinism (e.g., literals arrays in PropType objects)
220+
else if (Array.isArray(value)) {
221+
acc[key] = [...value].sort();
222+
}
223+
else {
224+
acc[key] = value;
225+
}
210226
return acc;
211227
}, {} as Record<string, unknown>);
212228

tests/e2e/cli.compare.test.ts

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -637,19 +637,15 @@ describe('CLI Compare Command Tests', () => {
637637
}], null, 2)
638638
);
639639

640-
// Compare - should detect ADDED FILE
641-
try {
642-
await execAsync(
643-
`node dist/cli/stamp.js context compare ${join(outDir1, 'context_main.json')} ${join(outDir2, 'context_main.json')}`
644-
);
645-
expect.fail('Should have detected drift');
646-
} catch (error: any) {
647-
expect(error.code).toBe(1);
648-
const output = error.stdout || '';
649-
expect(output).toContain('DRIFT');
650-
expect(output).toContain('ADDED FILE');
651-
expect(output).toContain('src/new-folder/context.json');
652-
}
640+
// Compare - should detect ADDED FILE (but additions are growth, not drift, so should PASS)
641+
const result = await execAsync(
642+
`node dist/cli/stamp.js context compare ${join(outDir1, 'context_main.json')} ${join(outDir2, 'context_main.json')}`
643+
);
644+
const output = result.stdout || '';
645+
// Additions are growth, not drift - should be PASS
646+
expect(output).toContain('PASS');
647+
expect(output).toContain('ADDED FILE');
648+
expect(output).toContain('src/new-folder/context.json');
653649
}, 60000);
654650

655651
it('should detect ORPHANED FILE when folder is removed', async () => {

tests/unit/commands/compare/core.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,8 @@ describe('diff', () => {
347347
const newIdx = index(newBundles);
348348

349349
const result = diff(oldIdx, newIdx);
350-
expect(result.status).toBe('DRIFT');
350+
// Additions are growth, not drift - should be PASS
351+
expect(result.status).toBe('PASS');
351352
expect(result.added).toContain('src/button.tsx');
352353
expect(result.removed).toHaveLength(0);
353354
});

tests/unit/commands/compare/multiFile.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,8 @@ describe('multiFileCompare', () => {
151151
newIndexFile: '/new/context_main.json',
152152
});
153153

154-
expect(result.status).toBe('DRIFT');
154+
// Additions are growth, not drift - should be PASS
155+
expect(result.status).toBe('PASS');
155156
expect(result.summary.addedFolders).toBe(1);
156157
});
157158

tests/unit/commands/compare/singleFile.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,8 @@ describe('compareCommand', () => {
129129
newFile: 'new.json',
130130
});
131131

132-
expect(result.status).toBe('DRIFT');
132+
// Additions are growth, not drift - should be PASS
133+
expect(result.status).toBe('PASS');
133134
expect(result.added).toContain('src/button.tsx'); // Note: lowercased
134135
});
135136

0 commit comments

Comments
 (0)