merge: maestro/tsk_zacmwH3chZpG [tsk_zacmwH3chZpG]
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { buildClassifyPrompt, parseClassification, type ClassifierFn } from '../src/executor/classify.js';
|
||||
import { Store } from '../src/store/index.js';
|
||||
import { buildServer } from '../src/api/server.js';
|
||||
import type { Event } from '../src/model/types.js';
|
||||
|
||||
function freshStore(): Store {
|
||||
return new Store(':memory:');
|
||||
}
|
||||
|
||||
// ---------- parseClassification(映射 + 兜底) ----------
|
||||
|
||||
test('parseClassification:三档正常映射 + 取理由', () => {
|
||||
for (const c of ['hard', 'medium', 'easy'] as const) {
|
||||
const r = parseClassification(`COMPLEXITY: ${c}\nREASON: 因为如此`);
|
||||
assert.equal(r.complexity, c);
|
||||
assert.equal(r.reason, '因为如此');
|
||||
}
|
||||
});
|
||||
|
||||
test('parseClassification:大小写/空白宽容', () => {
|
||||
const r = parseClassification(' complexity: HARD \nreason: 跨模块 ');
|
||||
assert.equal(r.complexity, 'hard');
|
||||
assert.equal(r.reason, '跨模块');
|
||||
});
|
||||
|
||||
test('parseClassification:多个 COMPLEXITY 取最后一个', () => {
|
||||
const r = parseClassification('COMPLEXITY: easy\n改判\nCOMPLEXITY: hard\nREASON: 复查后升级');
|
||||
assert.equal(r.complexity, 'hard');
|
||||
});
|
||||
|
||||
test('parseClassification:噪声/非法 → 兜底 medium', () => {
|
||||
for (const noise of ['', '我也不知道', 'COMPLEXITY: trivial\nREASON: 乱写', '随便聊聊']) {
|
||||
const r = parseClassification(noise);
|
||||
assert.equal(r.complexity, 'medium', `输入「${noise}」应兜底 medium`);
|
||||
assert.ok(r.reason.length > 0);
|
||||
}
|
||||
});
|
||||
|
||||
test('parseClassification:有档无理由 → 给占位理由', () => {
|
||||
const r = parseClassification('COMPLEXITY: easy');
|
||||
assert.equal(r.complexity, 'easy');
|
||||
assert.ok(r.reason.includes('未给出'));
|
||||
});
|
||||
|
||||
// ---------- buildClassifyPrompt ----------
|
||||
|
||||
test('buildClassifyPrompt:含三档判据 + 标题/说明 + 输出格式', () => {
|
||||
const p = buildClassifyPrompt('改个文案', '把按钮文字改一下');
|
||||
assert.match(p, /easy/);
|
||||
assert.match(p, /medium/);
|
||||
assert.match(p, /hard/);
|
||||
assert.match(p, /改个文案/);
|
||||
assert.match(p, /把按钮文字改一下/);
|
||||
assert.match(p, /COMPLEXITY:/);
|
||||
assert.match(p, /REASON:/);
|
||||
});
|
||||
|
||||
test('buildClassifyPrompt:无说明时占位', () => {
|
||||
const p = buildClassifyPrompt('只有标题');
|
||||
assert.match(p, /说明:(无)/);
|
||||
});
|
||||
|
||||
// ---------- Store.applyAutoComplexity(回填) ----------
|
||||
|
||||
test('applyAutoComplexity:medium 占位 → 回填 hard,状态重置 analyzing + 理由入 plan', () => {
|
||||
const s = freshStore();
|
||||
const events: Event[] = [];
|
||||
s.subscribe((e) => events.push(e));
|
||||
const p = s.createProject({ name: 'a', repoPath: '/tmp/a-' + Math.random() });
|
||||
const t = s.createTask({ projectId: p.id, title: 'x', complexity: 'medium' }); // speccing
|
||||
assert.equal(t.status, 'speccing');
|
||||
|
||||
const u = s.applyAutoComplexity(t.id, 'hard', '需要跨模块拆解');
|
||||
assert.equal(u.complexity, 'hard');
|
||||
assert.equal(u.status, 'analyzing');
|
||||
assert.match(u.plan ?? '', /需要跨模块拆解/);
|
||||
// 广播了带理由的 task.updated + status.changed
|
||||
const upd = events.filter((e) => e.type === 'task.updated' && e.payload.auto === 'classify').at(-1);
|
||||
assert.equal(upd?.payload.complexity, 'hard');
|
||||
assert.equal(upd?.payload.reason, '需要跨模块拆解');
|
||||
assert.ok(events.some((e) => e.type === 'status.changed' && e.payload.to === 'analyzing'));
|
||||
s.close();
|
||||
});
|
||||
|
||||
test('applyAutoComplexity:回填 easy → 状态重置 ready + 理由入 operations', () => {
|
||||
const s = freshStore();
|
||||
const p = s.createProject({ name: 'b', repoPath: '/tmp/b-' + Math.random() });
|
||||
const t = s.createTask({ projectId: p.id, title: 'y', complexity: 'medium' });
|
||||
const u = s.applyAutoComplexity(t.id, 'easy', '单文件机械改动');
|
||||
assert.equal(u.complexity, 'easy');
|
||||
assert.equal(u.status, 'ready');
|
||||
assert.match(u.operations ?? '', /单文件机械改动/);
|
||||
s.close();
|
||||
});
|
||||
|
||||
test('applyAutoComplexity:判定仍是 medium → 不变状态但记录理由 + 广播', () => {
|
||||
const s = freshStore();
|
||||
const events: Event[] = [];
|
||||
s.subscribe((e) => events.push(e));
|
||||
const p = s.createProject({ name: 'c', repoPath: '/tmp/c-' + Math.random() });
|
||||
const t = s.createTask({ projectId: p.id, title: 'z', complexity: 'medium' });
|
||||
const u = s.applyAutoComplexity(t.id, 'medium', '范围清晰、跨几处');
|
||||
assert.equal(u.complexity, 'medium');
|
||||
assert.equal(u.status, 'speccing'); // 未变
|
||||
assert.match(u.spec ?? '', /范围清晰/);
|
||||
assert.ok(events.some((e) => e.type === 'task.updated' && e.payload.auto === 'classify'));
|
||||
// medium===medium,不应产生 status.changed
|
||||
assert.ok(!events.some((e) => e.type === 'status.changed'));
|
||||
s.close();
|
||||
});
|
||||
|
||||
// ---------- 建任务集成(complexity='auto') ----------
|
||||
|
||||
/** 等待一次满足条件的事件(带超时,避免测试挂死) */
|
||||
function waitFor(store: Store, pred: (e: Event) => boolean, ms = 2000): Promise<Event> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => { off(); reject(new Error('等待事件超时')); }, ms);
|
||||
const off = store.subscribe((e) => {
|
||||
if (pred(e)) { clearTimeout(timer); off(); resolve(e); }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test('POST tasks complexity=auto:先 medium 落库,模型判定后异步回填 hard', async () => {
|
||||
const s = freshStore();
|
||||
const p = s.createProject({ name: 'srv', repoPath: '/tmp/srv-' + Math.random() });
|
||||
const classify: ClassifierFn = async (title) => {
|
||||
assert.equal(title, '拆个大功能');
|
||||
return { complexity: 'hard', reason: '跨子系统、有未知项' };
|
||||
};
|
||||
const app = buildServer({ store: s, classify });
|
||||
|
||||
const backfilled = waitFor(s, (e) => e.type === 'task.updated' && e.payload.auto === 'classify');
|
||||
const res = await app.inject({
|
||||
method: 'POST', url: `/api/projects/${p.id}/tasks`,
|
||||
payload: { title: '拆个大功能', complexity: 'auto' },
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
const created = res.json();
|
||||
assert.equal(created.complexity, 'medium'); // 占位
|
||||
assert.equal(created.status, 'speccing');
|
||||
|
||||
await backfilled;
|
||||
const after = s.getTask(created.id);
|
||||
assert.equal(after?.complexity, 'hard'); // 已回填
|
||||
assert.equal(after?.status, 'analyzing');
|
||||
assert.match(after?.plan ?? '', /跨子系统/);
|
||||
await app.close();
|
||||
s.close();
|
||||
});
|
||||
|
||||
test('POST tasks complexity=auto:模型不可用(兜底 medium)仍建成功且为 medium', async () => {
|
||||
const s = freshStore();
|
||||
const p = s.createProject({ name: 'srv2', repoPath: '/tmp/srv2-' + Math.random() });
|
||||
// 模拟 classifyComplexity 内部兜底:返回 medium
|
||||
const classify: ClassifierFn = async () => ({ complexity: 'medium', reason: '模型不可用,兜底 medium' });
|
||||
const app = buildServer({ store: s, classify });
|
||||
|
||||
const backfilled = waitFor(s, (e) => e.type === 'task.updated' && e.payload.auto === 'classify');
|
||||
const res = await app.inject({
|
||||
method: 'POST', url: `/api/projects/${p.id}/tasks`,
|
||||
payload: { title: '不确定的任务', complexity: 'auto' },
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
assert.equal(res.json().complexity, 'medium');
|
||||
|
||||
await backfilled;
|
||||
const after = s.getTask(res.json().id);
|
||||
assert.equal(after?.complexity, 'medium');
|
||||
assert.match(after?.spec ?? '', /兜底 medium/);
|
||||
await app.close();
|
||||
s.close();
|
||||
});
|
||||
|
||||
test('POST tasks:非法 complexity(非 auto/三档)被拒 400', async () => {
|
||||
const s = freshStore();
|
||||
const p = s.createProject({ name: 'srv3', repoPath: '/tmp/srv3-' + Math.random() });
|
||||
const app = buildServer({ store: s, classify: async () => ({ complexity: 'medium', reason: '' }) });
|
||||
const res = await app.inject({
|
||||
method: 'POST', url: `/api/projects/${p.id}/tasks`,
|
||||
payload: { title: 't', complexity: 'trivial' },
|
||||
});
|
||||
assert.equal(res.statusCode, 400);
|
||||
await app.close();
|
||||
s.close();
|
||||
});
|
||||
Reference in New Issue
Block a user