Skip to content

Commit 8c74db4

Browse files
sjsyrekclaude
andcommitted
fix(sync): report accurate key counts in --frozen drift message
`deepl sync --frozen` previously reported `Sync drift detected: 0 new, 0 stale keys.` whenever the drift was caused by a newly-added target locale (the `hasNewLocale` branch) — even though `--dry-run` against the same state correctly reported the backfill count. The frozen branch in `processBucket` short-circuited via `driftDetected = true` before promoting current-status keys missing a target-locale translation into `newKeysDelta`. The dry-run branch a few lines below already does this promotion; the frozen path now mirrors it. The drift message in `displayResult` also dropped `deletedKeys` entirely even though `processBucket` populates that count correctly under frozen drift (covered by an existing integration test). The message now uses the same nonzero-only join pattern as the success-path summary formatter, surfacing `new`, `stale`, and `deleted` counts when present. The drift exit code (10) is unchanged. The dry-run output is unchanged. Tests: - New integration test exercising the hasNewLocale frozen branch - Two new unit tests covering the broadened message format - Full unit (4294) and integration (804) suites pass; lint + type-check clean Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent df920ac commit 8c74db4

5 files changed

Lines changed: 95 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- **sync**: `deepl sync --frozen` now reports an accurate key count when drift is caused by a newly-added target locale. Previously the message read `Sync drift detected: 0 new, 0 stale keys.` because the frozen branch in `processBucket` short-circuited before promoting current-status keys missing a target-locale translation into `newKeys` — even though `--dry-run` against the same state correctly reported the backfill count. The drift exit code (10) is unchanged. The drift message now also surfaces `deletedKeys` and only mentions nonzero categories, mirroring the success-path summary format.
13+
1014
## [1.2.0] - 2026-04-25
1115

1216
### Added

src/cli/commands/sync-command.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,12 @@ export class SyncCommand {
547547
}
548548

549549
if (result.driftDetected) {
550-
Logger.info(`Sync drift detected: ${result.newKeys} new, ${result.staleKeys} stale keys.`);
550+
const driftParts: string[] = [];
551+
if (result.newKeys > 0) driftParts.push(`${result.newKeys} new`);
552+
if (result.staleKeys > 0) driftParts.push(`${result.staleKeys} stale`);
553+
if (result.deletedKeys > 0) driftParts.push(`${result.deletedKeys} deleted`);
554+
const driftSummary = driftParts.length > 0 ? driftParts.join(', ') : 'no key-level diffs surfaced';
555+
Logger.info(`Sync drift detected: ${driftSummary} keys.`);
551556
return;
552557
}
553558

src/sync/sync-process-bucket.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ export async function processBucket(
108108
const hasNew = toTranslate.some(d => d.status === 'new') || hasNewLocale;
109109
const hasStale = toTranslate.some(d => d.status === 'stale');
110110
if ((failMissing && (hasNew || deletedDiffs.length > 0)) || (failStale && hasStale)) {
111+
// Promote current keys missing a target-locale translation into newKeysDelta
112+
// so the displayed count matches dry-run for the same input state.
113+
if (hasNewLocale) {
114+
const currentDiffs = diffs.filter(d => d.status === 'current');
115+
out.newKeysDelta += currentDiffs.length;
116+
}
111117
out.driftDetected = true;
112118
return out;
113119
}

tests/integration/sync.integration.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,49 @@ buckets:
416416
expect(result.deletedKeys).toBeGreaterThanOrEqual(1);
417417
expect(result.success).toBe(false);
418418
});
419+
420+
it('should report newKeys for a newly-added target locale needing backfill', async () => {
421+
// Regression: previously the frozen branch detected drift via hasNewLocale
422+
// but returned without promoting current-status keys into newKeysDelta,
423+
// so displayResult printed "0 new, 0 stale" even though N keys needed
424+
// translation for the new locale. Mirrors the dry-run promotion at
425+
// sync-process-bucket.ts lines 124-127.
426+
writeYamlConfig(tmpDir, BASIC_CONFIG_YAML);
427+
writeSourceFile(tmpDir, 'locales/en.json', SOURCE_JSON);
428+
429+
nock(DEEPL_FREE_API_URL)
430+
.post('/v2/translate')
431+
.reply(200, {
432+
translations: [
433+
{ text: 'Auf Wiedersehen', detected_source_language: 'EN', billed_characters: 15 },
434+
{ text: 'Hallo', detected_source_language: 'EN', billed_characters: 5 },
435+
{ text: 'Willkommen', detected_source_language: 'EN', billed_characters: 10 },
436+
],
437+
});
438+
439+
const configInitial = await loadSyncConfig(tmpDir);
440+
await syncService.sync(configInitial);
441+
442+
// Add a second target locale (fr) without touching the source file.
443+
const expandedConfigYaml = `version: 1
444+
source_locale: en
445+
target_locales:
446+
- de
447+
- fr
448+
buckets:
449+
json:
450+
include:
451+
- "locales/en.json"
452+
`;
453+
writeYamlConfig(tmpDir, expandedConfigYaml);
454+
455+
const configAfter = await loadSyncConfig(tmpDir);
456+
const result = await syncService.sync(configAfter, { frozen: true });
457+
458+
expect(result.driftDetected).toBe(true);
459+
expect(result.success).toBe(false);
460+
expect(result.newKeys).toBeGreaterThan(0);
461+
});
419462
});
420463

421464
describe('dry run', () => {

tests/unit/sync/sync-command.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,42 @@ describe('SyncCommand', () => {
127127
expect(infoOutput.toLowerCase()).toContain('drift');
128128
});
129129

130+
it('should include new and deleted counts in drift message when both are nonzero', async () => {
131+
const result = makeResult({
132+
driftDetected: true,
133+
success: false,
134+
newKeys: 5,
135+
staleKeys: 0,
136+
deletedKeys: 2,
137+
});
138+
const mockService = createMockSyncService(result);
139+
const command = new SyncCommand(mockService);
140+
141+
await command.run({});
142+
143+
const infoOutput = logInfoSpy.mock.calls.map(c => String(c[0])).join('\n');
144+
expect(infoOutput).toContain('Sync drift detected: 5 new, 2 deleted keys.');
145+
});
146+
147+
it('should only mention nonzero categories in drift message', async () => {
148+
const result = makeResult({
149+
driftDetected: true,
150+
success: false,
151+
newKeys: 0,
152+
staleKeys: 0,
153+
deletedKeys: 3,
154+
});
155+
const mockService = createMockSyncService(result);
156+
const command = new SyncCommand(mockService);
157+
158+
await command.run({});
159+
160+
const infoOutput = logInfoSpy.mock.calls.map(c => String(c[0])).join('\n');
161+
expect(infoOutput).toContain('Sync drift detected: 3 deleted keys.');
162+
expect(infoOutput).not.toMatch(/\bnew\b/);
163+
expect(infoOutput).not.toMatch(/\bstale\b/);
164+
});
165+
130166
it('should output valid JSON to stdout when format is json', async () => {
131167
const result = makeResult();
132168
const mockService = createMockSyncService(result);

0 commit comments

Comments
 (0)