transduck 0.5.3 → 0.6.0
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/dist/backend.d.ts +1 -0
- package/dist/backend.js +6 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +64 -12
- package/dist/config.d.ts +1 -0
- package/dist/config.js +2 -0
- package/dist/handler.d.ts +2 -0
- package/dist/handler.js +79 -41
- package/dist/index.d.ts +17 -2
- package/dist/index.js +191 -92
- package/dist/providers/claude-api.d.ts +1 -0
- package/dist/providers/claude-api.js +11 -0
- package/dist/providers/claude-code.d.ts +1 -0
- package/dist/providers/claude-code.js +6 -0
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/openai-provider.d.ts +1 -0
- package/dist/providers/openai-provider.js +17 -0
- package/dist/result.d.ts +19 -0
- package/dist/result.js +26 -0
- package/dist/shared-store.d.ts +18 -0
- package/dist/shared-store.js +126 -0
- package/package.json +5 -1
- package/src/backend.ts +10 -0
- package/src/cli.ts +64 -12
- package/src/config.ts +4 -0
- package/src/handler.ts +81 -54
- package/src/index.ts +277 -98
- package/src/providers/claude-api.ts +16 -0
- package/src/providers/claude-code.ts +10 -0
- package/src/providers/index.ts +6 -0
- package/src/providers/openai-provider.ts +24 -0
- package/src/result.ts +30 -0
- package/src/shared-store.ts +157 -0
- package/tests/ait.test.ts +152 -14
- package/tests/backend.test.ts +34 -1
- package/tests/cli.test.ts +33 -0
- package/tests/config.test.ts +40 -0
- package/tests/result.test.ts +62 -0
- package/tests/shared-store.test.ts +210 -0
|
@@ -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
|
+
});
|