Commit 0ad9af5975cd38c5fef1e5333c6d78fd93149551

Authored by yibo
1 parent 54470be3

refactor(db): 删掉 system_agents.source_kind;legacy_path 留下,写清退出条件

28-followups §3.1。**审计结论与那条待办原来的写法不一样,两列要分开处理。**

## source_kind:删

与 origin 一一映射(global_agent↔builtin / company_hired↔created),
实查生产库 76 行零例外。全仓唯一读侧是 migrate() 里那条 origin 回填,
而那是给「origin 列出现之前写下的行」用的一次性迁移。

顺序:**先回填、再删列**,两步都用 hasColumn 守着(幂等)。反过来做不报错 ——
库能开、测试能过,只是那几行 origin 悄悄是错的。守卫
source-kind-drop-migration.test.js 走存量库重启那条路,专门钉这个顺序,
反向验过:弄反立刻红。

写侧散在 3 处生产 INSERT + 35 个测试文件。批量改的时候正则误伤了 4 处
(吃掉了 JS 默认参数、位置占位符、ON CONFLICT 的赋值行),逐个修回并复查了 diff ——
**这种跨几十个文件的机械改动,靠「全量跑一遍」兜底比靠正则写得对更可靠**。

## legacy_path:不能删,它还没死

原待办说「所有读取点都已改成从 id 推导」——那半句对新行成立,对存量行不成立:
· 新行 legacy_path 恒等于 system/agents/{id}(确实冗余);
· **存量行里存着批次 4 之前的公司侧路径**,而 graph-seed-service 判「悬空行 / 孤儿行」
  靠的正是这个路径指向的目录还在不在。从 id 推导出来的路径永远指向 system/agents/,
  那条判据会整个失效。

实查生产库:76 行里还有 2 行是老路径(org-planner / org-coder 指着已不存在的
data/companies/mycompany/agents/)—— 正是这条判据要认出来的那种行。

退出条件写进了 graph-db.js 的建表注释(不只留在文档里):
库里不再有 legacy_path <> 'system/agents/' || id 的行。

## 顺带记一条:getDb() 会就地迁移生产库

删列那次,生产库的列是被一条**查状态用的**一行 node -e 删掉的 ——
服务当时没重启,人也没打算迁移。结果是对的(幂等;与备份逐行核对 76 行一致、
origin 全部按映射回填正确),但「读一下状态」不该顺手改 schema 这件事值得知道。
闸门只挡测试进程,故意不挡普通脚本(挡了服务自己也起不来)。
注释里给了只读的正确姿势。

全量:140 文件 / 1299 测试全绿。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Showing 40 changed files with 268 additions and 109 deletions
docs/todo/system-design/28-followups.md
... ... @@ -54,10 +54,44 @@
54 54  
55 55 ## 三、未实施的工程项
56 56  
57   -### 3.1 `source_kind` / `legacy_path` 删列(验收 2 后半)
58   -两列仍在库里。`origin` 已经是真相来源,`legacy_path` 的所有**读**取点都已改成从 id 推导路径
59   -(唯二的例外是 `graph-seed-service` 里判断「悬空行」时拿它做存在性检查,那是正当用法)。
60   -删列前需确认没有遗漏的读取方。
  57 +### 3.1 ~~`source_kind`~~ 已删 / `legacy_path` **不能删**(验收 2 后半)
  58 +
  59 +**审计结论与本条原来的写法不一样,两列要分开处理。**
  60 +
  61 +**`source_kind` ✅ 已删(2026-08-20)**:它与 `origin` 是一一映射
  62 +(`global_agent`↔`builtin` / `company_hired`↔`created`),实查生产库 76 行零例外。
  63 +全仓唯一读侧是 `graph-db.migrate()` 里那条 origin 回填,而那是给「`origin` 列出现之前写下的行」
  64 +用的一次性迁移。所以顺序是**先回填、再删列**,两步都用 `hasColumn` 守着(幂等)。
  65 +写侧散在 3 处生产 INSERT + 35 个测试文件,一并清掉。
  66 +守卫 `source-kind-drop-migration.test.js` 走存量库重启那条路,**专门钉住顺序** ——
  67 +反过来做不会报错,只是那几行 origin 悄悄是错的(反向验过,弄反立刻红)。
  68 +
  69 +**`legacy_path` ❌ 不能删,它还没死**。原文说「所有读取点都已改成从 id 推导」——
  70 +那半句对新行成立,对存量行不成立:
  71 +
  72 +- 新写入的行 `legacy_path` 恒等于 `system/agents/{id}`(= 可由 id 推导,确实冗余);
  73 +- **存量行里存着批次 4 之前的公司侧路径**(`data/companies/{cid}/agents/{role}`),
  74 + 而 `graph-seed-service` 判「悬空行 / 孤儿行」正是靠**这个路径指向的目录还在不在**。
  75 + 从 id 推导出来的路径永远指向 `system/agents/`,那条判据会整个失效。
  76 +
  77 +实查生产库(2026-08-20):76 行里**还有 2 行**是老路径(`org-planner` / `org-coder`
  78 +指着已经不存在的 `data/companies/mycompany/agents/`)—— 正是这条判据要认出来的那种行。
  79 +
  80 +> **删它的退出条件**(写在 `graph-db.js` 建表注释里,别只留在这份文档):
  81 +> 库里不再有 `legacy_path <> 'system/agents/' || id` 的行。
  82 +> 到那一天它才真的零信息量。
  83 +
  84 +### 3.1b 顺带记一条:`getDb()` 会就地迁移生产库
  85 +
  86 +`graph-db.getDb()` 内部调 `migrate()`。**任何 require 本模块并调 `getDb()` 的进程都会改生产库的 schema**,
  87 +包括一条随手敲的 `node -e "...getDb()..."`。
  88 +
  89 +删 `source_kind` 那次,生产库的列就是被一条**查状态用的**一行脚本删掉的 ——
  90 +服务当时根本没重启,人也没打算做迁移。结果是对的(迁移幂等,76 行逐行核对与备份一致、
  91 +origin 全部按映射回填正确),但「读一下状态」不该顺手改 schema 这件事本身值得知道。
  92 +
  93 +`assertNotProdDbInTests` 那道闸门**只挡测试进程**,故意不挡普通脚本(挡了服务自己也起不来)。
  94 +要只读地看一眼,用 `new Database('data/graph.sqlite', {readonly: true})`,别走 `getDb()`。
