task-entry-draft-mode.test.js
5.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
/**
* task-entry-draft-mode.test.js —— POST /:companyId/tasks 的 draft:true 分支(39 号文档地基 1)
*
* 现有 `POST /:companyId/tasks` 建完任务无条件立刻入队执行("下达即执行")。
* `draft:true` 时只应该落库,不应该 enqueue/start——服务的是 agent(如 ceo)自主提的新任务,
* 不该像 task-planning 一样先经过 draft 就自动花预算。
*
* 隔离手法与 graph-engine.test.js / company-goal-task-routes.test.js 一致:先设
* GRAPH_DB_PATH / GRAPH_RUNS_DIR 再 require,真实的 data/graph.sqlite、data/companies/ 全程不被碰。
*/
const fs = require('fs');
const os = require('os');
const path = require('path');
const TEST_DB_PATH = '/tmp/test-task-entry-draft-mode.sqlite';
const DB_FILES = [TEST_DB_PATH, `${TEST_DB_PATH}-shm`, `${TEST_DB_PATH}-wal`];
const MODULES = [
'../../services/graph-db',
'../../services/work-graph-db',
'../../services/org-graph-service',
'../../services/graph-run-sse',
'../../routes/company',
];
const CID = 'draft-mode-test-co';
let wdb;
let orgSvc;
let server;
let base;
let origCompanyGet;
async function call(method, url, body) {
const r = await fetch(base + url, {
method,
headers: { 'Content-Type': 'application/json' },
body: body === undefined ? undefined : JSON.stringify(body),
});
return [r.status, await r.json()];
}
function ensureAgents(ids) {
const db = wdb.getDb();
const now = new Date().toISOString();
const ins = db.prepare(`
INSERT OR IGNORE INTO system_agents
(id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)
`);
for (const id of new Set(ids)) ins.run(id, id, `system/agents/${id}`, now, now);
}
describe('POST /:companyId/tasks draft 模式(39 号文档地基 1)', () => {
beforeAll(async () => {
for (const f of DB_FILES) { try { fs.unlinkSync(f); } catch { /* 本来就没有 */ } }
process.env.GRAPH_DB_PATH = TEST_DB_PATH;
process.env.GRAPH_RUNS_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'draft-mode-runs-'));
for (const m of MODULES) delete require.cache[require.resolve(m)];
wdb = require('../../services/work-graph-db');
orgSvc = require('../../services/org-graph-service');
const { CompanyService } = require('../../services/company-service');
origCompanyGet = CompanyService.prototype.get;
CompanyService.prototype.get = (companyId) =>
(companyId === CID ? { id: CID, name: CID, status: 'running' } : null);
// 一张最小 org graph:POST /tasks 建行前会先查「公司有没有 org graph」,没有直接 409,
// 这一步不是本文件要测的东西,先把它铺好。
ensureAgents(['org-planner', 'org-coder', 'org-validator']);
expect(
orgSvc.writeGraph(CID, {
nodes: ['org-planner', 'org-coder', 'org-validator'],
edges: [
{ from: 'org-planner', to: 'org-coder', condition: 'success' },
{ from: 'org-coder', to: 'org-validator', condition: 'success' },
],
}).ok
).toBe(true);
const express = require('express');
const app = express();
app.use(express.json());
app.use('/api/company', require('../../routes/company'));
await new Promise((resolve) => { server = app.listen(0, resolve); });
base = `http://127.0.0.1:${server.address().port}/api/company/${CID}`;
});
afterAll(() => {
if (server) server.close();
if (origCompanyGet) require('../../services/company-service').CompanyService.prototype.get = origCompanyGet;
for (const f of DB_FILES) { try { fs.unlinkSync(f); } catch { /* 已经删了 */ } }
try { fs.rmSync(process.env.GRAPH_RUNS_DIR, { recursive: true, force: true }); } catch { /* ignore */ }
delete process.env.GRAPH_DB_PATH;
delete process.env.GRAPH_RUNS_DIR;
for (const m of MODULES) delete require.cache[require.resolve(m)];
});
it('draft:true → 201 + status draft,不落 runId,task 不入队(current_run_id 仍为空)', async () => {
const [status, body] = await call('POST', '/tasks', {
task: '(草稿)先别执行的一条任务',
draft: true,
created_by: 'ceo',
});
expect(status).toBe(201);
expect(body.status).toBe('draft');
expect(body.runId).toBeUndefined();
expect(body.taskId).toBeTruthy();
expect(body.hint).toContain(`/tasks/${body.taskId}/enqueue`);
const row = wdb.getTask(body.taskId);
expect(row.status).toBe('draft');
expect(row.current_run_id).toBeFalsy();
expect(row.created_by).toBe('ceo');
});
it('draft:true 建出的任务,之后走既有 /enqueue 端点能正常下达(不是死路)', async () => {
const [, created] = await call('POST', '/tasks', { task: '(草稿)待下达', draft: true });
const [status, body] = await call('POST', `/tasks/${created.taskId}/enqueue`);
expect(status).toBe(201);
expect(['queued', 'running'].includes(body.status)).toBe(true);
const row = wdb.getTask(created.taskId);
expect(['queued', 'running'].includes(row.status)).toBe(true);
});
it('默认行为不变:不传 draft 时仍然立刻入队/起跑(存量调用方不受影响)', async () => {
const [status, body] = await call('POST', '/tasks', { task: '正常下达一条任务' });
expect(status).toBe(201);
expect(body.status).not.toBe('draft');
expect(body.runId).toBeTruthy();
});
});