principles-disciple 1.11.0 → 1.13.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/CHANGELOG.md +26 -0
- package/README.md +66 -0
- package/esbuild.config.js +1 -1
- package/openclaw.plugin.json +4 -4
- package/package.json +2 -3
- package/run-nocturnal.mjs +30 -0
- package/scripts/db-migrate.mjs +170 -0
- package/scripts/sync-plugin.mjs +250 -6
- package/src/commands/archive-impl.ts +136 -0
- package/src/commands/capabilities.ts +4 -2
- package/src/commands/context.ts +5 -1
- package/src/commands/disable-impl.ts +151 -0
- package/src/commands/evolution-status.ts +64 -19
- package/src/commands/export.ts +8 -6
- package/src/commands/focus.ts +8 -20
- package/src/commands/nocturnal-review.ts +5 -7
- package/src/commands/nocturnal-rollout.ts +1 -12
- package/src/commands/nocturnal-train.ts +21 -47
- package/src/commands/pain.ts +10 -5
- package/src/commands/principle-rollback.ts +4 -2
- package/src/commands/promote-impl.ts +274 -0
- package/src/commands/rollback-impl.ts +234 -0
- package/src/commands/rollback.ts +6 -3
- package/src/commands/samples.ts +2 -0
- package/src/commands/thinking-os.ts +3 -4
- package/src/commands/workflow-debug.ts +2 -1
- package/src/config/errors.ts +1 -0
- package/src/core/AGENTS.md +34 -0
- package/src/core/adaptive-thresholds.ts +4 -3
- package/src/core/code-implementation-storage.ts +241 -0
- package/src/core/config.ts +5 -2
- package/src/core/control-ui-db.ts +29 -10
- package/src/core/detection-funnel.ts +12 -7
- package/src/core/diagnostician-task-store.ts +156 -0
- package/src/core/dictionary.ts +4 -4
- package/src/core/empathy-keyword-matcher.ts +7 -3
- package/src/core/empathy-types.ts +13 -2
- package/src/core/event-log.ts +14 -6
- package/src/core/evolution-engine.ts +27 -31
- package/src/core/evolution-logger.ts +3 -2
- package/src/core/evolution-reducer.ts +110 -31
- package/src/core/evolution-types.ts +10 -0
- package/src/core/external-training-contract.ts +1 -0
- package/src/core/focus-history.ts +38 -24
- package/src/core/hygiene/tracker.ts +10 -6
- package/src/core/init.ts +5 -2
- package/src/core/migration.ts +3 -3
- package/src/core/model-deployment-registry.ts +6 -4
- package/src/core/model-training-registry.ts +5 -3
- package/src/core/nocturnal-arbiter.ts +13 -14
- package/src/core/nocturnal-artifact-lineage.ts +117 -0
- package/src/core/nocturnal-artificer.ts +257 -0
- package/src/core/nocturnal-candidate-scoring.ts +4 -2
- package/src/core/nocturnal-compliance.ts +67 -19
- package/src/core/nocturnal-dataset.ts +95 -2
- package/src/core/nocturnal-executability.ts +2 -3
- package/src/core/nocturnal-export.ts +6 -3
- package/src/core/nocturnal-rule-implementation-validator.ts +245 -0
- package/src/core/nocturnal-trajectory-extractor.ts +10 -3
- package/src/core/nocturnal-trinity.ts +319 -61
- package/src/core/pain-context-extractor.ts +29 -15
- package/src/core/pain.ts +7 -5
- package/src/core/path-resolver.ts +16 -15
- package/src/core/paths.ts +2 -1
- package/src/core/pd-task-reconciler.ts +463 -0
- package/src/core/pd-task-service.ts +42 -0
- package/src/core/pd-task-store.ts +77 -0
- package/src/core/pd-task-types.ts +128 -0
- package/src/core/principle-internalization/deprecated-readiness.ts +91 -0
- package/src/core/principle-internalization/internalization-routing-policy.ts +208 -0
- package/src/core/principle-internalization/lifecycle-metrics.ts +149 -0
- package/src/core/principle-internalization/lifecycle-read-model.ts +243 -0
- package/src/core/principle-internalization/lifecycle-refresh.ts +11 -0
- package/src/core/principle-internalization/principle-lifecycle-service.ts +167 -0
- package/src/core/principle-training-state.ts +95 -370
- package/src/core/principle-tree-ledger.ts +733 -0
- package/src/core/principle-tree-migration.ts +195 -0
- package/src/core/profile.ts +3 -1
- package/src/core/promotion-gate.ts +14 -18
- package/src/core/replay-engine.ts +562 -0
- package/src/core/risk-calculator.ts +6 -4
- package/src/core/rule-host-helpers.ts +39 -0
- package/src/core/rule-host-types.ts +82 -0
- package/src/core/rule-host.ts +245 -0
- package/src/core/rule-implementation-runtime.ts +38 -0
- package/src/core/schema/db-types.ts +16 -0
- package/src/core/schema/index.ts +26 -0
- package/src/core/schema/migration-runner.ts +207 -0
- package/src/core/schema/migrations/001-init-trajectory.ts +211 -0
- package/src/core/schema/migrations/002-init-central.ts +122 -0
- package/src/core/schema/migrations/003-init-workflow.ts +55 -0
- package/src/core/schema/migrations/004-add-thinking-and-gfi.ts +74 -0
- package/src/core/schema/migrations/index.ts +31 -0
- package/src/core/schema/schema-definitions.ts +650 -0
- package/src/core/session-tracker.ts +6 -4
- package/src/core/shadow-observation-registry.ts +6 -3
- package/src/core/system-logger.ts +2 -2
- package/src/core/thinking-models.ts +182 -46
- package/src/core/thinking-os-parser.ts +156 -0
- package/src/core/training-program.ts +7 -7
- package/src/core/trajectory.ts +42 -36
- package/src/core/workspace-context.ts +77 -11
- package/src/core/workspace-dir-validation.ts +152 -0
- package/src/hooks/AGENTS.md +31 -0
- package/src/hooks/bash-risk.ts +3 -1
- package/src/hooks/edit-verification.ts +9 -5
- package/src/hooks/gate-block-helper.ts +5 -1
- package/src/hooks/gate.ts +152 -5
- package/src/hooks/gfi-gate.ts +9 -2
- package/src/hooks/lifecycle-routing.ts +124 -0
- package/src/hooks/lifecycle.ts +12 -12
- package/src/hooks/llm.ts +17 -109
- package/src/hooks/message-sanitize.ts +5 -3
- package/src/hooks/pain.ts +19 -15
- package/src/hooks/progressive-trust-gate.ts +7 -1
- package/src/hooks/prompt.ts +169 -60
- package/src/hooks/subagent.ts +5 -4
- package/src/hooks/thinking-checkpoint.ts +2 -0
- package/src/hooks/trajectory-collector.ts +15 -12
- package/src/http/principles-console-route.ts +31 -68
- package/src/i18n/commands.ts +2 -2
- package/src/index.ts +130 -40
- package/src/service/central-database.ts +131 -43
- package/src/service/central-health-service.ts +47 -0
- package/src/service/central-overview-service.ts +135 -0
- package/src/service/central-sync-service.ts +87 -0
- package/src/service/control-ui-query-service.ts +46 -36
- package/src/service/event-log-auditor.ts +261 -0
- package/src/service/evolution-query-service.ts +23 -22
- package/src/service/evolution-worker.ts +565 -261
- package/src/service/health-query-service.ts +213 -36
- package/src/service/nocturnal-runtime.ts +8 -4
- package/src/service/nocturnal-service.ts +503 -59
- package/src/service/nocturnal-target-selector.ts +5 -7
- package/src/service/runtime-summary-service.ts +2 -1
- package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +25 -336
- package/src/service/subagent-workflow/dynamic-timeout.ts +30 -0
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +48 -386
- package/src/service/subagent-workflow/index.ts +2 -0
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +169 -284
- package/src/service/subagent-workflow/runtime-direct-driver.ts +114 -16
- package/src/service/subagent-workflow/subagent-error-utils.ts +25 -0
- package/src/service/subagent-workflow/types.ts +9 -4
- package/src/service/subagent-workflow/workflow-manager-base.ts +573 -0
- package/src/service/subagent-workflow/workflow-store.ts +71 -11
- package/src/service/trajectory-service.ts +2 -1
- package/src/tools/critique-prompt.ts +1 -1
- package/src/tools/deep-reflect.ts +175 -209
- package/src/tools/model-index.ts +2 -1
- package/src/types/event-types.ts +2 -2
- package/src/types/principle-tree-schema.ts +29 -23
- package/src/utils/file-lock.ts +5 -3
- package/src/utils/io.ts +5 -2
- package/src/utils/nlp.ts +5 -46
- package/src/utils/node-vm-polyfill.ts +11 -0
- package/src/utils/plugin-logger.ts +2 -0
- package/src/utils/retry.ts +572 -0
- package/src/utils/subagent-probe.ts +1 -1
- package/templates/langs/en/core/AGENTS.md +0 -13
- package/templates/langs/en/core/SOUL.md +1 -31
- package/templates/langs/en/core/TOOLS.md +0 -4
- package/templates/langs/en/principles/THINKING_OS.md +77 -0
- package/templates/langs/en/skills/admin/SKILL.md +0 -1
- package/templates/langs/en/skills/evolution-framework-update/SKILL.md +1 -1
- package/templates/langs/en/skills/pd-diagnostician/SKILL.md +18 -5
- package/templates/langs/zh/core/AGENTS.md +0 -22
- package/templates/langs/zh/core/SOUL.md +1 -31
- package/templates/langs/zh/core/TOOLS.md +0 -4
- package/templates/langs/zh/principles/THINKING_OS.md +77 -0
- package/templates/langs/zh/skills/admin/SKILL.md +0 -1
- package/templates/langs/zh/skills/evolution-framework-update/SKILL.md +1 -1
- package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +25 -4
- package/tests/commands/evolution-status.test.ts +119 -0
- package/tests/commands/implementation-lifecycle.test.ts +362 -0
- package/tests/core/code-implementation-storage.test.ts +398 -0
- package/tests/core/evolution-reducer.detector-metadata.test.ts +28 -28
- package/tests/core/nocturnal-artifact-lineage.test.ts +53 -0
- package/tests/core/nocturnal-artificer.test.ts +241 -0
- package/tests/core/nocturnal-compliance-p-principles.test.ts +133 -0
- package/tests/core/nocturnal-rule-implementation-validator.test.ts +127 -0
- package/tests/core/pd-task-store.test.ts +126 -0
- package/tests/core/principle-internalization/deprecated-readiness.test.ts +193 -0
- package/tests/core/principle-internalization/internalization-routing-policy.test.ts +212 -0
- package/tests/core/principle-internalization/lifecycle-metrics.test.ts +350 -0
- package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +211 -0
- package/tests/core/principle-training-state.test.ts +228 -1
- package/tests/core/principle-tree-ledger.test.ts +423 -0
- package/tests/core/regression-v1-9-1.test.ts +265 -0
- package/tests/core/replay-engine.test.ts +234 -0
- package/tests/core/rule-host-helpers.test.ts +120 -0
- package/tests/core/rule-host.test.ts +389 -0
- package/tests/core/rule-implementation-runtime.test.ts +64 -0
- package/tests/core/workspace-context.test.ts +53 -0
- package/tests/core/workspace-dir-validation.test.ts +272 -0
- package/tests/hooks/gate-rule-host-pipeline.test.ts +385 -0
- package/tests/hooks/pain.test.ts +74 -10
- package/tests/hooks/prompt.test.ts +63 -1
- package/tests/integration/principle-lifecycle.e2e.test.ts +197 -0
- package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +211 -0
- package/tests/service/data-endpoints-regression.test.ts +834 -0
- package/tests/service/evolution-worker.test.ts +0 -123
- package/tests/service/nocturnal-service-code-candidate.test.ts +330 -0
- package/tests/utils/nlp.test.ts +1 -19
- package/tests/utils/retry.test.ts +327 -0
- package/ui/src/App.tsx +1 -1
- package/ui/src/api.ts +4 -0
- package/ui/src/charts.tsx +366 -0
- package/ui/src/components/WorkspaceConfig.tsx +107 -75
- package/ui/src/i18n/ui.ts +89 -31
- package/ui/src/pages/EvolutionPage.tsx +1 -1
- package/ui/src/pages/OverviewPage.tsx +441 -81
- package/ui/src/pages/ThinkingModelsPage.tsx +287 -69
- package/ui/src/styles.css +43 -0
- package/ui/src/types.ts +17 -1
- package/src/agents/nocturnal-dreamer.md +0 -152
- package/src/agents/nocturnal-philosopher.md +0 -138
- package/src/agents/nocturnal-reflector.md +0 -126
- package/src/agents/nocturnal-scribe.md +0 -164
- package/templates/workspace/.principles/00-kernel.md +0 -51
- package/templates/workspace/.principles/DECISION_POLICY.json +0 -44
- package/templates/workspace/.principles/PRINCIPLES.md +0 -20
- package/templates/workspace/.principles/PROFILE.json +0 -54
- package/templates/workspace/.principles/PROFILE.schema.json +0 -56
- package/templates/workspace/.principles/THINKING_OS.md +0 -64
- package/templates/workspace/.principles/THINKING_OS_ARCHIVE.md +0 -7
- package/templates/workspace/.principles/THINKING_OS_CANDIDATES.md +0 -9
- package/templates/workspace/.principles/models/_INDEX.md +0 -27
- package/templates/workspace/.principles/models/first_principles.md +0 -62
- package/templates/workspace/.principles/models/marketing_4p.md +0 -52
- package/templates/workspace/.principles/models/porter_five.md +0 -63
- package/templates/workspace/.principles/models/swot.md +0 -60
- package/templates/workspace/.principles/models/user_story_map.md +0 -63
- package/templates/workspace/.state/WORKBOARD.json +0 -4
package/tests/hooks/pain.test.ts
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { handleAfterToolCall } from '../../src/hooks/pain';
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { handleAfterToolCall } from '../../src/hooks/pain.js';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import * as path from 'path';
|
|
5
|
-
import * as ioUtils from '../../src/utils/io';
|
|
6
|
-
import { WorkspaceContext } from '../../src/core/workspace-context';
|
|
7
|
-
import { EventLogService } from '../../src/core/event-log';
|
|
8
|
-
import { setInjectedProbationIds, clearSession } from '../../src/core/session-tracker';
|
|
5
|
+
import * as ioUtils from '../../src/utils/io.js';
|
|
6
|
+
import { WorkspaceContext } from '../../src/core/workspace-context.js';
|
|
7
|
+
import { EventLogService } from '../../src/core/event-log.js';
|
|
8
|
+
import { setInjectedProbationIds, clearSession } from '../../src/core/session-tracker.js';
|
|
9
9
|
|
|
10
10
|
vi.mock('fs');
|
|
11
11
|
vi.mock('../../src/utils/io.js');
|
|
12
|
-
vi.mock('../../src/core/
|
|
13
|
-
vi.
|
|
12
|
+
vi.mock('../../src/core/evolution-engine.js', () => ({
|
|
13
|
+
recordEvolutionSuccess: vi.fn(),
|
|
14
|
+
recordEvolutionFailure: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
vi.mock('../../src/core/evolution-logger.js', () => ({
|
|
17
|
+
createTraceId: vi.fn(() => 'trace-123'),
|
|
18
|
+
getEvolutionLogger: vi.fn(() => ({
|
|
19
|
+
logPainDetected: vi.fn(),
|
|
20
|
+
})),
|
|
21
|
+
}));
|
|
14
22
|
|
|
15
23
|
const mockEmitSync = vi.fn();
|
|
16
24
|
const mockRecordProbationFeedback = vi.fn();
|
|
25
|
+
const mockUpdatePrincipleValueMetrics = vi.fn();
|
|
17
26
|
|
|
18
27
|
describe('Post-Write Checks & Pain Hook', () => {
|
|
19
28
|
const workspaceDir = '/mock/workspace';
|
|
@@ -34,6 +43,9 @@ describe('Post-Write Checks & Pain Hook', () => {
|
|
|
34
43
|
recordToolCall: vi.fn(),
|
|
35
44
|
recordPainEvent: vi.fn(),
|
|
36
45
|
},
|
|
46
|
+
principleTreeLedger: {
|
|
47
|
+
updatePrincipleValueMetrics: mockUpdatePrincipleValueMetrics,
|
|
48
|
+
},
|
|
37
49
|
evolutionReducer: {
|
|
38
50
|
emitSync: mockEmitSync,
|
|
39
51
|
recordProbationFeedback: mockRecordProbationFeedback,
|
|
@@ -49,11 +61,16 @@ describe('Post-Write Checks & Pain Hook', () => {
|
|
|
49
61
|
vi.clearAllMocks();
|
|
50
62
|
mockEmitSync.mockReset();
|
|
51
63
|
mockRecordProbationFeedback.mockReset();
|
|
52
|
-
|
|
53
|
-
vi.
|
|
64
|
+
mockUpdatePrincipleValueMetrics.mockReset();
|
|
65
|
+
vi.spyOn(WorkspaceContext, 'fromHookContext').mockReturnValue(mockWctx as any);
|
|
66
|
+
vi.spyOn(EventLogService, 'get').mockReturnValue(mockEventLog as any);
|
|
54
67
|
clearSession('s-success');
|
|
55
68
|
});
|
|
56
69
|
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
vi.restoreAllMocks();
|
|
72
|
+
});
|
|
73
|
+
|
|
57
74
|
it('should ignore non-write tools', () => {
|
|
58
75
|
const mockCtx = { workspaceDir, sessionId: 's1' };
|
|
59
76
|
const mockEvent = { toolName: 'read', params: {}, result: { exitCode: 0 }, error: undefined };
|
|
@@ -138,4 +155,51 @@ describe('Post-Write Checks & Pain Hook', () => {
|
|
|
138
155
|
}));
|
|
139
156
|
});
|
|
140
157
|
|
|
158
|
+
it('should persist matched principle valueMetrics through the locked ledger owner without raw training-state writes', () => {
|
|
159
|
+
const mockCtx = { workspaceDir, sessionId: 's-metrics', api: { logger: {} } };
|
|
160
|
+
const mockEvent = {
|
|
161
|
+
toolName: 'write',
|
|
162
|
+
params: { file_path: 'src/main.ts' },
|
|
163
|
+
error: 'Delete failed for src/main.ts',
|
|
164
|
+
result: { exitCode: 1 },
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
vi.mocked(ioUtils.normalizePath).mockReturnValue('src/main.ts');
|
|
168
|
+
vi.mocked(ioUtils.isRisky).mockReturnValue(false);
|
|
169
|
+
vi.mocked(ioUtils.serializeKvLines).mockReturnValue('mocked-pain-flag-content');
|
|
170
|
+
vi.mocked(fs.existsSync).mockImplementation((filePath: fs.PathLike) => {
|
|
171
|
+
const normalizedPath = String(filePath).replace(/\\/g, '/');
|
|
172
|
+
return normalizedPath.includes('.principles/PROFILE.json');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
mockWctx.evolutionReducer.getActivePrinciples = vi.fn().mockReturnValue([
|
|
176
|
+
{
|
|
177
|
+
id: 'p-match',
|
|
178
|
+
trigger: 'delete src main',
|
|
179
|
+
valueMetrics: undefined,
|
|
180
|
+
},
|
|
181
|
+
]);
|
|
182
|
+
mockWctx.evolutionReducer.getPrincipleById = vi.fn().mockReturnValue({
|
|
183
|
+
id: 'p-match',
|
|
184
|
+
trigger: 'delete src main',
|
|
185
|
+
contextTags: ['write'],
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
handleAfterToolCall(mockEvent as any, mockCtx as any);
|
|
189
|
+
|
|
190
|
+
expect(mockUpdatePrincipleValueMetrics).toHaveBeenCalledWith(
|
|
191
|
+
'p-match',
|
|
192
|
+
expect.objectContaining({
|
|
193
|
+
painPreventedCount: 1,
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const trainingStateWrites = vi
|
|
198
|
+
.mocked(fs.writeFileSync)
|
|
199
|
+
.mock.calls
|
|
200
|
+
.filter(([targetPath]) => String(targetPath).includes('principle_training_state.json'));
|
|
201
|
+
|
|
202
|
+
expect(trainingStateWrites).toEqual([]);
|
|
203
|
+
});
|
|
204
|
+
|
|
141
205
|
});
|
|
@@ -5,9 +5,34 @@ import { WorkspaceContext } from '../../src/core/workspace-context';
|
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
|
|
8
|
+
const promptHookMocks = vi.hoisted(() => ({
|
|
9
|
+
empathyManagerCtor: vi.fn(),
|
|
10
|
+
startWorkflow: vi.fn(),
|
|
11
|
+
isSubagentRuntimeAvailable: vi.fn(() => false),
|
|
12
|
+
}));
|
|
13
|
+
|
|
8
14
|
vi.mock('fs');
|
|
9
15
|
vi.mock('../../src/core/session-tracker.js');
|
|
10
16
|
vi.mock('../../src/core/workspace-context.js');
|
|
17
|
+
vi.mock('../../src/service/subagent-workflow/index.js', () => {
|
|
18
|
+
class MockEmpathyObserverWorkflowManager {
|
|
19
|
+
constructor(...args: unknown[]) {
|
|
20
|
+
promptHookMocks.empathyManagerCtor(...args);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
startWorkflow(...args: unknown[]) {
|
|
24
|
+
return promptHookMocks.startWorkflow(...args);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
EmpathyObserverWorkflowManager: MockEmpathyObserverWorkflowManager,
|
|
30
|
+
empathyObserverWorkflowSpec: { name: 'mock-empathy-workflow' },
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
vi.mock('../../src/utils/subagent-probe.js', () => ({
|
|
34
|
+
isSubagentRuntimeAvailable: promptHookMocks.isSubagentRuntimeAvailable,
|
|
35
|
+
}));
|
|
11
36
|
|
|
12
37
|
// 🎭️Test Group: Model Resolution Functions 🎭️
|
|
13
38
|
describe('resolveModelFromConfig', () => {
|
|
@@ -184,6 +209,8 @@ describe('Prompt Context Injection Hook', () => {
|
|
|
184
209
|
|
|
185
210
|
beforeEach(() => {
|
|
186
211
|
vi.clearAllMocks();
|
|
212
|
+
promptHookMocks.startWorkflow.mockResolvedValue(undefined);
|
|
213
|
+
promptHookMocks.isSubagentRuntimeAvailable.mockReturnValue(false);
|
|
187
214
|
vi.mocked(sessionTracker.getSession).mockReturnValue(undefined);
|
|
188
215
|
mockWctx.evolutionReducer.getActivePrinciples.mockReturnValue([]);
|
|
189
216
|
mockWctx.evolutionReducer.getProbationPrinciples.mockReturnValue([]);
|
|
@@ -226,6 +253,42 @@ describe('Prompt Context Injection Hook', () => {
|
|
|
226
253
|
expect(mockConfig.get).toHaveBeenCalledWith('empathy_engine.enabled');
|
|
227
254
|
});
|
|
228
255
|
|
|
256
|
+
it('does not start empathy workflow when subagent runtime probe fails', async () => {
|
|
257
|
+
const randomSpy = vi.spyOn(Math, 'random').mockReturnValue(0.01);
|
|
258
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
259
|
+
mockConfig.get.mockReturnValue(undefined);
|
|
260
|
+
promptHookMocks.isSubagentRuntimeAvailable.mockReturnValue(false);
|
|
261
|
+
|
|
262
|
+
await handleBeforePromptBuild({
|
|
263
|
+
messages: [{ role: 'user', content: 'hello there' }],
|
|
264
|
+
} as any, {
|
|
265
|
+
workspaceDir,
|
|
266
|
+
trigger: 'user',
|
|
267
|
+
sessionId: 'session-empathy-probe',
|
|
268
|
+
api: {
|
|
269
|
+
logger: {
|
|
270
|
+
info: vi.fn(),
|
|
271
|
+
warn: vi.fn(),
|
|
272
|
+
error: vi.fn(),
|
|
273
|
+
debug: vi.fn(),
|
|
274
|
+
},
|
|
275
|
+
runtime: {
|
|
276
|
+
subagent: {
|
|
277
|
+
run: () => {
|
|
278
|
+
throw new Error('Plugin runtime subagent methods are only available during a gateway request');
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
} as any);
|
|
284
|
+
|
|
285
|
+
expect(promptHookMocks.isSubagentRuntimeAvailable).toHaveBeenCalled();
|
|
286
|
+
expect(promptHookMocks.empathyManagerCtor).not.toHaveBeenCalled();
|
|
287
|
+
expect(promptHookMocks.startWorkflow).not.toHaveBeenCalled();
|
|
288
|
+
|
|
289
|
+
randomSpy.mockRestore();
|
|
290
|
+
});
|
|
291
|
+
|
|
229
292
|
it('records latest user turn and flags explicit corrections', async () => {
|
|
230
293
|
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
231
294
|
|
|
@@ -1408,4 +1471,3 @@ describe('Prompt Context Injection Hook', () => {
|
|
|
1408
1471
|
});
|
|
1409
1472
|
});
|
|
1410
1473
|
});
|
|
1411
|
-
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-End Test: Principle Lifecycle
|
|
3
|
+
*
|
|
4
|
+
* Tests the complete principle lifecycle from creation to evaluation:
|
|
5
|
+
* 1. createPrincipleFromDiagnosis() → training store write
|
|
6
|
+
* 2. listEvaluablePrinciples() → returns the principle
|
|
7
|
+
* 3. executeNocturnalReflectionAsync() → full pipeline (Selector → Trinity → Arbiter → Persist)
|
|
8
|
+
*
|
|
9
|
+
* This test addresses the root causes identified in 5-WHY analysis:
|
|
10
|
+
* - #204: EvolutionReducer must write to training store
|
|
11
|
+
* - #205: NocturnalWorkflowManager must use executeNocturnalReflectionAsync
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import * as os from 'os';
|
|
18
|
+
import { EvolutionReducerImpl } from '../../src/core/evolution-reducer.js';
|
|
19
|
+
import { listEvaluablePrinciples, loadStore } from '../../src/core/principle-training-state.js';
|
|
20
|
+
import { updateTrainingStore } from '../../src/core/principle-tree-ledger.js';
|
|
21
|
+
import { PathResolver } from '../../src/core/path-resolver.js';
|
|
22
|
+
|
|
23
|
+
describe('Principle Lifecycle E2E', () => {
|
|
24
|
+
let tempDir: string;
|
|
25
|
+
let workspaceDir: string;
|
|
26
|
+
let stateDir: string;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-lifecycle-test-'));
|
|
30
|
+
workspaceDir = tempDir;
|
|
31
|
+
stateDir = path.join(workspaceDir, '.state');
|
|
32
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
33
|
+
|
|
34
|
+
// Create required directories
|
|
35
|
+
const resolver = new PathResolver({ workspaceDir });
|
|
36
|
+
const memoryDir = path.dirname(resolver.resolve('EVOLUTION_STREAM'));
|
|
37
|
+
if (!fs.existsSync(memoryDir)) {
|
|
38
|
+
fs.mkdirSync(memoryDir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('Training Store Integration (#204)', () => {
|
|
47
|
+
it('should set needs_training for auto-evaluable principles', () => {
|
|
48
|
+
// Arrange: Create EvolutionReducer with stateDir
|
|
49
|
+
const reducer = new EvolutionReducerImpl({ workspaceDir, stateDir });
|
|
50
|
+
|
|
51
|
+
// Act: Create a principle from diagnosis with complete detectorMetadata
|
|
52
|
+
const principleId = reducer.createPrincipleFromDiagnosis({
|
|
53
|
+
painId: 'pain-001',
|
|
54
|
+
painType: 'tool_failure',
|
|
55
|
+
triggerPattern: 'file not found',
|
|
56
|
+
action: 'check file existence before access',
|
|
57
|
+
source: 'test',
|
|
58
|
+
evaluability: 'weak_heuristic',
|
|
59
|
+
detectorMetadata: {
|
|
60
|
+
applicabilityTags: ['file-operations'],
|
|
61
|
+
positiveSignals: ['file-exists-check'],
|
|
62
|
+
negativeSignals: ['file-not-found-error'],
|
|
63
|
+
toolSequenceHints: [['fs.existsSync', 'fs.readFile']],
|
|
64
|
+
confidence: 'medium',
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Assert: Principle should exist in training store with needs_training status
|
|
69
|
+
expect(principleId).not.toBeNull();
|
|
70
|
+
|
|
71
|
+
const store = loadStore(stateDir);
|
|
72
|
+
expect(store[principleId!]).toBeDefined();
|
|
73
|
+
expect(store[principleId!].evaluability).toBe('weak_heuristic');
|
|
74
|
+
expect(store[principleId!].internalizationStatus).toBe('needs_training');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should set prompt_only for manual_only principles', () => {
|
|
78
|
+
// Arrange: Create EvolutionReducer with stateDir
|
|
79
|
+
const reducer = new EvolutionReducerImpl({ workspaceDir, stateDir });
|
|
80
|
+
|
|
81
|
+
// Act: Create a manual_only principle
|
|
82
|
+
const principleId = reducer.createPrincipleFromDiagnosis({
|
|
83
|
+
painId: 'pain-manual',
|
|
84
|
+
painType: 'user_frustration',
|
|
85
|
+
triggerPattern: 'user is confused',
|
|
86
|
+
action: 'ask clarifying question',
|
|
87
|
+
source: 'test',
|
|
88
|
+
evaluability: 'manual_only', // Cannot be auto-evaluated
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Assert: Principle should have prompt_only status
|
|
92
|
+
expect(principleId).not.toBeNull();
|
|
93
|
+
|
|
94
|
+
const store = loadStore(stateDir);
|
|
95
|
+
expect(store[principleId!]).toBeDefined();
|
|
96
|
+
expect(store[principleId!].evaluability).toBe('manual_only');
|
|
97
|
+
expect(store[principleId!].internalizationStatus).toBe('prompt_only');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should list auto-evaluable principles immediately after creation', () => {
|
|
101
|
+
// Arrange: Create reducer and auto-evaluable principle
|
|
102
|
+
const reducer = new EvolutionReducerImpl({ workspaceDir, stateDir });
|
|
103
|
+
const principleId = reducer.createPrincipleFromDiagnosis({
|
|
104
|
+
painId: 'pain-auto',
|
|
105
|
+
painType: 'tool_failure',
|
|
106
|
+
triggerPattern: 'timeout exceeded',
|
|
107
|
+
action: 'implement retry with backoff',
|
|
108
|
+
source: 'test',
|
|
109
|
+
evaluability: 'deterministic',
|
|
110
|
+
detectorMetadata: {
|
|
111
|
+
applicabilityTags: ['network-operations'],
|
|
112
|
+
positiveSignals: ['retry-success'],
|
|
113
|
+
negativeSignals: ['timeout-error'],
|
|
114
|
+
toolSequenceHints: [['fetch', 'retry']],
|
|
115
|
+
confidence: 'high',
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Assert: Principle should be immediately evaluable (no status upgrade needed)
|
|
120
|
+
const evaluablePrinciples = listEvaluablePrinciples(stateDir);
|
|
121
|
+
expect(evaluablePrinciples.length).toBeGreaterThan(0);
|
|
122
|
+
expect(evaluablePrinciples.some(p => p.principleId === principleId)).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should NOT list manual_only principles as evaluable', () => {
|
|
126
|
+
// Arrange: Create reducer and manual_only principle
|
|
127
|
+
const reducer = new EvolutionReducerImpl({ workspaceDir, stateDir });
|
|
128
|
+
reducer.createPrincipleFromDiagnosis({
|
|
129
|
+
painId: 'pain-003',
|
|
130
|
+
painType: 'user_frustration',
|
|
131
|
+
triggerPattern: 'user repeated same request',
|
|
132
|
+
action: 'ask clarifying question',
|
|
133
|
+
source: 'test',
|
|
134
|
+
evaluability: 'manual_only', // Should not be evaluable
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Act: List evaluable principles
|
|
138
|
+
const evaluablePrinciples = listEvaluablePrinciples(stateDir);
|
|
139
|
+
|
|
140
|
+
// Assert: No evaluable principles (manual_only is excluded)
|
|
141
|
+
expect(evaluablePrinciples).toHaveLength(0);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('State Consistency', () => {
|
|
146
|
+
it('should maintain consistency across evolution.jsonl, PRINCIPLES.md, and training store', () => {
|
|
147
|
+
// Arrange
|
|
148
|
+
const reducer = new EvolutionReducerImpl({ workspaceDir, stateDir });
|
|
149
|
+
|
|
150
|
+
// Act: Create multiple principles with complete detectorMetadata
|
|
151
|
+
const ids = [
|
|
152
|
+
reducer.createPrincipleFromDiagnosis({
|
|
153
|
+
painId: 'pain-a',
|
|
154
|
+
painType: 'tool_failure',
|
|
155
|
+
triggerPattern: 'pattern a',
|
|
156
|
+
action: 'action a',
|
|
157
|
+
source: 'test',
|
|
158
|
+
evaluability: 'deterministic',
|
|
159
|
+
detectorMetadata: {
|
|
160
|
+
applicabilityTags: ['test-a'],
|
|
161
|
+
positiveSignals: ['signal-a'],
|
|
162
|
+
negativeSignals: ['neg-a'],
|
|
163
|
+
toolSequenceHints: [['tool-a']],
|
|
164
|
+
confidence: 'high',
|
|
165
|
+
},
|
|
166
|
+
}),
|
|
167
|
+
reducer.createPrincipleFromDiagnosis({
|
|
168
|
+
painId: 'pain-b',
|
|
169
|
+
painType: 'subagent_error',
|
|
170
|
+
triggerPattern: 'pattern b',
|
|
171
|
+
action: 'action b',
|
|
172
|
+
source: 'test',
|
|
173
|
+
evaluability: 'weak_heuristic',
|
|
174
|
+
detectorMetadata: {
|
|
175
|
+
applicabilityTags: ['test-b'],
|
|
176
|
+
positiveSignals: ['signal-b'],
|
|
177
|
+
negativeSignals: ['neg-b'],
|
|
178
|
+
toolSequenceHints: [['tool-b']],
|
|
179
|
+
confidence: 'medium',
|
|
180
|
+
},
|
|
181
|
+
}),
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
// Assert: All should be in training store with needs_training (auto-evaluable)
|
|
185
|
+
const store = loadStore(stateDir);
|
|
186
|
+
ids.forEach(id => {
|
|
187
|
+
expect(id).not.toBeNull();
|
|
188
|
+
expect(store[id!]).toBeDefined();
|
|
189
|
+
expect(store[id!].internalizationStatus).toBe('needs_training');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Assert: Auto-evaluable principles are immediately available
|
|
193
|
+
const evaluablePrinciples = listEvaluablePrinciples(stateDir);
|
|
194
|
+
expect(evaluablePrinciples).toHaveLength(2);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E tests for tool hooks workspaceDir resolution
|
|
3
|
+
*
|
|
4
|
+
* Verifies that after_tool_call and before_tool_call hooks
|
|
5
|
+
* correctly resolve workspaceDir and write events to the correct location.
|
|
6
|
+
*
|
|
7
|
+
* This test addresses the bug where PluginHookToolContext lacks workspaceDir,
|
|
8
|
+
* causing events to be written to ~/.state/ instead of workspace directory.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import * as os from 'os';
|
|
15
|
+
|
|
16
|
+
// Mock OpenClaw API for testing
|
|
17
|
+
const createMockApi = (workspaceDir: string) => ({
|
|
18
|
+
runtime: {
|
|
19
|
+
agent: {
|
|
20
|
+
resolveAgentWorkspaceDir: vi.fn().mockReturnValue(workspaceDir),
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
config: {},
|
|
24
|
+
resolvePath: vi.fn().mockReturnValue(workspaceDir),
|
|
25
|
+
logger: {
|
|
26
|
+
warn: vi.fn(),
|
|
27
|
+
error: vi.fn(),
|
|
28
|
+
info: vi.fn(),
|
|
29
|
+
debug: vi.fn(),
|
|
30
|
+
},
|
|
31
|
+
pluginConfig: {},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('E2E: Tool Hooks workspaceDir Resolution', () => {
|
|
35
|
+
const testWorkspaceDir = path.join(os.tmpdir(), 'pd-tool-hooks-e2e-test');
|
|
36
|
+
const stateDir = path.join(testWorkspaceDir, '.state');
|
|
37
|
+
const logsDir = path.join(stateDir, 'logs');
|
|
38
|
+
const eventsFile = path.join(logsDir, 'events.jsonl');
|
|
39
|
+
|
|
40
|
+
beforeAll(() => {
|
|
41
|
+
// Create test workspace structure
|
|
42
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterAll(() => {
|
|
46
|
+
// Cleanup
|
|
47
|
+
if (fs.existsSync(testWorkspaceDir)) {
|
|
48
|
+
fs.rmSync(testWorkspaceDir, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('Scenario 1: ctx.workspaceDir is provided (future OpenClaw fix)', () => {
|
|
53
|
+
it('should use ctx.workspaceDir directly when valid', async () => {
|
|
54
|
+
const { validateWorkspaceDir } = await import('../../src/core/workspace-dir-validation.js');
|
|
55
|
+
|
|
56
|
+
const ctx = {
|
|
57
|
+
workspaceDir: testWorkspaceDir,
|
|
58
|
+
agentId: 'test-agent'
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const result = validateWorkspaceDir(ctx.workspaceDir);
|
|
62
|
+
expect(result).toBeNull(); // Valid
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('Scenario 2: ctx.workspaceDir is undefined (current OpenClaw behavior)', () => {
|
|
67
|
+
it('should fallback to agentId resolution', async () => {
|
|
68
|
+
const { resolveValidWorkspaceDir } = await import('../../src/core/workspace-dir-validation.js');
|
|
69
|
+
|
|
70
|
+
const mockApi = createMockApi(testWorkspaceDir);
|
|
71
|
+
const ctx = {
|
|
72
|
+
workspaceDir: undefined, // OpenClaw doesn't provide this
|
|
73
|
+
agentId: 'test-agent'
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const result = resolveValidWorkspaceDir(ctx, mockApi as any, { source: 'after_tool_call' });
|
|
77
|
+
|
|
78
|
+
expect(result).toBe(testWorkspaceDir);
|
|
79
|
+
expect(mockApi.runtime.agent.resolveAgentWorkspaceDir).toHaveBeenCalledWith(mockApi.config, 'test-agent');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should fallback to resolvePath when agentId is also undefined', async () => {
|
|
83
|
+
const { resolveValidWorkspaceDir } = await import('../../src/core/workspace-dir-validation.js');
|
|
84
|
+
|
|
85
|
+
const mockApi = createMockApi(testWorkspaceDir);
|
|
86
|
+
const ctx = {
|
|
87
|
+
workspaceDir: undefined,
|
|
88
|
+
agentId: undefined
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const result = resolveValidWorkspaceDir(ctx, mockApi as any, { source: 'after_tool_call' });
|
|
92
|
+
|
|
93
|
+
expect(result).toBe(testWorkspaceDir);
|
|
94
|
+
expect(mockApi.resolvePath).toHaveBeenCalledWith('.');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('Scenario 3: Events are written to correct location', () => {
|
|
99
|
+
it('should create events.jsonl in workspace directory with correct content', () => {
|
|
100
|
+
// Simulate event being written
|
|
101
|
+
const testEvent = {
|
|
102
|
+
ts: new Date().toISOString(),
|
|
103
|
+
type: 'hook_execution',
|
|
104
|
+
category: 'success',
|
|
105
|
+
data: { hook: 'after_tool_call' },
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
fs.appendFileSync(eventsFile, JSON.stringify(testEvent) + '\n', 'utf-8');
|
|
109
|
+
|
|
110
|
+
// Verify file exists in workspace directory
|
|
111
|
+
expect(fs.existsSync(eventsFile)).toBe(true);
|
|
112
|
+
|
|
113
|
+
// Verify content contains the expected hook
|
|
114
|
+
const content = fs.readFileSync(eventsFile, 'utf-8');
|
|
115
|
+
const lines = content.trim().split('\n');
|
|
116
|
+
const lastLine = JSON.parse(lines[lines.length - 1]);
|
|
117
|
+
expect(lastLine.data.hook).toBe('after_tool_call');
|
|
118
|
+
|
|
119
|
+
// Verify the path is NOT directly under home directory
|
|
120
|
+
// (it should be under a workspace subdirectory)
|
|
121
|
+
const homeDir = os.homedir();
|
|
122
|
+
const normalizedPath = path.normalize(eventsFile);
|
|
123
|
+
// Events file should not be directly at ~/.state/logs/events.jsonl
|
|
124
|
+
const directHomeEventsFile = path.join(homeDir, '.state', 'logs', 'events.jsonl');
|
|
125
|
+
// If it happens to be the same path, that's a problem
|
|
126
|
+
if (normalizedPath === path.normalize(directHomeEventsFile)) {
|
|
127
|
+
throw new Error('Events file is being written to ~/.state/ instead of workspace directory!');
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('Scenario 4: Warning is logged when workspaceDir resolution fails', () => {
|
|
133
|
+
it('should warn when falling back to resolvePath with invalid result', async () => {
|
|
134
|
+
const { resolveValidWorkspaceDir } = await import('../../src/core/workspace-dir-validation.js');
|
|
135
|
+
|
|
136
|
+
const mockApi = createMockApi(os.homedir()); // resolvePath returns home dir
|
|
137
|
+
mockApi.runtime.agent.resolveAgentWorkspaceDir.mockReturnValue(os.homedir()); // agentId also returns home
|
|
138
|
+
|
|
139
|
+
const warningMessages: string[] = [];
|
|
140
|
+
const onWarning = (msg: string) => warningMessages.push(msg);
|
|
141
|
+
|
|
142
|
+
const ctx = { workspaceDir: undefined, agentId: 'test-agent' };
|
|
143
|
+
|
|
144
|
+
const result = resolveValidWorkspaceDir(ctx, mockApi as any, {
|
|
145
|
+
source: 'test',
|
|
146
|
+
onWarning,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Should have warnings about invalid paths
|
|
150
|
+
expect(warningMessages.length).toBeGreaterThan(0);
|
|
151
|
+
expect(warningMessages.some(m => m.includes('FINAL FALLBACK') || m.includes('invalid'))).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('E2E: EventLog flushImmediately', () => {
|
|
157
|
+
const testWorkspaceDir = path.join(os.tmpdir(), 'pd-eventlog-flush-test');
|
|
158
|
+
const stateDir = path.join(testWorkspaceDir, '.state');
|
|
159
|
+
const logsDir = path.join(stateDir, 'logs');
|
|
160
|
+
const eventsFile = path.join(logsDir, 'events.jsonl');
|
|
161
|
+
|
|
162
|
+
beforeAll(() => {
|
|
163
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
afterAll(() => {
|
|
167
|
+
if (fs.existsSync(testWorkspaceDir)) {
|
|
168
|
+
fs.rmSync(testWorkspaceDir, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should flush events immediately when flushImmediately is true', async () => {
|
|
173
|
+
const { EventLog } = await import('../../src/core/event-log.js');
|
|
174
|
+
|
|
175
|
+
const eventLog = new EventLog(stateDir);
|
|
176
|
+
|
|
177
|
+
// Write with flushImmediately
|
|
178
|
+
eventLog.recordHookExecution({ hook: 'after_tool_call' }, { flushImmediately: true });
|
|
179
|
+
|
|
180
|
+
// File should exist immediately (not waiting for buffer or timer)
|
|
181
|
+
expect(fs.existsSync(eventsFile)).toBe(true);
|
|
182
|
+
|
|
183
|
+
// Content should be there
|
|
184
|
+
const content = fs.readFileSync(eventsFile, 'utf-8');
|
|
185
|
+
expect(content).toContain('after_tool_call');
|
|
186
|
+
|
|
187
|
+
eventLog.flush();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should not flush immediately when flushImmediately is false or omitted', async () => {
|
|
191
|
+
const { EventLog } = await import('../../src/core/event-log.js');
|
|
192
|
+
|
|
193
|
+
// Clean up
|
|
194
|
+
if (fs.existsSync(eventsFile)) {
|
|
195
|
+
fs.unlinkSync(eventsFile);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const eventLog = new EventLog(stateDir);
|
|
199
|
+
|
|
200
|
+
// Write without flushImmediately
|
|
201
|
+
eventLog.recordHookExecution({ hook: 'before_tool_call' });
|
|
202
|
+
|
|
203
|
+
// File might not exist yet (depends on buffer)
|
|
204
|
+
// But after manual flush, it should be there
|
|
205
|
+
eventLog.flush();
|
|
206
|
+
|
|
207
|
+
expect(fs.existsSync(eventsFile)).toBe(true);
|
|
208
|
+
const content = fs.readFileSync(eventsFile, 'utf-8');
|
|
209
|
+
expect(content).toContain('before_tool_call');
|
|
210
|
+
});
|
|
211
|
+
});
|