principles-disciple 1.83.0 → 1.84.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.
@@ -0,0 +1,347 @@
1
+ import { beforeEach, describe, expect, it, vi, afterEach } from 'vitest';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ import * as fs from 'fs';
5
+
6
+ import {
7
+ resolveCanonicalWorkspaceDir,
8
+ resolveHookWorkspaceDir,
9
+ resolveToolHookWorkspaceDirSafe,
10
+ } from '../../src/utils/workspace-resolver.js';
11
+ import type { CanonicalWorkspaceResult, HookWorkspaceResolutionResult } from '../../src/utils/workspace-resolver.js';
12
+
13
+ const homeDir = os.homedir();
14
+ const validWorkspace = path.join(os.tmpdir(), 'pd-test-workspace-hook-resolver');
15
+
16
+ function ensureDir(dir: string): void {
17
+ if (!fs.existsSync(dir)) {
18
+ fs.mkdirSync(dir, { recursive: true });
19
+ }
20
+ }
21
+
22
+ const noCanonical: () => null = () => null;
23
+
24
+ describe('resolveCanonicalWorkspaceDir', () => {
25
+ const originalEnv = { ...process.env };
26
+
27
+ beforeEach(() => {
28
+ process.env = { ...originalEnv };
29
+ delete process.env.PD_WORKSPACE_DIR;
30
+ delete process.env.OPENCLAW_WORKSPACE;
31
+ ensureDir(validWorkspace);
32
+ });
33
+
34
+ afterEach(() => {
35
+ process.env = { ...originalEnv };
36
+ });
37
+
38
+ it('resolves from PD_WORKSPACE_DIR env var with highest priority', () => {
39
+ process.env.PD_WORKSPACE_DIR = validWorkspace;
40
+ process.env.OPENCLAW_WORKSPACE = '/some/other/path';
41
+ const result = resolveCanonicalWorkspaceDir();
42
+ expect(result).not.toBeNull();
43
+ expect(result!.source).toBe('pd_env');
44
+ expect(result!.workspaceDir).toBe(path.resolve(validWorkspace));
45
+ });
46
+
47
+ it('resolves from OPENCLAW_WORKSPACE env var when PD_WORKSPACE_DIR is not set', () => {
48
+ process.env.OPENCLAW_WORKSPACE = validWorkspace;
49
+ const result = resolveCanonicalWorkspaceDir();
50
+ expect(result).not.toBeNull();
51
+ expect(result!.source).toBe('openclaw_env');
52
+ expect(result!.workspaceDir).toBe(path.resolve(validWorkspace));
53
+ });
54
+
55
+ it('prefers PD_WORKSPACE_DIR over OPENCLAW_WORKSPACE', () => {
56
+ const altWorkspace = path.join(os.tmpdir(), 'pd-test-workspace-alt');
57
+ ensureDir(altWorkspace);
58
+ process.env.PD_WORKSPACE_DIR = validWorkspace;
59
+ process.env.OPENCLAW_WORKSPACE = altWorkspace;
60
+ const result = resolveCanonicalWorkspaceDir();
61
+ expect(result).not.toBeNull();
62
+ expect(result!.source).toBe('pd_env');
63
+ expect(result!.workspaceDir).toBe(path.resolve(validWorkspace));
64
+ });
65
+
66
+ it('rejects home directory from PD_WORKSPACE_DIR', () => {
67
+ process.env.PD_WORKSPACE_DIR = homeDir;
68
+ const result = resolveCanonicalWorkspaceDir();
69
+ if (result?.source === 'pd_env') {
70
+ expect.fail('Should not resolve home directory from PD_WORKSPACE_DIR');
71
+ }
72
+ });
73
+
74
+ it('rejects empty string from PD_WORKSPACE_DIR', () => {
75
+ process.env.PD_WORKSPACE_DIR = '';
76
+ const result = resolveCanonicalWorkspaceDir();
77
+ if (result?.source === 'pd_env') {
78
+ expect.fail('Should not resolve empty PD_WORKSPACE_DIR');
79
+ }
80
+ });
81
+
82
+ it('always returns a result when PD_WORKSPACE_DIR points to a valid dir', () => {
83
+ process.env.PD_WORKSPACE_DIR = validWorkspace;
84
+ const result = resolveCanonicalWorkspaceDir();
85
+ expect(result).not.toBeNull();
86
+ expect(result!.workspaceDir).toBe(path.resolve(validWorkspace));
87
+ });
88
+ });
89
+
90
+ describe('resolveHookWorkspaceDir — PD canonical primary', () => {
91
+ const originalEnv = { ...process.env };
92
+ const logger = {
93
+ error: vi.fn(),
94
+ warn: vi.fn(),
95
+ info: vi.fn(),
96
+ debug: vi.fn(),
97
+ };
98
+
99
+ const api = {
100
+ runtime: {
101
+ agent: {
102
+ resolveAgentWorkspaceDir: vi.fn(),
103
+ },
104
+ },
105
+ config: {},
106
+ logger,
107
+ };
108
+
109
+ beforeEach(() => {
110
+ process.env = { ...originalEnv };
111
+ delete process.env.PD_WORKSPACE_DIR;
112
+ delete process.env.OPENCLAW_WORKSPACE;
113
+ vi.clearAllMocks();
114
+ ensureDir(validWorkspace);
115
+ });
116
+
117
+ afterEach(() => {
118
+ process.env = { ...originalEnv };
119
+ });
120
+
121
+ it('uses PD_WORKSPACE_DIR as primary source regardless of OpenClaw context', () => {
122
+ process.env.PD_WORKSPACE_DIR = validWorkspace;
123
+ const result = resolveHookWorkspaceDir(
124
+ { workspaceDir: '/some/openclaw/path', agentId: 'main' },
125
+ api as any,
126
+ 'test',
127
+ );
128
+ expect(result.ok).toBe(true);
129
+ if (result.ok) {
130
+ expect(result.source).toBe('pd_env');
131
+ expect(result.workspaceDir).toBe(path.resolve(validWorkspace));
132
+ }
133
+ });
134
+
135
+ it('emits consistency warning when PD canonical differs from OpenClaw context', () => {
136
+ const altWorkspace = path.join(os.tmpdir(), 'pd-test-workspace-alt-2');
137
+ ensureDir(altWorkspace);
138
+ process.env.PD_WORKSPACE_DIR = validWorkspace;
139
+ const result = resolveHookWorkspaceDir(
140
+ { workspaceDir: altWorkspace },
141
+ api as any,
142
+ 'test',
143
+ );
144
+ expect(result.ok).toBe(true);
145
+ if (result.ok) {
146
+ expect(result.source).toBe('pd_env');
147
+ expect(result.workspaceDir).toBe(path.resolve(validWorkspace));
148
+ expect(result.consistencyWarning).toContain('differs from OpenClaw context');
149
+ }
150
+ });
151
+
152
+ it('does not emit consistency warning when PD canonical matches OpenClaw context', () => {
153
+ process.env.PD_WORKSPACE_DIR = validWorkspace;
154
+ const result = resolveHookWorkspaceDir(
155
+ { workspaceDir: validWorkspace },
156
+ api as any,
157
+ 'test',
158
+ );
159
+ expect(result.ok).toBe(true);
160
+ if (result.ok) {
161
+ expect(result.consistencyWarning).toBeUndefined();
162
+ }
163
+ });
164
+
165
+ it('falls back to OpenClaw context when no PD explicit config exists', () => {
166
+ const result = resolveHookWorkspaceDir(
167
+ { workspaceDir: validWorkspace },
168
+ api as any,
169
+ 'test',
170
+ { canonicalResolver: noCanonical, explicitPdResolver: noCanonical },
171
+ );
172
+ expect(result.ok).toBe(true);
173
+ if (result.ok) {
174
+ expect(result.source).toBe('openclaw_context');
175
+ expect(result.workspaceDir).toBe(validWorkspace);
176
+ }
177
+ });
178
+
179
+ it('falls back to OpenClaw API when context is also missing', () => {
180
+ api.runtime.agent.resolveAgentWorkspaceDir.mockReturnValue(validWorkspace);
181
+ const result = resolveHookWorkspaceDir(
182
+ {},
183
+ api as any,
184
+ 'test',
185
+ { canonicalResolver: noCanonical, explicitPdResolver: noCanonical },
186
+ );
187
+ expect(result.ok).toBe(true);
188
+ if (result.ok) {
189
+ expect(result.source).toBe('openclaw_api');
190
+ expect(result.workspaceDir).toBe(validWorkspace);
191
+ }
192
+ });
193
+
194
+ it('returns structured failure with reason and nextAction when all sources fail', () => {
195
+ api.runtime.agent.resolveAgentWorkspaceDir.mockReturnValue(homeDir);
196
+ const result = resolveHookWorkspaceDir(
197
+ {},
198
+ api as any,
199
+ 'test_hook',
200
+ { canonicalResolver: noCanonical, explicitPdResolver: noCanonical },
201
+ );
202
+ expect(result.ok).toBe(false);
203
+ if (!result.ok) {
204
+ expect(result.reason).toBe('workspace_dir_unresolvable');
205
+ expect(result.nextAction).toContain('PD_WORKSPACE_DIR');
206
+ expect(result.nextAction).toContain('principles-disciple.json');
207
+ expect(result.message).toContain('test_hook');
208
+ }
209
+ });
210
+
211
+ it('rejects home directory from OpenClaw context and falls back to API', () => {
212
+ api.runtime.agent.resolveAgentWorkspaceDir.mockReturnValue(validWorkspace);
213
+ const result = resolveHookWorkspaceDir(
214
+ { workspaceDir: homeDir, agentId: 'main' },
215
+ api as any,
216
+ 'test',
217
+ { canonicalResolver: noCanonical, explicitPdResolver: noCanonical },
218
+ );
219
+ expect(result.ok).toBe(true);
220
+ if (result.ok) {
221
+ expect(result.source).toBe('openclaw_api');
222
+ expect(result.workspaceDir).toBe(validWorkspace);
223
+ }
224
+ });
225
+
226
+ it('ctx.workspaceDir takes priority over pd_default when no explicit PD source exists', () => {
227
+ // canonicalResolver returns pd_default, but ctx.workspaceDir is a real workspace
228
+ const pdDefaultResolver = (): CanonicalWorkspaceResult => ({
229
+ workspaceDir: path.join(homeDir, '.openclaw', 'workspace'),
230
+ source: 'pd_default',
231
+ });
232
+ const result = resolveHookWorkspaceDir(
233
+ { workspaceDir: validWorkspace },
234
+ api as any,
235
+ 'test',
236
+ { canonicalResolver: pdDefaultResolver, explicitPdResolver: noCanonical },
237
+ );
238
+ expect(result.ok).toBe(true);
239
+ if (result.ok) {
240
+ expect(result.source).toBe('openclaw_context');
241
+ expect(result.workspaceDir).toBe(validWorkspace);
242
+ }
243
+ });
244
+
245
+ it('returns failure when API throws and no other source works', () => {
246
+ api.runtime.agent.resolveAgentWorkspaceDir.mockImplementation(() => {
247
+ throw new Error('API unavailable');
248
+ });
249
+ const result = resolveHookWorkspaceDir(
250
+ {},
251
+ api as any,
252
+ 'test_hook',
253
+ { canonicalResolver: noCanonical, explicitPdResolver: noCanonical },
254
+ );
255
+ expect(result.ok).toBe(false);
256
+ if (!result.ok) {
257
+ expect(result.reason).toBe('workspace_dir_unresolvable');
258
+ expect(result.nextAction).toContain('PD_WORKSPACE_DIR');
259
+ }
260
+ });
261
+ });
262
+
263
+ describe('resolveToolHookWorkspaceDirSafe (backward compat)', () => {
264
+ const originalEnv = { ...process.env };
265
+ const logger = {
266
+ error: vi.fn(),
267
+ warn: vi.fn(),
268
+ info: vi.fn(),
269
+ debug: vi.fn(),
270
+ };
271
+
272
+ const api = {
273
+ runtime: {
274
+ agent: {
275
+ resolveAgentWorkspaceDir: vi.fn(),
276
+ },
277
+ },
278
+ config: {},
279
+ logger,
280
+ };
281
+
282
+ beforeEach(() => {
283
+ process.env = { ...originalEnv };
284
+ delete process.env.PD_WORKSPACE_DIR;
285
+ delete process.env.OPENCLAW_WORKSPACE;
286
+ vi.clearAllMocks();
287
+ ensureDir(validWorkspace);
288
+ });
289
+
290
+ afterEach(() => {
291
+ process.env = { ...originalEnv };
292
+ });
293
+
294
+ it('returns string when PD_WORKSPACE_DIR resolves', () => {
295
+ process.env.PD_WORKSPACE_DIR = validWorkspace;
296
+ const result = resolveToolHookWorkspaceDirSafe({}, api as any, 'test');
297
+ expect(result).toBe(path.resolve(validWorkspace));
298
+ });
299
+
300
+ it('logs consistency warning when PD canonical differs from context', () => {
301
+ const altWorkspace = path.join(os.tmpdir(), 'pd-test-workspace-alt-3');
302
+ ensureDir(altWorkspace);
303
+ process.env.PD_WORKSPACE_DIR = validWorkspace;
304
+ const result = resolveToolHookWorkspaceDirSafe(
305
+ { workspaceDir: altWorkspace },
306
+ api as any,
307
+ 'test',
308
+ );
309
+ expect(result).toBe(path.resolve(validWorkspace));
310
+ expect(logger.warn).toHaveBeenCalledWith(
311
+ expect.stringContaining('differs from OpenClaw context'),
312
+ );
313
+ });
314
+
315
+ it('returns pd_default when only default fallback is available', () => {
316
+ // When no explicit PD source, no ctx.workspaceDir, and no API resolution,
317
+ // resolveToolHookWorkspaceDirSafe falls back to pd_default
318
+ api.runtime.agent.resolveAgentWorkspaceDir.mockReturnValue(homeDir);
319
+ const result = resolveToolHookWorkspaceDirSafe(
320
+ {},
321
+ api as any,
322
+ 'test',
323
+ );
324
+ // pd_default (~/.openclaw/workspace) is used as last resort
325
+ expect(result).toBeDefined();
326
+ expect(result).toContain('.openclaw');
327
+ });
328
+
329
+ it('returns undefined and logs when all sources including pd_default fail', () => {
330
+ api.runtime.agent.resolveAgentWorkspaceDir.mockImplementation(() => {
331
+ throw new Error('no workspace');
332
+ });
333
+ const result = resolveToolHookWorkspaceDirSafe(
334
+ {},
335
+ api as any,
336
+ 'test_hook',
337
+ { canonicalResolver: noCanonical, explicitPdResolver: noCanonical },
338
+ );
339
+ expect(result).toBeUndefined();
340
+ expect(logger.warn).toHaveBeenCalled();
341
+ const warnCalls = logger.warn.mock.calls.map((c: unknown[]) => String(c[0]));
342
+ const fullWarn = warnCalls.join('\n');
343
+ expect(fullWarn).toContain('Cannot resolve workspace directory');
344
+ expect(fullWarn).toContain('PD_WORKSPACE_DIR');
345
+ expect(fullWarn).toContain('principles-disciple.json');
346
+ });
347
+ });