/** * 46 号文档 Phase 1:`triggerSubtaskPlanning` 写共享 artifacts 区 + 登记 `graph_files` * 索引之后,任务级共享机制(R-19.3,`graph-engine.describeInputs`)要能把它自动递给 * 这个 task 下任意 agent_call 节点的提示词——这是本文档设计的「与 org-planner 的衔接点」 * 真正落地的地方(复用已验证在跑的 R-9.3 任务级共享机制,不是新写一条注入逻辑)。 * * 隔离手法同 `graph-boundary.test.js`/`task-decomposition-materialize.test.js`: * 先设 GRAPH_DB_PATH / GRAPH_RUNS_DIR 再 require,agent 调用全走注入,不起 Claude CLI。 */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import fs from 'fs'; import os from 'os'; import path from 'path'; const TEST_DB_PATH = '/tmp/test-subtask-planning-materialize.sqlite'; const DB_FILES = [TEST_DB_PATH, `${TEST_DB_PATH}-shm`, `${TEST_DB_PATH}-wal`]; const MODULES = [ '../../services/graph-db', '../../services/graph-service', '../../services/org-graph-service', '../../services/work-graph-db', '../../services/graph-run-sse', '../../services/graph-engine', '../../services/subtask-planning', ]; describe('46 号文档 Phase 1:subtask-plan.json 共享区落地 + 任务级共享注入', () => { let wdb; let engine; let subtaskPlanning; let runsDir; beforeAll(() => { for (const f of DB_FILES) { try { fs.unlinkSync(f); } catch {} } process.env.GRAPH_DB_PATH = TEST_DB_PATH; runsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subtask-planning-runs-')); process.env.GRAPH_RUNS_DIR = runsDir; for (const m of MODULES) delete require.cache[require.resolve(m)]; wdb = require('../../services/work-graph-db'); engine = require('../../services/graph-engine'); subtaskPlanning = require('../../services/subtask-planning'); }); afterAll(() => { for (const f of DB_FILES) { try { fs.unlinkSync(f); } catch {} } delete process.env.GRAPH_DB_PATH; try { fs.rmSync(runsDir, { recursive: true, force: true }); } catch {} delete process.env.GRAPH_RUNS_DIR; for (const m of MODULES) delete require.cache[require.resolve(m)]; }); function ensureAgent(id) { const db = wdb.getDb(); const now = new Date().toISOString(); 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', ?, ?)`) .run(id, id, `system/agents/${id}`, now, now); } /** wdb.createRun 只回 {ok, id, nodeRunIds},不是完整行 —— 服务层要的 root_run_id / * company_id 得回读一次(company.js 的 enqueueTaskRun 也是这么用的,同一处坑)。 */ function createRunRow(opts) { const created = wdb.createRun(opts); return wdb.getRun(created.id); } it('triggerSubtaskPlanning 写共享 artifacts/subtask-plan.json + 登记 graph_files', async () => { // setPlanSubtasks 注入替换的是整条 planSubtasks(含 normalizeSteps 那一步), // 单条不合法数据被丢弃/重新编号是 normalizeSteps 自己的职责,已在 // subtask-planning.test.js 单独钉住;这里注入的是"已经规整过"的 steps, // 测的是 triggerSubtaskPlanning 往后那一段(写文件 + 登记索引)。 const companyId = 'co-subplan-1'; const goal = wdb.createGoal({ companyId, title: 'g', description: '贪吃蛇游戏' }); const task = wdb.createTask({ goalId: goal.id, title: '实现贪吃蛇', description: 'd' }); const run = createRunRow({ graphKind: 'work', graphId: 'placeholder', companyId, taskId: task.id }); subtaskPlanning.setPlanSubtasks(async () => ({ steps: [ { seq: 1, title: '定义游戏状态与棋盘', why: '', depends_on_hint: [] }, { seq: 2, title: '实现移动与碰撞检测', why: '', depends_on_hint: [1] }, ], tokens: 42, })); try { await subtaskPlanning.triggerSubtaskPlanning({ run, task }); } finally { subtaskPlanning.setPlanSubtasks(null); } const file = path.join(runsDir, run.root_run_id, 'artifacts', 'subtask-plan.json'); expect(fs.existsSync(file)).toBe(true); const content = JSON.parse(fs.readFileSync(file, 'utf-8')); expect(content.task_id).toBe(task.id); expect(content.steps).toHaveLength(2); expect(content.steps[0].title).toBe('定义游戏状态与棋盘'); expect(content.steps[1].depends_on_hint).toEqual([1]); const rows = wdb.findFiles({ companyId, taskId: task.id, kinds: ['subtask_plan'] }); expect(rows).toHaveLength(1); expect(rows[0].rel_path).toContain('subtask-plan.json'); }); it('这个 task 下任意 agent_call 节点的提示词都能自动看到 subtask_plan(任务级共享,不受相邻性限制)', async () => { const companyId = 'co-subplan-2'; ensureAgent('impl'); const goal = wdb.createGoal({ companyId, title: 'g', description: '贪吃蛇游戏' }); const task = wdb.createTask({ goalId: goal.id, title: '实现贪吃蛇', description: 'd' }); // 先起一条"入队时的" run 触发 subtask-planning(模拟 enqueueTaskRun 的时序) const enqueueRun = createRunRow({ graphKind: 'work', graphId: 'placeholder', companyId, taskId: task.id }); subtaskPlanning.setPlanSubtasks(async () => ({ steps: [{ title: '定义游戏状态' }], tokens: 10, })); try { await subtaskPlanning.triggerSubtaskPlanning({ run: enqueueRun, task }); } finally { subtaskPlanning.setPlanSubtasks(null); } // 真正跑图的是另一条 run(同一个 task,不同 run —— 与真实场景一致:enqueueTaskRun 建的 // 那条 run 才是真正被执行的图,这里只是拆开两步验证互不依赖时序)。 const g = wdb.createWorkGraph({ companyId, nodes: [{ nodeName: 'impl', systemAgentId: 'impl' }], edges: [], }); expect(g.errors || []).toEqual([]); const execRun = createRunRow({ graphKind: 'work', graphId: g.id, companyId, taskId: task.id }); engine.markRunReady(execRun.id); let captured = null; engine.setAgentExecutor(async (ctx) => { captured = ctx.task; return { status: 'success' }; }); try { engine.startRun(execRun.id, { mode: 'manual' }); await engine.advanceOne(execRun.id); } finally { engine.setAgentExecutor(null); } expect(captured).toContain('[上游输入'); expect(captured).toContain('subtask_plan(任务级共享)'); expect(captured).toContain(path.join(runsDir, enqueueRun.root_run_id, 'artifacts', 'subtask-plan.json')); }); it('planSubtasks 产出 0 条步骤时不写文件、不报错(软失败,不影响调用方)', async () => { const companyId = 'co-subplan-3'; const goal = wdb.createGoal({ companyId, title: 'g', description: 'd' }); const task = wdb.createTask({ goalId: goal.id, title: 't', description: 'd' }); const run = createRunRow({ graphKind: 'work', graphId: 'placeholder', companyId, taskId: task.id }); subtaskPlanning.setPlanSubtasks(async () => ({ steps: [], tokens: 5 })); try { await expect(subtaskPlanning.triggerSubtaskPlanning({ run, task })).resolves.toBeUndefined(); } finally { subtaskPlanning.setPlanSubtasks(null); } const file = path.join(runsDir, run.root_run_id, 'artifacts', 'subtask-plan.json'); expect(fs.existsSync(file)).toBe(false); }); it('planSubtasks 本身抛错时 triggerSubtaskPlanning 吞掉,不向调用方传播(46 号文档:绝不能拖累入队本身)', async () => { const companyId = 'co-subplan-4'; const goal = wdb.createGoal({ companyId, title: 'g', description: 'd' }); const task = wdb.createTask({ goalId: goal.id, title: 't', description: 'd' }); const run = createRunRow({ graphKind: 'work', graphId: 'placeholder', companyId, taskId: task.id }); subtaskPlanning.setPlanSubtasks(async () => { throw new Error('CLI 起不来'); }); try { await expect(subtaskPlanning.triggerSubtaskPlanning({ run, task })).resolves.toBeUndefined(); } finally { subtaskPlanning.setPlanSubtasks(null); } }); });