@zhushanwen/subagent-core 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -2
- package/agents/analyst.md +60 -0
- package/agents/coder.md +69 -0
- package/agents/debugger.md +66 -0
- package/agents/doc-reviewer.md +49 -0
- package/agents/explorer.md +63 -0
- package/agents/general-purpose.md +32 -0
- package/agents/orchestrator.md +61 -0
- package/agents/planner.md +53 -0
- package/agents/researcher.md +67 -0
- package/agents/reviewer.md +73 -0
- package/dist/chunk-4KN4TTG7.js +240 -0
- package/dist/chunk-APZY4IME.js +27 -0
- package/dist/chunk-X7SZ5HLQ.js +66 -0
- package/dist/execution/engine/engines/zcode/constants.cjs +65 -0
- package/dist/execution/engine/engines/zcode/constants.d.cts +93 -1
- package/dist/execution/engine/engines/zcode/constants.d.ts +93 -1
- package/dist/execution/engine/engines/zcode/constants.js +43 -1
- package/dist/execution/engine/engines/zcode/reader.d.cts +1 -1
- package/dist/execution/engine/engines/zcode/reader.d.ts +1 -1
- package/dist/execution/engine/engines/zcode/reader.js +4 -234
- package/dist/execution/engine/paths.js +7 -19
- package/dist/index.cjs +14709 -100
- package/dist/index.d.cts +4053 -200
- package/dist/index.d.ts +4053 -200
- package/dist/index.js +14358 -102
- package/dist/{types-BxyAidGf.d.cts → types-DpUO16pj.d.cts} +464 -1
- package/dist/{types-BxyAidGf.d.ts → types-DpUO16pj.d.ts} +464 -1
- package/package.json +6 -3
- package/src/__tests__/review-fix-loop-script.test.ts +90 -2
- package/src/__tests__/review-fix-loop-utils.test.ts +21 -0
- package/src/__tests__/smoke.test.ts +9 -2
- package/src/execution/__tests__/agent-profile.test.ts +232 -0
- package/src/execution/__tests__/agents-assembly.test.ts +219 -0
- package/src/execution/__tests__/chat-engine-routing.test.ts +71 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +1 -0
- package/src/execution/__tests__/create-concurrency-pool.test.ts +194 -0
- package/src/execution/__tests__/delivery-methods.test.ts +2 -0
- package/src/execution/__tests__/descendant-sweep.test.ts +269 -0
- package/src/execution/__tests__/dialog-queue.test.ts +199 -1
- package/src/execution/__tests__/epipe-fallback.test.ts +2 -0
- package/src/execution/__tests__/execution-runtime-face.test.ts +272 -0
- package/src/execution/__tests__/finalize-record.test.ts +106 -0
- package/src/execution/__tests__/gc-timer.test.ts +2 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +2 -0
- package/src/execution/__tests__/get-state-handshake.test.ts +127 -0
- package/src/execution/__tests__/keep-alive-no-progress.test.ts +378 -0
- package/src/execution/__tests__/kill-all-escalation.test.ts +196 -0
- package/src/execution/__tests__/lifecycle-manager-idle-timer-identity.test.ts +117 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +39 -0
- package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +99 -0
- package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +113 -0
- package/src/execution/__tests__/notify-ledger.test.ts +294 -0
- package/src/execution/__tests__/record-store-orphan-revive.test.ts +159 -0
- package/src/execution/__tests__/record-store.test.ts +83 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +4 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +1 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +407 -17
- package/src/execution/__tests__/run-spawn-integration.test.ts +1 -1
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +12 -4
- package/src/execution/__tests__/service-kill-escalation.test.ts +89 -0
- package/src/execution/__tests__/session-pending.test.ts +228 -0
- package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +110 -0
- package/src/execution/__tests__/session-runner-close-prune.test.ts +181 -0
- package/src/execution/__tests__/session-runner-epipe.test.ts +1 -0
- package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +226 -0
- package/src/execution/__tests__/settled-watchdog.test.ts +289 -0
- package/src/execution/__tests__/spawned-children.test.ts +153 -10
- package/src/execution/__tests__/subagent-actions-core.test.ts +997 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +71 -10
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +174 -0
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +266 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +3 -0
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +316 -0
- package/src/execution/__tests__/timeout-integration.test.ts +7 -7
- package/src/execution/__tests__/ui-request-handler-factory.test.ts +95 -1
- package/src/execution/__tests__/ui-request-queue.test.ts +25 -0
- package/src/execution/__tests__/worktree-git-ops.test.ts +571 -0
- package/src/execution/__tests__/worktree-reconcile-aging.test.ts +204 -0
- package/src/execution/agent-registry.ts +225 -1
- package/src/execution/agents-assembly.ts +88 -0
- package/src/execution/concurrency-pool.ts +71 -9
- package/src/execution/dialog-queue.ts +101 -3
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +49 -2
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +87 -1
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +26 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +83 -1
- package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +131 -0
- package/src/execution/engine/__tests__/registry.test.ts +90 -1
- package/src/execution/engine/engine-discovery.ts +12 -16
- package/src/execution/engine/engines/pi/task-spec-mapper.ts +3 -3
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +274 -0
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +36 -0
- package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +472 -0
- package/src/execution/engine/engines/zcode/__tests__/preparer-appserver.test.ts +387 -0
- package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +780 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +815 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +462 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +119 -1
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +136 -6
- package/src/execution/engine/engines/zcode/appserver-home.ts +442 -0
- package/src/execution/engine/engines/zcode/appserver-probe.ts +141 -0
- package/src/execution/engine/engines/zcode/connection.ts +585 -0
- package/src/execution/engine/engines/zcode/constants.ts +129 -0
- package/src/execution/engine/engines/zcode/golden-sample.ts +54 -8
- package/src/execution/engine/engines/zcode/preparer.ts +25 -22
- package/src/execution/engine/engines/zcode/session-channel.ts +656 -0
- package/src/execution/engine/engines/zcode/zcode-engine.ts +909 -41
- package/src/execution/engine/host-task-spec.ts +3 -3
- package/src/execution/engine/port.ts +41 -1
- package/src/execution/engine/registry.ts +53 -0
- package/src/execution/finalize-record.ts +63 -5
- package/src/execution/get-state-handshake.ts +85 -10
- package/src/execution/lifecycle-manager.ts +27 -3
- package/src/execution/manifest-store.ts +53 -62
- package/src/execution/notifier.ts +10 -10
- package/src/execution/notify-ledger.ts +117 -16
- package/src/execution/record-entry.ts +8 -2
- package/src/execution/record-store.ts +29 -15
- package/src/execution/session-pending.ts +213 -62
- package/src/execution/session-runner.ts +974 -128
- package/src/execution/sessions-index.ts +10 -55
- package/src/execution/settled-watchdog.ts +99 -0
- package/src/execution/subagent-actions-core.ts +686 -0
- package/src/execution/subagent-service.ts +396 -49
- package/src/execution/ui-request-handler-factory.ts +27 -4
- package/src/execution/worktree-git-ops.ts +397 -0
- package/src/execution/worktree-manager.ts +72 -10
- package/src/execution/worktree-registry.ts +6 -12
- package/src/index.ts +413 -6
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +42 -3
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -4
- package/src/orchestration/__tests__/args-meta.test.ts +358 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +1 -8
- package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +312 -0
- package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +382 -0
- package/src/orchestration/__tests__/file-run-store-prune.test.ts +168 -0
- package/src/orchestration/__tests__/file-run-store-throttle.test.ts +168 -0
- package/src/orchestration/__tests__/file-run-store.test.ts +389 -0
- package/src/orchestration/__tests__/helpers/flush-microtasks.ts +13 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +34 -0
- package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +357 -0
- package/src/orchestration/__tests__/lifecycle-recover-crashed.test.ts +289 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +77 -0
- package/src/orchestration/__tests__/run-snapshot.test.ts +353 -0
- package/src/orchestration/__tests__/script-generate.test.ts +314 -0
- package/src/orchestration/__tests__/worker-pending-timeout-abort.test.ts +275 -0
- package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +78 -1
- package/src/orchestration/__tests__/workflow-files.test.ts +186 -0
- package/src/orchestration/__tests__/workflow-run-summary.test.ts +119 -0
- package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +124 -0
- package/src/orchestration/agent-opts-resolver.ts +7 -7
- package/src/orchestration/args-meta.ts +198 -0
- package/src/orchestration/error-recovery.ts +416 -146
- package/src/orchestration/execute-agent-call.ts +9 -9
- package/src/orchestration/file-run-store.ts +327 -0
- package/src/orchestration/launcher.ts +35 -26
- package/src/orchestration/lifecycle.ts +355 -79
- package/src/orchestration/models/agent-call.ts +7 -7
- package/src/orchestration/models/budget.ts +5 -5
- package/src/orchestration/models/run-runtime.ts +13 -13
- package/src/orchestration/models/trace.ts +8 -8
- package/src/orchestration/models/workflow-run.ts +27 -27
- package/src/orchestration/models/workflow-script.ts +4 -4
- package/src/orchestration/run-snapshot.ts +266 -0
- package/src/orchestration/script-generate.ts +154 -0
- package/src/orchestration/script-lint.ts +33 -33
- package/src/orchestration/worker-handle.ts +10 -10
- package/src/orchestration/worker-host.ts +10 -10
- package/src/orchestration/worker-script-builder.ts +401 -346
- package/src/orchestration/workflow-files.ts +37 -11
- package/src/orchestration/workflow-run-summary.ts +69 -0
- package/src/orchestration/workflow-script-registry-impl.ts +31 -9
- package/src/shared/__tests__/agent-ref.test.ts +209 -2
- package/src/shared/__tests__/atomic-write.test.ts +267 -0
- package/src/shared/__tests__/bounded-serialize.test.ts +236 -0
- package/src/shared/__tests__/injection-render.test.ts +518 -0
- package/src/shared/__tests__/resource-discovery-host-roots.test.ts +474 -0
- package/src/shared/__tests__/resource-discovery.test.ts +3 -2
- package/src/shared/agent-ref.ts +143 -2
- package/src/shared/atomic-write.ts +320 -0
- package/src/shared/bounded-serialize.ts +154 -0
- package/src/shared/injection-render.ts +279 -0
- package/src/shared/meta-parser.ts +41 -1
- package/src/shared/resource-discovery.ts +104 -37
- package/src/shared/resource-meta.ts +14 -0
- package/src/shared/xml-injection.ts +9 -9
- package/workflows/README.md +9 -9
- package/workflows/chain.js +4 -2
- package/workflows/map-reduce.js +5 -3
- package/workflows/parallel.js +5 -3
- package/workflows/review-fix-loop-utils.cjs +14 -3
- package/workflows/review-fix-loop.js +18 -7
- package/workflows/scatter-gather.js +4 -2
- package/dist/chunk-3VOERJPJ.js +0 -22
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
// preparer-appserver.test.ts —— [R4] D7 全量语义测试:常驻 HOME 锚定不变量 /
|
|
2
|
+
// allProviders 引导 / 凭据内容 hash 刷新 / 目录锁(O_EXCL 互斥、pid 活即持有、心跳
|
|
3
|
+
// 不参与否决、双接管者竞争闭环、派生后缀)/ pidfile 孤儿自愈三重判据(D6③)。
|
|
4
|
+
// 进程形态用真实短命 node 子进程(活 pid / 死 pid / 命令行形态匹配全真实验证,
|
|
5
|
+
// macOS 本机 ps 可用——CI 同构)。
|
|
6
|
+
// 文件名系 appserver-home.ts 自 preparer.ts 拆分前遗留——被测对象(import 面)为
|
|
7
|
+
// appserver-home.ts,与 preparer.ts 现行 spawn 池语义无关。
|
|
8
|
+
|
|
9
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as os from "node:os";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
15
|
+
|
|
16
|
+
import { resolveJournalPath, resolvePoolDir } from "../../../paths.ts";
|
|
17
|
+
import {
|
|
18
|
+
ZCODE_APPSERVER_LOCKFILE_NAME,
|
|
19
|
+
ZCODE_APPSERVER_PIDFILE_NAME,
|
|
20
|
+
ZCODE_APPSERVER_POOL_KEY,
|
|
21
|
+
ZCODE_POOL_CONFIG_SUFFIX,
|
|
22
|
+
ZCODE_POOL_DB_RELATIVE_PATH,
|
|
23
|
+
} from "../constants.ts";
|
|
24
|
+
import {
|
|
25
|
+
acquireAppServerHome,
|
|
26
|
+
acquireAppServerHomeLock,
|
|
27
|
+
bootstrapAppServerConfig,
|
|
28
|
+
hashPoolConfigProviders,
|
|
29
|
+
hashProviderRegistry,
|
|
30
|
+
isPidAlive,
|
|
31
|
+
isLockHeldByUs,
|
|
32
|
+
probePidLstart,
|
|
33
|
+
reapOrphanAppServer,
|
|
34
|
+
writeAppServerPidFile,
|
|
35
|
+
} from "../appserver-home.ts";
|
|
36
|
+
|
|
37
|
+
const PROVIDER_A = "prov-a";
|
|
38
|
+
const PROVIDER_B = "prov-b";
|
|
39
|
+
|
|
40
|
+
let tmpRoot: string;
|
|
41
|
+
let dataDir: string;
|
|
42
|
+
let v2Path: string;
|
|
43
|
+
/** 每用例启动的活子进程(afterAll 统一收割防泄漏)。 */
|
|
44
|
+
const liveChildren: ChildProcess[] = [];
|
|
45
|
+
|
|
46
|
+
function writeJson(p: string, v: unknown): void {
|
|
47
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
48
|
+
fs.writeFileSync(p, JSON.stringify(v, null, 2));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function baseV2(): Record<string, unknown> {
|
|
52
|
+
return {
|
|
53
|
+
provider: {
|
|
54
|
+
[PROVIDER_A]: { options: { apiKey: "key-a", baseURL: "https://a.example" }, models: { m1: {} } },
|
|
55
|
+
[PROVIDER_B]: { options: { apiKey: "key-b" }, models: { m2: {} } },
|
|
56
|
+
"no-key": { options: {}, models: { m3: {} } },
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 活着的真实子进程(挂 30s——pid 活性/锁判定用)。 */
|
|
62
|
+
function liveChild(): ChildProcess {
|
|
63
|
+
const child = spawn(process.execPath, ["-e", "setTimeout(() => {}, 30000)"], { stdio: "ignore" });
|
|
64
|
+
liveChildren.push(child);
|
|
65
|
+
return child;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 命令行含 app-server 形态的活子进程(三重判据全过分支用)。 */
|
|
69
|
+
function appServerLikeChild(): ChildProcess {
|
|
70
|
+
const child = spawn(
|
|
71
|
+
process.execPath,
|
|
72
|
+
["-e", "/*app-server*/ setTimeout(() => {}, 30000)"],
|
|
73
|
+
{ stdio: "ignore" },
|
|
74
|
+
);
|
|
75
|
+
liveChildren.push(child);
|
|
76
|
+
return child;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 已退出的子进程 pid(等退出完成——死 pid 判定用)。 */
|
|
80
|
+
async function deadPid(): Promise<number> {
|
|
81
|
+
const child = spawn(process.execPath, ["-e", "process.exit(0)"], { stdio: "ignore" });
|
|
82
|
+
const code = await new Promise<number | null>((resolve) => {
|
|
83
|
+
child.once("exit", (c) => resolve(c));
|
|
84
|
+
child.once("error", () => resolve(-1));
|
|
85
|
+
});
|
|
86
|
+
expect(code).toBe(0);
|
|
87
|
+
return child.pid ?? -1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function writeLock(dir: string, pid: number, opts: { mtime?: Date } = {}): string {
|
|
91
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
92
|
+
const lockPath = path.join(dir, ZCODE_APPSERVER_LOCKFILE_NAME);
|
|
93
|
+
fs.writeFileSync(lockPath, JSON.stringify({ pid, acquiredAt: Date.now() }));
|
|
94
|
+
if (opts.mtime !== undefined) fs.utimesSync(lockPath, opts.mtime, opts.mtime);
|
|
95
|
+
return lockPath;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function readJson(p: string): Record<string, unknown> {
|
|
99
|
+
return JSON.parse(fs.readFileSync(p, "utf8")) as Record<string, unknown>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
beforeEach(() => {
|
|
103
|
+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "zcode-prep-appserver-"));
|
|
104
|
+
dataDir = path.join(tmpRoot, "data");
|
|
105
|
+
v2Path = path.join(tmpRoot, "v2.json");
|
|
106
|
+
writeJson(v2Path, baseV2());
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
afterEach(() => {
|
|
110
|
+
for (const child of liveChildren.splice(0)) {
|
|
111
|
+
try {
|
|
112
|
+
child.kill("SIGKILL");
|
|
113
|
+
} catch {
|
|
114
|
+
// 已退出——无妨
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ============================================================
|
|
121
|
+
// 锚定不变量与 allProviders 引导(D7①)
|
|
122
|
+
// ============================================================
|
|
123
|
+
|
|
124
|
+
describe("常驻 HOME 锚定不变量 + allProviders 引导", () => {
|
|
125
|
+
it("poolDir == HOME == db 所在目录:HOME=resolvePoolDir(...,'home-appserver'),dbPath 相对锚 HOME,journal 同落该池", async () => {
|
|
126
|
+
const home = await acquireAppServerHome({ engineDataDir: dataDir, modelRef: `${PROVIDER_A}/m1`, sources: { v2ConfigPath: v2Path } });
|
|
127
|
+
expect(home.poolKey).toBe(ZCODE_APPSERVER_POOL_KEY);
|
|
128
|
+
const poolDir = resolvePoolDir(dataDir, "zcode", ZCODE_APPSERVER_POOL_KEY);
|
|
129
|
+
expect(home.homeDir).toBe(poolDir);
|
|
130
|
+
// db 锚定:dbPath 相对路径 join 池目录后即 SQLite 落点
|
|
131
|
+
expect(path.join(home.homeDir, ZCODE_POOL_DB_RELATIVE_PATH)).toBe(
|
|
132
|
+
path.join(poolDir, ".zcode", "cli", "db", "db.sqlite"),
|
|
133
|
+
);
|
|
134
|
+
// journal 同池(record id 文件名——任务间无冲突)
|
|
135
|
+
expect(resolveJournalPath(dataDir, "zcode", home.poolKey, "bg-1")).toBe(
|
|
136
|
+
path.join(poolDir, "journal-bg-1.jsonl"),
|
|
137
|
+
);
|
|
138
|
+
// 锁文件落 HOME 内
|
|
139
|
+
expect(home.lockPath).toBe(path.join(poolDir, ZCODE_APPSERVER_LOCKFILE_NAME));
|
|
140
|
+
expect(fs.existsSync(home.lockPath)).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("config 写入全部带 apiKey 的 provider(无 apiKey 条目不写);无 plugins 块;model.main 落任务模型", async () => {
|
|
144
|
+
const home = await acquireAppServerHome({ engineDataDir: dataDir, modelRef: `${PROVIDER_B}/m2`, sources: { v2ConfigPath: v2Path } });
|
|
145
|
+
expect(home.wroteConfig).toBe(true);
|
|
146
|
+
expect(home.providerIds.sort()).toEqual([PROVIDER_A, PROVIDER_B]);
|
|
147
|
+
const configPath = path.join(home.homeDir, ...ZCODE_POOL_CONFIG_SUFFIX);
|
|
148
|
+
const written = readJson(configPath);
|
|
149
|
+
const provider = written["provider"] as Record<string, unknown>;
|
|
150
|
+
expect(Object.keys(provider).sort()).toEqual([PROVIDER_A, PROVIDER_B]);
|
|
151
|
+
expect(written["model"]).toEqual({ main: `${PROVIDER_B}/m2` });
|
|
152
|
+
expect("plugins" in written).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("二次获取(同内容源)不重写 config(内容 hash 命中——mtime 免重写的常驻形态等价物)", async () => {
|
|
156
|
+
const first = await acquireAppServerHome({ engineDataDir: dataDir, modelRef: `${PROVIDER_A}/m1`, sources: { v2ConfigPath: v2Path } });
|
|
157
|
+
// 换模型(model.main 变化)+ 内容不变:hash 只覆盖 provider 段——不重写
|
|
158
|
+
fs.utimesSync(v2Path, new Date(), new Date());
|
|
159
|
+
const configPath = path.join(first.homeDir, ...ZCODE_POOL_CONFIG_SUFFIX);
|
|
160
|
+
const before = fs.statSync(configPath).mtimeMs;
|
|
161
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
162
|
+
const second = await bootstrapAppServerConfig({ homeDir: first.homeDir, modelRef: `${PROVIDER_B}/m2`, sources: { v2ConfigPath: v2Path } });
|
|
163
|
+
expect(second.wroteConfig).toBe(false);
|
|
164
|
+
expect(fs.statSync(configPath).mtimeMs).toBe(before);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("凭据变更(provider 内容变化)→ 内容 hash 不一致 → 重写", async () => {
|
|
168
|
+
const first = await acquireAppServerHome({ engineDataDir: dataDir, modelRef: `${PROVIDER_A}/m1`, sources: { v2ConfigPath: v2Path } });
|
|
169
|
+
const configPath = path.join(first.homeDir, ...ZCODE_POOL_CONFIG_SUFFIX);
|
|
170
|
+
// hash 单点:池内现状 hash === 引导 hash
|
|
171
|
+
expect(hashPoolConfigProviders(configPath)).toBe(first.providerHash);
|
|
172
|
+
// 变更 apiKey
|
|
173
|
+
const mutated = baseV2() as { provider: Record<string, { options: { apiKey: string } }> };
|
|
174
|
+
mutated.provider[PROVIDER_A]!.options.apiKey = "key-a-v2";
|
|
175
|
+
writeJson(v2Path, mutated);
|
|
176
|
+
const second = await bootstrapAppServerConfig({ homeDir: first.homeDir, modelRef: `${PROVIDER_A}/m1`, sources: { v2ConfigPath: v2Path } });
|
|
177
|
+
expect(second.wroteConfig).toBe(true);
|
|
178
|
+
expect(hashPoolConfigProviders(configPath)).toBe(second.providerHash);
|
|
179
|
+
const written = readJson(configPath)["provider"] as Record<string, { options: { apiKey: string } }>;
|
|
180
|
+
expect(written[PROVIDER_A]!.options.apiKey).toBe("key-a-v2");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("池内 config 损坏 → hash 判 undefined → 恒重写(torn write 防线)", async () => {
|
|
184
|
+
const first = await acquireAppServerHome({ engineDataDir: dataDir, modelRef: `${PROVIDER_A}/m1`, sources: { v2ConfigPath: v2Path } });
|
|
185
|
+
const configPath = path.join(first.homeDir, ...ZCODE_POOL_CONFIG_SUFFIX);
|
|
186
|
+
fs.writeFileSync(configPath, "{torn", "utf8");
|
|
187
|
+
expect(hashPoolConfigProviders(configPath)).toBeUndefined();
|
|
188
|
+
const second = await bootstrapAppServerConfig({ homeDir: first.homeDir, modelRef: `${PROVIDER_A}/m1`, sources: { v2ConfigPath: v2Path } });
|
|
189
|
+
expect(second.wroteConfig).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("provider 全无 apiKey → 引导报 engine_credential_missing", async () => {
|
|
193
|
+
writeJson(v2Path, { provider: { [PROVIDER_A]: { options: {} } } });
|
|
194
|
+
await expect(
|
|
195
|
+
acquireAppServerHome({ engineDataDir: dataDir, modelRef: `${PROVIDER_A}/m1`, sources: { v2ConfigPath: v2Path } }),
|
|
196
|
+
).rejects.toThrow(/engine_credential_missing/);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("hashProviderRegistry:provider 键序变化不产生假差异;内容变化必差异", () => {
|
|
200
|
+
const a = new Map([[PROVIDER_A, { options: { apiKey: "k" } }], [PROVIDER_B, { options: { apiKey: "k2" } }]]);
|
|
201
|
+
const reordered = new Map([[PROVIDER_B, { options: { apiKey: "k2" } }], [PROVIDER_A, { options: { apiKey: "k" } }]]);
|
|
202
|
+
expect(hashProviderRegistry(a)).toBe(hashProviderRegistry(reordered));
|
|
203
|
+
const changed = new Map([[PROVIDER_A, { options: { apiKey: "k" } }], [PROVIDER_B, { options: { apiKey: "k3" } }]]);
|
|
204
|
+
expect(hashProviderRegistry(a)).not.toBe(hashProviderRegistry(changed));
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// ============================================================
|
|
209
|
+
// 目录锁(D7 所有权隔离)
|
|
210
|
+
// ============================================================
|
|
211
|
+
|
|
212
|
+
describe("目录锁(O_EXCL / pid 活即持有 / 接管 / 派生)", () => {
|
|
213
|
+
it("空目录 → O_EXCL 建锁获得固定名 HOME,lockfile.pid=本进程", () => {
|
|
214
|
+
const lock = acquireAppServerHomeLock(dataDir);
|
|
215
|
+
expect(lock.poolKey).toBe(ZCODE_APPSERVER_POOL_KEY);
|
|
216
|
+
expect(lock.tookOver).toBe(false);
|
|
217
|
+
expect(readJson(lock.lockPath)).toMatchObject({ pid: process.pid });
|
|
218
|
+
expect(isLockHeldByUs(lock.lockPath)).toBe(true);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("锁被活宿主持有 → 派生 home-appserver-2,不碰他人锁/pidfile", () => {
|
|
222
|
+
const holder = liveChild();
|
|
223
|
+
const base = resolvePoolDir(dataDir, "zcode", ZCODE_APPSERVER_POOL_KEY);
|
|
224
|
+
writeLock(base, holder.pid!);
|
|
225
|
+
// 他人 pidfile(D6③ 时序:活宿主持有时派生方不触碰)
|
|
226
|
+
const pidfile = path.join(base, ZCODE_APPSERVER_PIDFILE_NAME);
|
|
227
|
+
fs.writeFileSync(pidfile, JSON.stringify({ pid: holder.pid, lstart: "whatever" }));
|
|
228
|
+
|
|
229
|
+
const lock = acquireAppServerHomeLock(dataDir);
|
|
230
|
+
expect(lock.poolKey).toBe(`${ZCODE_APPSERVER_POOL_KEY}-2`);
|
|
231
|
+
expect(lock.homeDir).not.toBe(base);
|
|
232
|
+
// 他人锁原样(pid 未被改写)
|
|
233
|
+
expect(readJson(path.join(base, ZCODE_APPSERVER_LOCKFILE_NAME))).toMatchObject({ pid: holder.pid });
|
|
234
|
+
// 他人 pidfile 原样
|
|
235
|
+
expect(fs.existsSync(pidfile)).toBe(true);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("pid 活即持有:心跳 mtime 过期不参与否决(古老 mtime 仍判活 → 派生)", () => {
|
|
239
|
+
const holder = liveChild();
|
|
240
|
+
const base = resolvePoolDir(dataDir, "zcode", ZCODE_APPSERVER_POOL_KEY);
|
|
241
|
+
const ancient = new Date(Date.now() - 3 * 60 * 60 * 1000);
|
|
242
|
+
writeLock(base, holder.pid!, { mtime: ancient });
|
|
243
|
+
const lock = acquireAppServerHomeLock(dataDir);
|
|
244
|
+
expect(lock.poolKey).toBe(`${ZCODE_APPSERVER_POOL_KEY}-2`); // 未被偷锁
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("持锁宿主已死 → 接管:删旧锁 + O_EXCL 重建(lockfile.pid=本进程,HOME 用固定名)", async () => {
|
|
248
|
+
const dead = await deadPid();
|
|
249
|
+
expect(isPidAlive(dead)).toBe(false);
|
|
250
|
+
const base = resolvePoolDir(dataDir, "zcode", ZCODE_APPSERVER_POOL_KEY);
|
|
251
|
+
writeLock(base, dead);
|
|
252
|
+
const lock = acquireAppServerHomeLock(dataDir);
|
|
253
|
+
expect(lock.poolKey).toBe(ZCODE_APPSERVER_POOL_KEY); // 接管固定名 HOME
|
|
254
|
+
expect(lock.tookOver).toBe(true);
|
|
255
|
+
expect(readJson(lock.lockPath)).toMatchObject({ pid: process.pid });
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("双接管者竞争闭环:接管 unlink 后他方先行建锁(活 pid)→ 本方 O_EXCL 失败重走循环 → 派生", async () => {
|
|
259
|
+
const dead = await deadPid();
|
|
260
|
+
const base = resolvePoolDir(dataDir, "zcode", ZCODE_APPSERVER_POOL_KEY);
|
|
261
|
+
writeLock(base, dead);
|
|
262
|
+
const rival = liveChild();
|
|
263
|
+
const lock = acquireAppServerHomeLock(dataDir, {
|
|
264
|
+
hooks: {
|
|
265
|
+
// 竞争窗口注入:本方删旧锁后、O_EXCL 前,他方抢先以活 pid 建锁
|
|
266
|
+
afterTakeoverUnlink: (lockPath) => {
|
|
267
|
+
fs.writeFileSync(lockPath, JSON.stringify({ pid: rival.pid, acquiredAt: Date.now() }));
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
// 本方 O_EXCL 失败 → 重读锁 → 活 pid(rival)→ 派生 -2;且 rival 的锁未被覆盖
|
|
272
|
+
expect(lock.poolKey).toBe(`${ZCODE_APPSERVER_POOL_KEY}-2`);
|
|
273
|
+
expect(readJson(path.join(base, ZCODE_APPSERVER_LOCKFILE_NAME))).toMatchObject({ pid: rival.pid });
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("锁文件损坏(非 JSON)→ 按无主接管重建", () => {
|
|
277
|
+
const base = resolvePoolDir(dataDir, "zcode", ZCODE_APPSERVER_POOL_KEY);
|
|
278
|
+
fs.mkdirSync(base, { recursive: true });
|
|
279
|
+
fs.writeFileSync(path.join(base, ZCODE_APPSERVER_LOCKFILE_NAME), "not json");
|
|
280
|
+
const lock = acquireAppServerHomeLock(dataDir);
|
|
281
|
+
expect(lock.poolKey).toBe(ZCODE_APPSERVER_POOL_KEY);
|
|
282
|
+
expect(lock.tookOver).toBe(true);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("acquireAppServerHome 组合:接管路径触发 pidfile 孤儿回收,非接管路径不触发", async () => {
|
|
286
|
+
// 非接管(空目录):orphanReap 恒 not-applicable
|
|
287
|
+
const fresh = await acquireAppServerHome({ engineDataDir: dataDir, modelRef: `${PROVIDER_A}/m1`, sources: { v2ConfigPath: v2Path } });
|
|
288
|
+
expect(fresh.orphanReap).toBe("not-applicable");
|
|
289
|
+
// 接管(死宿主锁 + 无 pidfile):回收执行(no-pidfile 分支)
|
|
290
|
+
const dead = await deadPid();
|
|
291
|
+
writeLock(resolvePoolDir(dataDir, "zcode", ZCODE_APPSERVER_POOL_KEY), dead);
|
|
292
|
+
// 锁被本进程测试进程重新持有前需先释放(acquire 会接管同目录——直接再跑组合入口)
|
|
293
|
+
const took = await acquireAppServerHome({ engineDataDir: dataDir, modelRef: `${PROVIDER_A}/m1`, sources: { v2ConfigPath: v2Path } });
|
|
294
|
+
expect(took.tookOver).toBe(true);
|
|
295
|
+
expect(["no-pidfile", "pid-dead", "criteria-mismatch", "reaped"]).toContain(took.orphanReap);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// ============================================================
|
|
300
|
+
// pidfile 孤儿自愈三重判据(D6③)
|
|
301
|
+
// ============================================================
|
|
302
|
+
|
|
303
|
+
describe("pidfile 孤儿自愈(三重判据)", () => {
|
|
304
|
+
it("三判据全过(pid 活 + lstart 一致 + 命令行含 app-server)→ 回收(SIGTERM)+ pidfile 清理", async () => {
|
|
305
|
+
const homeDir = resolvePoolDir(dataDir, "zcode", ZCODE_APPSERVER_POOL_KEY);
|
|
306
|
+
fs.mkdirSync(homeDir, { recursive: true });
|
|
307
|
+
const orphan = appServerLikeChild();
|
|
308
|
+
await writeAppServerPidFile(homeDir, orphan.pid!);
|
|
309
|
+
const status = await reapOrphanAppServer(homeDir, { graceMs: 500 });
|
|
310
|
+
expect(status).toBe("reaped");
|
|
311
|
+
expect(fs.existsSync(path.join(homeDir, ZCODE_APPSERVER_PIDFILE_NAME))).toBe(false);
|
|
312
|
+
// 回收生效:进程在 grace 内死亡(SIGTERM 对 node 默认终止)
|
|
313
|
+
await vi.waitFor(() => expect(isPidAlive(orphan.pid!)).toBe(false), { timeout: 3_000 });
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("时间戳不一致(pid 活但 lstart 不符——pid 复用形态)→ 不杀,清陈旧 pidfile", async () => {
|
|
317
|
+
const homeDir = resolvePoolDir(dataDir, "zcode", ZCODE_APPSERVER_POOL_KEY);
|
|
318
|
+
fs.mkdirSync(homeDir, { recursive: true });
|
|
319
|
+
const victim = appServerLikeChild();
|
|
320
|
+
fs.writeFileSync(
|
|
321
|
+
path.join(homeDir, ZCODE_APPSERVER_PIDFILE_NAME),
|
|
322
|
+
JSON.stringify({ pid: victim.pid, lstart: "Mon Jan 1 00:00:00 2020", startedAt: 1 }),
|
|
323
|
+
);
|
|
324
|
+
const status = await reapOrphanAppServer(homeDir);
|
|
325
|
+
expect(status).toBe("criteria-mismatch");
|
|
326
|
+
expect(isPidAlive(victim.pid!)).toBe(true); // 不误杀
|
|
327
|
+
expect(fs.existsSync(path.join(homeDir, ZCODE_APPSERVER_PIDFILE_NAME))).toBe(false);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("命令行不匹配 app-server 形态(wrapper 假阴性)→ 不杀,清陈旧 pidfile", async () => {
|
|
331
|
+
const homeDir = resolvePoolDir(dataDir, "zcode", ZCODE_APPSERVER_POOL_KEY);
|
|
332
|
+
fs.mkdirSync(homeDir, { recursive: true });
|
|
333
|
+
const victim = liveChild(); // 普通 node -e setTimeout——无 app-server 字样
|
|
334
|
+
const lstart = await probePidLstart(victim.pid!);
|
|
335
|
+
expect(lstart).toBeDefined();
|
|
336
|
+
fs.writeFileSync(
|
|
337
|
+
path.join(homeDir, ZCODE_APPSERVER_PIDFILE_NAME),
|
|
338
|
+
JSON.stringify({ pid: victim.pid, lstart, startedAt: Date.now() }),
|
|
339
|
+
);
|
|
340
|
+
const status = await reapOrphanAppServer(homeDir);
|
|
341
|
+
expect(status).toBe("criteria-mismatch");
|
|
342
|
+
expect(isPidAlive(victim.pid!)).toBe(true);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("pid 已死(宿主崩溃残留)→ 不杀任何进程,清陈旧 pidfile", async () => {
|
|
346
|
+
const homeDir = resolvePoolDir(dataDir, "zcode", ZCODE_APPSERVER_POOL_KEY);
|
|
347
|
+
fs.mkdirSync(homeDir, { recursive: true });
|
|
348
|
+
const dead = await deadPid();
|
|
349
|
+
fs.writeFileSync(
|
|
350
|
+
path.join(homeDir, ZCODE_APPSERVER_PIDFILE_NAME),
|
|
351
|
+
JSON.stringify({ pid: dead, lstart: "x", startedAt: 1 }),
|
|
352
|
+
);
|
|
353
|
+
expect(await reapOrphanAppServer(homeDir)).toBe("pid-dead");
|
|
354
|
+
expect(fs.existsSync(path.join(homeDir, ZCODE_APPSERVER_PIDFILE_NAME))).toBe(false);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("无 pidfile → no-op", async () => {
|
|
358
|
+
const homeDir = resolvePoolDir(dataDir, "zcode", ZCODE_APPSERVER_POOL_KEY);
|
|
359
|
+
fs.mkdirSync(homeDir, { recursive: true });
|
|
360
|
+
expect(await reapOrphanAppServer(homeDir)).toBe("no-pidfile");
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("writeAppServerPidFile 记录真实 ps lstart(三重判据数据源自洽)", async () => {
|
|
364
|
+
const homeDir = resolvePoolDir(dataDir, "zcode", ZCODE_APPSERVER_POOL_KEY);
|
|
365
|
+
fs.mkdirSync(homeDir, { recursive: true });
|
|
366
|
+
const child = appServerLikeChild();
|
|
367
|
+
await writeAppServerPidFile(homeDir, child.pid!);
|
|
368
|
+
const pidfile = readJson(path.join(homeDir, ZCODE_APPSERVER_PIDFILE_NAME));
|
|
369
|
+
expect(pidfile["pid"]).toBe(child.pid);
|
|
370
|
+
const recorded = pidfile["lstart"];
|
|
371
|
+
expect(typeof recorded).toBe("string");
|
|
372
|
+
expect(recorded).toBe(await probePidLstart(child.pid!));
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// ============================================================
|
|
377
|
+
// pid 活性探测边界
|
|
378
|
+
// ============================================================
|
|
379
|
+
|
|
380
|
+
describe("isPidAlive 边界", () => {
|
|
381
|
+
it("非法 pid / pid 0 / 负值 → 恒死", () => {
|
|
382
|
+
expect(isPidAlive(0)).toBe(false);
|
|
383
|
+
expect(isPidAlive(-1)).toBe(false);
|
|
384
|
+
expect(isPidAlive(Number.NaN)).toBe(false);
|
|
385
|
+
expect(isPidAlive(1.5)).toBe(false);
|
|
386
|
+
});
|
|
387
|
+
});
|