/** * 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(); }); });