61 95  
62 96 ### 3.2 任务态清理策略(§5.6)
63 97 只有设计,没有实现。任务目录 `system/agents/{aid}/{cid}/{taskId}/` 在源码树里无限增长。
... ...
server/services/graph-db.js
... ... @@ -143,10 +143,20 @@ function migrate(database) {
143 143 // 回填放在 if 外面,且写成「只改需要改的行」:ALTER 与 UPDATE 是两条语句,
144 144 // 中间挂掉的话 company_hired 行会停在默认的 'builtin' 上,下次 migrate 必须能补上。
145 145 // WHERE 里的 origin <> 'created' 让第二遍变成零行更新(幂等)。
146   - database.exec(`
147   - UPDATE system_agents SET origin = 'created'
148   - WHERE source_kind = 'company_hired' AND origin <> 'created'
149   - `);
  146 + //
  147 + // **顺序不能反**:先把 source_kind 的信息回填进 origin,再删列。反过来做的话,
  148 + // 「origin 列出现之前写下的行」永远回不到正确的 origin —— 那正是这条回填存在的理由。
  149 + // 两步都用 hasColumn 守着,所以删过之后第二遍是零操作(幂等)。
  150 + if (hasColumn(database, 'system_agents', 'source_kind')) {
  151 + database.exec(`
  152 + UPDATE system_agents SET origin = 'created'
  153 + WHERE source_kind = 'company_hired' AND origin <> 'created'
  154 + `);
  155 + // 28-followups §3.1:source_kind 与 origin 一一映射,零信息量,读侧只有上面那一条回填。
  156 + // 删列会让所有还带着它的 INSERT 报错,所以这一步必须跑在**任何写入之前** ——
  157 + // migrate() 由 getDb() 在建表之后立刻调用,正是那个位置。
  158 + database.exec(`ALTER TABLE system_agents DROP COLUMN source_kind`);
  159 + }
150 160  
151 161 // 7. 28 批次 4:company_agents 的存量回填。
152 162 backfillCompanyAgents(database);
... ... @@ -229,6 +239,18 @@ function createEdgesTable(database) {
229 239 *
230 240 * 需要读真库的测试显式写 `process.env.GRAPH_DB_PATH = <真库路径>` 表明意图即可。
231 241 */
  242 +/**
  243 + * ⚠️ **`getDb()` 会跑 `migrate()`** —— 也就是说,**任何 require 本模块并调 getDb() 的进程
  244 + * 都会就地修改生产库的 schema**,包括一条随手敲的 `node -e "...getDb()..."`。
  245 + *
  246 + * 这不是假想:2026-08-20 删 `source_kind` 那次,生产库的列是被一条**查状态用的**
  247 + * 一行脚本删掉的 —— 服务当时根本没重启,人也没打算迁移。结果是对的(迁移本身幂等、
  248 + * 76 行逐行核对无误),但「读一下状态」不该顺手改 schema 这件事本身值得知道。
  249 + *
  250 + * 下面这道闸门**只挡测试进程**(避免污染共享数据),故意不挡普通脚本 ——
  251 + * 挡了的话服务自己也起不来。要只读地看一眼,用
  252 + * `new (require('better-sqlite3'))('data/graph.sqlite', {readonly:true})`,别走 getDb()。
  253 + */
232 254 function assertNotProdDbInTests() {
233 255 if (!process.env.VITEST) return;
234 256 if (process.env.GRAPH_DB_PATH) return;
... ... @@ -258,10 +280,19 @@ function getDb() {
258 280 name TEXT NOT NULL,
259 281 scope TEXT NOT NULL CHECK (scope IN ('public','company')),
260 282 owner_company_id TEXT,
261   - -- source_kind / legacy_path 是 28 批次 1 之后的**遗留列**:读侧已全部改走
262   - -- origin + 「路径恒为 system/agents/{id}」的推导,这两列只为存量库和还没改完的
263   - -- 批次(company_agents 表落地在批次 4)留一版兼容,写入侧仍按 origin 一一映射维护。
264   - source_kind TEXT NOT NULL CHECK (source_kind IN ('global_agent','template','company_hired')),
  283 + -- legacy_path 是 28 批次 1 之后的遗留列,但**还没死**(28-followups §3.1)。
  284 + -- 新写入的行它恒等于 system/agents/{id}(= 可由 id 推导,冗余),
  285 + -- 但存量行里存着批次 4 之前的公司侧路径(data/companies/{cid}/agents/{role}),
  286 + -- 而 graph-seed-service 判「悬空行 / 孤儿行」正是靠「这个路径指向的目录还在不在」。
  287 + -- 删它之前的退出条件:库里不再有 legacy_path <> 'system/agents/' || id 的行。
  288 + -- (2026-08-20 实查生产库:76 行里还有 2 行不满足 —— org-planner / org-coder
  289 + -- 指着已经不存在的 data/companies/mycompany/agents/。所以这一版还不能删。)
  290 + --
  291 + -- source_kind 已于 2026-08-20 删除:它与 origin 一一映射
  292 + -- (global_agent 对 builtin / company_hired 对 created),实查生产库 76 行零例外,
  293 + -- 唯一读侧是 migrate() 里那条 origin 回填,而那是给「origin 列出现之前的行」用的一次性迁移。
  294 + -- 注意:本段刻意不用反引号 —— 这里在 JS 模板字符串里面,反引号会把它提前截断
  295 + -- (踩过两次:一次在这张表,一次在 work-graph-db 的 DDL)。
265 296 legacy_path TEXT NOT NULL,
266 297 status TEXT NOT NULL DEFAULT 'active',
267 298 benchmark_score REAL,
... ...
server/services/graph-seed-service.js
... ... @@ -372,7 +372,7 @@ function reconcileDanglingRows() {
372 372 * 返回本体路径,等于在公司毫不知情的情况下换掉它的 agent.md。这个死角由批次 4/5
373 373 * (把私有拷贝毕业成本体)根治,不在这里硬来。
374 374 * - 那行是**悬空的**(isStaleCreatedRow)→ 就地转正成 builtin:scope / owner_company_id /
375   - * origin / source_kind / legacy_path 全部改写。原来的「一律 skip」让污染不可逆:
  375 + * origin / legacy_path 全部改写。原来的「一律 skip」让污染不可逆:
376 376 * 公司目录删了、行还占着本体 id,本体永远补不回来。转正而不是删了重建,
377 377 * 是为了让还引着这行的 org_graph_nodes 外键继续有效。
378 378 *
... ... @@ -404,17 +404,16 @@ function seedAgents() {
404 404 // status 也要取:ON CONFLICT 分支要拿它判「这行已经毕业了,别被 config.json 推回去」
405 405 const findExisting = db.prepare(`SELECT id, origin, legacy_path, status FROM system_agents WHERE id = ?`);
406 406  
407   - // ON CONFLICT 分支必须连 scope/owner_company_id/origin/source_kind 一起改写,接管才算完成:
  407 + // ON CONFLICT 分支必须连 scope/owner_company_id/origin 一起改写,接管才算完成:
408 408 // 只改 legacy_path 会留下一行「legacy_path 指向 system/agents/ 却仍标着 created」的半吊子记录。
409 409 // 对本来就是 builtin 的行这几列是原值写原值。
410 410 const upsert = db.prepare(`
411   - INSERT INTO system_agents (id, name, scope, owner_company_id, source_kind, origin, legacy_path, status, benchmark_score, created_at, updated_at)
412   - VALUES (@id, @name, 'public', NULL, 'global_agent', 'builtin', @legacy_path, @status, @benchmark_score, @created_at, @updated_at)
  411 + INSERT INTO system_agents (id, name, scope, owner_company_id, origin, legacy_path, status, benchmark_score, created_at, updated_at)
  412 + VALUES (@id, @name, 'public', NULL, 'builtin', @legacy_path, @status, @benchmark_score, @created_at, @updated_at)
413 413 ON CONFLICT(id) DO UPDATE SET
414 414 name = excluded.name,
415 415 scope = excluded.scope,
416 416 owner_company_id = excluded.owner_company_id,
417   - source_kind = excluded.source_kind,
418 417 origin = excluded.origin,
419 418 legacy_path = excluded.legacy_path,
420 419 status = excluded.status,
... ... @@ -523,8 +522,8 @@ function seedCompanyAgents() {
523 522  
524 523 const findAgent = db.prepare(`SELECT id FROM system_agents WHERE id = ?`);
525 524 const insertAgent = db.prepare(`
526   - INSERT INTO system_agents (id, name, scope, owner_company_id, source_kind, origin, legacy_path, status, benchmark_score, created_at, updated_at)
527   - VALUES (@id, @name, 'company', @owner_company_id, 'company_hired', 'created', @legacy_path, 'active', NULL, @created_at, @updated_at)
  525 + INSERT INTO system_agents (id, name, scope, owner_company_id, origin, legacy_path, status, benchmark_score, created_at, updated_at)
  526 + VALUES (@id, @name, 'company', @owner_company_id, 'created', @legacy_path, 'active', NULL, @created_at, @updated_at)
528 527 ON CONFLICT(id) DO NOTHING
529 528 `);
530 529 const findNode = db.prepare(`
... ...
server/services/graph-service.js
... ... @@ -85,8 +85,8 @@ function ensureSystemAgentRow(companyId, systemAgentId, roleName, opts = {}) {
85 85  
86 86 const now = new Date().toISOString();
87 87 db.prepare(`
88   - INSERT INTO system_agents (id, name, scope, owner_company_id, source_kind, origin, legacy_path, status, benchmark_score, created_at, updated_at)
89   - VALUES (@id, @name, 'company', @owner_company_id, 'company_hired', 'created', @legacy_path, @status, NULL, @created_at, @updated_at)
  88 + INSERT INTO system_agents (id, name, scope, owner_company_id, origin, legacy_path, status, benchmark_score, created_at, updated_at)
  89 + VALUES (@id, @name, 'company', @owner_company_id, 'created', @legacy_path, @status, NULL, @created_at, @updated_at)
90 90 `).run({
91 91 id: systemAgentId,
92 92 name: systemAgentId,
... ...
server/tests/integration/company-create-mainline.test.js
... ... @@ -85,8 +85,8 @@ describe(&#39;28 终验 · 「新建公司」主线(真本体)&#39;, () =&gt; {
85 85 const now = new Date().toISOString();
86 86 const ins = db().prepare(`
87 87 INSERT OR REPLACE INTO system_agents
88   - (id, name, scope, owner_company_id, source_kind, origin, legacy_path, status, created_at, updated_at)
89   - VALUES (?, ?, 'public', NULL, 'global_agent', 'builtin', ?, ?, ?, ?)
  88 + (id, name, scope, owner_company_id, origin, legacy_path, status, created_at, updated_at)
  89 + VALUES (?, ?, 'public', NULL, 'builtin', ?, ?, ?, ?)
90 90 `);
91 91 for (const id of REAL_BODIES) {
92 92 const cfg = JSON.parse(fs.readFileSync(path.join(locator.bodyDir(id), 'config.json'), 'utf-8'));
... ...
server/tests/integration/graph-agent-p1-acceptance.test.js
... ... @@ -80,8 +80,8 @@ describe(&#39;本体共享 + 公司态下沉 — 端到端验收(28 §八 4/5)&#39;,
80 80 // 镜像生产种子(seedAgents:两个 id 都是 origin='builtin' 的本体行)
81 81 for (const id of ['qa-judge', COLLISION_ROLE]) {
82 82 db.prepare(`
83   - INSERT INTO system_agents (id, name, scope, owner_company_id, source_kind, origin, legacy_path, status, benchmark_score, created_at, updated_at)
84   - VALUES (?, ?, 'public', NULL, 'global_agent', 'builtin', ?, 'active', NULL, ?, ?)
  83 + INSERT INTO system_agents (id, name, scope, owner_company_id, origin, legacy_path, status, benchmark_score, created_at, updated_at)
  84 + VALUES (?, ?, 'public', NULL, 'builtin', ?, 'active', NULL, ?, ?)
85 85 `).run(id, id, `system/agents/${id}`, now, now);
86 86 }
87 87  
... ...
server/tests/integration/graph-agent-p1-hire.test.js
... ... @@ -62,8 +62,8 @@ describe(&#39;company-service.hire() — 引用本体 + 公司态下沉&#39;, () =&gt; {
62 62 const db = graphDb.getDb();
63 63 const now = new Date().toISOString();
64 64 db.prepare(`
65   - INSERT INTO system_agents (id, name, scope, owner_company_id, source_kind, origin, legacy_path, status, benchmark_score, created_at, updated_at)
66   - VALUES (?, ?, 'public', NULL, 'global_agent', 'builtin', ?, 'active', NULL, ?, ?)
  65 + INSERT INTO system_agents (id, name, scope, owner_company_id, origin, legacy_path, status, benchmark_score, created_at, updated_at)
  66 + VALUES (?, ?, 'public', NULL, 'builtin', ?, 'active', NULL, ?, ?)
67 67 `).run('qa-judge', 'qa-judge', 'system/agents/qa-judge', now, now);
68 68  
69 69 // 固定 id 的代价:上一次跑要是崩在中途,公司目录会留在盘上,而 create() 撞见已存在的
... ...
server/tests/unit/agent-body-draft-gate.test.js
... ... @@ -102,8 +102,8 @@ describe(&#39;新造本体的 draft 闸门(28 批次 5)&#39;, () =&gt; {
102 102 const now = new Date().toISOString();
103 103 graphDb.getDb().prepare(`
104 104 INSERT OR REPLACE INTO system_agents
105   - (id, name, scope, owner_company_id, source_kind, origin, legacy_path, status, created_at, updated_at)
106   - VALUES (?, ?, 'public', NULL, 'global_agent', 'builtin', ?, ?, ?, ?)
  105 + (id, name, scope, owner_company_id, origin, legacy_path, status, created_at, updated_at)
  106 + VALUES (?, ?, 'public', NULL, 'builtin', ?, ?, ?, ?)
107 107 `).run(id, id, `system/agents/${id}`, cfg.status || 'active', now, now);
108 108 };
109 109  
... ... @@ -216,8 +216,8 @@ describe(&#39;新造本体的 draft 闸门(28 批次 5)&#39;, () =&gt; {
216 216 for (const status of ['enabled', 'idle', 'ready', 'trained', 'active']) {
217 217 const id = `qa-judge`;
218 218 db.prepare(`INSERT OR REPLACE INTO system_agents
219   - (id,name,scope,owner_company_id,source_kind,origin,legacy_path,status,benchmark_score,created_at,updated_at)
220   - VALUES (?,?,'public',NULL,'global_agent','builtin',?, ?,NULL,?,?)`)
  219 + (id,name,scope,owner_company_id,origin,legacy_path,status,benchmark_score,created_at,updated_at)
  220 + VALUES (?,?,'public',NULL,'builtin',?, ?,NULL,?,?)`)
221 221 .run(id, id, `system/agents/${id}`, status, now, now);
222 222 db.prepare(`DELETE FROM company_agents WHERE role_name = ?`).run(id);
223 223  
... ...
server/tests/unit/agent-transcript.test.js
... ... @@ -98,8 +98,8 @@ describe(&#39;agent-transcript:会话原文读取&#39;, () =&gt; {
98 98 const now = new Date().toISOString();
99 99 db.prepare(`
100 100 INSERT OR IGNORE INTO system_agents
101   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
102   - VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)
  101 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  102 + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)
103 103 `).run(ROLE, ROLE, `system/agents/${ROLE}`, now, now);
104 104  
105 105 // 雇佣关系:listRoles 的角色清单来自这张表(28 批次 4-19),没有这行就一个角色都列不出来
... ...
server/tests/unit/base-graduation-gate.test.js
... ... @@ -62,8 +62,8 @@ describe(&#39;_base 骨架不算定义已填满(哨兵闸门)&#39;, () =&gt; {
62 62 const now = new Date().toISOString();
63 63 graphDb.getDb().prepare(`
64 64 INSERT OR REPLACE INTO system_agents
65   - (id, name, scope, owner_company_id, source_kind, origin, legacy_path, status, created_at, updated_at)
66   - VALUES (?, ?, 'public', NULL, 'global_agent', 'builtin', ?, ?, ?, ?)
  65 + (id, name, scope, owner_company_id, origin, legacy_path, status, created_at, updated_at)
  66 + VALUES (?, ?, 'public', NULL, 'builtin', ?, ?, ?, ?)
67 67 `).run(id, id, `system/agents/${id}`, cfg.status || 'active', now, now);
68 68 };
69 69 const agentRow = (id) =>
... ...
server/tests/unit/boundary-guard-new-body.test.js
... ... @@ -88,8 +88,8 @@ describe(&#39;边界守卫 · 新建 agent 本体不判越界(28 §九)&#39;, () =&gt;
88 88 const ts = new Date().toISOString();
89 89 wdb.getDb().prepare(`
90 90 INSERT OR IGNORE INTO system_agents
91   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
92   - VALUES ('hr', 'hr', 'public', NULL, 'global_agent', 'system/agents/hr', 'active', ?, ?)
  91 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  92 + VALUES ('hr', 'hr', 'public', NULL, 'system/agents/hr', 'active', ?, ?)
93 93 `).run(ts, ts);
94 94 const g = wdb.createWorkGraph({ companyId, nodes: ['hr'], edges: [] });
95 95 expect(g.errors || []).toEqual([]);
... ...
server/tests/unit/company-hire-org-layer.test.js
... ... @@ -100,8 +100,8 @@ describe(&#39;hire() 的 org / work 分层(27-P3B W7)&#39;, () =&gt; {
100 100 const now = new Date().toISOString();
101 101 for (const id of TEMPLATES) {
102 102 db.prepare(`
103   - INSERT INTO system_agents (id, name, scope, owner_company_id, source_kind, legacy_path, status, benchmark_score, created_at, updated_at)
104   - VALUES (?, ?, 'public', NULL, 'template', ?, 'active', NULL, ?, ?)
  103 + INSERT INTO system_agents (id, name, scope, owner_company_id, legacy_path, status, benchmark_score, created_at, updated_at)
  104 + VALUES (?, ?, 'public', NULL, ?, 'active', NULL, ?, ?)
105 105 `).run(id, id, `system/agents/${id}`, now, now);
106 106 }
107 107  
... ...
server/tests/unit/goal-task-acceptance.test.js
... ... @@ -180,8 +180,8 @@ describe(&#39;27-P3C 批 1 验收(§八 4 / 5 / 6 / 10 / 11 / 12b)&#39;, () =&gt; {
180 180 const now = new Date().toISOString();
181 181 db.prepare(`
182 182 INSERT OR IGNORE INTO system_agents
183   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
184   - VALUES ('acc-worker', 'acc-worker', 'public', NULL, 'global_agent', 'system/agents/acc-worker', 'active', ?, ?)
  183 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  184 + VALUES ('acc-worker', 'acc-worker', 'public', NULL, 'system/agents/acc-worker', 'active', ?, ?)
185 185 `).run(now, now);
186 186 const g = wdb.createWorkGraph({ companyId: cid, nodes: ['acc-worker'], edges: [] });
187 187 expect(g.errors || []).toEqual([]);
... ...
server/tests/unit/graph-boundary.test.js
... ... @@ -60,8 +60,8 @@ describe(&#39;graph-engine 项目根与边界守卫&#39;, () =&gt; {
60 60 const ts = new Date().toISOString();
61 61 const ins = db.prepare(`
62 62 INSERT OR IGNORE INTO system_agents
63   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
64   - VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)
  63 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  64 + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)
65 65 `);
66 66 for (const id of new Set(ids)) ins.run(id, id, `system/agents/${id}`, ts, ts);
67 67 }
... ...
server/tests/unit/graph-cross-layer-address.test.js
... ... @@ -82,8 +82,8 @@ describe(&#39;R-17.1 跨层地址 · 两半的必填性(W10 事实核实)&#39;, () =
82 82 const ts = new Date().toISOString();
83 83 const ins = db.prepare(`
84 84 INSERT OR IGNORE INTO system_agents
85   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
86   - VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)
  85 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  86 + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)
87 87 `);
88 88 for (const id of new Set(ids)) ins.run(id, id, `system/agents/${id}`, ts, ts);
89 89 }
... ...
server/tests/unit/graph-db.test.js
... ... @@ -40,9 +40,9 @@ describe(&#39;graph-db&#39;, () =&gt; {
40 40 const d = graphDb.getDb();
41 41 const now = new Date().toISOString();
42 42 d.prepare(`
43   - INSERT INTO system_agents (id, name, scope, owner_company_id, source_kind, legacy_path, status, benchmark_score, created_at, updated_at)
44   - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
45   - `).run('test-agent-env-check', 'Test Agent', 'public', null, 'global_agent', 'system/agents/test-agent-env-check', 'active', null, now, now);
  43 + INSERT INTO system_agents (id, name, scope, owner_company_id, legacy_path, status, benchmark_score, created_at, updated_at)
  44 + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
  45 + `).run('test-agent-env-check', 'Test Agent', 'public', null, 'system/agents/test-agent-env-check', 'active', null, now, now);
46 46  
47 47 // The row must have landed in the overridden file, not the default one.
48 48 expect(fs.existsSync(TEST_DB_PATH)).toBe(true);
... ...
server/tests/unit/graph-edge-contract-role.test.js
... ... @@ -95,8 +95,8 @@ describe(&#39;R-19.A2 契约 role 落库 + PATCH 通道的 data-contract 校验&#39;, ()
95 95 const ts = new Date().toISOString();
96 96 const ins = db.prepare(`
97 97 INSERT OR IGNORE INTO system_agents
98   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
99   - VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)
  98 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  99 + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)
100 100 `);
101 101 for (const id of ['brd-planning', 'prd-planning', 'sdd-planning']) {
102 102 ins.run(id, id, `system/agents/${id}`, ts, ts);
... ...
server/tests/unit/graph-edge-contracts.test.js
... ... @@ -67,8 +67,8 @@ describe(&#39;R-19.A 边上的数据契约:声明 → 落库 → 读回&#39;, () =&gt; {
67 67 const ts = new Date().toISOString();
68 68 const ins = db.prepare(`
69 69 INSERT OR IGNORE INTO system_agents
70   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
71   - VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)
  70 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  71 + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)
72 72 `);
73 73 for (const id of new Set(ids)) ins.run(id, id, `system/agents/${id}`, ts, ts);
74 74 }
... ...
server/tests/unit/graph-engine-fail-reason.test.js
... ... @@ -92,8 +92,8 @@ describe(&#39;引擎自判失败的 fail_reason 与失败上下文(W12 补完 / W9
92 92 const ts = new Date().toISOString();
93 93 const ins = db.prepare(`
94 94 INSERT OR IGNORE INTO system_agents
95   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
96   - VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)
  95 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  96 + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)
97 97 `);
98 98 for (const id of new Set(ids)) ins.run(id, id, `system/agents/${id}`, ts, ts);
99 99 }
... ...
server/tests/unit/graph-engine-node-error.test.js
... ... @@ -82,8 +82,8 @@ describe(&#39;graph-engine 节点失败留痕(W12)&#39;, () =&gt; {
82 82 const now = new Date().toISOString();
83 83 const ins = db.prepare(`
84 84 INSERT OR IGNORE INTO system_agents
85   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
86   - VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)
  85 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  86 + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)
87 87 `);
88 88 for (const id of new Set(ids)) ins.run(id, id, `system/agents/${id}`, now, now);
89 89 }
... ...
server/tests/unit/graph-engine-revive-parent.test.js
... ... @@ -85,8 +85,8 @@ describe(&#39;子 run 转终态回写父节点&#39;, () =&gt; {
85 85 const now = new Date().toISOString();
86 86 const ins = db.prepare(`
87 87 INSERT OR IGNORE INTO system_agents
88   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
89   - VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)
  88 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  89 + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)
90 90 `);
91 91 for (const id of new Set(ids)) ins.run(id, id, `system/agents/${id}`, now, now);
92 92 }
... ...
server/tests/unit/graph-engine.test.js
... ... @@ -98,8 +98,8 @@ describe(&#39;graph-engine&#39;, () =&gt; {
98 98 const now = new Date().toISOString();
99 99 const ins = db.prepare(`
100 100 INSERT OR IGNORE INTO system_agents
101   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
102   - VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)
  101 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  102 + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)
103 103 `);
104 104 for (const id of new Set(ids)) ins.run(id, id, `system/agents/${id}`, now, now);
105 105 }
... ...
server/tests/unit/graph-files-index.test.js
... ... @@ -75,8 +75,8 @@ describe(&#39;R-19 产物索引:graph_files / graph_edge_contracts&#39;, () =&gt; {
75 75 const ts = new Date().toISOString();
76 76 const ins = db.prepare(`
77 77 INSERT OR IGNORE INTO system_agents
78   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
79   - VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)
  78 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  79 + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)
80 80 `);
81 81 for (const id of new Set(ids)) ins.run(id, id, `system/agents/${id}`, ts, ts);
82 82 }
... ...
server/tests/unit/graph-real-chain-smoke.test.js
... ... @@ -75,8 +75,8 @@ describe(&#39;图执行真实链路 smokeï¼ˆé›¶æ³¨å…¥ï¼Œåªæ¢æŽ‰çœŸ CLI å­è¿›ç¨‹ï¼
75 75 const db = wdb.getDb();
76 76 const now = new Date().toISOString();
77 77 db.prepare(`INSERT OR IGNORE INTO system_agents
78   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
79   - VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)`)
  78 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  79 + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)`)
80 80 .run(AGENT, AGENT, `system/agents/${AGENT}`, now, now);
81 81  
82 82 const g = wdb.createWorkGraph({ companyId: COMPANY, nodes: [{ nodeName: AGENT, systemAgentId: AGENT }], edges: [] });
... ... @@ -142,14 +142,14 @@ describe(&#39;图执行真实链路 smokeï¼ˆé›¶æ³¨å…¥ï¼Œåªæ¢æŽ‰çœŸ CLI å­è¿›ç¨‹ï¼
142 142 const db = wdb.getDb();
143 143 const now = new Date().toISOString();
144 144 db.prepare(`INSERT OR IGNORE INTO system_agents
145   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
146   - VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)`)
  145 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  146 + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)`)
147 147 .run(PLANNER, PLANNER, `system/agents/${PLANNER}`, now, now);
148 148 // subgraph èŠ‚ç‚¹è‡ªå·±ä¹Ÿè¦æœ‰ system_agents 行(work_graph_nodes.system_agent_id 是外键)。
149 149 // å®ƒåªæ˜¯ã€Œå“ªä¸ª org 角色拥有这张å­å›¾ã€çš„å字,ä¸ä¼šè¢«çœŸçš„调用 —— 调的是 PLANNER。
150 150 db.prepare(`INSERT OR IGNORE INTO system_agents
151   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
152   - VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)`)
  151 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  152 + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)`)
153 153 .run(WRAPPER, WRAPPER, `system/agents/${WRAPPER}`, now, now);
154 154 db.prepare(`INSERT OR IGNORE INTO company_agents (company_id, system_agent_id, role_name, status, hired_at)
155 155 VALUES (?, ?, ?, 'active', ?)`).run(COMPANY, PLANNER, PLANNER, now);
... ... @@ -213,8 +213,8 @@ describe(&#39;图执行真实链路 smokeï¼ˆé›¶æ³¨å…¥ï¼Œåªæ¢æŽ‰çœŸ CLI å­è¿›ç¨‹ï¼
213 213 const db = wdb.getDb();
214 214 const now = new Date().toISOString();
215 215 db.prepare(`INSERT OR IGNORE INTO system_agents
216   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
217   - VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)`)
  216 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  217 + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)`)
218 218 .run(VALIDATOR, VALIDATOR, `system/agents/${VALIDATOR}`, now, now);
219 219  
220 220 const g = wdb.createWorkGraph({
... ...
server/tests/unit/graph-run-route-io.test.js
... ... @@ -53,8 +53,8 @@ describe(&#39;GET /api/graph-runs/:id — R-19 输入输出三字段&#39;, () =&gt; {
53 53 const ts = new Date().toISOString();
54 54 const ins = db.prepare(`
55 55 INSERT OR IGNORE INTO system_agents
56   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
57   - VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)
  56 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  57 + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)
58 58 `);
59 59 for (const id of ['brd-planning', 'prd-planning']) ins.run(id, id, `system/agents/${id}`, ts, ts);
60 60  
... ...
server/tests/unit/graph-run-route-slot-busy.test.js
... ... @@ -60,8 +60,8 @@ describe(&#39;POST /api/graph-runs/... 撞上坑满 → 409(不是 500)&#39;, () =&gt;
60 60 const now = new Date().toISOString();
61 61 const insAgent = db.prepare(`
62 62 INSERT OR IGNORE INTO system_agents
63   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
64   - VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)
  63 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  64 + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)
65 65 `);
66 66 for (const id of ['solo', 'solo2']) insAgent.run(id, id, `system/agents/${id}`, now, now);
67 67  
... ...
server/tests/unit/graph-run-slot-recovery.test.js
... ... @@ -98,8 +98,8 @@ describe(&#39;恢复路径与执行坑&#39;, () =&gt; {
98 98 const now = new Date().toISOString();
99 99 const ins = wdb.getDb().prepare(`
100 100 INSERT OR IGNORE INTO system_agents
101   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
102   - VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)
  101 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  102 + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)
103 103 `);
104 104 for (const id of new Set(ids)) ins.run(id, id, `system/agents/${id}`, now, now);
105 105 }
... ...
server/tests/unit/graph-seed-reconcile.test.js
... ... @@ -63,11 +63,11 @@ describe(&#39;graph-seed 对账在存量形态上退出码为 0&#39;, () =&gt; {
63 63 const db = graphDb.getDb();
64 64 const now = new Date().toISOString();
65 65 const insert = db.prepare(`
66   - INSERT INTO system_agents (id, name, scope, owner_company_id, source_kind, origin, legacy_path, status, benchmark_score, created_at, updated_at)
67   - VALUES (@id, @name, @scope, @owner, @source_kind, @origin, @legacy_path, 'active', NULL, @now, @now)
  66 + INSERT INTO system_agents (id, name, scope, owner_company_id, origin, legacy_path, status, benchmark_score, created_at, updated_at)
  67 + VALUES (@id, @name, @scope, @owner, @origin, @legacy_path, 'active', NULL, @now, @now)
68 68 `);
69   - insert.run({ id: 'base', name: 'base', scope: 'public', owner: null, source_kind: 'global_agent', origin: 'builtin', legacy_path: 'system/templates/base', now });
70   - insert.run({ id: 'graduated', name: 'graduated', scope: 'company', owner: 'live-co', source_kind: 'company_hired', origin: 'created', legacy_path: 'system/agents/graduated', now });
  69 + insert.run({ id: 'base', name: 'base', scope: 'public', owner: null, origin: 'builtin', legacy_path: 'system/templates/base', now });
  70 + insert.run({ id: 'graduated', name: 'graduated', scope: 'company', owner: 'live-co', origin: 'created', legacy_path: 'system/agents/graduated', now });
71 71  
72 72 // ── 幽灵公司的名册行:目录(连同 company.json)已经不在,company_agents 那行还在 ──
73 73 // 这张表批次 4-19 之后是**唯一被读的名册**,heartbeat-service.start() 拿它挂心跳时
... ...
server/tests/unit/graph-seed-service.test.js
... ... @@ -148,8 +148,8 @@ describe(&#39;graph-seed-service (hermetic fixture root)&#39;, () =&gt; {
148 148 function insertHired(id, ownerCompanyId, legacyPath) {
149 149 const now = new Date().toISOString();
150 150 graphDb.getDb().prepare(`
151   - INSERT INTO system_agents (id, name, scope, owner_company_id, source_kind, origin, legacy_path, status, benchmark_score, created_at, updated_at)
152   - VALUES (?, ?, 'company', ?, 'company_hired', 'created', ?, 'active', NULL, ?, ?)
  151 + INSERT INTO system_agents (id, name, scope, owner_company_id, origin, legacy_path, status, benchmark_score, created_at, updated_at)
  152 + VALUES (?, ?, 'company', ?, 'created', ?, 'active', NULL, ?, ?)
153 153 `).run(id, id, ownerCompanyId, legacyPath, now, now);
154 154 }
155 155  
... ... @@ -429,9 +429,9 @@ describe(&#39;graph-seed-service verifyCounts (real repo root)&#39;, () =&gt; {
429 429 const db = graphDb.getDb();
430 430 const now = new Date().toISOString();
431 431 db.prepare(`
432   - INSERT INTO system_agents (id, name, scope, owner_company_id, source_kind, legacy_path, status, benchmark_score, created_at, updated_at)
433   - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
434   - `).run('bogus-agent-no-fs-backing', 'Bogus', 'public', null, 'global_agent', 'system/agents/bogus-agent-no-fs-backing', 'active', null, now, now);
  432 + INSERT INTO system_agents (id, name, scope, owner_company_id, legacy_path, status, benchmark_score, created_at, updated_at)
  433 + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
  434 + `).run('bogus-agent-no-fs-backing', 'Bogus', 'public', null, 'system/agents/bogus-agent-no-fs-backing', 'active', null, now, now);
435 435  
436 436 const v = seedService.verifyCounts();
437 437 expect(v.ok).toBe(false);
... ...
server/tests/unit/graph-service.test.js
... ... @@ -35,13 +35,13 @@ describe(&#39;graph-service&#39;, () =&gt; {
35 35 db.exec('DELETE FROM org_graph_nodes; DELETE FROM system_agents;');
36 36 });
37 37  
38   - function insertSystemAgent({ id, origin = 'builtin', source_kind = 'global_agent', legacy_path = `system/agents/${id}`, scope = 'public', owner_company_id = null }) {
  38 + function insertSystemAgent({ id, origin = 'builtin', legacy_path = `system/agents/${id}`, scope = 'public', owner_company_id = null }) {
39 39 const db = graphDb.getDb();
40 40 const now = new Date().toISOString();
41 41 db.prepare(`
42   - INSERT INTO system_agents (id, name, scope, owner_company_id, source_kind, origin, legacy_path, status, benchmark_score, created_at, updated_at)
43   - VALUES (?, ?, ?, ?, ?, ?, ?, 'active', NULL, ?, ?)
44   - `).run(id, id, scope, owner_company_id, source_kind, origin, legacy_path, now, now);
  42 + INSERT INTO system_agents (id, name, scope, owner_company_id, origin, legacy_path, status, benchmark_score, created_at, updated_at)
  43 + VALUES (?, ?, ?, ?, ?, ?, 'active', NULL, ?, ?)
  44 + `).run(id, id, scope, owner_company_id, origin, legacy_path, now, now);
45 45 }
46 46  
47 47 describe('lookupSharedAgentMd', () => {
... ...
server/tests/unit/heartbeat-company-scoped.test.js
... ... @@ -85,8 +85,8 @@ describe(&#39;心跳设置按公司隔离(28 遗留项)&#39;, () =&gt; {
85 85 const db = graphDb.getDb();
86 86 const now = new Date().toISOString();
87 87 db.prepare(`
88   - INSERT OR REPLACE INTO system_agents (id, name, scope, owner_company_id, source_kind, legacy_path, status, benchmark_score, created_at, updated_at)
89   - VALUES (?, ?, 'public', NULL, 'template', ?, 'active', NULL, ?, ?)
  88 + INSERT OR REPLACE INTO system_agents (id, name, scope, owner_company_id, legacy_path, status, benchmark_score, created_at, updated_at)
  89 + VALUES (?, ?, 'public', NULL, ?, 'active', NULL, ?, ?)
90 90 `).run(BODY_ID, BODY_ID, `system/agents/${BODY_ID}`, now, now);
91 91 for (const cid of [CO_A, CO_B]) {
92 92 db.prepare(`INSERT OR REPLACE INTO company_agents (company_id, system_agent_id, role_name, status, hired_at)
... ...
server/tests/unit/org-layer-contracts.test.js
... ... @@ -122,8 +122,8 @@ describe(&#39;R-19 org 层数据契约(读回方 = buildPlanningPrompt)&#39;, () =&gt;
122 122 const ts = new Date().toISOString();
123 123 const ins = db.prepare(`
124 124 INSERT OR IGNORE INTO system_agents
125   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
126   - VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)
  125 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  126 + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)
127 127 `);
128 128 for (const id of new Set(ids)) ins.run(id, id, `system/agents/${id}`, ts, ts);
129 129 }
... ...
server/tests/unit/p3c-recovery-blind-spots.test.js
... ... @@ -110,8 +110,8 @@ describe(&#39;批 3 恢复路径的盲区&#39;, () =&gt; {
110 110 const now = new Date().toISOString();
111 111 const ins = wdb.getDb().prepare(`
112 112 INSERT OR IGNORE INTO system_agents
113   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
114   - VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)
  113 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  114 + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)
115 115 `);
116 116 for (const id of new Set(ids)) ins.run(id, id, `system/agents/${id}`, now, now);
117 117 }
... ...
server/tests/unit/p3c-recovery-http.test.js
... ... @@ -94,8 +94,8 @@ describe(&#39;恢复动作的 HTTP 结局:退得回队列 → 2xx,退不回 →
94 94 const now = new Date().toISOString();
95 95 const ins = wdb.getDb().prepare(`
96 96 INSERT OR IGNORE INTO system_agents
97   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
98   - VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)
  97 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  98 + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)
99 99 `);
100 100 for (const id of new Set(ids)) ins.run(id, id, `system/agents/${id}`, now, now);
101 101 }
... ...
server/tests/unit/prompt-write-boundary.test.js
... ... @@ -93,8 +93,8 @@ describe(&#39;提示词的交付物归宿:graph 节点 vs agent-chat 两条链路&#39;
93 93 const ts = new Date().toISOString();
94 94 db.prepare(`
95 95 INSERT OR IGNORE INTO system_agents
96   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
97   - VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)
  96 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  97 + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)
98 98 `).run(AGENT_ID, AGENT_ID, `system/agents/${AGENT_ID}`, ts, ts);
99 99  
100 100 const g = wdb.createWorkGraph({
... ...
server/tests/unit/source-kind-drop-migration.test.js 0 → 100644
  1 +/**
  2 + * 28-followups §3.1 —— `system_agents.source_kind` 删列的**存量库路径**。
  3 + *
  4 + * 单独钉这一条的理由与 tasks-executor-migration 一样:新建库那条路由单测天然覆盖
  5 + * (建表块里已经没有这一列),「存量库 + 重启」这条必须另外安排人走一遍。
  6 + *
  7 + * ── 要守的三件事 ──────────────────────────────────────────────────────
  8 + * ① **顺序**:先把 source_kind 的信息回填进 origin,再删列。反过来做,
  9 + * 「origin 列出现之前写下的行」永远回不到正确的 origin —— 那正是那条回填存在的理由。
  10 + * 这一条最容易在重构里被弄反,而且弄反之后**不报错**:库能开、测试能过,
  11 + * 只有那几行 origin 悄悄是错的。
  12 + * ② **幂等**:删过之后第二次开库不能抛(`no such column: source_kind`)。
  13 + * ③ **位置**:删列必须跑在任何写入之前。migrate() 由 getDb() 在建表之后立刻调用,
  14 + * 正是那个位置;挪到别处的话,带着 source_kind 的 INSERT 会先撞 NOT NULL。
  15 + *
  16 + * ── 为什么 legacy_path 不一起删 ────────────────────────────────────────
  17 + * 它**还没死**:新行里它恒等于 system/agents/{id}(冗余),但存量行里存着批次 4 之前的
  18 + * 公司侧路径,而 graph-seed-service 判「悬空行 / 孤儿行」靠的就是它指向的目录还在不在。
  19 + * 实查生产库(2026-08-20)76 行里有 2 行仍是老路径。退出条件写在 graph-db.js 的建表注释里。
  20 + */
  21 +import { describe, it, expect } from 'vitest';
  22 +import { createRequire } from 'module';
  23 +import fs from 'fs';
  24 +import os from 'os';
  25 +import path from 'path';
  26 +const require_ = createRequire(import.meta.url);
  27 +
  28 +/** 用独立 DB 文件跑一遍 getDb()(写法照抄 tasks-executor-migration.test.js) */
  29 +function freshDb(dbPath) {
  30 + process.env.GRAPH_DB_PATH = dbPath;
  31 + for (const m of ['../../services/graph-db', '../../services/work-graph-db']) {
  32 + delete require_.cache[require_.resolve(m)];
  33 + }
  34 + const graphDb = require_('../../services/graph-db');
  35 + graphDb.getDb();
  36 + return graphDb;
  37 +}
  38 +
  39 +const cols = (db) => db.pragma('table_info(system_agents)').map((c) => c.name);
  40 +
  41 +describe('system_agents.source_kind 删列', () => {
  42 + it('存量库重启:先回填 origin 再删列,且不炸', () => {
  43 + const dbPath = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'sk-drop-')), 'graph.sqlite');
  44 + const graphDb = freshDb(dbPath);
  45 + expect(cols(graphDb.getDb())).not.toContain('source_kind'); // 新建库本来就没有
  46 + graphDb.getDb().close();
  47 +
  48 + // 退回删列之前的形态:把列加回来,并造两行「origin 列出现之前」的记录 ——
  49 + // 它们的 origin 是加列时的默认值 'builtin',真相只存在 source_kind 里。
  50 + const Database = require_('better-sqlite3');
  51 + const raw = new Database(dbPath);
  52 + raw.exec(`ALTER TABLE system_agents ADD COLUMN source_kind TEXT NOT NULL DEFAULT 'global_agent'`);
  53 + const now = new Date().toISOString();
  54 + const ins = raw.prepare(`INSERT INTO system_agents
  55 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at, origin, source_kind)
  56 + VALUES (?, ?, ?, ?, ?, 'active', ?, ?, 'builtin', ?)`);
  57 + ins.run('legacy-hired', 'legacy-hired', 'company', 'co-x', 'system/agents/legacy-hired', now, now, 'company_hired');
  58 + ins.run('legacy-global', 'legacy-global', 'public', null, 'system/agents/legacy-global', now, now, 'global_agent');
  59 + expect(raw.pragma('table_info(system_agents)').map((c) => c.name)).toContain('source_kind');
  60 + raw.close();
  61 +
  62 + // 重开进程 = 服务重启
  63 + const graphDb2 = freshDb(dbPath);
  64 + const db2 = graphDb2.getDb();
  65 +
  66 + expect(cols(db2)).not.toContain('source_kind'); // ② 列没了
  67 + // ① 顺序:company_hired 那行的 origin 必须已经被回填成 created。
  68 + // 先删后填的话这里会停在 'builtin' —— 而且不会有任何报错。
  69 + const get = (id) => db2.prepare('SELECT origin FROM system_agents WHERE id = ?').get(id);
  70 + expect(get('legacy-hired').origin).toBe('created');
  71 + expect(get('legacy-global').origin).toBe('builtin'); // 对照:不该被一起改写
  72 + db2.close();
  73 +
  74 + // ③ 幂等:再重启一次,不抛,结果不变
  75 + const graphDb3 = freshDb(dbPath);
  76 + expect(cols(graphDb3.getDb())).not.toContain('source_kind');
  77 + expect(graphDb3.getDb().prepare('SELECT origin FROM system_agents WHERE id = ?').get('legacy-hired').origin).toBe('created');
  78 +
  79 + fs.rmSync(path.dirname(dbPath), { recursive: true, force: true });
  80 + delete process.env.GRAPH_DB_PATH;
  81 + });
  82 +
  83 + it('legacy_path 仍在,且删它的退出条件写在了建表注释里(别顺手一起删)', () => {
  84 + const dbPath = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'sk-drop2-')), 'graph.sqlite');
  85 + const graphDb = freshDb(dbPath);
  86 + expect(cols(graphDb.getDb())).toContain('legacy_path');
  87 + graphDb.getDb().close();
  88 + fs.rmSync(path.dirname(dbPath), { recursive: true, force: true });
  89 + delete process.env.GRAPH_DB_PATH;
  90 +
  91 + // 退出条件必须留在源码里可被读到 —— 否则下一个人只会看到一列「没人读的冗余字段」
  92 + const src = fs.readFileSync(path.join(__dirname, '../../services/graph-db.js'), 'utf-8');
  93 + expect(src).toContain("legacy_path <> 'system/agents/' || id");
  94 + });
  95 +});
... ...
server/tests/unit/task-blocked-reason.test.js
... ... @@ -74,8 +74,8 @@ describe(&#39;blocked task 的原因回写&#39;, () =&gt; {
74 74  
75 75 db.prepare(`
76 76 INSERT OR IGNORE INTO system_agents
77   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
78   - VALUES ('sdd-planning', 'sdd-planning', 'public', NULL, 'global_agent', 'system/agents/sdd-planning', 'active', ?, ?)
  77 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  78 + VALUES ('sdd-planning', 'sdd-planning', 'public', NULL, 'system/agents/sdd-planning', 'active', ?, ?)
79 79 `).run(now, now);
80 80  
81 81 const graph = wdb.createWorkGraph({ companyId, nodes: ['sdd-planning'], edges: [] });
... ...
server/tests/unit/task-queue-acceptance.test.js
... ... @@ -130,8 +130,8 @@ describe(&#39;批 3 排队 · §八 验收&#39;, () =&gt; {
130 130 const now = new Date().toISOString();
131 131 const ins = wdb.getDb().prepare(`
132 132 INSERT OR IGNORE INTO system_agents
133   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
134   - VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)
  133 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  134 + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)
135 135 `);
136 136 for (const id of new Set(ids)) ins.run(id, id, `system/agents/${id}`, now, now);
137 137 }
... ...
server/tests/unit/task-scheduler.test.js
... ... @@ -112,8 +112,8 @@ describe(&#39;task-scheduler&#39;, () =&gt; {
112 112 const now = new Date().toISOString();
113 113 const ins = wdb.getDb().prepare(`
114 114 INSERT OR IGNORE INTO system_agents
115   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
116   - VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)
  115 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  116 + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)
117 117 `);
118 118 for (const id of new Set(ids)) ins.run(id, id, `system/agents/${id}`, now, now);
119 119 }
... ...
server/tests/unit/task-slots-parallel.test.js
... ... @@ -71,8 +71,8 @@ try {
71 71 const db = wdb.getDb();
72 72 const now = new Date().toISOString();
73 73 db.prepare(
74   - "INSERT OR IGNORE INTO system_agents (id,name,scope,owner_company_id,source_kind,legacy_path,status,created_at,updated_at)"
75   - + " VALUES ('solo','solo','public',NULL,'global_agent','system/agents/solo','active',?,?)"
  74 + "INSERT OR IGNORE INTO system_agents (id,name,scope,owner_company_id,legacy_path,status,created_at,updated_at)"
  75 + + " VALUES ('solo','solo','public',NULL,'system/agents/solo','active',?,?)"
76 76 ).run(now, now);
77 77 const g = wdb.createWorkGraph({ companyId: cid, nodes: ['solo'], edges: [] });
78 78 if ((g.errors || []).length) throw new Error(JSON.stringify(g.errors));
... ... @@ -186,8 +186,8 @@ describe(&#39;批 4 · 公司级执行位(max_parallel_tasks)&#39;, () =&gt; {
186 186 const now = new Date().toISOString();
187 187 const ins = wdb.getDb().prepare(`
188 188 INSERT OR IGNORE INTO system_agents
189   - (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
190   - VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)
  189 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  190 + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)
191 191 `);
192 192 for (const id of new Set(ids)) ins.run(id, id, `system/agents/${id}`, now, now);
193 193 }
... ...