Skip to content

Commit 62d4238

Browse files
sjsyrekclaude
andcommitted
docs(cache): document deterministic cache key generation
Added comprehensive documentation and test to verify cache key generation is deterministic regardless of option order. - Added test verifying identical cache keys for different option orders - Documented intentional property ordering in generateCacheKey() method - Added explanatory comments explaining why property order matters - Prevents future developers from accidentally breaking cache key generation **Implementation Note**: The current implementation already handles this correctly by creating a new object with fixed property order. This commit documents the intentional design to prevent accidental cache key breakage. **Impact**: Ensures consistent cache behavior and prevents reduced cache hit rates from property ordering changes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 12cc3fd commit 62d4238

3 files changed

Lines changed: 102 additions & 9 deletions

File tree

CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Added
11+
- **Cache Key Determinism Documentation** - Documented intentional property ordering for cache keys
12+
- Added comprehensive test to verify cache keys are identical regardless of option order
13+
- Added detailed documentation explaining why property order matters in `generateCacheKey()`
14+
- Property order in cache data object is now explicitly documented as intentional
15+
- Prevents accidental cache key breakage from property reordering
16+
- **Impact**: Ensures future developers understand the importance of property order
17+
- Location: `src/services/translation.ts:386-417`, test added to `tests/unit/translation-service.test.ts`
18+
1019
### Changed
1120
- **Test Coverage Enhancement** - Comprehensive integration and E2E test expansion
1221
- Created 10 new test files covering critical workflows and CLI behavior

src/services/translation.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -385,20 +385,29 @@ export class TranslationService {
385385

386386
/**
387387
* Generate cache key from text and options
388+
*
389+
* IMPORTANT: This method creates a new object with properties in a FIXED order
390+
* to ensure deterministic cache keys. Two translation requests with identical
391+
* parameters must generate the same cache key, regardless of the order in which
392+
* properties were specified in the input options object.
393+
*
394+
* The property order in `cacheData` is intentional and must not be changed,
395+
* as it directly affects cache key generation via JSON.stringify().
388396
*/
389397
private generateCacheKey(text: string, options: TranslationOptions): string {
390-
// Create a stable representation of the translation request
398+
// Create a stable representation with deterministic property order
399+
// Property order matters because JSON.stringify() preserves insertion order
391400
const cacheData = {
392-
text,
393-
targetLang: options.targetLang,
394-
sourceLang: options.sourceLang,
395-
formality: options.formality,
396-
glossaryId: options.glossaryId,
397-
context: options.context,
398-
// Note: preserveFormatting doesn't affect translation output caching
401+
text, // 1. Text to translate
402+
targetLang: options.targetLang, // 2. Target language
403+
sourceLang: options.sourceLang, // 3. Source language (if specified)
404+
formality: options.formality, // 4. Formality level
405+
glossaryId: options.glossaryId, // 5. Glossary ID
406+
context: options.context, // 6. Context hint
407+
// Note: preserveFormatting doesn't affect translation output, so not cached
399408
};
400409

401-
// Generate SHA-256 hash
410+
// Generate SHA-256 hash of the stable representation
402411
const hash = crypto
403412
.createHash('sha256')
404413
.update(JSON.stringify(cacheData))

tests/unit/translation-service.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -907,5 +907,80 @@ describe('TranslationService', () => {
907907
expect(mockCacheService.set).not.toHaveBeenCalled();
908908
expect(mockDeepLClient.translate).toHaveBeenCalledTimes(1);
909909
});
910+
911+
it('should generate deterministic cache keys regardless of option order (BUG #1)', async () => {
912+
// This test demonstrates that cache keys must be deterministic
913+
// Two requests with identical options but different ordering should use the same cache key
914+
915+
// Reset mocks to ensure clean state
916+
mockCacheService.get.mockReturnValue(null); // No cache hits
917+
mockDeepLClient.translate.mockResolvedValue({
918+
text: 'Hola',
919+
});
920+
921+
// First call with options in one order
922+
await translationService.translate('Hello', {
923+
targetLang: 'es',
924+
sourceLang: 'en',
925+
formality: 'more',
926+
glossaryId: 'glossary-123',
927+
context: 'greeting',
928+
});
929+
930+
// Second call with identical options but different ordering
931+
await translationService.translate('Hello', {
932+
context: 'greeting',
933+
glossaryId: 'glossary-123',
934+
formality: 'more',
935+
sourceLang: 'en',
936+
targetLang: 'es',
937+
});
938+
939+
// Verify the cache keys are the same by checking cache.set calls
940+
const cacheSetCalls = mockCacheService.set.mock.calls;
941+
expect(cacheSetCalls.length).toBe(2); // Two calls, should generate same key
942+
943+
// Extract the cache keys (first parameter of each set call)
944+
const key1 = cacheSetCalls[0]?.[0] as string;
945+
const key2 = cacheSetCalls[1]?.[0] as string;
946+
947+
// The cache keys MUST be identical despite different option ordering
948+
// This is critical for cache hit rate - identical requests should use the same key
949+
expect(key1).toBe(key2);
950+
});
951+
952+
it('should use cached result when options are provided in different order', async () => {
953+
// Set up cache to return a hit for the second call
954+
let callCount = 0;
955+
mockCacheService.get.mockImplementation(() => {
956+
callCount++;
957+
if (callCount === 1) {
958+
return null; // First call: miss
959+
}
960+
return { text: 'Hola (cached)' }; // Second call: hit
961+
});
962+
963+
mockDeepLClient.translate.mockResolvedValue({
964+
text: 'Hola (fresh)',
965+
});
966+
967+
// First call
968+
const result1 = await translationService.translate('Hello', {
969+
targetLang: 'es',
970+
formality: 'more',
971+
});
972+
expect(result1.text).toBe('Hola (fresh)');
973+
expect(mockDeepLClient.translate).toHaveBeenCalledTimes(1);
974+
975+
// Second call with same options but different order
976+
const result2 = await translationService.translate('Hello', {
977+
formality: 'more',
978+
targetLang: 'es',
979+
});
980+
981+
// Should use cached result
982+
expect(result2.text).toBe('Hola (cached)');
983+
expect(mockDeepLClient.translate).toHaveBeenCalledTimes(1); // Still only 1 API call
984+
});
910985
});
911986
});

0 commit comments

Comments
 (0)