principles-disciple 1.34.2 → 1.36.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/.dependency-cruiser.json +19 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +6 -3
- package/src/config/defaults/runtime.ts +100 -24
- package/src/core/correction-cue-learner.ts +23 -8
- package/src/core/event-log.ts +87 -20
- package/src/core/init.ts +2 -2
- package/src/core/nocturnal-candidate-scoring.ts +6 -6
- package/src/core/nocturnal-trinity-types.ts +94 -0
- package/src/core/nocturnal-trinity.ts +35 -99
- package/src/core/session-tracker.ts +7 -6
- package/src/core/system-logger.ts +104 -12
- package/src/core/workspace-dir-service.ts +40 -6
- package/src/core/workspace-dir-validation.ts +5 -37
- package/src/hooks/prompt.ts +3 -3
- package/src/hooks/trajectory-collector.ts +7 -7
- package/src/index.ts +8 -68
- package/src/service/central-sync-service.ts +3 -8
- package/src/service/correction-observer-workflow-manager.ts +2 -2
- package/src/service/evolution-worker.ts +13 -22
- package/src/service/keyword-optimization-service.ts +2 -2
- package/src/service/nocturnal-service.ts +62 -43
- package/src/service/subagent-workflow/correction-observer-types.ts +69 -0
- package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +246 -0
- package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +4 -4
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +4 -4
- package/src/service/subagent-workflow/index.ts +13 -0
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +2 -2
- package/src/service/subagent-workflow/types.ts +69 -3
- package/src/utils/shadow-fingerprint.ts +42 -0
- package/src/utils/workspace-resolver.ts +54 -0
- package/tests/core/correction-cue-learner.test.ts +345 -0
- package/tests/core/workspace-dir-validation.test.ts +1 -1
- package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +3 -3
- package/vitest.config.ts +53 -6
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import {
|
|
5
|
+
CorrectionCueLearner,
|
|
6
|
+
loadCorrectionKeywordStore,
|
|
7
|
+
saveCorrectionKeywordStore,
|
|
8
|
+
_resetCorrectionCueCache,
|
|
9
|
+
_resetCorrectionCueLearnerInstance,
|
|
10
|
+
} from '../../src/core/correction-cue-learner.js';
|
|
11
|
+
import {
|
|
12
|
+
CORRECTION_SEED_KEYWORDS,
|
|
13
|
+
MAX_CORRECTION_KEYWORDS,
|
|
14
|
+
} from '../../src/core/correction-types.js';
|
|
15
|
+
|
|
16
|
+
// ── Mock fs (hoisted — vi.mock runs before imports) ──────────────────────────
|
|
17
|
+
|
|
18
|
+
vi.mock('fs', () => ({
|
|
19
|
+
existsSync: vi.fn(() => false),
|
|
20
|
+
readFileSync: vi.fn(() => ''),
|
|
21
|
+
writeFileSync: vi.fn(),
|
|
22
|
+
renameSync: vi.fn(),
|
|
23
|
+
mkdirSync: vi.fn(),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
import * as fs from 'fs';
|
|
27
|
+
|
|
28
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
function tempDir(): string {
|
|
31
|
+
return path.join(os.tmpdir(), `correction-cue-test-${Date.now()}-${Math.random()}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Test setup: reset module-level cache and singleton between tests ─────────
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
vi.clearAllMocks();
|
|
38
|
+
_resetCorrectionCueCache();
|
|
39
|
+
_resetCorrectionCueLearnerInstance();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
43
|
+
// CORR-01: Seed keywords
|
|
44
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
45
|
+
|
|
46
|
+
describe('CORR-01: Seed keywords', () => {
|
|
47
|
+
it('should create store with 16 seed keywords on first load', () => {
|
|
48
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
49
|
+
const dir = tempDir();
|
|
50
|
+
const store = loadCorrectionKeywordStore(dir);
|
|
51
|
+
expect(store.keywords).toHaveLength(16);
|
|
52
|
+
expect(store.version).toBe(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should set source=seed and non-empty addedAt for all seed keywords', () => {
|
|
56
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
57
|
+
const dir = tempDir();
|
|
58
|
+
const store = loadCorrectionKeywordStore(dir);
|
|
59
|
+
for (const kw of store.keywords) {
|
|
60
|
+
expect(kw.source).toBe('seed');
|
|
61
|
+
expect(kw.addedAt).not.toBe('');
|
|
62
|
+
expect(kw.addedAt).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should have all 16 exact terms from CORRECTION_SEED_KEYWORDS', () => {
|
|
67
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
68
|
+
const dir = tempDir();
|
|
69
|
+
const store = loadCorrectionKeywordStore(dir);
|
|
70
|
+
const terms = store.keywords.map((k) => k.term);
|
|
71
|
+
for (const seed of CORRECTION_SEED_KEYWORDS) {
|
|
72
|
+
expect(terms).toContain(seed.term);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
78
|
+
// CORR-03: Atomic write
|
|
79
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
80
|
+
|
|
81
|
+
describe('CORR-03: Atomic write', () => {
|
|
82
|
+
it('should write to .tmp file before rename', () => {
|
|
83
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
84
|
+
vi.mocked(fs.readFileSync).mockReturnValue(
|
|
85
|
+
JSON.stringify({ keywords: CORRECTION_SEED_KEYWORDS.map((k) => ({ ...k, addedAt: '2026-01-01T00:00:00Z' })), version: 1 })
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const dir = tempDir();
|
|
89
|
+
const store = {
|
|
90
|
+
keywords: CORRECTION_SEED_KEYWORDS.map((k) => ({ ...k, addedAt: '2026-01-01T00:00:00Z' })),
|
|
91
|
+
version: 1,
|
|
92
|
+
lastOptimizedAt: '2026-01-01T00:00:00Z',
|
|
93
|
+
};
|
|
94
|
+
saveCorrectionKeywordStore(dir, store);
|
|
95
|
+
|
|
96
|
+
const writeCall = vi.mocked(fs.writeFileSync).mock.calls[0];
|
|
97
|
+
const tmpPath = writeCall[0] as string;
|
|
98
|
+
expect(tmpPath).toMatch(/\.tmp$/);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should rename from tmp path to final path after write', () => {
|
|
102
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
103
|
+
vi.mocked(fs.readFileSync).mockReturnValue(
|
|
104
|
+
JSON.stringify({ keywords: CORRECTION_SEED_KEYWORDS.map((k) => ({ ...k, addedAt: '2026-01-01T00:00:00Z' })), version: 1 })
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const dir = tempDir();
|
|
108
|
+
const store = {
|
|
109
|
+
keywords: CORRECTION_SEED_KEYWORDS.map((k) => ({ ...k, addedAt: '2026-01-01T00:00:00Z' })),
|
|
110
|
+
version: 1,
|
|
111
|
+
lastOptimizedAt: '2026-01-01T00:00:00Z',
|
|
112
|
+
};
|
|
113
|
+
saveCorrectionKeywordStore(dir, store);
|
|
114
|
+
|
|
115
|
+
const renameCalls = vi.mocked(fs.renameSync).mock.calls;
|
|
116
|
+
expect(renameCalls).toHaveLength(1);
|
|
117
|
+
const [from, to] = renameCalls[0];
|
|
118
|
+
expect(from).toMatch(/\.tmp$/);
|
|
119
|
+
expect(to).not.toMatch(/\.tmp$/);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should call mkdirSync with recursive:true before writing', () => {
|
|
123
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
124
|
+
vi.mocked(fs.readFileSync).mockReturnValue(
|
|
125
|
+
JSON.stringify({ keywords: CORRECTION_SEED_KEYWORDS.map((k) => ({ ...k, addedAt: '2026-01-01T00:00:00Z' })), version: 1 })
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const dir = tempDir();
|
|
129
|
+
const store = {
|
|
130
|
+
keywords: CORRECTION_SEED_KEYWORDS.map((k) => ({ ...k, addedAt: '2026-01-01T00:00:00Z' })),
|
|
131
|
+
version: 1,
|
|
132
|
+
lastOptimizedAt: '2026-01-01T00:00:00Z',
|
|
133
|
+
};
|
|
134
|
+
saveCorrectionKeywordStore(dir, store);
|
|
135
|
+
|
|
136
|
+
expect(vi.mocked(fs.mkdirSync)).toHaveBeenCalledWith(dir, { recursive: true });
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
141
|
+
// CORR-04: Cache invalidation
|
|
142
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
143
|
+
|
|
144
|
+
describe('CORR-04: Cache invalidation', () => {
|
|
145
|
+
it('should invalidate cache after save so next load re-reads from disk', () => {
|
|
146
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
147
|
+
vi.mocked(fs.readFileSync).mockReturnValue(
|
|
148
|
+
JSON.stringify({
|
|
149
|
+
keywords: CORRECTION_SEED_KEYWORDS.map((k) => ({ ...k, addedAt: '2026-01-01T00:00:00Z' })),
|
|
150
|
+
version: 1,
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const dir = tempDir();
|
|
155
|
+
loadCorrectionKeywordStore(dir);
|
|
156
|
+
expect(vi.mocked(fs.readFileSync)).toHaveBeenCalled();
|
|
157
|
+
|
|
158
|
+
const store = loadCorrectionKeywordStore(dir);
|
|
159
|
+
saveCorrectionKeywordStore(dir, store);
|
|
160
|
+
|
|
161
|
+
// After save, cache is null — next load must re-read. Verify by changing
|
|
162
|
+
// the mock return and confirming the new data is picked up.
|
|
163
|
+
vi.mocked(fs.readFileSync).mockClear();
|
|
164
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ keywords: [], version: 1 }));
|
|
165
|
+
|
|
166
|
+
const store2 = loadCorrectionKeywordStore(dir);
|
|
167
|
+
expect(vi.mocked(fs.readFileSync)).toHaveBeenCalled();
|
|
168
|
+
expect(store2.keywords).toHaveLength(0); // proves re-read happened
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
173
|
+
// CORR-05: 200-term limit
|
|
174
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
175
|
+
|
|
176
|
+
describe('CORR-05: 200-term limit', () => {
|
|
177
|
+
it('should throw when adding keyword beyond 200 terms', () => {
|
|
178
|
+
const keywords = Array.from({ length: 200 }, (_, i) => ({
|
|
179
|
+
term: `keyword-${i}`,
|
|
180
|
+
weight: 0.5,
|
|
181
|
+
source: 'seed' as const,
|
|
182
|
+
addedAt: '2026-01-01T00:00:00Z',
|
|
183
|
+
}));
|
|
184
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
185
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ keywords, version: 1 }));
|
|
186
|
+
|
|
187
|
+
const dir = tempDir();
|
|
188
|
+
const learner = new CorrectionCueLearner(dir);
|
|
189
|
+
expect(learner.getStore().keywords).toHaveLength(200);
|
|
190
|
+
|
|
191
|
+
expect(() => learner.add({ term: 'new-keyword', weight: 0.5, source: 'user' })).toThrow(
|
|
192
|
+
'Correction keyword store limit reached (200 terms)'
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should allow add when at 199 terms', () => {
|
|
197
|
+
const keywords = Array.from({ length: 199 }, (_, i) => ({
|
|
198
|
+
term: `keyword-${i}`,
|
|
199
|
+
weight: 0.5,
|
|
200
|
+
source: 'seed' as const,
|
|
201
|
+
addedAt: '2026-01-01T00:00:00Z',
|
|
202
|
+
}));
|
|
203
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
204
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ keywords, version: 1 }));
|
|
205
|
+
|
|
206
|
+
const dir = tempDir();
|
|
207
|
+
const learner = new CorrectionCueLearner(dir);
|
|
208
|
+
expect(learner.getStore().keywords).toHaveLength(199);
|
|
209
|
+
|
|
210
|
+
expect(() => learner.add({ term: 'new-keyword', weight: 0.5, source: 'user' })).not.toThrow();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should not modify store when add fails due to limit', () => {
|
|
214
|
+
const keywords = Array.from({ length: 200 }, (_, i) => ({
|
|
215
|
+
term: `keyword-${i}`,
|
|
216
|
+
weight: 0.5,
|
|
217
|
+
source: 'seed' as const,
|
|
218
|
+
addedAt: '2026-01-01T00:00:00Z',
|
|
219
|
+
}));
|
|
220
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
221
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ keywords, version: 1 }));
|
|
222
|
+
|
|
223
|
+
const dir = tempDir();
|
|
224
|
+
const learner = new CorrectionCueLearner(dir);
|
|
225
|
+
try {
|
|
226
|
+
learner.add({ term: 'new-keyword', weight: 0.5, source: 'user' });
|
|
227
|
+
} catch {
|
|
228
|
+
// expected
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
expect(learner.getStore().keywords).toHaveLength(200);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
236
|
+
// CORR-11: Equivalence to detectCorrectionCue
|
|
237
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
238
|
+
|
|
239
|
+
describe('CORR-11: Equivalence to detectCorrectionCue', () => {
|
|
240
|
+
beforeEach(() => {
|
|
241
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Reference implementation using find() — first match wins (same as detectCorrectionCue).
|
|
246
|
+
*/
|
|
247
|
+
function detectCorrectionCueLegacy(text: string): string | null {
|
|
248
|
+
const normalized = text.trim().toLowerCase().replace(/[.,!?;:,。!?;:]/g, '');
|
|
249
|
+
const cues = CORRECTION_SEED_KEYWORDS.map((k) => k.term);
|
|
250
|
+
return cues.find((cue) => normalized.includes(cue)) ?? null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Tests using first-match semantics: find() returns the FIRST keyword in the
|
|
255
|
+
* array whose term appears in the normalized text, not the longest match.
|
|
256
|
+
*
|
|
257
|
+
* Order of CORRECTION_SEED_KEYWORDS array (first 8 Chinese):
|
|
258
|
+
* '不是这个', '不对', '错了', '搞错了', '理解错了', '你理解错了', '重新来', '再试一次'
|
|
259
|
+
*
|
|
260
|
+
* So "我搞错了" → "错了" is found first (index 2) before "搞错了" (index 3).
|
|
261
|
+
* "你理解错了" → "错了" is found first (index 2) before "理解错了" (index 4) and "你理解错了" (index 5).
|
|
262
|
+
*/
|
|
263
|
+
it.each([
|
|
264
|
+
// Chinese cases — note: first match wins
|
|
265
|
+
['不是这个', '不是这个'], // exact match
|
|
266
|
+
['你不对啊', '不对'], // first match is '不对' (index 1)
|
|
267
|
+
['错了!', '错了'], // exact match (index 2)
|
|
268
|
+
['我搞错了', '错了'], // '错了' appears first in array (index 2 < index 3)
|
|
269
|
+
['你理解错了', '错了'], // '错了' appears first in array (index 2 < index 4)
|
|
270
|
+
['重新来一遍', '重新来'], // exact match
|
|
271
|
+
['再试一次行不行', '再试一次'], // exact match
|
|
272
|
+
// English cases
|
|
273
|
+
['you are wrong', 'you are wrong'], // exact match
|
|
274
|
+
['wrong file', 'wrong file'], // exact match
|
|
275
|
+
['not this one', 'not this'], // exact match
|
|
276
|
+
['redo it', 'redo'], // exact match (index 11)
|
|
277
|
+
['try again', 'try again'], // exact match (index 12)
|
|
278
|
+
['do it again', 'again'], // 'again' is index 13
|
|
279
|
+
['please redo', 'redo'], // 'redo' found first (index 11 < index 14)
|
|
280
|
+
['please try again', 'try again'], // 'try again' found first (index 12 < index 15)
|
|
281
|
+
])('should match "%s" → "%s"', (text, expected) => {
|
|
282
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
283
|
+
const dir = tempDir();
|
|
284
|
+
const learner = new CorrectionCueLearner(dir);
|
|
285
|
+
const result = learner.match(text);
|
|
286
|
+
expect(result.matched).toBe(true);
|
|
287
|
+
expect(result.matchedTerms).toContain(expected);
|
|
288
|
+
expect(result.score).toBeGreaterThan(0);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should produce same result as legacy detectCorrectionCue for varied inputs', () => {
|
|
292
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
293
|
+
const dir = tempDir();
|
|
294
|
+
const learner = new CorrectionCueLearner(dir);
|
|
295
|
+
|
|
296
|
+
const cases = [
|
|
297
|
+
'这个可以,没问题',
|
|
298
|
+
'不对,应该是这样',
|
|
299
|
+
'你再试试这个方法',
|
|
300
|
+
'nothing wrong here',
|
|
301
|
+
'please be careful',
|
|
302
|
+
'can you try again?',
|
|
303
|
+
'I think you are wrong about this',
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
for (const text of cases) {
|
|
307
|
+
const legacyResult = detectCorrectionCueLegacy(text);
|
|
308
|
+
const learnerResult = learner.match(text);
|
|
309
|
+
|
|
310
|
+
if (legacyResult !== null) {
|
|
311
|
+
expect(learnerResult.matched).toBe(true);
|
|
312
|
+
expect(learnerResult.matchedTerms).toContain(legacyResult);
|
|
313
|
+
expect(learnerResult.score).toBeGreaterThan(0);
|
|
314
|
+
} else {
|
|
315
|
+
expect(learnerResult.matched).toBe(false);
|
|
316
|
+
expect(learnerResult.matchedTerms).toEqual([]);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('should match regardless of surrounding punctuation', () => {
|
|
322
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
323
|
+
const dir = tempDir();
|
|
324
|
+
const learner = new CorrectionCueLearner(dir);
|
|
325
|
+
|
|
326
|
+
const variations = ['不对', '不对!', '不对?', '。不对', '不对。', ' 不对 ', '不对啊'];
|
|
327
|
+
for (const text of variations) {
|
|
328
|
+
const result = learner.match(text);
|
|
329
|
+
expect(result.matched).toBe(true);
|
|
330
|
+
expect(result.matchedTerms).toContain('不对');
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should return positive score when matched, 0 when not matched', () => {
|
|
335
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
336
|
+
const dir = tempDir();
|
|
337
|
+
const learner = new CorrectionCueLearner(dir);
|
|
338
|
+
expect(learner.match('不是这个').score).toBeGreaterThan(0);
|
|
339
|
+
expect(learner.match('这个可以').score).toBe(0);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should export MAX_CORRECTION_KEYWORDS = 200', () => {
|
|
343
|
+
expect(MAX_CORRECTION_KEYWORDS).toBe(200);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import * as os from 'os';
|
|
3
|
-
import { validateWorkspaceDir, resolveValidWorkspaceDir, logWorkspaceDirHealth } from '../../src/core/workspace-dir-
|
|
3
|
+
import { validateWorkspaceDir, resolveValidWorkspaceDir, logWorkspaceDirHealth } from '../../src/core/workspace-dir-service.js';
|
|
4
4
|
|
|
5
5
|
const homeDir = os.homedir();
|
|
6
6
|
|
|
@@ -65,7 +65,7 @@ describe('E2E: Tool Hooks workspaceDir Resolution', () => {
|
|
|
65
65
|
|
|
66
66
|
describe('Scenario 2: ctx.workspaceDir is undefined (current OpenClaw behavior)', () => {
|
|
67
67
|
it('should fallback to agentId resolution', async () => {
|
|
68
|
-
const { resolveValidWorkspaceDir } = await import('../../src/core/workspace-dir-
|
|
68
|
+
const { resolveValidWorkspaceDir } = await import('../../src/core/workspace-dir-service.js');
|
|
69
69
|
|
|
70
70
|
const mockApi = createMockApi(testWorkspaceDir);
|
|
71
71
|
const ctx = {
|
|
@@ -80,7 +80,7 @@ describe('E2E: Tool Hooks workspaceDir Resolution', () => {
|
|
|
80
80
|
});
|
|
81
81
|
|
|
82
82
|
it('should refuse to guess a workspace when agentId is also undefined', async () => {
|
|
83
|
-
const { resolveValidWorkspaceDir } = await import('../../src/core/workspace-dir-
|
|
83
|
+
const { resolveValidWorkspaceDir } = await import('../../src/core/workspace-dir-service.js');
|
|
84
84
|
|
|
85
85
|
const mockApi = createMockApi(testWorkspaceDir);
|
|
86
86
|
const ctx = {
|
|
@@ -131,7 +131,7 @@ describe('E2E: Tool Hooks workspaceDir Resolution', () => {
|
|
|
131
131
|
|
|
132
132
|
describe('Scenario 4: Invalid workspace candidates are rejected', () => {
|
|
133
133
|
it('should return undefined when all workspace resolution candidates are invalid', async () => {
|
|
134
|
-
const { resolveValidWorkspaceDir } = await import('../../src/core/workspace-dir-
|
|
134
|
+
const { resolveValidWorkspaceDir } = await import('../../src/core/workspace-dir-service.js');
|
|
135
135
|
|
|
136
136
|
const mockApi = createMockApi(os.homedir());
|
|
137
137
|
mockApi.runtime.agent.resolveAgentWorkspaceDir.mockReturnValue(os.homedir());
|
package/vitest.config.ts
CHANGED
|
@@ -1,12 +1,41 @@
|
|
|
1
1
|
import { defineConfig } from 'vitest/config';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Vitest configuration with test layering
|
|
5
|
+
*
|
|
6
|
+
* LAYERS:
|
|
7
|
+
* - unit: Mock-based tests, no real DB (fast, parallel)
|
|
8
|
+
* - integration: Tests using real SQLite DB (requires threads pool)
|
|
9
|
+
*
|
|
10
|
+
* USAGE:
|
|
11
|
+
* - npm test → run all tests
|
|
12
|
+
* - npm run test:unit → run unit tests only (fast)
|
|
13
|
+
* - npm run test:integration → run integration tests only
|
|
14
|
+
*
|
|
15
|
+
* WHY threads pool?
|
|
16
|
+
* better-sqlite3 native handles don't clean up properly in fork subprocesses,
|
|
17
|
+
* causing teardown hangs. Threads pool handles this correctly.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// Integration tests: use real SQLite database
|
|
21
|
+
const integrationTests = [
|
|
22
|
+
'tests/core/control-ui-db.test.ts',
|
|
23
|
+
'tests/core/evolution-logger.test.ts',
|
|
24
|
+
'tests/core/nocturnal-e2e.test.ts',
|
|
25
|
+
'tests/core/nocturnal-trajectory-extractor.test.ts',
|
|
26
|
+
'tests/core/replay-engine.test.ts',
|
|
27
|
+
'tests/core/trajectory.test.ts',
|
|
28
|
+
'tests/integration/**/*.test.ts',
|
|
29
|
+
'tests/integration/**/*.test.tsx',
|
|
30
|
+
'tests/service/nocturnal-service-code-candidate.test.ts',
|
|
31
|
+
'tests/service/nocturnal-target-selector.test.ts',
|
|
32
|
+
];
|
|
33
|
+
|
|
3
34
|
export default defineConfig({
|
|
4
35
|
test: {
|
|
5
36
|
environment: 'node',
|
|
6
37
|
include: ['tests/**/*.test.ts', 'tests/**/*.test.tsx'],
|
|
7
|
-
|
|
8
|
-
pool: 'forks',
|
|
9
|
-
// 确保测试完成后进程能正常退出
|
|
38
|
+
pool: 'threads',
|
|
10
39
|
teardownTimeout: 15000,
|
|
11
40
|
coverage: {
|
|
12
41
|
provider: 'v8',
|
|
@@ -18,6 +47,24 @@ export default defineConfig({
|
|
|
18
47
|
branches: 60,
|
|
19
48
|
statements: 70,
|
|
20
49
|
},
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
50
|
+
},
|
|
51
|
+
// Workspace projects for layered testing
|
|
52
|
+
projects: [
|
|
53
|
+
{
|
|
54
|
+
test: {
|
|
55
|
+
name: 'unit',
|
|
56
|
+
include: ['tests/**/*.test.ts', 'tests/**/*.test.tsx'],
|
|
57
|
+
exclude: integrationTests,
|
|
58
|
+
pool: 'threads',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
test: {
|
|
63
|
+
name: 'integration',
|
|
64
|
+
include: integrationTests,
|
|
65
|
+
pool: 'threads',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
});
|