transduck 0.5.3 → 0.6.1

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.
@@ -0,0 +1,210 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { createHash } from 'crypto';
3
+
4
+ function contentHash(sourceText: string, projectContextHash: string, stringContextHash: string): string {
5
+ return createHash('sha256')
6
+ .update(sourceText + '\x00' + projectContextHash + '\x00' + stringContextHash)
7
+ .digest('hex');
8
+ }
9
+
10
+ // Mock pg module
11
+ const mockQuery = vi.fn();
12
+ const mockEnd = vi.fn();
13
+
14
+ vi.mock('pg', () => ({
15
+ default: {
16
+ Pool: vi.fn(() => ({
17
+ query: mockQuery,
18
+ end: mockEnd,
19
+ })),
20
+ },
21
+ Pool: vi.fn(() => ({
22
+ query: mockQuery,
23
+ end: mockEnd,
24
+ })),
25
+ }));
26
+
27
+ const { SharedStore } = await import('../src/shared-store.js');
28
+
29
+ describe('SharedStore', () => {
30
+ let store: InstanceType<typeof SharedStore>;
31
+
32
+ beforeEach(async () => {
33
+ vi.clearAllMocks();
34
+ store = new SharedStore('postgres://localhost/test');
35
+ mockQuery.mockResolvedValueOnce({}); // for CREATE TABLE
36
+ await store.initialize();
37
+ vi.clearAllMocks(); // clear the initialize call
38
+ });
39
+
40
+ it('creates the table on initialize', async () => {
41
+ const fresh = new SharedStore('postgres://localhost/test');
42
+ mockQuery.mockResolvedValueOnce({});
43
+ await fresh.initialize();
44
+ expect(mockQuery).toHaveBeenCalledOnce();
45
+ expect(mockQuery.mock.calls[0][0]).toContain('CREATE TABLE IF NOT EXISTS');
46
+ expect(mockQuery.mock.calls[0][0]).toContain('transduck_translations');
47
+ });
48
+
49
+ it('lookup returns translated text when found', async () => {
50
+ mockQuery.mockResolvedValueOnce({
51
+ rows: [{ translated_text: 'Hallo' }],
52
+ });
53
+ const result = await store.lookup({
54
+ sourceText: 'Hello',
55
+ sourceLang: 'EN',
56
+ targetLang: 'DE',
57
+ projectContextHash: 'ph',
58
+ stringContextHash: 'sh',
59
+ });
60
+ expect(result).toBe('Hallo');
61
+ expect(mockQuery.mock.calls[0][1]).toEqual([
62
+ 'EN', 'DE', contentHash('Hello', 'ph', 'sh'),
63
+ ]);
64
+ });
65
+
66
+ it('lookup returns null when not found', async () => {
67
+ mockQuery.mockResolvedValueOnce({ rows: [] });
68
+ mockQuery.mockResolvedValueOnce({ rows: [] }); // secondary context check
69
+ const result = await store.lookup({
70
+ sourceText: 'Hello',
71
+ sourceLang: 'EN',
72
+ targetLang: 'DE',
73
+ projectContextHash: 'ph',
74
+ stringContextHash: 'sh',
75
+ });
76
+ expect(result).toBeNull();
77
+ });
78
+
79
+ it('lookup logs debug when same text exists with different context hash', async () => {
80
+ // Primary lookup misses
81
+ mockQuery.mockResolvedValueOnce({ rows: [] });
82
+ // Secondary check finds a row with different context
83
+ mockQuery.mockResolvedValueOnce({ rows: [{ '?column?': 1 }] });
84
+ const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
85
+ const result = await store.lookup({
86
+ sourceText: 'Hello',
87
+ sourceLang: 'EN',
88
+ targetLang: 'DE',
89
+ projectContextHash: 'ph',
90
+ stringContextHash: 'sh',
91
+ });
92
+ expect(result).toBeNull();
93
+ expect(debugSpy).toHaveBeenCalledOnce();
94
+ expect(debugSpy.mock.calls[0][0]).toContain('different project context hash');
95
+ debugSpy.mockRestore();
96
+ });
97
+
98
+ it('lookup does not log debug when no alternative context exists', async () => {
99
+ // Primary lookup misses
100
+ mockQuery.mockResolvedValueOnce({ rows: [] });
101
+ // Secondary check also misses
102
+ mockQuery.mockResolvedValueOnce({ rows: [] });
103
+ const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
104
+ const result = await store.lookup({
105
+ sourceText: 'Hello',
106
+ sourceLang: 'EN',
107
+ targetLang: 'DE',
108
+ projectContextHash: 'ph',
109
+ stringContextHash: 'sh',
110
+ });
111
+ expect(result).toBeNull();
112
+ expect(debugSpy).not.toHaveBeenCalled();
113
+ debugSpy.mockRestore();
114
+ });
115
+
116
+ it('insert calls query with correct params', async () => {
117
+ mockQuery.mockResolvedValueOnce({});
118
+ await store.insert({
119
+ sourceText: 'Hello',
120
+ sourceLang: 'EN',
121
+ targetLang: 'DE',
122
+ projectContextHash: 'ph',
123
+ stringContextHash: 'sh',
124
+ stringContext: 'greeting',
125
+ translatedText: 'Hallo',
126
+ model: 'gpt-4.1-mini',
127
+ status: 'translated',
128
+ });
129
+ expect(mockQuery).toHaveBeenCalledOnce();
130
+ const [sql, params] = mockQuery.mock.calls[0];
131
+ expect(sql).toContain('INSERT INTO');
132
+ expect(sql).toContain('ON CONFLICT DO NOTHING');
133
+ expect(params[0]).toBe('EN');
134
+ expect(params[1]).toBe('DE');
135
+ expect(params[3]).toBe('Hello');
136
+ expect(params[4]).toBe('Hallo');
137
+ expect(params[5]).toBe('gpt-4.1-mini');
138
+ expect(params[6]).toBe('translated');
139
+ });
140
+
141
+ it('lookupPlural returns category map', async () => {
142
+ mockQuery.mockResolvedValueOnce({
143
+ rows: [
144
+ { plural_category: 'one', translated_text: 'ein Hund' },
145
+ { plural_category: 'other', translated_text: 'Hunde' },
146
+ ],
147
+ });
148
+ const result = await store.lookupPlural({
149
+ sourceText: 'dog\x00dogs',
150
+ sourceLang: 'EN',
151
+ targetLang: 'DE',
152
+ projectContextHash: 'ph',
153
+ stringContextHash: 'sh',
154
+ });
155
+ expect(result).toEqual({ one: 'ein Hund', other: 'Hunde' });
156
+ });
157
+
158
+ it('lookupPlural returns empty object when no rows', async () => {
159
+ mockQuery.mockResolvedValueOnce({ rows: [] });
160
+ const result = await store.lookupPlural({
161
+ sourceText: 'dog\x00dogs',
162
+ sourceLang: 'EN',
163
+ targetLang: 'DE',
164
+ projectContextHash: 'ph',
165
+ stringContextHash: 'sh',
166
+ });
167
+ expect(result).toEqual({});
168
+ });
169
+
170
+ it('insertPlural calls query with plural category', async () => {
171
+ mockQuery.mockResolvedValueOnce({});
172
+ await store.insertPlural({
173
+ sourceText: 'dog\x00dogs',
174
+ sourceLang: 'EN',
175
+ targetLang: 'DE',
176
+ projectContextHash: 'ph',
177
+ stringContextHash: 'sh',
178
+ stringContext: '',
179
+ pluralCategory: 'one',
180
+ translatedText: 'ein Hund',
181
+ model: 'gpt-4.1-mini',
182
+ status: 'translated',
183
+ });
184
+ expect(mockQuery).toHaveBeenCalledOnce();
185
+ const [sql, params] = mockQuery.mock.calls[0];
186
+ expect(sql).toContain('INSERT INTO');
187
+ expect(params[3]).toBe('one');
188
+ expect(params[5]).toBe('ein Hund');
189
+ });
190
+
191
+ it('stats returns aggregated counts', async () => {
192
+ mockQuery.mockResolvedValueOnce({
193
+ rows: [
194
+ { target_lang: 'DE', status: 'translated', count: '10' },
195
+ { target_lang: 'DE', status: 'failed', count: '2' },
196
+ { target_lang: 'ES', status: 'translated', count: '5' },
197
+ ],
198
+ });
199
+ const st = await store.stats();
200
+ expect(st.totalTranslations).toBe(15);
201
+ expect(st.totalFailed).toBe(2);
202
+ expect(st.byLanguage).toEqual({ DE: 10, ES: 5 });
203
+ });
204
+
205
+ it('close ends the pool', async () => {
206
+ mockEnd.mockResolvedValueOnce(undefined);
207
+ await store.close();
208
+ expect(mockEnd).toHaveBeenCalledOnce();
209
+ });
210
+ });