principles-disciple 1.16.0 → 1.18.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/README.md +13 -5
- package/openclaw.plugin.json +4 -4
- package/package.json +1 -1
- package/src/commands/archive-impl.ts +3 -3
- package/src/commands/capabilities.ts +1 -1
- package/src/commands/context.ts +3 -3
- package/src/commands/disable-impl.ts +1 -1
- package/src/commands/evolution-status.ts +2 -2
- package/src/commands/focus.ts +2 -2
- package/src/commands/nocturnal-train.ts +6 -6
- package/src/commands/pain.ts +4 -4
- package/src/commands/pd-reflect.ts +87 -0
- package/src/commands/rollback-impl.ts +4 -4
- package/src/commands/rollback.ts +2 -2
- package/src/commands/samples.ts +2 -2
- package/src/commands/workflow-debug.ts +1 -1
- package/src/config/errors.ts +1 -1
- package/src/core/adaptive-thresholds.ts +1 -1
- package/src/core/code-implementation-storage.ts +2 -2
- package/src/core/config.ts +1 -1
- package/src/core/diagnostician-task-store.ts +2 -2
- package/src/core/empathy-keyword-matcher.ts +3 -3
- package/src/core/event-log.ts +5 -5
- package/src/core/evolution-engine.ts +4 -4
- package/src/core/evolution-logger.ts +1 -1
- package/src/core/evolution-reducer.ts +3 -3
- package/src/core/evolution-types.ts +5 -5
- package/src/core/external-training-contract.ts +1 -1
- package/src/core/focus-history.ts +14 -14
- package/src/core/hygiene/tracker.ts +1 -1
- package/src/core/init.ts +2 -2
- package/src/core/model-deployment-registry.ts +2 -2
- package/src/core/model-training-registry.ts +2 -2
- package/src/core/nocturnal-arbiter.ts +1 -1
- package/src/core/nocturnal-artificer.ts +2 -2
- package/src/core/nocturnal-candidate-scoring.ts +2 -2
- package/src/core/nocturnal-compliance.ts +4 -3
- package/src/core/nocturnal-dataset.ts +3 -3
- package/src/core/nocturnal-export.ts +4 -4
- package/src/core/nocturnal-rule-implementation-validator.ts +1 -1
- package/src/core/nocturnal-snapshot-contract.ts +112 -0
- package/src/core/nocturnal-trajectory-extractor.ts +7 -5
- package/src/core/nocturnal-trinity.ts +480 -158
- package/src/core/pain-context-extractor.ts +3 -3
- package/src/core/pain.ts +124 -11
- package/src/core/path-resolver.ts +4 -4
- package/src/core/pd-task-reconciler.ts +10 -10
- package/src/core/pd-task-service.ts +1 -1
- package/src/core/pd-task-store.ts +1 -1
- package/src/core/principle-internalization/deprecated-readiness.ts +1 -1
- package/src/core/principle-training-state.ts +2 -2
- package/src/core/principle-tree-ledger.ts +7 -7
- package/src/core/promotion-gate.ts +9 -9
- package/src/core/replay-engine.ts +12 -12
- package/src/core/risk-calculator.ts +1 -1
- package/src/core/rule-host-types.ts +2 -2
- package/src/core/rule-host.ts +5 -5
- package/src/core/schema/db-types.ts +1 -1
- package/src/core/schema/schema-definitions.ts +1 -1
- package/src/core/session-tracker.ts +96 -4
- package/src/core/shadow-observation-registry.ts +3 -3
- package/src/core/system-logger.ts +2 -2
- package/src/core/thinking-os-parser.ts +1 -1
- package/src/core/training-program.ts +2 -2
- package/src/core/trajectory.ts +8 -8
- package/src/core/workspace-context.ts +2 -2
- package/src/core/workspace-dir-service.ts +85 -0
- package/src/core/workspace-dir-validation.ts +30 -107
- package/src/hooks/bash-risk.ts +3 -3
- package/src/hooks/edit-verification.ts +4 -4
- package/src/hooks/gate-block-helper.ts +4 -4
- package/src/hooks/gate.ts +10 -10
- package/src/hooks/gfi-gate.ts +7 -7
- package/src/hooks/lifecycle.ts +2 -2
- package/src/hooks/llm.ts +1 -1
- package/src/hooks/pain.ts +25 -5
- package/src/hooks/progressive-trust-gate.ts +7 -7
- package/src/hooks/prompt.ts +24 -5
- package/src/hooks/subagent.ts +2 -2
- package/src/hooks/thinking-checkpoint.ts +2 -2
- package/src/hooks/trajectory-collector.ts +1 -1
- package/src/http/principles-console-route.ts +14 -6
- package/src/i18n/commands.ts +4 -0
- package/src/index.ts +181 -185
- package/src/service/central-health-service.ts +1 -1
- package/src/service/central-overview-service.ts +3 -3
- package/src/service/evolution-query-service.ts +1 -1
- package/src/service/evolution-worker.ts +221 -109
- package/src/service/health-query-service.ts +27 -17
- package/src/service/monitoring-query-service.ts +3 -3
- package/src/service/nocturnal-runtime.ts +4 -4
- package/src/service/nocturnal-service.ts +40 -23
- package/src/service/nocturnal-target-selector.ts +11 -4
- package/src/service/runtime-summary-service.ts +1 -1
- package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +1 -1
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +3 -3
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +16 -13
- package/src/service/subagent-workflow/runtime-direct-driver.ts +10 -6
- package/src/service/subagent-workflow/types.ts +4 -4
- package/src/service/subagent-workflow/workflow-manager-base.ts +5 -5
- package/src/service/subagent-workflow/workflow-store.ts +2 -2
- package/src/tools/critique-prompt.ts +2 -3
- package/src/tools/deep-reflect.ts +17 -16
- package/src/tools/model-index.ts +1 -1
- package/src/utils/file-lock.ts +1 -1
- package/src/utils/io.ts +7 -2
- package/src/utils/nlp.ts +1 -1
- package/src/utils/plugin-logger.ts +2 -2
- package/src/utils/retry.ts +3 -2
- package/src/utils/subagent-probe.ts +20 -33
- package/templates/langs/en/skills/pd-pain-signal/SKILL.md +8 -7
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +111 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/task-specs.mjs +1 -1
- package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +1 -1
- package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +8 -7
- package/templates/pain_settings.json +1 -1
- package/tests/build-artifacts.test.ts +4 -58
- package/tests/commands/pd-reflect.test.ts +49 -0
- package/tests/core/nocturnal-snapshot-contract.test.ts +70 -0
- package/tests/core/pain-auto-repair.test.ts +96 -0
- package/tests/core/pain-integration.test.ts +483 -0
- package/tests/core/pain.test.ts +5 -4
- package/tests/core/workspace-dir-service.test.ts +68 -0
- package/tests/core/workspace-dir-validation.test.ts +56 -192
- package/tests/hooks/pain.test.ts +20 -0
- package/tests/http/principles-console-route.test.ts +42 -20
- package/tests/integration/empathy-workflow-integration.test.ts +1 -2
- package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +9 -17
- package/tests/service/empathy-observer-workflow-manager.test.ts +1 -2
- package/tests/service/evolution-worker.nocturnal.test.ts +118 -109
- package/tests/service/nocturnal-runtime-hardening.test.ts +33 -0
- package/tests/utils/subagent-probe.test.ts +32 -0
package/tests/core/pain.test.ts
CHANGED
|
@@ -107,13 +107,13 @@ describe('Pain Detection Module', () => {
|
|
|
107
107
|
const missing = validatePainFlag({
|
|
108
108
|
source: 'tool_failure',
|
|
109
109
|
score: '70',
|
|
110
|
-
// missing time, reason
|
|
110
|
+
// missing time, reason (session_id/agent_id are optional)
|
|
111
111
|
});
|
|
112
112
|
|
|
113
113
|
expect(missing).toContain('time');
|
|
114
114
|
expect(missing).toContain('reason');
|
|
115
|
-
expect(missing).toContain('session_id');
|
|
116
|
-
expect(missing).toContain('agent_id');
|
|
115
|
+
expect(missing).not.toContain('session_id');
|
|
116
|
+
expect(missing).not.toContain('agent_id');
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
it('should report empty string fields as missing', () => {
|
|
@@ -126,7 +126,8 @@ describe('Pain Detection Module', () => {
|
|
|
126
126
|
agent_id: 'main',
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
// session_id/agent_id are optional — empty values are acceptable
|
|
130
|
+
expect(missing).toEqual([]);
|
|
130
131
|
});
|
|
131
132
|
});
|
|
132
133
|
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import { resolveRequiredWorkspaceDir, resolveWorkspaceDir } from '../../src/core/workspace-dir-service.js';
|
|
4
|
+
|
|
5
|
+
const homeDir = os.homedir();
|
|
6
|
+
|
|
7
|
+
describe('workspace-dir-service', () => {
|
|
8
|
+
const logger = {
|
|
9
|
+
error: vi.fn(),
|
|
10
|
+
warn: vi.fn(),
|
|
11
|
+
info: vi.fn(),
|
|
12
|
+
debug: vi.fn(),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const api = {
|
|
16
|
+
runtime: {
|
|
17
|
+
agent: {
|
|
18
|
+
resolveAgentWorkspaceDir: vi.fn(),
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
config: {},
|
|
22
|
+
logger,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
api.runtime.agent.resolveAgentWorkspaceDir.mockReturnValue('/resolved/workspace');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('accepts valid ctx.workspaceDir directly', () => {
|
|
31
|
+
const resolved = resolveRequiredWorkspaceDir(api as any, { workspaceDir: '/active/workspace', agentId: 'main' }, { source: 'test' });
|
|
32
|
+
expect(resolved).toBe('/active/workspace');
|
|
33
|
+
expect(api.runtime.agent.resolveAgentWorkspaceDir).not.toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('rejects home directory and resolves from agent id when available', () => {
|
|
37
|
+
const resolved = resolveWorkspaceDir(api as any, { workspaceDir: homeDir, agentId: 'worker-1' }, { source: 'test' });
|
|
38
|
+
expect(resolved).toBe('/resolved/workspace');
|
|
39
|
+
expect(api.runtime.agent.resolveAgentWorkspaceDir).toHaveBeenCalledWith(api.config, 'worker-1');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('throws in required mode when no valid workspace can be resolved', () => {
|
|
43
|
+
api.runtime.agent.resolveAgentWorkspaceDir.mockImplementation(() => {
|
|
44
|
+
throw new Error('no agent workspace');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(() => resolveRequiredWorkspaceDir(api as any, { workspaceDir: homeDir }, { source: 'command' })).toThrow(
|
|
48
|
+
/unable to resolve a valid workspace directory/i,
|
|
49
|
+
);
|
|
50
|
+
expect(logger.error).toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('returns undefined in optional mode when no valid workspace can be resolved', () => {
|
|
54
|
+
api.runtime.agent.resolveAgentWorkspaceDir.mockImplementation(() => {
|
|
55
|
+
throw new Error('no agent workspace');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const resolved = resolveWorkspaceDir(api as any, { workspaceDir: homeDir }, { source: 'hook' });
|
|
59
|
+
expect(resolved).toBeUndefined();
|
|
60
|
+
expect(logger.warn).toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('uses explicit fallbackAgentId when caller provides one', () => {
|
|
64
|
+
const resolved = resolveRequiredWorkspaceDir(api as any, {}, { source: 'startup', fallbackAgentId: 'main' });
|
|
65
|
+
expect(resolved).toBe('/resolved/workspace');
|
|
66
|
+
expect(api.runtime.agent.resolveAgentWorkspaceDir).toHaveBeenCalledWith(api.config, 'main');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -1,68 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
* Unit tests for workspace-dir-validation.ts
|
|
3
|
-
*
|
|
4
|
-
* Tests the core validation logic and 3-tier fallback strategy
|
|
5
|
-
* for resolving correct workspaceDir in tool hooks.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
9
2
|
import * as os from 'os';
|
|
10
3
|
import { validateWorkspaceDir, resolveValidWorkspaceDir, logWorkspaceDirHealth } from '../../src/core/workspace-dir-validation.js';
|
|
11
4
|
|
|
12
5
|
const homeDir = os.homedir();
|
|
13
6
|
|
|
14
7
|
describe('validateWorkspaceDir', () => {
|
|
15
|
-
it('
|
|
16
|
-
|
|
17
|
-
expect(
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('should return error for null (treated as undefined)', () => {
|
|
21
|
-
const result = validateWorkspaceDir(null as unknown as string);
|
|
22
|
-
expect(result).toBe('workspaceDir is undefined/null');
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('should return error for empty string (treated as falsy)', () => {
|
|
26
|
-
const result = validateWorkspaceDir('');
|
|
27
|
-
// Empty string is falsy, so it's treated like undefined
|
|
28
|
-
expect(result).toContain('undefined/null');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('should return error for root directory', () => {
|
|
32
|
-
const result = validateWorkspaceDir('/');
|
|
33
|
-
expect(result).toContain('root or empty');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('should return error for home directory', () => {
|
|
37
|
-
const result = validateWorkspaceDir(homeDir);
|
|
38
|
-
expect(result).toContain('equals home directory');
|
|
39
|
-
expect(result).toContain(homeDir);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should return error for home directory with trailing slash', () => {
|
|
43
|
-
const result = validateWorkspaceDir(`${homeDir}/`);
|
|
44
|
-
expect(result).toContain('home directory');
|
|
8
|
+
it('rejects undefined or null values', () => {
|
|
9
|
+
expect(validateWorkspaceDir(undefined)).toBe('workspaceDir is undefined/null');
|
|
10
|
+
expect(validateWorkspaceDir(null as unknown as string)).toBe('workspaceDir is undefined/null');
|
|
45
11
|
});
|
|
46
12
|
|
|
47
|
-
it('
|
|
48
|
-
|
|
49
|
-
expect(
|
|
13
|
+
it('rejects home directory and root-like paths', () => {
|
|
14
|
+
expect(validateWorkspaceDir(homeDir)).toContain('home directory');
|
|
15
|
+
expect(validateWorkspaceDir(`${homeDir}/`)).toContain('home directory');
|
|
16
|
+
expect(validateWorkspaceDir('/')).toContain('root or empty');
|
|
17
|
+
expect(validateWorkspaceDir('')).toContain('undefined/null');
|
|
50
18
|
});
|
|
51
19
|
|
|
52
|
-
it('
|
|
53
|
-
|
|
54
|
-
expect(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
it('should return null (valid) for temp directory', () => {
|
|
58
|
-
const result = validateWorkspaceDir('/tmp/test-workspace');
|
|
59
|
-
expect(result).toBeNull();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('should return null (valid) for Windows-style path', () => {
|
|
63
|
-
// On Linux, Windows paths are just regular paths
|
|
64
|
-
const result = validateWorkspaceDir('C:\\Users\\test\\workspace');
|
|
65
|
-
expect(result).toBeNull();
|
|
20
|
+
it('accepts normal workspace paths', () => {
|
|
21
|
+
expect(validateWorkspaceDir('/home/user/projects/workspace-main')).toBeNull();
|
|
22
|
+
expect(validateWorkspaceDir('/tmp/test-workspace')).toBeNull();
|
|
23
|
+
expect(validateWorkspaceDir('C:\\Users\\test\\workspace')).toBeNull();
|
|
66
24
|
});
|
|
67
25
|
});
|
|
68
26
|
|
|
@@ -81,108 +39,65 @@ describe('resolveValidWorkspaceDir', () => {
|
|
|
81
39
|
},
|
|
82
40
|
},
|
|
83
41
|
config: {},
|
|
84
|
-
resolvePath: vi.fn(),
|
|
85
42
|
logger: mockLogger,
|
|
86
43
|
};
|
|
87
44
|
|
|
88
45
|
beforeEach(() => {
|
|
89
46
|
vi.clearAllMocks();
|
|
90
|
-
mockApi.resolvePath.mockReturnValue('/default/workspace');
|
|
91
47
|
mockApi.runtime.agent.resolveAgentWorkspaceDir.mockReturnValue('/resolved/from/agent');
|
|
92
48
|
});
|
|
93
49
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
50
|
+
it('returns ctx.workspaceDir when valid', () => {
|
|
51
|
+
const result = resolveValidWorkspaceDir(
|
|
52
|
+
{ workspaceDir: '/valid/workspace', agentId: 'main' },
|
|
53
|
+
mockApi as any,
|
|
54
|
+
{ source: 'test' },
|
|
55
|
+
);
|
|
97
56
|
|
|
98
|
-
it('should return ctx.workspaceDir when valid', () => {
|
|
99
|
-
const ctx = { workspaceDir: '/valid/workspace', agentId: 'agent-1' };
|
|
100
|
-
const result = resolveValidWorkspaceDir(ctx, mockApi as any, { source: 'test' });
|
|
101
|
-
|
|
102
57
|
expect(result).toBe('/valid/workspace');
|
|
103
|
-
expect(
|
|
58
|
+
expect(mockApi.runtime.agent.resolveAgentWorkspaceDir).not.toHaveBeenCalled();
|
|
104
59
|
});
|
|
105
60
|
|
|
106
|
-
it('
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
expect(mockLogger.warn).toHaveBeenCalled();
|
|
113
|
-
});
|
|
61
|
+
it('falls back to agent resolution when ctx.workspaceDir is invalid', () => {
|
|
62
|
+
const result = resolveValidWorkspaceDir(
|
|
63
|
+
{ workspaceDir: homeDir, agentId: 'main' },
|
|
64
|
+
mockApi as any,
|
|
65
|
+
{ source: 'test' },
|
|
66
|
+
);
|
|
114
67
|
|
|
115
|
-
it('should try agentId resolution when ctx.workspaceDir is undefined', () => {
|
|
116
|
-
const ctx = { workspaceDir: undefined, agentId: 'agent-1' };
|
|
117
|
-
const result = resolveValidWorkspaceDir(ctx, mockApi as any, { source: 'test' });
|
|
118
|
-
|
|
119
|
-
expect(mockApi.runtime.agent.resolveAgentWorkspaceDir).toHaveBeenCalledWith(mockApi.config, 'agent-1');
|
|
120
68
|
expect(result).toBe('/resolved/from/agent');
|
|
69
|
+
expect(mockApi.runtime.agent.resolveAgentWorkspaceDir).toHaveBeenCalledWith(mockApi.config, 'main');
|
|
121
70
|
});
|
|
122
71
|
|
|
123
|
-
it('
|
|
124
|
-
|
|
125
|
-
const result = resolveValidWorkspaceDir(ctx, mockApi as any, { source: 'test' });
|
|
126
|
-
|
|
127
|
-
expect(mockApi.resolvePath).toHaveBeenCalledWith('.');
|
|
128
|
-
expect(result).toBe('/default/workspace');
|
|
129
|
-
});
|
|
72
|
+
it('returns undefined when no valid workspace can be resolved', () => {
|
|
73
|
+
mockApi.runtime.agent.resolveAgentWorkspaceDir.mockReturnValue(homeDir);
|
|
130
74
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const ctx = { workspaceDir: undefined, agentId: 'unknown-agent' };
|
|
137
|
-
const result = resolveValidWorkspaceDir(ctx, mockApi as any, { source: 'test' });
|
|
138
|
-
|
|
139
|
-
expect(mockApi.resolvePath).toHaveBeenCalledWith('.');
|
|
140
|
-
expect(result).toBe('/default/workspace');
|
|
141
|
-
expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('failed to resolve from agentId'));
|
|
142
|
-
});
|
|
75
|
+
const result = resolveValidWorkspaceDir(
|
|
76
|
+
{ workspaceDir: undefined, agentId: 'main' },
|
|
77
|
+
mockApi as any,
|
|
78
|
+
{ source: 'test' },
|
|
79
|
+
);
|
|
143
80
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const ctx = { workspaceDir: undefined, agentId: 'agent-1' };
|
|
148
|
-
const result = resolveValidWorkspaceDir(ctx, mockApi as any, { source: 'test' });
|
|
149
|
-
|
|
150
|
-
expect(mockApi.resolvePath).toHaveBeenCalledWith('.');
|
|
151
|
-
expect(result).toBe('/default/workspace');
|
|
152
|
-
expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('invalid'));
|
|
81
|
+
expect(result).toBeUndefined();
|
|
82
|
+
expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('unable to resolve a valid workspace directory'));
|
|
153
83
|
});
|
|
154
84
|
|
|
155
|
-
it('
|
|
156
|
-
mockApi.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
expect(result).toBe(homeDir);
|
|
162
|
-
expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('FINAL FALLBACK'));
|
|
163
|
-
});
|
|
85
|
+
it('supports explicit fallbackAgentId', () => {
|
|
86
|
+
mockApi.runtime.agent.resolveAgentWorkspaceDir
|
|
87
|
+
.mockImplementationOnce(() => {
|
|
88
|
+
throw new Error('agent missing');
|
|
89
|
+
})
|
|
90
|
+
.mockReturnValueOnce('/resolved/from/fallback');
|
|
164
91
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const result = resolveValidWorkspaceDir(ctx, mockApi as any, {
|
|
172
|
-
source: 'test',
|
|
173
|
-
onWarning: customWarning,
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
expect(customWarning).toHaveBeenCalled();
|
|
177
|
-
});
|
|
92
|
+
const result = resolveValidWorkspaceDir(
|
|
93
|
+
{ workspaceDir: undefined, agentId: 'worker-1' },
|
|
94
|
+
mockApi as any,
|
|
95
|
+
{ source: 'test', fallbackAgentId: 'main' },
|
|
96
|
+
);
|
|
178
97
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
mockApi.
|
|
182
|
-
|
|
183
|
-
resolveValidWorkspaceDir(ctx, mockApi as any);
|
|
184
|
-
|
|
185
|
-
expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('unknown:'));
|
|
98
|
+
expect(result).toBe('/resolved/from/fallback');
|
|
99
|
+
expect(mockApi.runtime.agent.resolveAgentWorkspaceDir).toHaveBeenNthCalledWith(1, mockApi.config, 'worker-1');
|
|
100
|
+
expect(mockApi.runtime.agent.resolveAgentWorkspaceDir).toHaveBeenNthCalledWith(2, mockApi.config, 'main');
|
|
186
101
|
});
|
|
187
102
|
});
|
|
188
103
|
|
|
@@ -201,7 +116,6 @@ describe('logWorkspaceDirHealth', () => {
|
|
|
201
116
|
},
|
|
202
117
|
},
|
|
203
118
|
config: {},
|
|
204
|
-
resolvePath: vi.fn().mockReturnValue('/valid/workspace'),
|
|
205
119
|
logger: mockLogger,
|
|
206
120
|
};
|
|
207
121
|
|
|
@@ -209,64 +123,14 @@ describe('logWorkspaceDirHealth', () => {
|
|
|
209
123
|
vi.clearAllMocks();
|
|
210
124
|
});
|
|
211
125
|
|
|
212
|
-
it('
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining('✓'));
|
|
126
|
+
it('logs info when workspace is valid', () => {
|
|
127
|
+
logWorkspaceDirHealth({ workspaceDir: '/valid/workspace' }, 'startup', mockApi as any);
|
|
128
|
+
expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining('workspaceDir="/valid/workspace" OK'));
|
|
217
129
|
});
|
|
218
130
|
|
|
219
|
-
it('
|
|
220
|
-
mockApi.resolvePath.mockReturnValue(homeDir);
|
|
221
|
-
|
|
222
|
-
const ctx = { workspaceDir: undefined };
|
|
223
|
-
logWorkspaceDirHealth(ctx, 'startup', mockApi as any);
|
|
224
|
-
|
|
225
|
-
expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining(homeDir));
|
|
226
|
-
});
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
describe('Integration: 3-tier fallback chain', () => {
|
|
230
|
-
const mockLogger = {
|
|
231
|
-
warn: vi.fn(),
|
|
232
|
-
error: vi.fn(),
|
|
233
|
-
info: vi.fn(),
|
|
234
|
-
debug: vi.fn(),
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
it('should follow the complete fallback chain: ctx.workspaceDir -> agentId -> resolvePath', () => {
|
|
238
|
-
const mockApi = {
|
|
239
|
-
runtime: {
|
|
240
|
-
agent: {
|
|
241
|
-
resolveAgentWorkspaceDir: vi.fn().mockReturnValue('/workspace/from-agent'),
|
|
242
|
-
},
|
|
243
|
-
},
|
|
244
|
-
config: {},
|
|
245
|
-
resolvePath: vi.fn().mockReturnValue('/workspace/from-resolvePath'),
|
|
246
|
-
logger: mockLogger,
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
// Case 1: ctx.workspaceDir is valid - use it directly
|
|
250
|
-
const ctx1 = { workspaceDir: '/workspace/from-ctx', agentId: 'agent-1' };
|
|
251
|
-
const result1 = resolveValidWorkspaceDir(ctx1, mockApi as any, { source: 'test' });
|
|
252
|
-
expect(result1).toBe('/workspace/from-ctx');
|
|
253
|
-
expect(mockApi.runtime.agent.resolveAgentWorkspaceDir).not.toHaveBeenCalled();
|
|
254
|
-
|
|
255
|
-
vi.clearAllMocks();
|
|
256
|
-
|
|
257
|
-
// Case 2: ctx.workspaceDir is invalid, agentId resolution works
|
|
258
|
-
const ctx2 = { workspaceDir: homeDir, agentId: 'agent-1' };
|
|
259
|
-
const result2 = resolveValidWorkspaceDir(ctx2, mockApi as any, { source: 'test' });
|
|
260
|
-
expect(result2).toBe('/workspace/from-agent');
|
|
261
|
-
expect(mockApi.runtime.agent.resolveAgentWorkspaceDir).toHaveBeenCalled();
|
|
262
|
-
|
|
263
|
-
vi.clearAllMocks();
|
|
264
|
-
|
|
265
|
-
// Case 3: ctx.workspaceDir is invalid, agentId resolution returns invalid, use resolvePath
|
|
131
|
+
it('logs error when workspace remains unresolved', () => {
|
|
266
132
|
mockApi.runtime.agent.resolveAgentWorkspaceDir.mockReturnValue(homeDir);
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
expect(result3).toBe('/workspace/from-resolvePath');
|
|
270
|
-
expect(mockApi.resolvePath).toHaveBeenCalledWith('.');
|
|
133
|
+
logWorkspaceDirHealth({ workspaceDir: undefined }, 'startup', mockApi as any);
|
|
134
|
+
expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining('workspaceDir="undefined"'));
|
|
271
135
|
});
|
|
272
136
|
});
|
package/tests/hooks/pain.test.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
|
2
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 os from 'os';
|
|
5
6
|
import * as ioUtils from '../../src/utils/io.js';
|
|
6
7
|
import { WorkspaceContext } from '../../src/core/workspace-context.js';
|
|
7
8
|
import { EventLogService } from '../../src/core/event-log.js';
|
|
@@ -82,6 +83,25 @@ describe('Post-Write Checks & Pain Hook', () => {
|
|
|
82
83
|
expect(mockEmitSync).not.toHaveBeenCalled();
|
|
83
84
|
});
|
|
84
85
|
|
|
86
|
+
it('skips processing when no valid workspace can be resolved', () => {
|
|
87
|
+
const mockCtx = { workspaceDir: undefined, agentId: 'main', sessionId: 's-invalid' };
|
|
88
|
+
const mockEvent = { toolName: 'write', params: {}, result: { exitCode: 0 }, error: undefined };
|
|
89
|
+
const mockApi = {
|
|
90
|
+
runtime: {
|
|
91
|
+
agent: {
|
|
92
|
+
resolveAgentWorkspaceDir: vi.fn().mockReturnValue(os.homedir()),
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
config: {},
|
|
96
|
+
logger: { warn: vi.fn(), error: vi.fn(), info: vi.fn(), debug: vi.fn() },
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
handleAfterToolCall(mockEvent as any, mockCtx as any, mockApi as any);
|
|
100
|
+
|
|
101
|
+
expect(WorkspaceContext.fromHookContext).not.toHaveBeenCalled();
|
|
102
|
+
expect(mockApi.runtime.agent.resolveAgentWorkspaceDir).toHaveBeenCalledWith(mockApi.config, 'main');
|
|
103
|
+
});
|
|
104
|
+
|
|
85
105
|
it('should capture pain on tool error with correct source', () => {
|
|
86
106
|
const mockCtx = { workspaceDir, sessionId: 's1', api: { logger: {} } };
|
|
87
107
|
const mockEvent = {
|
|
@@ -48,6 +48,17 @@ function createRequest(method: string, url: string, body?: string, headers?: Rec
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
describe('principles-console-route', () => {
|
|
51
|
+
const createApi = () => ({
|
|
52
|
+
rootDir: '/plugin',
|
|
53
|
+
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
54
|
+
runtime: {
|
|
55
|
+
agent: {
|
|
56
|
+
resolveAgentWorkspaceDir: vi.fn(() => '/workspace'),
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
config: {},
|
|
60
|
+
});
|
|
61
|
+
|
|
51
62
|
it('serves overview JSON from the plugin API route', async () => {
|
|
52
63
|
vi.mocked(ControlUiQueryService).mockImplementation(function MockControlUiQueryService() {
|
|
53
64
|
return {
|
|
@@ -56,11 +67,8 @@ describe('principles-console-route', () => {
|
|
|
56
67
|
} as any;
|
|
57
68
|
} as any);
|
|
58
69
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
resolvePath: vi.fn(() => '/workspace'),
|
|
62
|
-
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
63
|
-
} as any);
|
|
70
|
+
const api = createApi();
|
|
71
|
+
const route = createPrinciplesConsoleRoute(api as any);
|
|
64
72
|
|
|
65
73
|
const response = new MockResponse() as unknown as ServerResponse;
|
|
66
74
|
const handled = await route.handler(
|
|
@@ -71,14 +79,11 @@ describe('principles-console-route', () => {
|
|
|
71
79
|
expect(handled).toBe(true);
|
|
72
80
|
expect((response as any).statusCode).toBe(200);
|
|
73
81
|
expect((response as any).body).toContain('"workspaceDir": "/workspace"');
|
|
82
|
+
expect(api.runtime.agent.resolveAgentWorkspaceDir).toHaveBeenCalled();
|
|
74
83
|
});
|
|
75
84
|
|
|
76
85
|
it('rejects unsupported asset methods with 405', async () => {
|
|
77
|
-
const route = createPrinciplesConsoleRoute(
|
|
78
|
-
rootDir: '/plugin',
|
|
79
|
-
resolvePath: vi.fn(() => '/workspace'),
|
|
80
|
-
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
81
|
-
} as any);
|
|
86
|
+
const route = createPrinciplesConsoleRoute(createApi() as any);
|
|
82
87
|
|
|
83
88
|
const response = new MockResponse() as unknown as ServerResponse;
|
|
84
89
|
const handled = await route.handler(
|
|
@@ -97,11 +102,7 @@ describe('principles-console-route', () => {
|
|
|
97
102
|
} as any;
|
|
98
103
|
} as any);
|
|
99
104
|
|
|
100
|
-
const route = createPrinciplesConsoleRoute(
|
|
101
|
-
rootDir: '/plugin',
|
|
102
|
-
resolvePath: vi.fn(() => '/workspace'),
|
|
103
|
-
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
104
|
-
} as any);
|
|
105
|
+
const route = createPrinciplesConsoleRoute(createApi() as any);
|
|
105
106
|
|
|
106
107
|
const response = new MockResponse() as unknown as ServerResponse;
|
|
107
108
|
const handled = await route.handler(
|
|
@@ -122,11 +123,7 @@ describe('principles-console-route', () => {
|
|
|
122
123
|
} as any;
|
|
123
124
|
} as any);
|
|
124
125
|
|
|
125
|
-
const route = createPrinciplesConsoleRoute(
|
|
126
|
-
rootDir: '/plugin',
|
|
127
|
-
resolvePath: vi.fn(() => '/workspace'),
|
|
128
|
-
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
129
|
-
} as any);
|
|
126
|
+
const route = createPrinciplesConsoleRoute(createApi() as any);
|
|
130
127
|
|
|
131
128
|
const response = new MockResponse() as unknown as ServerResponse;
|
|
132
129
|
const handled = await route.handler(
|
|
@@ -137,4 +134,29 @@ describe('principles-console-route', () => {
|
|
|
137
134
|
expect(handled).toBe(true);
|
|
138
135
|
expect((response as any).statusCode).toBe(404);
|
|
139
136
|
});
|
|
137
|
+
|
|
138
|
+
it('fails fast when workspace resolution is unavailable', async () => {
|
|
139
|
+
vi.mocked(ControlUiQueryService).mockImplementation(function MockControlUiQueryService() {
|
|
140
|
+
return {
|
|
141
|
+
getOverview: () => ({ workspaceDir: '/workspace', generatedAt: 'now', dataFreshness: null }),
|
|
142
|
+
dispose: vi.fn(),
|
|
143
|
+
} as any;
|
|
144
|
+
} as any);
|
|
145
|
+
|
|
146
|
+
const api = createApi();
|
|
147
|
+
api.runtime.agent.resolveAgentWorkspaceDir = vi.fn(() => {
|
|
148
|
+
throw new Error('workspace unavailable');
|
|
149
|
+
});
|
|
150
|
+
const route = createPrinciplesConsoleRoute(api as any);
|
|
151
|
+
|
|
152
|
+
const response = new MockResponse() as unknown as ServerResponse;
|
|
153
|
+
const handled = await route.handler(
|
|
154
|
+
createRequest('GET', '/plugins/principles/api/overview'),
|
|
155
|
+
response,
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(handled).toBe(true);
|
|
159
|
+
expect((response as any).statusCode).toBe(500);
|
|
160
|
+
expect((response as any).body).toContain('unable to resolve a valid workspace directory');
|
|
161
|
+
});
|
|
140
162
|
});
|
|
@@ -20,8 +20,7 @@ import { handleBeforePromptBuild } from '../../src/hooks/prompt.js';
|
|
|
20
20
|
import { WorkspaceContext } from '../../src/core/workspace-context.js';
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* Helper to create a mock function
|
|
24
|
-
* This is required because isSubagentRuntimeAvailable() checks constructor.name === 'AsyncFunction'.
|
|
23
|
+
* Helper to create a mock async function for workflow-manager tests.
|
|
25
24
|
*/
|
|
26
25
|
function mockAsyncFn<T extends (...args: any[]) => Promise<any>>(impl: (...args: any[]) => any) {
|
|
27
26
|
const fn = vi.fn(impl) as unknown as T;
|
|
@@ -79,7 +79,7 @@ describe('E2E: Tool Hooks workspaceDir Resolution', () => {
|
|
|
79
79
|
expect(mockApi.runtime.agent.resolveAgentWorkspaceDir).toHaveBeenCalledWith(mockApi.config, 'test-agent');
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
it('should
|
|
82
|
+
it('should refuse to guess a workspace when agentId is also undefined', async () => {
|
|
83
83
|
const { resolveValidWorkspaceDir } = await import('../../src/core/workspace-dir-validation.js');
|
|
84
84
|
|
|
85
85
|
const mockApi = createMockApi(testWorkspaceDir);
|
|
@@ -90,8 +90,8 @@ describe('E2E: Tool Hooks workspaceDir Resolution', () => {
|
|
|
90
90
|
|
|
91
91
|
const result = resolveValidWorkspaceDir(ctx, mockApi as any, { source: 'after_tool_call' });
|
|
92
92
|
|
|
93
|
-
expect(result).
|
|
94
|
-
expect(mockApi.resolvePath).
|
|
93
|
+
expect(result).toBeUndefined();
|
|
94
|
+
expect(mockApi.resolvePath).not.toHaveBeenCalled();
|
|
95
95
|
});
|
|
96
96
|
});
|
|
97
97
|
|
|
@@ -129,26 +129,18 @@ describe('E2E: Tool Hooks workspaceDir Resolution', () => {
|
|
|
129
129
|
});
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
-
describe('Scenario 4:
|
|
133
|
-
it('should
|
|
132
|
+
describe('Scenario 4: Invalid workspace candidates are rejected', () => {
|
|
133
|
+
it('should return undefined when all workspace resolution candidates are invalid', async () => {
|
|
134
134
|
const { resolveValidWorkspaceDir } = await import('../../src/core/workspace-dir-validation.js');
|
|
135
135
|
|
|
136
|
-
const mockApi = createMockApi(os.homedir());
|
|
137
|
-
mockApi.runtime.agent.resolveAgentWorkspaceDir.mockReturnValue(os.homedir());
|
|
138
|
-
|
|
139
|
-
const warningMessages: string[] = [];
|
|
140
|
-
const onWarning = (msg: string) => warningMessages.push(msg);
|
|
136
|
+
const mockApi = createMockApi(os.homedir());
|
|
137
|
+
mockApi.runtime.agent.resolveAgentWorkspaceDir.mockReturnValue(os.homedir());
|
|
141
138
|
|
|
142
139
|
const ctx = { workspaceDir: undefined, agentId: 'test-agent' };
|
|
143
140
|
|
|
144
|
-
const result = resolveValidWorkspaceDir(ctx, mockApi as any, {
|
|
145
|
-
source: 'test',
|
|
146
|
-
onWarning,
|
|
147
|
-
});
|
|
141
|
+
const result = resolveValidWorkspaceDir(ctx, mockApi as any, { source: 'test' });
|
|
148
142
|
|
|
149
|
-
|
|
150
|
-
expect(warningMessages.length).toBeGreaterThan(0);
|
|
151
|
-
expect(warningMessages.some(m => m.includes('FINAL FALLBACK') || m.includes('invalid'))).toBe(true);
|
|
143
|
+
expect(result).toBeUndefined();
|
|
152
144
|
});
|
|
153
145
|
});
|
|
154
146
|
});
|
|
@@ -6,8 +6,7 @@ import { EmpathyObserverWorkflowManager } from '../../src/service/subagent-workf
|
|
|
6
6
|
import type { SubagentWorkflowSpec } from '../../src/service/subagent-workflow/types.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Helper to create a mock function
|
|
10
|
-
* This is required because isSubagentRuntimeAvailable() checks constructor.name === 'AsyncFunction'.
|
|
9
|
+
* Helper to create a mock async function for workflow-manager tests.
|
|
11
10
|
*/
|
|
12
11
|
function mockAsyncFn<T extends (...args: any[]) => Promise<any>>(impl: (...args: any[]) => any) {
|
|
13
12
|
const fn = vi.fn(impl) as unknown as T;
|