principles-disciple 1.107.0 → 1.108.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.
Files changed (63) hide show
  1. package/openclaw.plugin.json +1 -1
  2. package/package.json +2 -2
  3. package/src/core/init.ts +3 -1
  4. package/src/core/workspace-dir-validation.ts +3 -3
  5. package/tests/core-anti-growth.test.ts +0 -13
  6. package/tests/hooks/prompt-characterization.test.ts +1 -11
  7. package/tests/hooks/prompt-diet.test.ts +3 -11
  8. package/tests/hooks/prompt-size-guard.test.ts +0 -10
  9. package/tests/hooks/runtime-v2-prompt-activation.test.ts +0 -10
  10. package/tests/index.test.ts +1 -1
  11. package/tests/runtime-v2-discovery-guard.test.ts +1 -2
  12. package/vitest.config.ts +2 -3
  13. package/vitest.unit.config.ts +12 -0
  14. package/src/core/evolution-hook.ts +0 -74
  15. package/src/core/file-storage-adapter.ts +0 -203
  16. package/src/core/merge-gate-audit.ts +0 -314
  17. package/src/core/pain-context-extractor.ts +0 -306
  18. package/src/core/pain-lifecycle.ts +0 -38
  19. package/src/core/pain-signal-adapter.ts +0 -42
  20. package/src/core/pain-signal.ts +0 -22
  21. package/src/core/principle-injector.ts +0 -84
  22. package/src/core/principle-tree-migration.ts +0 -196
  23. package/src/core/storage-adapter.ts +0 -65
  24. package/src/core/telemetry-event.ts +0 -109
  25. package/src/core/training-program.ts +0 -632
  26. package/src/core/workspace-dir-service.ts +0 -119
  27. package/src/hooks/lifecycle-routing.ts +0 -125
  28. package/src/service/event-log-auditor.ts +0 -284
  29. package/src/service/evolution-queue-lock.ts +0 -47
  30. package/src/service/failure-classifier.ts +0 -79
  31. package/src/service/internalization-trigger-adapter.ts +0 -302
  32. package/src/service/monitoring-query-service.ts +0 -67
  33. package/src/service/subagent-workflow/index.ts +0 -17
  34. package/src/tools/critique-prompt.ts +0 -1
  35. package/src/tools/model-index.ts +0 -1
  36. package/src/types/event-payload.ts +0 -16
  37. package/src/utils/glob-match.ts +0 -50
  38. package/src/utils/nlp.ts +0 -25
  39. package/src/utils/plugin-logger.ts +0 -97
  40. package/src/utils/subagent-probe.ts +0 -81
  41. package/tests/core/evolution-hook.test.ts +0 -123
  42. package/tests/core/file-storage-adapter.test.ts +0 -285
  43. package/tests/core/merge-gate-audit.test.ts +0 -117
  44. package/tests/core/pain-context-extractor.test.ts +0 -279
  45. package/tests/core/pain-lifecycle.test.ts +0 -38
  46. package/tests/core/pain-signal-adapter.test.ts +0 -116
  47. package/tests/core/pain-signal.test.ts +0 -190
  48. package/tests/core/principle-injector.test.ts +0 -90
  49. package/tests/core/principle-tree-migration.test.ts +0 -77
  50. package/tests/core/storage-conformance.test.ts +0 -429
  51. package/tests/core/telemetry-event.test.ts +0 -119
  52. package/tests/core/training-program.test.ts +0 -472
  53. package/tests/core/workspace-dir-service.test.ts +0 -68
  54. package/tests/core/workspace-dir-validation.test.ts +0 -143
  55. package/tests/integration/internalization-trigger-guard.test.ts +0 -69
  56. package/tests/integration/pain-lifecycle-e2e.test.ts +0 -75
  57. package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +0 -209
  58. package/tests/service/failure-classifier.test.ts +0 -171
  59. package/tests/service/internalization-trigger-adapter.test.ts +0 -251
  60. package/tests/service/monitoring-query-service.test.ts +0 -67
  61. package/tests/utils/nlp.test.ts +0 -35
  62. package/tests/utils/plugin-logger.test.ts +0 -156
  63. package/tests/utils/subagent-probe.test.ts +0 -79
