principles-disciple 1.32.0 → 1.34.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/openclaw.plugin.json +4 -4
- package/package.json +1 -1
- package/src/core/correction-cue-learner.ts +203 -0
- package/src/core/correction-types.ts +88 -0
- package/src/core/evolution-logger.ts +3 -3
- package/src/core/init.ts +67 -0
- package/src/service/correction-observer-types.ts +58 -0
- package/src/service/correction-observer-workflow-manager.ts +218 -0
- package/src/service/evolution-worker.ts +172 -146
- package/src/service/nocturnal-service.ts +4 -1
- package/src/service/subagent-workflow/index.ts +14 -0
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +3 -1
- package/tests/service/evolution-worker.nocturnal.test.ts +14 -1
- package/tests/service/evolution-worker.timeout.test.ts +350 -0
- package/tests/commands/implementation-lifecycle.test.ts +0 -362
- package/tests/core/detection-funnel.test.ts +0 -63
- package/tests/core/evolution-e2e.test.ts +0 -58
- package/tests/core/evolution-engine-gate-integration.test.ts +0 -543
- package/tests/core/evolution-engine.test.ts +0 -562
- package/tests/core/evolution-reducer.test.ts +0 -180
- package/tests/core/evolution-user-stories.e2e.test.ts +0 -249
- package/tests/core/local-worker-routing.test.ts +0 -757
- package/tests/core/rule-host.test.ts +0 -389
- package/tests/core/trajectory-correction-pain.test.ts +0 -180
- package/tests/hooks/gate-edit-verification.test.ts +0 -435
- package/tests/hooks/llm.test.ts +0 -308
- package/tests/hooks/progressive-trust-gate.test.ts +0 -277
- package/tests/hooks/prompt.test.ts +0 -1473
- package/tests/index.integration.test.ts +0 -179
- package/tests/index.shadow-routing.integration.test.ts +0 -140
- package/tests/service/evolution-worker.test.ts +0 -462
- package/tests/service/nocturnal-service.test.ts +0 -577
- package/tests/service/nocturnal-workflow-manager.test.ts +0 -441
- package/tests/tools/critique-prompt.test.ts +0 -260
- package/tests/tools/deep-reflect.test.ts +0 -232
- package/tests/tools/model-index.test.ts +0 -246
- package/tests/ui/app.test.tsx +0 -114
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import * as os from 'os';
|
|
5
|
-
import { createDeepReflectTool, deepReflectTool } from '../../src/tools/deep-reflect.js';
|
|
6
|
-
import { EventLogService } from '../../src/core/event-log.js';
|
|
7
|
-
import type { OpenClawPluginApi, PluginRuntime } from '../../src/openclaw-sdk.js';
|
|
8
|
-
|
|
9
|
-
vi.mock('../../src/service/subagent-workflow/deep-reflect-workflow-manager.js', () => ({
|
|
10
|
-
DeepReflectWorkflowManager: vi.fn().mockImplementation(function() {
|
|
11
|
-
return globalThis.__DR_MOCK_MANAGER;
|
|
12
|
-
}),
|
|
13
|
-
deepReflectWorkflowSpec: { workflowType: 'deep_reflect' },
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
vi.mock('../../src/core/config.js', () => ({
|
|
17
|
-
loadConfig: vi.fn(() => ({ mode: 'enabled', enabled: true, timeout_ms: 300 })),
|
|
18
|
-
}));
|
|
19
|
-
|
|
20
|
-
vi.mock('../../src/core/paths.js', () => ({
|
|
21
|
-
resolvePdPath: vi.fn((_dir: string, key: string) => {
|
|
22
|
-
const base = globalThis.__TEST_TEMP_DIR || '/tmp';
|
|
23
|
-
if (key === 'STATE_DIR') return path.join(base, '.state');
|
|
24
|
-
if (key === 'REFLECTION_LOG') return path.join(base, 'memory', 'REFLECTION_LOG.md');
|
|
25
|
-
if (key === 'PAIN_SETTINGS') return path.join(base, '.principles', 'PAIN_SETTINGS.json');
|
|
26
|
-
return path.join(base, key);
|
|
27
|
-
}),
|
|
28
|
-
}));
|
|
29
|
-
|
|
30
|
-
declare global {
|
|
31
|
-
var __TEST_TEMP_DIR: string | undefined;
|
|
32
|
-
var __DR_MOCK_MANAGER: any;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
describe('createDeepReflectTool (workflow helper path)', () => {
|
|
36
|
-
let mockApi: OpenClawPluginApi;
|
|
37
|
-
let tempDir: string;
|
|
38
|
-
|
|
39
|
-
beforeEach(() => {
|
|
40
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'deep-reflect-test-'));
|
|
41
|
-
globalThis.__TEST_TEMP_DIR = tempDir;
|
|
42
|
-
|
|
43
|
-
fs.mkdirSync(path.join(tempDir, '.state', 'logs'), { recursive: true });
|
|
44
|
-
fs.mkdirSync(path.join(tempDir, 'memory'), { recursive: true });
|
|
45
|
-
|
|
46
|
-
const mockSubagent = {
|
|
47
|
-
run: vi.fn().mockResolvedValue({ runId: 'test-run-123' }),
|
|
48
|
-
waitForRun: vi.fn().mockResolvedValue({ status: 'ok' }),
|
|
49
|
-
getSessionMessages: vi.fn().mockResolvedValue({ messages: [], assistantTexts: [] }),
|
|
50
|
-
deleteSession: vi.fn().mockResolvedValue(undefined),
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
mockApi = {
|
|
54
|
-
id: "test-plugin",
|
|
55
|
-
name: "Test Plugin",
|
|
56
|
-
source: "local",
|
|
57
|
-
workspaceDir: tempDir,
|
|
58
|
-
config: { workspaceDir: tempDir },
|
|
59
|
-
runtime: { subagent: mockSubagent } as unknown as PluginRuntime,
|
|
60
|
-
logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() },
|
|
61
|
-
registerTool: vi.fn(),
|
|
62
|
-
registerHook: vi.fn(),
|
|
63
|
-
registerHttpRoute: vi.fn(),
|
|
64
|
-
registerService: vi.fn(),
|
|
65
|
-
registerCommand: vi.fn(),
|
|
66
|
-
resolvePath: vi.fn((p: string) => path.join(tempDir, p)),
|
|
67
|
-
on: vi.fn(),
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
globalThis.__DR_MOCK_MANAGER = {
|
|
71
|
-
startWorkflow: vi.fn().mockResolvedValue({
|
|
72
|
-
workflowId: 'wf-test-123',
|
|
73
|
-
childSessionKey: 'agent:main:subagent:workflow-test-123',
|
|
74
|
-
state: 'active',
|
|
75
|
-
}),
|
|
76
|
-
dispose: vi.fn(),
|
|
77
|
-
store: {
|
|
78
|
-
getWorkflow: vi.fn().mockReturnValue({
|
|
79
|
-
workflow_id: 'wf-test-123',
|
|
80
|
-
state: 'completed',
|
|
81
|
-
child_session_key: 'agent:main:subagent:workflow-test-123',
|
|
82
|
-
}),
|
|
83
|
-
},
|
|
84
|
-
};
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
afterEach(() => {
|
|
88
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
89
|
-
globalThis.__TEST_TEMP_DIR = undefined;
|
|
90
|
-
globalThis.__DR_MOCK_MANAGER = undefined;
|
|
91
|
-
vi.clearAllMocks();
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
const executeTool = async (rawParams: Record<string, unknown>) => {
|
|
95
|
-
const tool = createDeepReflectTool(mockApi);
|
|
96
|
-
return tool.execute('test-call-id', rawParams);
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
describe('参数验证', () => {
|
|
100
|
-
it('context 为空时应返回错误', async () => {
|
|
101
|
-
const result = await executeTool({ context: '' });
|
|
102
|
-
expect(result.content[0].text).toContain('必须提供反思上下文');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('workspaceDir 缺失时应返回错误', async () => {
|
|
106
|
-
const noWsApi = {
|
|
107
|
-
...mockApi,
|
|
108
|
-
workspaceDir: undefined,
|
|
109
|
-
config: {},
|
|
110
|
-
resolvePath: vi.fn(() => undefined),
|
|
111
|
-
};
|
|
112
|
-
const tool = createDeepReflectTool(noWsApi as any);
|
|
113
|
-
const result = await tool.execute('test-call-id', { context: 'test' });
|
|
114
|
-
// effectiveWorkspaceDir resolves to undefined, tool should fail gracefully
|
|
115
|
-
expect(result.content[0].text).toMatch(/失败|required|Workspace/i);
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
describe('workflow 执行', () => {
|
|
120
|
-
it('应创建 DeepReflectWorkflowManager 并调用 startWorkflow', async () => {
|
|
121
|
-
const { DeepReflectWorkflowManager } = await import('../../src/service/subagent-workflow/deep-reflect-workflow-manager.js');
|
|
122
|
-
const reflectionLogPath = path.join(tempDir, 'memory', 'REFLECTION_LOG.md');
|
|
123
|
-
fs.writeFileSync(reflectionLogPath, `# Reflection Log\n\n### Insights\nTest insight here\n\n`);
|
|
124
|
-
|
|
125
|
-
const result = await executeTool({ context: 'Need to improve caching.' });
|
|
126
|
-
|
|
127
|
-
expect(DeepReflectWorkflowManager).toHaveBeenCalledWith({
|
|
128
|
-
workspaceDir: tempDir,
|
|
129
|
-
logger: mockApi.logger,
|
|
130
|
-
subagent: mockApi.runtime.subagent,
|
|
131
|
-
});
|
|
132
|
-
expect(globalThis.__DR_MOCK_MANAGER.startWorkflow).toHaveBeenCalled();
|
|
133
|
-
expect(result.content[0].text).toContain('Test insight here');
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('应传递正确的 taskInput 给 startWorkflow', async () => {
|
|
137
|
-
const { DeepReflectWorkflowManager } = await import('../../src/service/subagent-workflow/deep-reflect-workflow-manager.js');
|
|
138
|
-
const reflectionLogPath = path.join(tempDir, 'memory', 'REFLECTION_LOG.md');
|
|
139
|
-
fs.writeFileSync(reflectionLogPath, `# Reflection Log\n\n### Insights\nDone\n\n`);
|
|
140
|
-
|
|
141
|
-
await executeTool({ context: 'Test context', depth: 3 });
|
|
142
|
-
|
|
143
|
-
expect(DeepReflectWorkflowManager).toHaveBeenCalledWith(
|
|
144
|
-
expect.objectContaining({ workspaceDir: tempDir })
|
|
145
|
-
);
|
|
146
|
-
const startArgs = globalThis.__DR_MOCK_MANAGER.startWorkflow.mock.calls[0][1];
|
|
147
|
-
expect(startArgs.taskInput).toEqual({
|
|
148
|
-
context: 'Test context',
|
|
149
|
-
depth: 3,
|
|
150
|
-
model_id: undefined,
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
describe('超时处理', () => {
|
|
156
|
-
it('workflow 超时应返回超时提示', async () => {
|
|
157
|
-
// Create PAIN_SETTINGS.json to override default 60s timeout
|
|
158
|
-
const settingsPath = path.join(tempDir, '.principles', 'PAIN_SETTINGS.json');
|
|
159
|
-
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
160
|
-
fs.writeFileSync(settingsPath, JSON.stringify({ deep_reflection: { timeout_ms: 300 } }));
|
|
161
|
-
|
|
162
|
-
globalThis.__DR_MOCK_MANAGER.store.getWorkflow.mockReturnValue({
|
|
163
|
-
workflow_id: 'wf-test-123',
|
|
164
|
-
state: 'active',
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
const result = await executeTool({ context: 'Timeout test.' });
|
|
168
|
-
|
|
169
|
-
expect(result.content[0].text).toContain('超时');
|
|
170
|
-
expect(globalThis.__DR_MOCK_MANAGER.dispose).toHaveBeenCalled();
|
|
171
|
-
}, 10000);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
describe('错误处理', () => {
|
|
175
|
-
it('workflow terminal_error 应返回错误信息', async () => {
|
|
176
|
-
globalThis.__DR_MOCK_MANAGER.store.getWorkflow.mockReturnValue({
|
|
177
|
-
workflow_id: 'wf-test-123',
|
|
178
|
-
state: 'terminal_error',
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
const result = await executeTool({ context: 'Error test.' });
|
|
182
|
-
|
|
183
|
-
expect(result.content[0].text).toContain('失败');
|
|
184
|
-
expect(globalThis.__DR_MOCK_MANAGER.dispose).toHaveBeenCalled();
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it('startWorkflow 抛出异常应返回友好错误', async () => {
|
|
188
|
-
globalThis.__DR_MOCK_MANAGER.startWorkflow.mockRejectedValue(new Error('Network error'));
|
|
189
|
-
|
|
190
|
-
const result = await executeTool({ context: 'Crash test.' });
|
|
191
|
-
|
|
192
|
-
expect(result.content[0].text).toContain('失败');
|
|
193
|
-
expect(result.content[0].text).toContain('Network error');
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
describe('向后兼容:model_id 参数', () => {
|
|
198
|
-
it('当传入 model_id 时,应输出警告日志', async () => {
|
|
199
|
-
const reflectionLogPath = path.join(tempDir, 'memory', 'REFLECTION_LOG.md');
|
|
200
|
-
fs.writeFileSync(reflectionLogPath, `# Reflection Log\n\n### Insights\nDone\n\n`);
|
|
201
|
-
|
|
202
|
-
await executeTool({ model_id: 'T-01', context: 'Test.' });
|
|
203
|
-
|
|
204
|
-
expect(mockApi.logger!.warn).toHaveBeenCalledWith(
|
|
205
|
-
expect.stringContaining('deprecated')
|
|
206
|
-
);
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
describe('工具参数定义', () => {
|
|
211
|
-
it('model_id 应为可选参数', () => {
|
|
212
|
-
const schema = deepReflectTool.parameters;
|
|
213
|
-
const hasModelId = 'model_id' in schema.properties;
|
|
214
|
-
if (hasModelId) {
|
|
215
|
-
const required = (schema as any).required || [];
|
|
216
|
-
expect(required).not.toContain('model_id');
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it('context 应为必需参数', () => {
|
|
221
|
-
const schema = deepReflectTool.parameters;
|
|
222
|
-
const required = (schema as any).required || [];
|
|
223
|
-
expect(required).toContain('context');
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it('depth 应为可选参数', () => {
|
|
227
|
-
const schema = deepReflectTool.parameters;
|
|
228
|
-
const required = (schema as any).required || [];
|
|
229
|
-
expect(required).not.toContain('depth');
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
});
|
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import * as os from 'os';
|
|
5
|
-
|
|
6
|
-
// 导入待测试的函数
|
|
7
|
-
import { loadModelIndex } from '../../src/tools/model-index.js';
|
|
8
|
-
import type { OpenClawPluginApi } from '../../src/openclaw-sdk.js';
|
|
9
|
-
|
|
10
|
-
describe('loadModelIndex', () => {
|
|
11
|
-
let tempDir: string;
|
|
12
|
-
let mockApi: OpenClawPluginApi;
|
|
13
|
-
|
|
14
|
-
beforeEach(() => {
|
|
15
|
-
// 创建临时目录
|
|
16
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'model-index-test-'));
|
|
17
|
-
|
|
18
|
-
mockApi = {
|
|
19
|
-
id: "test-plugin",
|
|
20
|
-
name: "Test Plugin",
|
|
21
|
-
source: "local",
|
|
22
|
-
config: {},
|
|
23
|
-
runtime: {} as any,
|
|
24
|
-
logger: {
|
|
25
|
-
info: vi.fn(),
|
|
26
|
-
error: vi.fn(),
|
|
27
|
-
warn: vi.fn(),
|
|
28
|
-
debug: vi.fn(),
|
|
29
|
-
},
|
|
30
|
-
registerTool: vi.fn(),
|
|
31
|
-
registerHook: vi.fn(),
|
|
32
|
-
registerHttpRoute: vi.fn(),
|
|
33
|
-
registerChannel: vi.fn(),
|
|
34
|
-
registerGatewayMethod: vi.fn(),
|
|
35
|
-
registerCli: vi.fn(),
|
|
36
|
-
registerService: vi.fn(),
|
|
37
|
-
registerProvider: vi.fn(),
|
|
38
|
-
registerCommand: vi.fn(),
|
|
39
|
-
resolvePath: vi.fn(),
|
|
40
|
-
on: vi.fn(),
|
|
41
|
-
} as unknown as OpenClawPluginApi;
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
afterEach(() => {
|
|
45
|
-
// 清理临时目录
|
|
46
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
describe('边界情况', () => {
|
|
50
|
-
it('当 workspaceDir 为 undefined 时,应返回默认消息', () => {
|
|
51
|
-
const result = loadModelIndex(undefined, mockApi);
|
|
52
|
-
|
|
53
|
-
expect(result).toBe('(暂无扩展思维模型)');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('当 workspaceDir 为空字符串时,应返回默认消息', () => {
|
|
57
|
-
const result = loadModelIndex('', mockApi);
|
|
58
|
-
|
|
59
|
-
expect(result).toBe('(暂无扩展思维模型)');
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe('索引文件存在', () => {
|
|
64
|
-
it('应返回索引文件内容', () => {
|
|
65
|
-
const modelsDir = path.join(tempDir, '.principles', 'models');
|
|
66
|
-
fs.mkdirSync(modelsDir, { recursive: true });
|
|
67
|
-
|
|
68
|
-
const indexContent = `# 扩展思维模型索引
|
|
69
|
-
|
|
70
|
-
| ID | 名称 | 适用场景 |
|
|
71
|
-
|----|------|----------|
|
|
72
|
-
| MARKETING_4P | 营销4P | 营销策略 |
|
|
73
|
-
`;
|
|
74
|
-
fs.writeFileSync(path.join(modelsDir, '_INDEX.md'), indexContent);
|
|
75
|
-
|
|
76
|
-
const result = loadModelIndex(tempDir, mockApi);
|
|
77
|
-
|
|
78
|
-
expect(result).toContain('扩展思维模型索引');
|
|
79
|
-
expect(result).toContain('MARKETING_4P');
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
describe('索引文件不存在', () => {
|
|
84
|
-
it('当 models 目录不存在时,应返回默认消息(无警告)', () => {
|
|
85
|
-
const result = loadModelIndex(tempDir, mockApi);
|
|
86
|
-
|
|
87
|
-
expect(result).toBe('(暂无扩展思维模型)');
|
|
88
|
-
expect(mockApi.logger!.warn).not.toHaveBeenCalled();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('当 models 目录存在但索引文件不存在时,应返回默认消息并输出警告日志', () => {
|
|
92
|
-
const modelsDir = path.join(tempDir, '.principles', 'models');
|
|
93
|
-
fs.mkdirSync(modelsDir, { recursive: true });
|
|
94
|
-
|
|
95
|
-
// 创建一个模型文件,但不创建索引文件
|
|
96
|
-
fs.writeFileSync(path.join(modelsDir, 'marketing_4p.md'), '# 营销4P模型');
|
|
97
|
-
|
|
98
|
-
const result = loadModelIndex(tempDir, mockApi);
|
|
99
|
-
|
|
100
|
-
expect(result).toBe('(暂无扩展思维模型)');
|
|
101
|
-
expect(mockApi.logger!.warn).toHaveBeenCalledWith(
|
|
102
|
-
expect.stringContaining('_INDEX.md not found')
|
|
103
|
-
);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe('文件大小限制', () => {
|
|
108
|
-
it('当索引文件超过 50KB 时,应返回默认消息并输出警告日志', () => {
|
|
109
|
-
const modelsDir = path.join(tempDir, '.principles', 'models');
|
|
110
|
-
fs.mkdirSync(modelsDir, { recursive: true });
|
|
111
|
-
|
|
112
|
-
// 创建一个超过 50KB 的文件
|
|
113
|
-
const largeContent = 'x'.repeat(51 * 1024);
|
|
114
|
-
fs.writeFileSync(path.join(modelsDir, '_INDEX.md'), largeContent);
|
|
115
|
-
|
|
116
|
-
const result = loadModelIndex(tempDir, mockApi);
|
|
117
|
-
|
|
118
|
-
expect(result).toBe('(暂无扩展思维模型)');
|
|
119
|
-
expect(mockApi.logger!.warn).toHaveBeenCalledWith(
|
|
120
|
-
expect.stringContaining('Index file too large')
|
|
121
|
-
);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('当索引文件刚好 50KB 时,应正常返回内容', () => {
|
|
125
|
-
const modelsDir = path.join(tempDir, '.principles', 'models');
|
|
126
|
-
fs.mkdirSync(modelsDir, { recursive: true });
|
|
127
|
-
|
|
128
|
-
// 创建一个刚好 50KB 的文件
|
|
129
|
-
const exactContent = 'x'.repeat(50 * 1024);
|
|
130
|
-
fs.writeFileSync(path.join(modelsDir, '_INDEX.md'), exactContent);
|
|
131
|
-
|
|
132
|
-
const result = loadModelIndex(tempDir, mockApi);
|
|
133
|
-
|
|
134
|
-
expect(result).toBe(exactContent);
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
describe('错误处理', () => {
|
|
139
|
-
it('当读取文件失败时,应返回默认消息并记录错误日志', () => {
|
|
140
|
-
const modelsDir = path.join(tempDir, '.principles', 'models');
|
|
141
|
-
fs.mkdirSync(modelsDir, { recursive: true });
|
|
142
|
-
|
|
143
|
-
const indexPath = path.join(modelsDir, '_INDEX.md');
|
|
144
|
-
|
|
145
|
-
// 创建一个目录而非文件,导致读取失败
|
|
146
|
-
fs.mkdirSync(indexPath, { recursive: true });
|
|
147
|
-
|
|
148
|
-
const result = loadModelIndex(tempDir, mockApi);
|
|
149
|
-
|
|
150
|
-
expect(result).toBe('(暂无扩展思维模型)');
|
|
151
|
-
expect(mockApi.logger!.warn).toHaveBeenCalledWith(
|
|
152
|
-
expect.stringContaining('Failed to load model index')
|
|
153
|
-
);
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
describe('api 参数可选', () => {
|
|
158
|
-
it('当 api 为 undefined 时,不应抛出错误', () => {
|
|
159
|
-
const result = loadModelIndex(tempDir);
|
|
160
|
-
|
|
161
|
-
expect(result).toBe('(暂无扩展思维模型)');
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
describe('自定义路径支持', () => {
|
|
166
|
-
it('应从配置文件读取自定义 modelsDir', () => {
|
|
167
|
-
const stateDir = path.join(tempDir, '.state');
|
|
168
|
-
fs.mkdirSync(stateDir, { recursive: true });
|
|
169
|
-
|
|
170
|
-
const config = {
|
|
171
|
-
deep_reflection: {
|
|
172
|
-
modelsDir: 'custom-models'
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
fs.writeFileSync(path.join(stateDir, 'pain_settings.json'), JSON.stringify(config));
|
|
176
|
-
|
|
177
|
-
const customModelsDir = path.join(tempDir, 'custom-models');
|
|
178
|
-
fs.mkdirSync(customModelsDir, { recursive: true });
|
|
179
|
-
|
|
180
|
-
const indexContent = `# 自定义模型索引\n|CUSTOM|自定义模型|测试|`;
|
|
181
|
-
fs.writeFileSync(path.join(customModelsDir, '_INDEX.md'), indexContent);
|
|
182
|
-
|
|
183
|
-
const result = loadModelIndex(tempDir, mockApi);
|
|
184
|
-
|
|
185
|
-
expect(result).toContain('自定义模型');
|
|
186
|
-
expect(mockApi.logger!.debug).toHaveBeenCalledWith(
|
|
187
|
-
expect.stringContaining('custom-models')
|
|
188
|
-
);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it('支持绝对路径的自定义 modelsDir', () => {
|
|
192
|
-
const stateDir = path.join(tempDir, '.state');
|
|
193
|
-
fs.mkdirSync(stateDir, { recursive: true });
|
|
194
|
-
|
|
195
|
-
const absCustomDir = fs.mkdtempSync(path.join(os.tmpdir(), 'abs-models-'));
|
|
196
|
-
|
|
197
|
-
const config = {
|
|
198
|
-
deep_reflection: {
|
|
199
|
-
modelsDir: absCustomDir
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
fs.writeFileSync(path.join(stateDir, 'pain_settings.json'), JSON.stringify(config));
|
|
203
|
-
|
|
204
|
-
const indexContent = `# 绝对路径模型\n|ABS|绝对路径|测试|`;
|
|
205
|
-
fs.writeFileSync(path.join(absCustomDir, '_INDEX.md'), indexContent);
|
|
206
|
-
|
|
207
|
-
const result = loadModelIndex(tempDir, mockApi);
|
|
208
|
-
|
|
209
|
-
expect(result).toContain('绝对路径');
|
|
210
|
-
|
|
211
|
-
fs.rmSync(absCustomDir, { recursive: true, force: true });
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('当配置无效时应回退到默认路径', () => {
|
|
215
|
-
const stateDir = path.join(tempDir, '.state');
|
|
216
|
-
fs.mkdirSync(stateDir, { recursive: true });
|
|
217
|
-
|
|
218
|
-
const config = {
|
|
219
|
-
deep_reflection: {
|
|
220
|
-
modelsDir: 123
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
fs.writeFileSync(path.join(stateDir, 'pain_settings.json'), JSON.stringify(config));
|
|
224
|
-
|
|
225
|
-
const result = loadModelIndex(tempDir, mockApi);
|
|
226
|
-
|
|
227
|
-
expect(result).toBe('(暂无扩展思维模型)');
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it('当自定义路径不存在时应返回默认消息', () => {
|
|
231
|
-
const stateDir = path.join(tempDir, '.state');
|
|
232
|
-
fs.mkdirSync(stateDir, { recursive: true });
|
|
233
|
-
|
|
234
|
-
const config = {
|
|
235
|
-
deep_reflection: {
|
|
236
|
-
modelsDir: 'non-existent-path'
|
|
237
|
-
}
|
|
238
|
-
};
|
|
239
|
-
fs.writeFileSync(path.join(stateDir, 'pain_settings.json'), JSON.stringify(config));
|
|
240
|
-
|
|
241
|
-
const result = loadModelIndex(tempDir, mockApi);
|
|
242
|
-
|
|
243
|
-
expect(result).toBe('(暂无扩展思维模型)');
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
});
|
package/tests/ui/app.test.tsx
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
|
|
3
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
-
import { cleanup, render, screen, waitFor, fireEvent } from '@testing-library/react';
|
|
5
|
-
import React from 'react';
|
|
6
|
-
import { App } from '../../ui/src/App';
|
|
7
|
-
|
|
8
|
-
// Mock localStorage
|
|
9
|
-
const localStorageMock = (() => {
|
|
10
|
-
let store: Record<string, string> = {};
|
|
11
|
-
return {
|
|
12
|
-
getItem: (key: string) => store[key] || null,
|
|
13
|
-
setItem: (key: string, value: string) => { store[key] = value; },
|
|
14
|
-
removeItem: (key: string) => { delete store[key]; },
|
|
15
|
-
clear: () => { store = {}; },
|
|
16
|
-
};
|
|
17
|
-
})();
|
|
18
|
-
|
|
19
|
-
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
|
|
20
|
-
|
|
21
|
-
describe('Principles Console App', () => {
|
|
22
|
-
beforeEach(() => {
|
|
23
|
-
localStorageMock.clear();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
afterEach(() => {
|
|
27
|
-
cleanup();
|
|
28
|
-
vi.unstubAllGlobals();
|
|
29
|
-
window.history.replaceState({}, '', '/');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('shows login page when not authenticated', async () => {
|
|
33
|
-
window.history.replaceState({}, '', '/plugins/principles/');
|
|
34
|
-
vi.stubGlobal('fetch', vi.fn((input: RequestInfo | URL) => {
|
|
35
|
-
return Promise.resolve(new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 }));
|
|
36
|
-
}));
|
|
37
|
-
|
|
38
|
-
render(<App />);
|
|
39
|
-
|
|
40
|
-
await waitFor(() => {
|
|
41
|
-
expect(screen.getByText('Principles Console')).toBeTruthy();
|
|
42
|
-
expect(screen.getByText('AI Agent 进化流程监控平台')).toBeTruthy();
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('renders the overview page after successful login', async () => {
|
|
47
|
-
window.history.replaceState({}, '', '/plugins/principles/overview');
|
|
48
|
-
let overviewCallCount = 0;
|
|
49
|
-
|
|
50
|
-
vi.stubGlobal('fetch', vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
|
|
51
|
-
const url = String(input);
|
|
52
|
-
|
|
53
|
-
if (url.includes('/api/central/overview') || url.includes('/api/overview')) {
|
|
54
|
-
overviewCallCount++;
|
|
55
|
-
const hasAuth = init?.headers && JSON.stringify(init.headers).includes('Bearer');
|
|
56
|
-
|
|
57
|
-
if (hasAuth || overviewCallCount > 1) {
|
|
58
|
-
return Promise.resolve(new Response(JSON.stringify({
|
|
59
|
-
workspaceDir: '/workspace',
|
|
60
|
-
generatedAt: '2026-03-19T10:00:00.000Z',
|
|
61
|
-
dataFreshness: '2026-03-19T10:00:00.000Z',
|
|
62
|
-
summary: {
|
|
63
|
-
repeatErrorRate: 0.2,
|
|
64
|
-
userCorrectionRate: 0.1,
|
|
65
|
-
pendingSamples: 2,
|
|
66
|
-
approvedSamples: 1,
|
|
67
|
-
thinkingCoverageRate: 0.4,
|
|
68
|
-
painEvents: 3,
|
|
69
|
-
principleEventCount: 5,
|
|
70
|
-
},
|
|
71
|
-
dailyTrend: [],
|
|
72
|
-
topRegressions: [],
|
|
73
|
-
sampleQueue: { counters: { pending: 2 }, preview: [] },
|
|
74
|
-
thinkingSummary: { activeModels: 2, dormantModels: 7, effectiveModels: 1, coverageRate: 0.4 },
|
|
75
|
-
centralInfo: { workspaceCount: 3, workspaces: ['workspace-main', 'workspace-builder', 'workspace-pm'] },
|
|
76
|
-
}), { status: 200 }));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return Promise.resolve(new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 }));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return Promise.resolve(new Response(JSON.stringify({
|
|
83
|
-
configs: [],
|
|
84
|
-
workspaces: [],
|
|
85
|
-
summary: { totalModels: 0, activeModels: 0, dormantModels: 0, effectiveModels: 0, coverageRate: 0 },
|
|
86
|
-
topModels: [],
|
|
87
|
-
dormantModels: [],
|
|
88
|
-
effectiveModels: [],
|
|
89
|
-
scenarioMatrix: [],
|
|
90
|
-
coverageTrend: [],
|
|
91
|
-
}), { status: 200 }));
|
|
92
|
-
}));
|
|
93
|
-
|
|
94
|
-
render(<App />);
|
|
95
|
-
|
|
96
|
-
// Wait for login page
|
|
97
|
-
await waitFor(() => {
|
|
98
|
-
expect(screen.getByPlaceholderText('请输入您的 Gateway Token')).toBeTruthy();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
// Enter token and submit
|
|
102
|
-
const input = screen.getByPlaceholderText('请输入您的 Gateway Token');
|
|
103
|
-
fireEvent.change(input, { target: { value: 'test-token-123' } });
|
|
104
|
-
|
|
105
|
-
const loginButton = screen.getByRole('button', { name: /登 录/ });
|
|
106
|
-
fireEvent.click(loginButton);
|
|
107
|
-
|
|
108
|
-
// Wait for overview page
|
|
109
|
-
await waitFor(() => {
|
|
110
|
-
expect(screen.getByText('Workspace health and queue pressure')).toBeTruthy();
|
|
111
|
-
expect(screen.getByText('Pending Samples')).toBeTruthy();
|
|
112
|
-
}, { timeout: 5000 });
|
|
113
|
-
});
|
|
114
|
-
});
|