merge: maestro/tsk_O9q0_u78_NPo [tsk_O9q0_u78_NPo]

This commit is contained in:
maestro
2026-06-13 11:35:20 +08:00
6 changed files with 512 additions and 6 deletions
+183
View File
@@ -0,0 +1,183 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { readTranscript, assertWithinTranscriptDir, TranscriptError } from '../src/executor/transcript.js';
import { Store } from '../src/store/index.js';
import { buildServer } from '../src/api/server.js';
/** 临时数据目录 + transcripts 子目录;返回 {dir, transcriptsDir} */
function tmpData(): { dir: string; transcripts: string } {
const dir = mkdtempSync(join(tmpdir(), 'maestro-transcript-'));
const transcripts = join(dir, 'transcripts');
mkdirSync(transcripts, { recursive: true });
return { dir, transcripts };
}
/** 写一行原始 SDK 风格消息到 jsonl */
function jsonl(lines: unknown[]): string {
return lines.map((l) => JSON.stringify(l)).join('\n') + '\n';
}
const SAMPLE = [
{ type: 'system', subtype: 'init', session_id: 's1' },
{ type: 'assistant', session_id: 's1', message: { role: 'assistant', content: [
{ type: 'text', text: '我先看一下文件结构。' },
{ type: 'tool_use', id: 'tu1', name: 'Bash', input: { command: 'ls -la', description: '列目录' } },
] } },
{ type: 'user', session_id: 's1', message: { role: 'user', content: [
{ type: 'tool_result', tool_use_id: 'tu1', content: 'total 4\nfile.txt', is_error: false },
] } },
{ type: 'maestro.model_fallback', from: 'opusX', to: 'sonnetY', reason: 'not_found' },
{ type: 'result', subtype: 'success', is_error: false, result: '完成:改了一行。' },
];
test('readTranscript:样例 jsonl → 结构化消息数组', () => {
const { transcripts } = tmpData();
const ref = join(transcripts, 'run_abc.jsonl');
writeFileSync(ref, jsonl(SAMPLE));
const msgs = readTranscript(ref, transcripts);
const types = msgs.map((m) => m.type);
// system/init 不外传;其余按序结构化
assert.deepEqual(types, ['text', 'tool_use', 'tool_result', 'fallback', 'result']);
const text = msgs[0];
assert.equal(text.role, 'assistant');
assert.equal(text.text, '我先看一下文件结构。');
const toolUse = msgs[1];
assert.equal(toolUse.name, 'Bash');
assert.match(toolUse.input!, /ls -la/);
const toolResult = msgs[2];
assert.match(toolResult.text!, /file\.txt/);
assert.equal(toolResult.isError, false);
const fb = msgs[3];
assert.equal(fb.from, 'opusX');
assert.equal(fb.to, 'sonnetY');
assert.equal(msgs[4].text, '完成:改了一行。');
});
test('脱敏:含 secret/token/env 的工具参数被剔除', () => {
const { transcripts } = tmpData();
const ref = join(transcripts, 'run_secret.jsonl');
writeFileSync(ref, jsonl([
{ type: 'assistant', message: { content: [
{ type: 'tool_use', name: 'Deploy', input: {
host: 'example.com',
apiKey: 'sk-LIVE-should-not-leak',
token: 'ghp_secrettoken',
env: { ANTHROPIC_API_KEY: 'sk-ant-xxx' },
password: 'hunter2',
note: 'safe-to-show',
} },
] } },
]));
const msgs = readTranscript(ref, transcripts);
const input = msgs[0].input!;
assert.doesNotMatch(input, /should-not-leak/);
assert.doesNotMatch(input, /ghp_secrettoken/);
assert.doesNotMatch(input, /sk-ant-xxx/);
assert.doesNotMatch(input, /hunter2/);
assert.match(input, /\[已脱敏\]/);
// 非敏感字段保留
assert.match(input, /example\.com/);
assert.match(input, /safe-to-show/);
});
test('长内容被截断并标记 truncated', () => {
const { transcripts } = tmpData();
const ref = join(transcripts, 'run_long.jsonl');
const big = 'x'.repeat(10_000);
writeFileSync(ref, jsonl([
{ type: 'user', message: { content: [{ type: 'tool_result', content: big }] } },
]));
const msgs = readTranscript(ref, transcripts);
assert.equal(msgs[0].truncated, true);
assert.ok(msgs[0].text!.length < big.length);
});
test('路径穿越被拒绝(invalid_path', () => {
const { transcripts } = tmpData();
assert.throws(
() => assertWithinTranscriptDir(join(transcripts, '..', '..', 'etc', 'passwd'), transcripts),
(e: unknown) => e instanceof TranscriptError && e.code === 'invalid_path',
);
// 合法路径放行
assert.doesNotThrow(() => assertWithinTranscriptDir(join(transcripts, 'run_ok.jsonl'), transcripts));
});
test('坏行容错:非法 JSON 行被跳过,不影响其余解析', () => {
const { transcripts } = tmpData();
const ref = join(transcripts, 'run_bad.jsonl');
writeFileSync(ref, 'not-json\n' + JSON.stringify({ type: 'result', result: 'ok' }) + '\n\n');
const msgs = readTranscript(ref, transcripts);
assert.equal(msgs.length, 1);
assert.equal(msgs[0].text, 'ok');
});
// ── 端点测试(Fastify inject)──
test('GET /api/runs/:id/transcript:合法 run → 结构化消息', async () => {
const { dir, transcripts } = tmpData();
process.env.MAESTRO_DATA_DIR = dir; // transcriptDir() 在调用时读取
const store = new Store(':memory:');
const p = store.createProject({ name: 't', repoPath: '/tmp/t-' + Math.random() });
const task = store.createTask({ projectId: p.id, title: 'demo', complexity: 'easy' });
const run = store.startRun(task.id, 'executor');
const ref = join(transcripts, run.id + '.jsonl');
writeFileSync(ref, jsonl(SAMPLE));
store.finishRun(run.id, 'succeeded', { transcriptRef: ref });
const app = buildServer({ store });
const res = await app.inject({ method: 'GET', url: `/api/runs/${run.id}/transcript` });
assert.equal(res.statusCode, 200);
const body = res.json();
assert.equal(body.runId, run.id);
assert.equal(body.kind, 'executor');
assert.ok(Array.isArray(body.messages));
assert.equal(body.messages.length, 5);
await app.close();
store.close();
});
test('GET /api/runs/:id/transcript:未知 runId → 404', async () => {
const store = new Store(':memory:');
const app = buildServer({ store });
const res = await app.inject({ method: 'GET', url: '/api/runs/run_nope/transcript' });
assert.equal(res.statusCode, 404);
await app.close();
store.close();
});
test('GET /api/runs/:id/transcript:无转录记录 → 404', async () => {
const store = new Store(':memory:');
const p = store.createProject({ name: 't', repoPath: '/tmp/t-' + Math.random() });
const task = store.createTask({ projectId: p.id, title: 'demo', complexity: 'easy' });
const run = store.startRun(task.id, 'executor'); // 未 finishRuntranscriptRef = null
const app = buildServer({ store });
const res = await app.inject({ method: 'GET', url: `/api/runs/${run.id}/transcript` });
assert.equal(res.statusCode, 404);
await app.close();
store.close();
});
test('GET /api/runs/:id/transcript:越界路径 → 400', async () => {
const { dir } = tmpData();
process.env.MAESTRO_DATA_DIR = dir;
const store = new Store(':memory:');
const p = store.createProject({ name: 't', repoPath: '/tmp/t-' + Math.random() });
const task = store.createTask({ projectId: p.id, title: 'demo', complexity: 'easy' });
const run = store.startRun(task.id, 'executor');
// 指向 transcriptDir 之外(穿越)
store.finishRun(run.id, 'failed', { transcriptRef: '/etc/passwd' });
const app = buildServer({ store });
const res = await app.inject({ method: 'GET', url: `/api/runs/${run.id}/transcript` });
assert.equal(res.statusCode, 400);
await app.close();
store.close();
});