@@ -1,90 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import type { PrincipleInjector, InjectionContext } from '../../src/core/principle-injector.js';
3
- import { DefaultPrincipleInjector } from '../../src/core/principle-injector.js';
4
- import type { InjectablePrinciple } from '../../src/core/principle-injection.js';
5
- import { selectPrinciplesForInjection, formatPrinciple } from '../../src/core/principle-injection.js';
6
-
7
- // ---------------------------------------------------------------------------
8
- // Helpers
9
- // ---------------------------------------------------------------------------
10
-
11
- function makePrinciple(overrides: Partial<InjectablePrinciple> = {}): InjectablePrinciple {
12
- return {
13
- id: overrides.id ?? 'P_001',
14
- text: overrides.text ?? 'Always verify file content before editing',
15
- priority: overrides.priority ?? 'P1',
16
- createdAt: overrides.createdAt ?? '2026-04-01T00:00:00.000Z',
17
- };
18
- }
19
-
20
- // ---------------------------------------------------------------------------
21
- // Tests
22
- // ---------------------------------------------------------------------------
23
-
24
- describe('DefaultPrincipleInjector', () => {
25
- const injector: PrincipleInjector = new DefaultPrincipleInjector();
26
-
27
- it('getRelevantPrinciples delegates to selectPrinciplesForInjection', () => {
28
- const principles = [
29
- makePrinciple({ id: 'P0', priority: 'P0', text: 'Critical rule' }),
30
- makePrinciple({ id: 'P1', priority: 'P1', text: 'Standard rule' }),
31
- makePrinciple({ id: 'P2', priority: 'P2', text: 'Low priority rule' }),
32
- makePrinciple({ id: 'P1b', priority: 'P1', text: 'Another standard rule' }),
33
- makePrinciple({ id: 'P0b', priority: 'P0', text: 'Another critical rule' }),
34
- ];
35
- const context: InjectionContext = { domain: 'coding', sessionId: 's-1', budgetChars: 4000 };
36
-
37
- const result = injector.getRelevantPrinciples(principles, context);
38
- const expected = selectPrinciplesForInjection(principles, 4000).selected;
39
-
40
- expect(result).toEqual(expected);
41
- });
42
-
43
- it('formatForInjection delegates to formatPrinciple', () => {
44
- const principle = makePrinciple({ id: 'P_001', text: 'Test' });
45
-
46
- const result = injector.formatForInjection(principle);
47
- const expected = formatPrinciple(makePrinciple({ id: 'P_001', text: 'Test' }));
48
-
49
- expect(result).toBe(expected);
50
- });
51
-
52
- it('formatForInjection returns "- [ID] text" format', () => {
53
- const result = injector.formatForInjection(
54
- makePrinciple({ id: 'P_001', text: 'Verify before edit' }),
55
- );
56
- expect(result).toBe('- [P_001] Verify before edit');
57
- });
58
-
59
- it('getRelevantPrinciples respects budget constraint', () => {
60
- const principles = Array.from({ length: 20 }, (_, i) =>
61
- makePrinciple({
62
- id: `P_${i.toString().padStart(3, '0')}`,
63
- text: 'A'.repeat(300), // 300 chars each
64
- priority: i === 0 ? 'P0' : 'P2',
65
- }),
66
- );
67
- const context: InjectionContext = { domain: 'coding', sessionId: 's-1', budgetChars: 500 };
68
-
69
- const result = injector.getRelevantPrinciples(principles, context);
70
-
71
- // Should include at least the forced P0, total chars may exceed budget slightly
72
- expect(result.length).toBeGreaterThan(0);
73
- expect(result.some(p => p.priority === 'P0')).toBe(true);
74
- });
75
-
76
- it('getRelevantPrinciples returns empty array for empty input', () => {
77
- const context: InjectionContext = { domain: 'coding', sessionId: 's-1', budgetChars: 4000 };
78
- const result = injector.getRelevantPrinciples([], context);
79
- expect(result).toEqual([]);
80
- });
81
- });
82
-
83
- describe('InjectionContext', () => {
84
- it('has domain, sessionId, and budgetChars fields', () => {
85
- const ctx: InjectionContext = { domain: 'coding', sessionId: 's-1', budgetChars: 4000 };
86
- expect(ctx.domain).toBe('coding');
87
- expect(ctx.sessionId).toBe('s-1');
88
- expect(ctx.budgetChars).toBe(4000);
89
- });
90
- });
@@ -1,77 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as os from 'os';
3
- import * as path from 'path';
4
- import { afterEach, describe, expect, it } from 'vitest';
5
- import { needsMigration } from '../../src/core/principle-tree-migration.js';
6
- import { safeRmDir } from '../test-utils.js';
7
-
8
- function writeLedger(stateDir: string, payload: unknown): void {
9
- fs.mkdirSync(stateDir, { recursive: true });
10
- fs.writeFileSync(
11
- path.join(stateDir, 'principle_training_state.json'),
12
- JSON.stringify(payload, null, 2),
13
- 'utf8'
14
- );
15
- }
16
-
17
- describe('principle-tree-migration', () => {
18
- const tempDirs: string[] = [];
19
-
20
- afterEach(() => {
21
- while (tempDirs.length > 0) {
22
- const dir = tempDirs.pop();
23
- if (dir) safeRmDir(dir);
24
- }
25
- });
26
-
27
- it('requires migration when trainingStore and tree.principles counts match but ids differ', () => {
28
- const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-migration-'));
29
- const stateDir = path.join(workspaceDir, '.state');
30
- tempDirs.push(workspaceDir);
31
-
32
- writeLedger(stateDir, {
33
- P_001: {
34
- principleId: 'P_001',
35
- evaluability: 'manual_only',
36
- applicableOpportunityCount: 0,
37
- observedViolationCount: 0,
38
- complianceRate: 0,
39
- violationTrend: 0,
40
- generatedSampleCount: 0,
41
- approvedSampleCount: 0,
42
- includedTrainRunIds: [],
43
- deployedCheckpointIds: [],
44
- internalizationStatus: 'prompt_only',
45
- },
46
- _tree: {
47
- principles: {
48
- P_999: {
49
- id: 'P_999',
50
- version: 1,
51
- text: 'Other principle',
52
- triggerPattern: '',
53
- action: '',
54
- status: 'candidate',
55
- priority: 'P1',
56
- scope: 'general',
57
- evaluability: 'manual_only',
58
- valueScore: 0,
59
- adherenceRate: 0,
60
- painPreventedCount: 0,
61
- derivedFromPainIds: [],
62
- ruleIds: [],
63
- conflictsWithPrincipleIds: [],
64
- createdAt: '2026-04-10T00:00:00.000Z',
65
- updatedAt: '2026-04-10T00:00:00.000Z',
66
- },
67
- },
68
- rules: {},
69
- implementations: {},
70
- metrics: {},
71
- lastUpdated: '2026-04-10T00:00:00.000Z',
72
- },
73
- });
74
-
75
- expect(needsMigration(stateDir)).toBe(true);
76
- });
77
- });
@@ -1,429 +0,0 @@
1
- /**
2
- * Storage Conformance Suite for StorageAdapter implementations.
3
- *
4
- * This suite accepts a factory function that creates a StorageAdapter instance
5
- * and tests the following contract guarantees:
6
- *
7
- * 1. Atomic writes/reads: data written is data read back
8
- * 2. Concurrent mutation with locks: overlapping mutateLedger calls serialize
9
- * 3. Persistence across restarts: data survives adapter re-creation
10
- * 4. Error handling: malformed state is handled gracefully
11
- *
12
- * Usage:
13
- * import { describeStorageConformance } from './storage-conformance.test.js';
14
- * describeStorageConformance('FileStorageAdapter', () => new FileStorageAdapter(tmpDir));
15
- *
16
- * The factory receives a fresh temp directory for each test.
17
- */
18
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
19
- import * as fs from 'fs';
20
- import * as os from 'os';
21
- import * as path from 'path';
22
- import type { StorageAdapter } from '../../src/core/storage-adapter.js';
23
- import type { HybridLedgerStore } from '../../src/core/principle-tree-ledger.js';
24
- import { safeRmDir } from '../test-utils.js';
25
-
26
- // ---------------------------------------------------------------------------
27
- // Helpers
28
- // ---------------------------------------------------------------------------
29
-
30
- function createTmpDir(): string {
31
- return fs.mkdtempSync(path.join(os.tmpdir(), 'pd-storage-conformance-'));
32
- }
33
-
34
- function createEmptyStore(): HybridLedgerStore {
35
- return {
36
- trainingStore: {},
37
- tree: {
38
- principles: {},
39
- rules: {},
40
- implementations: {},
41
- metrics: {},
42
- lastUpdated: new Date(0).toISOString(),
43
- },
44
- };
45
- }
46
-
47
- function createTestPrinciple(id: string, text?: string) {
48
- return {
49
- id,
50
- version: 1,
51
- text: text ?? `Test principle ${id}`,
52
- triggerPattern: 'test',
53
- action: 'verify',
54
- status: 'candidate' as const,
55
- priority: 'P1' as const,
56
- scope: 'general',
57
- evaluability: 'manual_only' as const,
58
- valueScore: 0,
59
- adherenceRate: 0,
60
- painPreventedCount: 0,
61
- derivedFromPainIds: [] as string[],
62
- ruleIds: [] as string[],
63
- conflictsWithPrincipleIds: [] as string[],
64
- createdAt: '2026-04-17T00:00:00.000Z',
65
- updatedAt: '2026-04-17T00:00:00.000Z',
66
- };
67
- }
68
-
69
- // ---------------------------------------------------------------------------
70
- // Exported conformance suite
71
- // ---------------------------------------------------------------------------
72
-
73
- export type StorageAdapterFactory = (stateDir: string) => StorageAdapter;
74
-
75
- export function describeStorageConformance(
76
- name: string,
77
- factory: StorageAdapterFactory,
78
- ): void {
79
- describe(`Storage Conformance: ${name}`, () => {
80
- let tmpDir: string;
81
- let adapter: StorageAdapter;
82
-
83
- beforeEach(() => {
84
- tmpDir = createTmpDir();
85
- adapter = factory(tmpDir);
86
- });
87
-
88
- afterEach(() => {
89
- safeRmDir(tmpDir);
90
- });
91
-
92
- // -------------------------------------------------------------------------
93
- // 1. Atomic writes/reads
94
- // -------------------------------------------------------------------------
95
-
96
- describe('atomic writes and reads', () => {
97
- it('loadLedger returns empty store when no data exists', async () => {
98
- const store = await adapter.loadLedger();
99
- expect(store.trainingStore).toEqual({});
100
- expect(store.tree.principles).toEqual({});
101
- expect(store.tree.rules).toEqual({});
102
- });
103
-
104
- it('saveLedger then loadLedger returns the same data', async () => {
105
- const store = createEmptyStore();
106
- store.tree.principles['P-001'] = createTestPrinciple('P-001', 'Write before delete');
107
-
108
- await adapter.saveLedger(store);
109
- const loaded = await adapter.loadLedger();
110
-
111
- expect(loaded.tree.principles['P-001']).toBeDefined();
112
- expect(loaded.tree.principles['P-001'].text).toBe('Write before delete');
113
- expect(loaded.tree.principles['P-001'].id).toBe('P-001');
114
- });
115
-
116
- it('saveLedger overwrites previous data atomically', async () => {
117
- const store1 = createEmptyStore();
118
- store1.tree.principles['P-001'] = createTestPrinciple('P-001', 'First write');
119
- await adapter.saveLedger(store1);
120
-
121
- const store2 = createEmptyStore();
122
- store2.tree.principles['P-002'] = createTestPrinciple('P-002', 'Second write');
123
- await adapter.saveLedger(store2);
124
-
125
- const loaded = await adapter.loadLedger();
126
- // Second write should have replaced the first
127
- expect(loaded.tree.principles['P-001']).toBeUndefined();
128
- expect(loaded.tree.principles['P-002']).toBeDefined();
129
- expect(loaded.tree.principles['P-002'].text).toBe('Second write');
130
- });
131
-
132
- it('preserves all store fields across save/load cycle', async () => {
133
- const store = createEmptyStore();
134
- store.trainingStore['tp-1'] = {
135
- principleId: 'tp-1',
136
- evaluability: 'weak_heuristic',
137
- applicableOpportunityCount: 5,
138
- observedViolationCount: 2,
139
- complianceRate: 0.6,
140
- violationTrend: -0.1,
141
- generatedSampleCount: 3,
142
- approvedSampleCount: 2,
143
- includedTrainRunIds: ['run-1', 'run-2'],
144
- deployedCheckpointIds: ['ckpt-1'],
145
- internalizationStatus: 'in_training',
146
- };
147
- store.tree.principles['P-001'] = createTestPrinciple('P-001');
148
-
149
- await adapter.saveLedger(store);
150
- const loaded = await adapter.loadLedger();
151
-
152
- // Verify training store
153
- expect(loaded.trainingStore['tp-1'].evaluability).toBe('weak_heuristic');
154
- expect(loaded.trainingStore['tp-1'].complianceRate).toBe(0.6);
155
- expect(loaded.trainingStore['tp-1'].includedTrainRunIds).toEqual(['run-1', 'run-2']);
156
- expect(loaded.trainingStore['tp-1'].deployedCheckpointIds).toEqual(['ckpt-1']);
157
-
158
- // Verify tree principles
159
- expect(loaded.tree.principles['P-001']).toBeDefined();
160
- });
161
-
162
- it('mutateLedger reads and writes atomically', async () => {
163
- await adapter.saveLedger(createEmptyStore());
164
-
165
- const result = await adapter.mutateLedger((store) => {
166
- store.tree.principles['P-MUT'] = createTestPrinciple('P-MUT', 'Mutated');
167
- return Object.keys(store.tree.principles).length;
168
- });
169
-
170
- expect(result).toBe(1);
171
-
172
- const loaded = await adapter.loadLedger();
173
- expect(loaded.tree.principles['P-MUT'].text).toBe('Mutated');
174
- });
175
-
176
- it('mutateLedger returns the value from the mutate function', async () => {
177
- await adapter.saveLedger(createEmptyStore());
178
-
179
- const count = await adapter.mutateLedger((store) => {
180
- return Object.keys(store.tree.principles).length;
181
- });
182
-
183
- expect(count).toBe(0);
184
- });
185
- });
186
-
187
- // -------------------------------------------------------------------------
188
- // 2. Concurrent mutation with locks
189
- // -------------------------------------------------------------------------
190
-
191
- describe('concurrent mutation with locks', () => {
192
- it('serializes overlapping mutateLedger calls', async () => {
193
- await adapter.saveLedger(createEmptyStore());
194
-
195
- // Launch 5 concurrent mutations that each add a principle
196
- const operations = Array.from({ length: 5 }, (_, i) =>
197
- adapter.mutateLedger((store) => {
198
- store.tree.principles[`P-CONC-${i}`] = createTestPrinciple(`P-CONC-${i}`, `Concurrent ${i}`);
199
- }),
200
- );
201
-
202
- await Promise.all(operations);
203
-
204
- // All 5 should be present — no lost updates
205
- const loaded = await adapter.loadLedger();
206
- for (let i = 0; i < 5; i++) {
207
- expect(loaded.tree.principles[`P-CONC-${i}`]).toBeDefined();
208
- expect(loaded.tree.principles[`P-CONC-${i}`].text).toBe(`Concurrent ${i}`);
209
- }
210
- });
211
-
212
- it('concurrent mutateLedger calls with return values all resolve correctly', async () => {
213
- await adapter.saveLedger(createEmptyStore());
214
-
215
- // Seed with a principle
216
- await adapter.mutateLedger((store) => {
217
- store.tree.principles['P-SEED'] = createTestPrinciple('P-SEED');
218
- });
219
-
220
- // Concurrent reads that return the count
221
- const operations = Array.from({ length: 3 }, () =>
222
- adapter.mutateLedger((store) => {
223
- return Object.keys(store.tree.principles).length;
224
- }),
225
- );
226
-
227
- const results = await Promise.all(operations);
228
-
229
- // Each should see at least 1 (the seed) since mutations serialize
230
- for (const count of results) {
231
- expect(count).toBeGreaterThanOrEqual(1);
232
- }
233
- });
234
-
235
- it('no data loss under concurrent writes of different keys', async () => {
236
- await adapter.saveLedger(createEmptyStore());
237
-
238
- // One writes principles, another writes training store entries
239
- const op1 = adapter.mutateLedger((store) => {
240
- store.tree.principles['P-PRIN'] = createTestPrinciple('P-PRIN', 'Principle side');
241
- });
242
- const op2 = adapter.mutateLedger((store) => {
243
- store.trainingStore['tp-train'] = {
244
- principleId: 'tp-train',
245
- evaluability: 'deterministic',
246
- applicableOpportunityCount: 1,
247
- observedViolationCount: 0,
248
- complianceRate: 1.0,
249
- violationTrend: 0,
250
- generatedSampleCount: 0,
251
- approvedSampleCount: 0,
252
- includedTrainRunIds: [],
253
- deployedCheckpointIds: [],
254
- internalizationStatus: 'prompt_only',
255
- };
256
- });
257
-
258
- await Promise.all([op1, op2]);
259
-
260
- const loaded = await adapter.loadLedger();
261
- expect(loaded.tree.principles['P-PRIN']).toBeDefined();
262
- expect(loaded.trainingStore['tp-train']).toBeDefined();
263
- });
264
- });
265
-
266
- // -------------------------------------------------------------------------
267
- // 3. Persistence across restarts
268
- // -------------------------------------------------------------------------
269
-
270
- describe('persistence across restarts', () => {
271
- it('data persists when adapter is re-created on the same directory', async () => {
272
- const store = createEmptyStore();
273
- store.tree.principles['P-PERSIST'] = createTestPrinciple('P-PERSIST', 'Survives restart');
274
-
275
- // Write with first adapter instance
276
- await adapter.saveLedger(store);
277
-
278
- // Simulate restart: create a new adapter pointing to the same dir
279
- const restartedAdapter = factory(tmpDir);
280
- const loaded = await restartedAdapter.loadLedger();
281
-
282
- expect(loaded.tree.principles['P-PERSIST']).toBeDefined();
283
- expect(loaded.tree.principles['P-PERSIST'].text).toBe('Survives restart');
284
- });
285
-
286
- it('mutateLedger changes persist across adapter re-creation', async () => {
287
- await adapter.saveLedger(createEmptyStore());
288
-
289
- await adapter.mutateLedger((store) => {
290
- store.tree.principles['P-MUT-PERSIST'] = createTestPrinciple('P-MUT-PERSIST', 'Mutate survives');
291
- });
292
-
293
- // New adapter instance
294
- const restartedAdapter = factory(tmpDir);
295
- const loaded = await restartedAdapter.loadLedger();
296
-
297
- expect(loaded.tree.principles['P-MUT-PERSIST']).toBeDefined();
298
- expect(loaded.tree.principles['P-MUT-PERSIST'].text).toBe('Mutate survives');
299
- });
300
-
301
- it('training store data persists across adapter re-creation', async () => {
302
- const store = createEmptyStore();
303
- store.trainingStore['tp-persist'] = {
304
- principleId: 'tp-persist',
305
- evaluability: 'weak_heuristic',
306
- applicableOpportunityCount: 10,
307
- observedViolationCount: 3,
308
- complianceRate: 0.7,
309
- violationTrend: -0.2,
310
- generatedSampleCount: 5,
311
- approvedSampleCount: 4,
312
- includedTrainRunIds: ['run-a'],
313
- deployedCheckpointIds: [],
314
- internalizationStatus: 'needs_training',
315
- };
316
-
317
- await adapter.saveLedger(store);
318
-
319
- const restartedAdapter = factory(tmpDir);
320
- const loaded = await restartedAdapter.loadLedger();
321
-
322
- expect(loaded.trainingStore['tp-persist']).toBeDefined();
323
- expect(loaded.trainingStore['tp-persist'].applicableOpportunityCount).toBe(10);
324
- expect(loaded.trainingStore['tp-persist'].internalizationStatus).toBe('needs_training');
325
- });
326
- });
327
-
328
- // -------------------------------------------------------------------------
329
- // 4. Error handling
330
- // -------------------------------------------------------------------------
331
-
332
- describe('error handling', () => {
333
- it('loadLedger handles corrupted JSON gracefully', async () => {
334
- // Write invalid JSON to the ledger file
335
- const filePath = path.join(tmpDir, 'principle_training_state.json');
336
- fs.mkdirSync(tmpDir, { recursive: true });
337
- fs.writeFileSync(filePath, '{ invalid json !!!', 'utf8');
338
-
339
- // Should not throw — should return empty store
340
- const store = await adapter.loadLedger();
341
- expect(store.trainingStore).toEqual({});
342
- expect(store.tree.principles).toEqual({});
343
- });
344
-
345
- it('loadLedger handles empty file', async () => {
346
- const filePath = path.join(tmpDir, 'principle_training_state.json');
347
- fs.mkdirSync(tmpDir, { recursive: true });
348
- fs.writeFileSync(filePath, '', 'utf8');
349
-
350
- const store = await adapter.loadLedger();
351
- expect(store.trainingStore).toEqual({});
352
- });
353
-
354
- it('loadLedger handles file with null content', async () => {
355
- const filePath = path.join(tmpDir, 'principle_training_state.json');
356
- fs.mkdirSync(tmpDir, { recursive: true });
357
- fs.writeFileSync(filePath, 'null', 'utf8');
358
-
359
- const store = await adapter.loadLedger();
360
- expect(store.trainingStore).toEqual({});
361
- });
362
-
363
- it('saveLedger throws on write errors (read-only dir)', async () => {
364
- // Create a scenario where mkdir would succeed but write fails:
365
- // make the parent path a regular file so attempting to create nested dirs fails with ENOTDIR
366
- const nestedParent = path.join(tmpDir, 'nonexistent');
367
- fs.writeFileSync(nestedParent, 'blocker', 'utf8');
368
- const readOnlyDir = path.join(nestedParent, 'nested');
369
- const roAdapter = factory(readOnlyDir);
370
-
371
- const store = createEmptyStore();
372
- expect.assertions(1);
373
- await expect(roAdapter.saveLedger(store)).rejects.toThrow(Error);
374
- });
375
-
376
- it('mutateLedger propagates errors from the mutate function', async () => {
377
- await adapter.saveLedger(createEmptyStore());
378
-
379
- await expect(
380
- adapter.mutateLedger(() => {
381
- throw new Error('Intentional test error');
382
- }),
383
- ).rejects.toThrow('Intentional test error');
384
- });
385
-
386
- it('async mutateLedger propagates async errors', async () => {
387
- await adapter.saveLedger(createEmptyStore());
388
-
389
- await expect(
390
- adapter.mutateLedger(async () => {
391
- await new Promise((r) => setTimeout(r, 5));
392
- throw new Error('Async intentional error');
393
- }),
394
- ).rejects.toThrow('Async intentional error');
395
- });
396
- });
397
-
398
- // -------------------------------------------------------------------------
399
- // 5. Async support
400
- // -------------------------------------------------------------------------
401
-
402
- describe('async mutate support', () => {
403
- it('supports async mutate functions', async () => {
404
- await adapter.saveLedger(createEmptyStore());
405
-
406
- const result = await adapter.mutateLedger(async (store) => {
407
- await new Promise((r) => setTimeout(r, 10));
408
- store.tree.principles['P-ASYNC'] = createTestPrinciple('P-ASYNC', 'Async mutation');
409
- return 'async-result';
410
- });
411
-
412
- expect(result).toBe('async-result');
413
- const loaded = await adapter.loadLedger();
414
- expect(loaded.tree.principles['P-ASYNC']).toBeDefined();
415
- expect(loaded.tree.principles['P-ASYNC'].text).toBe('Async mutation');
416
- });
417
- });
418
- });
419
- }
420
-
421
- // ---------------------------------------------------------------------------
422
- // Run conformance suite against FileStorageAdapter
423
- // ---------------------------------------------------------------------------
424
-
425
- import { FileStorageAdapter } from '../../src/core/file-storage-adapter.js';
426
-
427
- describeStorageConformance('FileStorageAdapter', (stateDir) => {
428
- return new FileStorageAdapter(stateDir);
429
- });