Files
maestro/test/memory-inject.test.ts
T
wangjia fe7923a618 feat(agent): L2/L4 记忆注入——项目规范 + 全局规范文件喂进 4 处 prompt
- L2 projects.agent_rules 列(schema+迁移+类型+mapper+patchProject+API 透传)
- L4 protocol.readGlobalAgentRules():worker 直接读 <data>/agent-global.md(非DB,
  不读用户 ~/.claude/CLAUDE.md,避免个人习惯与受控执行冲突)
- runner.rulesHeaderLines 注入 executor/planner/conflict;reviewer 同注入
- 顺序:全局规范(L4)→项目规范(L2)→任务内容→上次失败(L1)→执行约束

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 22:29:29 +08:00

86 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { buildPrompt, buildPlannerPrompt, buildConflictPrompt } from '../src/executor/runner.js';
import { Store } from '../src/store/index.js';
import type { Task, Project } from '../src/model/types.js';
function fakeProject(over: Partial<Project> = {}): Project {
return { defaultBranch: 'main', agentRules: null, ...over } as Project;
}
function fakeTask(over: Partial<Task> = {}): Task {
return {
id: 'tsk_x', title: '改个东西', complexity: 'medium', depth: 1,
operations: '把 A 改成 B', spec: null, plan: null, deps: [],
...over,
} as Task;
}
const SECTION = '上次失败原因';
test('L1: 无 lastRunError 时三处 prompt 都不含「上次失败原因」段', () => {
const t = fakeTask();
assert.ok(!buildPrompt(t).includes(SECTION));
assert.ok(!buildPlannerPrompt(fakeTask({ complexity: 'medium' }), 'spec').includes(SECTION));
assert.ok(!buildPlannerPrompt(fakeTask({ complexity: 'hard' }), 'decompose').includes(SECTION));
assert.ok(!buildConflictPrompt(t, ['a.ts']).includes(SECTION));
});
test('L1: 有 lastRunError 时三处 prompt 注入失败原因', () => {
const err = 'verify 失败:npm test 退出码 1\nFAIL src/foo.test.ts';
const t = fakeTask({ lastRunError: err });
for (const p of [
buildPrompt(t),
buildPlannerPrompt(fakeTask({ complexity: 'medium', lastRunError: err }), 'spec'),
buildPlannerPrompt(fakeTask({ complexity: 'hard', lastRunError: err }), 'decompose'),
buildConflictPrompt(t, ['a.ts']),
]) {
assert.ok(p.includes(SECTION), '应含失败段标题');
assert.ok(p.includes('npm test 退出码 1'), '应含具体错误');
}
});
test('L1: 超长错误被截断到 2000 字符', () => {
const err = 'X'.repeat(5000);
const p = buildPrompt(fakeTask({ lastRunError: err }));
assert.ok(p.includes('已截断'));
assert.ok(!p.includes('X'.repeat(2100)));
});
test('L2: project.agentRules 注入 executor/planner/conflict/,未设则不注入', () => {
const t = fakeTask();
const withRules = fakeProject({ agentRules: '本项目禁止改 schema.sql' });
assert.ok(buildPrompt(t, withRules).includes('项目规范'));
assert.ok(buildPrompt(t, withRules).includes('禁止改 schema.sql'));
assert.ok(buildPlannerPrompt(fakeTask({ complexity: 'hard' }), 'decompose', withRules).includes('禁止改 schema.sql'));
assert.ok(buildConflictPrompt(t, ['a.ts'], withRules).includes('禁止改 schema.sql'));
// 未设规范 → 不出现「项目规范」标题
assert.ok(!buildPrompt(t, fakeProject()).includes('项目规范'));
assert.ok(!buildPrompt(t).includes('项目规范')); // 连 project 都没传
});
test('L4: globalRules 注入且排在项目规范之前', () => {
const t = fakeTask();
const p = buildPrompt(t, fakeProject({ agentRules: 'PROJ_RULE' }), 'GLOBAL_RULE');
assert.ok(p.includes('全局执行规范'));
assert.ok(p.includes('GLOBAL_RULE'));
assert.ok(p.indexOf('GLOBAL_RULE') < p.indexOf('PROJ_RULE'), '全局规范在项目规范之前');
// 全局规范在任务内容之前(注入顺序:L4→L2→任务内容)
assert.ok(p.indexOf('GLOBAL_RULE') < p.indexOf('任务内容'));
});
test('lastRunErrorOf 取最近一次失败 run 的错误(任意 kind', () => {
const s = new Store(':memory:');
const p = s.createProject({ name: 'l1', repoPath: '/tmp/l1-' + Math.random() });
const t = s.createTask({ projectId: p.id, title: 'feat', complexity: 'easy' });
assert.equal(s.lastRunErrorOf(t.id), null, '无失败 run → null');
s.setOperations(t.id, 'do it');
s.transition(t.id, 'queued');
s.transition(t.id, 'executing');
const run = s.startRun(t.id, 'executor', { worktree: '/tmp/w', branch: 'b' });
s.failTaskAttempt(t.id, run.id, '第一次失败:编译错误');
assert.equal(s.lastRunErrorOf(t.id), '第一次失败:编译错误');
s.close();
});