@zhushanwen/subagent-core 0.6.0 → 0.7.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 +1 -1
- package/dist/chunk-KXVBIVOU.js +16 -0
- package/dist/chunk-T2SYBW3I.js +24 -0
- package/dist/chunk-VURUAGMM.js +2305 -0
- package/dist/{types-C3fE3R9x.d.cts → engine-discovery-scan-ocpM8FmI.d.cts} +770 -668
- package/dist/{types-C3fE3R9x.d.ts → engine-discovery-scan-ocpM8FmI.d.ts} +770 -668
- package/dist/execution/engine/engine-discovery-scan.cjs +2210 -0
- package/dist/execution/engine/engine-discovery-scan.d.cts +4 -0
- package/dist/execution/engine/engine-discovery-scan.d.ts +4 -0
- package/dist/execution/engine/engine-discovery-scan.js +20 -0
- package/dist/execution/engine/paths.cjs +6 -23
- package/dist/execution/engine/paths.d.cts +1 -8
- package/dist/execution/engine/paths.d.ts +1 -8
- package/dist/execution/engine/paths.js +1 -1
- package/dist/execution/relay-env.cjs +12 -28
- package/dist/execution/relay-env.d.cts +1 -33
- package/dist/execution/relay-env.d.ts +1 -33
- package/dist/execution/relay-env.js +5 -3
- package/dist/index.cjs +14045 -16059
- package/dist/index.d.cts +1035 -1579
- package/dist/index.d.ts +1035 -1579
- package/dist/index.js +9838 -13800
- package/dist.bundle/index.cjs +28875 -30323
- package/package.json +20 -26
- package/src/__tests__/record-store-last-line.test.ts +16 -7
- package/src/core/error-message.ts +7 -6
- package/src/core/host-services.ts +8 -2
- package/src/core/notify-ports.ts +17 -2
- package/src/execution/__tests__/chat-engine-routing.test.ts +62 -28
- package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +255 -0
- package/src/execution/__tests__/collect-coordinator-service.test.ts +36 -47
- package/src/execution/__tests__/collect-mixed-dispatch.test.ts +20 -53
- package/src/execution/__tests__/delivery-methods.test.ts +179 -237
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +0 -2
- package/src/execution/__tests__/execute-nesting.test.ts +20 -13
- package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +20 -33
- package/src/execution/__tests__/gc-timer.test.ts +7 -22
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +20 -10
- package/src/execution/__tests__/helpers/fake-engine-port.ts +187 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +36 -9
- package/src/execution/__tests__/helpers/subagent-service-mocks.ts +1 -1
- package/src/execution/__tests__/idle-gc.test.ts +200 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +26 -21
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +5 -2
- package/src/execution/__tests__/reconcile-sweep.test.ts +200 -0
- package/src/execution/__tests__/record-store.test.ts +60 -6
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +20 -13
- package/src/execution/__tests__/round-supervisor.test.ts +293 -0
- package/src/execution/__tests__/session-pending.test.ts +119 -17
- package/src/execution/__tests__/stream-sink-retirement.test.ts +10 -10
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +1 -11
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +33 -30
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +2 -9
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +59 -129
- package/src/execution/__tests__/subagent-service.test.ts +2 -2
- package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +109 -7
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +108 -36
- package/src/execution/__tests__/sync-collect-recovery.test.ts +21 -36
- package/src/execution/__tests__/workflow-state-root.test.ts +232 -0
- package/src/execution/agent-registry.ts +6 -2
- package/src/execution/dialog-queue.ts +6 -73
- package/src/execution/engine/__tests__/common/capability-gate.test.ts +115 -15
- package/src/execution/engine/__tests__/common/errors.test.ts +11 -18
- package/src/execution/engine/__tests__/common/kill-chain.test.ts +7 -0
- package/src/execution/engine/__tests__/common/nesting-guard.test.ts +9 -3
- package/src/execution/engine/__tests__/common/session-view-service.test.ts +90 -3
- package/src/execution/engine/__tests__/conformance/H9-test-disposition.md +116 -0
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +377 -0
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/recorded.fixture.json +331 -0
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +213 -0
- package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +230 -0
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +69 -42
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +4 -81
- package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +67 -44
- package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +81 -54
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +133 -70
- package/src/execution/engine/__tests__/conformance/engine-crash.test.ts +143 -0
- package/src/execution/engine/__tests__/conformance/fake-engine-capabilities.ts +19 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +7 -2
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +6 -5
- package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +398 -0
- package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +221 -0
- package/src/execution/engine/__tests__/conformance/round-liveness-supervisor.test.ts +302 -0
- package/src/execution/engine/__tests__/conformance/static-spawn-guard.test.ts +58 -0
- package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +558 -0
- package/src/execution/engine/__tests__/host-task-spec.test.ts +45 -0
- package/src/execution/engine/__tests__/model-ref-split.test.ts +74 -0
- package/src/execution/engine/__tests__/registry.test.ts +206 -1
- package/src/execution/engine/__tests__/routing.test.ts +97 -0
- package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +97 -0
- package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +294 -0
- package/src/execution/engine/client/__tests__/engine-client.test.ts +516 -0
- package/src/execution/engine/client/__tests__/mirror.test.ts +67 -0
- package/src/execution/engine/client/__tests__/pid-file.test.ts +257 -0
- package/src/execution/engine/client/__tests__/protocol-closure.test.ts +95 -0
- package/src/execution/engine/client/__tests__/remote-engine.test.ts +457 -0
- package/src/execution/engine/client/client-options.ts +103 -0
- package/src/execution/engine/client/engine-client.ts +687 -0
- package/src/execution/engine/client/mirror.ts +167 -0
- package/src/execution/engine/client/pid-file.ts +392 -0
- package/src/execution/engine/client/reaper.ts +95 -0
- package/src/execution/engine/client/remote-engine.ts +441 -0
- package/src/execution/engine/client/reverse-router.ts +255 -0
- package/src/execution/engine/common/capability-gate.ts +109 -22
- package/src/execution/engine/common/errors.ts +9 -25
- package/src/execution/engine/common/event-journal.ts +4 -50
- package/src/execution/engine/common/journal-replay.ts +3 -1
- package/src/execution/engine/common/kill-chain.ts +18 -251
- package/src/execution/engine/common/nesting-guard.ts +18 -114
- package/src/execution/engine/common/pool-manager.ts +4 -0
- package/src/execution/engine/common/session-view-service.ts +56 -84
- package/src/execution/engine/config.ts +151 -0
- package/src/execution/engine/d8-compat.ts +346 -0
- package/src/execution/engine/engine-discovery-roots.ts +107 -0
- package/src/execution/engine/engine-discovery-scan.ts +269 -0
- package/src/execution/engine/engine-discovery.ts +30 -7
- package/src/execution/engine/engine-inspect-package.ts +282 -0
- package/src/execution/engine/engine-manifest.ts +244 -0
- package/src/execution/engine/host/__tests__/host-askuser-endpoint.test.ts +123 -0
- package/src/execution/engine/host/__tests__/host-bridge.test.ts +135 -0
- package/src/execution/engine/host/__tests__/spawned-children.test.ts +73 -0
- package/src/execution/engine/host/host-bridge.ts +150 -0
- package/src/execution/engine/host/host-ui-endpoint.ts +47 -0
- package/src/execution/engine/host/pi-host-binding.ts +91 -0
- package/src/execution/engine/host/spawned-children.ts +139 -0
- package/src/execution/engine/host-task-spec.ts +29 -2
- package/src/execution/engine/model-validation.ts +55 -2
- package/src/execution/engine/paths.ts +16 -41
- package/src/execution/engine/port.ts +48 -2
- package/src/execution/engine/registry.ts +153 -34
- package/src/execution/engine/routing.ts +225 -52
- package/src/execution/engine/types.ts +25 -200
- package/src/execution/finalize-record.ts +25 -0
- package/src/execution/idle-gc.ts +89 -14
- package/src/execution/lifecycle-manager.ts +10 -7
- package/src/execution/lifecycle-predicates.ts +10 -8
- package/src/execution/record-store.ts +29 -2
- package/src/execution/relay-env.ts +19 -36
- package/src/execution/round-supervisor/domain.ts +89 -0
- package/src/execution/round-supervisor/index.ts +33 -0
- package/src/execution/round-supervisor/notify-accounting.ts +87 -0
- package/src/execution/round-supervisor/reconcile-sweep.ts +235 -0
- package/src/execution/round-supervisor/service-binding.test.ts +661 -0
- package/src/execution/round-supervisor/service-binding.ts +273 -0
- package/src/execution/round-supervisor/supervisor.ts +426 -0
- package/src/execution/session-pending.ts +68 -74
- package/src/execution/settled-watchdog.ts +39 -0
- package/src/execution/subagent-service.ts +746 -400
- package/src/execution/subprocess-agent-runner.ts +16 -8
- package/src/execution/types.ts +41 -117
- package/src/execution/ui-channels.ts +13 -214
- package/src/execution/ui-request-handler-factory.ts +7 -4
- package/src/execution/ui-request-observability.ts +2 -2
- package/src/execution/workflow-state-root.ts +67 -0
- package/src/execution/worktree-git-ops.ts +5 -0
- package/src/execution/worktree-manager.ts +11 -1
- package/src/index.ts +55 -26
- package/src/orchestration/__tests__/execute-agent-call.test.ts +1 -1
- package/src/orchestration/execute-agent-call.ts +1 -1
- package/src/orchestration/file-run-store.ts +144 -59
- package/src/orchestration/models/types.ts +43 -42
- package/src/shared/zcode-model-ref.ts +31 -0
- package/dist/chunk-43ONBFZX.js +0 -240
- package/dist/chunk-A4IVZWVX.js +0 -31
- package/dist/chunk-APZY4IME.js +0 -27
- package/dist/chunk-V3VHZ2VX.js +0 -59
- package/dist/execution/engine/engines/zcode/constants.cjs +0 -107
- package/dist/execution/engine/engines/zcode/constants.d.cts +0 -135
- package/dist/execution/engine/engines/zcode/constants.d.ts +0 -135
- package/dist/execution/engine/engines/zcode/constants.js +0 -54
- package/dist/execution/engine/engines/zcode/reader.cjs +0 -276
- package/dist/execution/engine/engines/zcode/reader.d.cts +0 -21
- package/dist/execution/engine/engines/zcode/reader.d.ts +0 -21
- package/dist/execution/engine/engines/zcode/reader.js +0 -9
- package/src/__tests__/append-system-prompt-assembly.test.ts +0 -243
- package/src/__tests__/fr4-get-state-handshake.test.ts +0 -125
- package/src/__tests__/session-runner.test.ts +0 -69
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +0 -490
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +0 -191
- package/src/execution/__tests__/descendant-sweep-guards.test.ts +0 -220
- package/src/execution/__tests__/descendant-sweep.test.ts +0 -269
- package/src/execution/__tests__/engine-model-validation.test.ts +0 -741
- package/src/execution/__tests__/epipe-fallback.test.ts +0 -248
- package/src/execution/__tests__/execution-runtime-face.test.ts +0 -272
- package/src/execution/__tests__/get-state-handshake.test.ts +0 -127
- package/src/execution/__tests__/helpers/session-runner-mocks.ts +0 -113
- package/src/execution/__tests__/keep-alive-no-progress.test.ts +0 -280
- package/src/execution/__tests__/kill-all-escalation.test.ts +0 -196
- package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +0 -113
- package/src/execution/__tests__/output-collector.test.ts +0 -469
- package/src/execution/__tests__/pi-invocation.test.ts +0 -166
- package/src/execution/__tests__/recursive-visibility-env.test.ts +0 -149
- package/src/execution/__tests__/rpc-mode.test.ts +0 -89
- package/src/execution/__tests__/run-and-finalize-anchoring.test.ts +0 -387
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +0 -277
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +0 -254
- package/src/execution/__tests__/run-spawn-edges.test.ts +0 -1079
- package/src/execution/__tests__/run-spawn-integration.test.ts +0 -933
- package/src/execution/__tests__/run-spawn-resume.test.ts +0 -322
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +0 -196
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +0 -207
- package/src/execution/__tests__/service-kill-escalation.test.ts +0 -91
- package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +0 -110
- package/src/execution/__tests__/session-runner-close-prune.test.ts +0 -181
- package/src/execution/__tests__/session-runner-dispatch.test.ts +0 -489
- package/src/execution/__tests__/session-runner-epipe.test.ts +0 -179
- package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +0 -227
- package/src/execution/__tests__/session-runner-lifecycle-helpers.test.ts +0 -290
- package/src/execution/__tests__/session-runner-schema-env.test.ts +0 -352
- package/src/execution/__tests__/settled-watchdog.test.ts +0 -524
- package/src/execution/__tests__/spawn-args.test.ts +0 -446
- package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +0 -192
- package/src/execution/__tests__/spawn-event-adapter.test.ts +0 -167
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +0 -208
- package/src/execution/__tests__/spawned-children.test.ts +0 -236
- package/src/execution/__tests__/start-sync-model-guard.test.ts +0 -150
- package/src/execution/__tests__/stdin-writer.test.ts +0 -464
- package/src/execution/__tests__/subagent-service-message-close.test.ts +0 -693
- package/src/execution/__tests__/temp-prompt.test.ts +0 -53
- package/src/execution/__tests__/timeout-integration.test.ts +0 -616
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +0 -194
- package/src/execution/__tests__/turn-limiter.test.ts +0 -65
- package/src/execution/__tests__/ui-request-handler.test.ts +0 -205
- package/src/execution/__tests__/ui-request-queue.test.ts +0 -298
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +0 -230
- package/src/execution/engine/__tests__/common/schema-emulation.test.ts +0 -128
- package/src/execution/engine/__tests__/common/session-view-service-zcode-dbpath.test.ts +0 -149
- package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +0 -28
- package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +0 -128
- package/src/execution/engine/__tests__/paths.test.ts +0 -39
- package/src/execution/engine/common/schema-emulation.ts +0 -189
- package/src/execution/engine/common/session-view-projection.ts +0 -51
- package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +0 -615
- package/src/execution/engine/engines/pi/__tests__/reader.test.ts +0 -155
- package/src/execution/engine/engines/pi/__tests__/spawn-opts-direct.test.ts +0 -257
- package/src/execution/engine/engines/pi/argv-mirror.ts +0 -135
- package/src/execution/engine/engines/pi/get-state-handshake.ts +0 -179
- package/src/execution/engine/engines/pi/output-collector.ts +0 -317
- package/src/execution/engine/engines/pi/pi-engine.ts +0 -811
- package/src/execution/engine/engines/pi/pi-invocation.ts +0 -120
- package/src/execution/engine/engines/pi/reader.ts +0 -59
- package/src/execution/engine/engines/pi/registration.ts +0 -37
- package/src/execution/engine/engines/pi/session-runner.ts +0 -2840
- package/src/execution/engine/engines/pi/spawn-event-adapter.ts +0 -404
- package/src/execution/engine/engines/pi/stdin-writer.ts +0 -199
- package/src/execution/engine/engines/pi/temp-prompt.ts +0 -62
- package/src/execution/engine/engines/pi/turn-limiter.ts +0 -102
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +0 -301
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +0 -36
- package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +0 -321
- package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +0 -473
- package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +0 -43
- package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +0 -124
- package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +0 -210
- package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +0 -71
- package/src/execution/engine/engines/zcode/__tests__/session-channel-dispose-harvest.test.ts +0 -247
- package/src/execution/engine/engines/zcode/__tests__/session-channel-turn-timers.test.ts +0 -451
- package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +0 -792
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +0 -782
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +0 -168
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-dispose.test.ts +0 -210
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-retry.test.ts +0 -353
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-status.test.ts +0 -282
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-timeout.test.ts +0 -372
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +0 -145
- package/src/execution/engine/engines/zcode/appserver-launcher.ts +0 -167
- package/src/execution/engine/engines/zcode/connection.ts +0 -598
- package/src/execution/engine/engines/zcode/constants.ts +0 -176
- package/src/execution/engine/engines/zcode/golden-sample.ts +0 -47
- package/src/execution/engine/engines/zcode/parser.ts +0 -102
- package/src/execution/engine/engines/zcode/preparer.ts +0 -235
- package/src/execution/engine/engines/zcode/reader.ts +0 -381
- package/src/execution/engine/engines/zcode/registration.ts +0 -37
- package/src/execution/engine/engines/zcode/session-channel.ts +0 -968
- package/src/execution/engine/engines/zcode/zcode-engine.ts +0 -1431
- package/src/execution/round-settlement.ts +0 -93
- package/src/execution/ui-request-queue.ts +0 -215
- package/src/shared/schema-env.ts +0 -44
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
// engine-discovery-scan.test.ts —— [W4] 引擎发现器(impl-plan §2.4 / A4/A12):
|
|
2
|
+
// manifest schema 字段级正反例、三级搜索路径、L1 分隔符与非法条目、engines.json 投影
|
|
3
|
+
// 合流与幂等零写、同 id 覆盖留痕、bin 不可执行标记、ensureEngineDiscovered 补扫。
|
|
4
|
+
//
|
|
5
|
+
// 纪律:fixture 一律 mkdtempSync(tmpdir()) 自建自删(禁真实数据目录);L2 根在用例内
|
|
6
|
+
// 显式传 nodeModuleRoots(屏蔽宿主 node_modules 的真实引擎包污染);env 用注入对象
|
|
7
|
+
// (不动 process.env——vitest.setup 净化面之外零残留)。
|
|
8
|
+
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as os from "node:os";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
14
|
+
|
|
15
|
+
import type { SubagentEnginesFile } from "@xyz-agent/extension-protocol";
|
|
16
|
+
|
|
17
|
+
import { configureCore, resetCoreForTests } from "../../../core/host-services.ts";
|
|
18
|
+
import type { DiscoveryRoot } from "../../../core/host-services.ts";
|
|
19
|
+
import { clearEngines, getEngine, hasEngine, listEngines, registerEngine } from "../registry.ts";
|
|
20
|
+
import type { EnginePort } from "../port.ts";
|
|
21
|
+
import { getEnginesFilePath, syncEnginesFile } from "../engine-discovery.ts";
|
|
22
|
+
import {
|
|
23
|
+
ENGINE_ROOTS_ENV,
|
|
24
|
+
deriveNodeModuleRoots,
|
|
25
|
+
discoverAndRegisterEngines,
|
|
26
|
+
ensureEngineDiscovered,
|
|
27
|
+
parseEngineRootsEnv,
|
|
28
|
+
scanEngines,
|
|
29
|
+
} from "../engine-discovery-scan.ts";
|
|
30
|
+
|
|
31
|
+
// ── fixture helpers ─────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/** 可执行 bin 脚本内容(POSIX shebang;发现器只做 accessSync(X_OK),不执行)。 */
|
|
34
|
+
const BIN_CONTENT = "#!/bin/sh\nexit 0\n";
|
|
35
|
+
|
|
36
|
+
interface PkgOptions {
|
|
37
|
+
/** bin 文件权限;缺省 0o755(可执行)。0o644 用于「不可执行」反例。 */
|
|
38
|
+
binMode?: number;
|
|
39
|
+
/** bin 文件不落盘(bin 缺失反例)。 */
|
|
40
|
+
omitBin?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 在 rootDir 下造一个带 manifest 的引擎包,返回包目录。 */
|
|
44
|
+
function makeEnginePkg(
|
|
45
|
+
rootDir: string,
|
|
46
|
+
pkgDirName: string,
|
|
47
|
+
manifest: Record<string, unknown>,
|
|
48
|
+
opts: PkgOptions = {},
|
|
49
|
+
): string {
|
|
50
|
+
const pkgDir = path.join(rootDir, pkgDirName);
|
|
51
|
+
fs.mkdirSync(pkgDir, { recursive: true });
|
|
52
|
+
const binRel = "./bin/cli.mjs";
|
|
53
|
+
fs.writeFileSync(
|
|
54
|
+
path.join(pkgDir, "package.json"),
|
|
55
|
+
JSON.stringify({
|
|
56
|
+
name: `test-${pkgDirName}`,
|
|
57
|
+
bin: { "test-engine-cli": binRel },
|
|
58
|
+
"xyz-agent": { subagentEngine: { id: pkgDirName, bin: "test-engine-cli", protocol: 1, ...manifest } },
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
if (!opts.omitBin) {
|
|
62
|
+
const binPath = path.join(pkgDir, binRel);
|
|
63
|
+
fs.mkdirSync(path.dirname(binPath), { recursive: true });
|
|
64
|
+
fs.writeFileSync(binPath, BIN_CONTENT);
|
|
65
|
+
fs.chmodSync(binPath, opts.binMode ?? 0o755);
|
|
66
|
+
}
|
|
67
|
+
return pkgDir;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 收集宿主日志的 HostServices 注入(warn/debug 留痕断言用)。 */
|
|
71
|
+
function makeLogCollector(): { logs: Array<{ level: string; message: string }>; host: Parameters<typeof configureCore>[0] } {
|
|
72
|
+
const logs: Array<{ level: string; message: string }> = [];
|
|
73
|
+
const host = {
|
|
74
|
+
dataRoot: () => "/test-data-root",
|
|
75
|
+
log: (level: string, _component: string, message: string) => {
|
|
76
|
+
logs.push({ level, message });
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
return { logs, host: host as unknown as Parameters<typeof configureCore>[0] };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 扫描选项基线:屏蔽 L2(宿主 node_modules 污染)、env 注入(roots 走 L1 env 根)、
|
|
83
|
+
* 数据根走 env。 */
|
|
84
|
+
function scanOpts(overrides: {
|
|
85
|
+
env?: Record<string, string | undefined>;
|
|
86
|
+
/** L1 发现根(经 ENGINE_ROOTS_ENV 注入;与 overrides.env 拼接)。 */
|
|
87
|
+
roots?: string[];
|
|
88
|
+
nodeModuleRoots?: string[];
|
|
89
|
+
extraRoots?: DiscoveryRoot[];
|
|
90
|
+
agentDir?: string;
|
|
91
|
+
}) {
|
|
92
|
+
const env: Record<string, string | undefined> = {
|
|
93
|
+
XYZ_AGENT_DATA_DIR: "/test-data-dir",
|
|
94
|
+
...overrides.env,
|
|
95
|
+
};
|
|
96
|
+
if (overrides.roots !== undefined) {
|
|
97
|
+
env[ENGINE_ROOTS_ENV] = overrides.roots.join(path.delimiter);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
hostKind: "test",
|
|
101
|
+
env,
|
|
102
|
+
nodeModuleRoots: overrides.nodeModuleRoots ?? [],
|
|
103
|
+
...(overrides.extraRoots !== undefined ? { extraRoots: overrides.extraRoots } : {}),
|
|
104
|
+
...(overrides.agentDir !== undefined ? { agentDir: overrides.agentDir } : {}),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function stubEngine(id: string): EnginePort {
|
|
109
|
+
return {
|
|
110
|
+
id,
|
|
111
|
+
capabilities: () => ({
|
|
112
|
+
schemaEnforcement: "emulated",
|
|
113
|
+
steer: "unsupported",
|
|
114
|
+
conversation: "unsupported",
|
|
115
|
+
personaInjection: "prompt",
|
|
116
|
+
eventGranularity: "coarse",
|
|
117
|
+
sandbox: "none",
|
|
118
|
+
sessionRead: "outcome-only",
|
|
119
|
+
resume: "unsupported",
|
|
120
|
+
interrupt: "kill-only",
|
|
121
|
+
permissionMode: "ignored",
|
|
122
|
+
maxTurns: false,
|
|
123
|
+
}),
|
|
124
|
+
probe: async () => ({ ok: true, engineVersion: "test", checks: [] }),
|
|
125
|
+
run: async () => {
|
|
126
|
+
throw new Error("unused");
|
|
127
|
+
},
|
|
128
|
+
interact: async () => {
|
|
129
|
+
throw new Error("unused");
|
|
130
|
+
},
|
|
131
|
+
read: async () => ({ engineId: id, turns: [], source: "outcome-only" }),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const FULL_CAPABILITIES = {
|
|
136
|
+
schemaEnforcement: "native",
|
|
137
|
+
steer: "native",
|
|
138
|
+
conversation: "native",
|
|
139
|
+
personaInjection: "file",
|
|
140
|
+
eventGranularity: "stream",
|
|
141
|
+
sandbox: "native",
|
|
142
|
+
sessionRead: "full",
|
|
143
|
+
resume: "native",
|
|
144
|
+
interrupt: "native",
|
|
145
|
+
permissionMode: "native",
|
|
146
|
+
maxTurns: true,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
let tmpRoot: string;
|
|
150
|
+
let collectedLogs: Array<{ level: string; message: string }>;
|
|
151
|
+
|
|
152
|
+
beforeEach(() => {
|
|
153
|
+
tmpRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "engine-scan-"));
|
|
154
|
+
const collector = makeLogCollector();
|
|
155
|
+
collectedLogs = collector.logs;
|
|
156
|
+
configureCore(collector.host);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
afterEach(() => {
|
|
160
|
+
resetCoreForTests();
|
|
161
|
+
clearEngines();
|
|
162
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ── manifest schema 字段级(§2.4)────────────────────────────────
|
|
166
|
+
|
|
167
|
+
describe("manifest schema 字段级解析", () => {
|
|
168
|
+
it("正例:完整 manifest → discovered,capabilities/modelCatalog/displayName 原样入快照", () => {
|
|
169
|
+
makeEnginePkg(tmpRoot, "foo", {
|
|
170
|
+
displayName: "Foo Engine",
|
|
171
|
+
description: "test engine",
|
|
172
|
+
envPrefixes: ["FOO_"],
|
|
173
|
+
modelCatalog: { dynamic: false, models: [{ id: "m1", aliases: ["one"], canonicalRef: "prov/m1" }] },
|
|
174
|
+
capabilities: FULL_CAPABILITIES,
|
|
175
|
+
});
|
|
176
|
+
const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
|
|
177
|
+
expect(result.discovered).toHaveLength(1);
|
|
178
|
+
const entry = result.discovered[0];
|
|
179
|
+
expect(entry.id).toBe("foo");
|
|
180
|
+
expect(entry.descriptor.kind).toBe("cli");
|
|
181
|
+
expect(entry.descriptor.capabilities).toEqual(FULL_CAPABILITIES);
|
|
182
|
+
expect(entry.descriptor.manifest).toEqual({
|
|
183
|
+
displayName: "Foo Engine",
|
|
184
|
+
modelCatalog: { dynamic: false, models: [{ id: "m1", aliases: ["one"], canonicalRef: "prov/m1" }] },
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("bin 解析:manifest bin = package.json bin map 的 key → 入口绝对路径", () => {
|
|
189
|
+
makeEnginePkg(tmpRoot, "binmap", { capabilities: FULL_CAPABILITIES });
|
|
190
|
+
const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
|
|
191
|
+
expect(result.discovered[0].descriptor.command).toBe(
|
|
192
|
+
path.join(tmpRoot, "binmap", "bin/cli.mjs"),
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("必需字段缺失 → skip:id / bin / protocol 各自缺失均不装载", () => {
|
|
197
|
+
for (const key of ["id", "bin", "protocol"]) {
|
|
198
|
+
const manifest: Record<string, unknown> = { capabilities: FULL_CAPABILITIES };
|
|
199
|
+
if (key === "protocol") manifest["protocol"] = undefined;
|
|
200
|
+
else if (key === "bin") manifest["bin"] = undefined;
|
|
201
|
+
else manifest["id"] = undefined;
|
|
202
|
+
makeEnginePkg(tmpRoot, `missing-${key}`, manifest);
|
|
203
|
+
}
|
|
204
|
+
const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
|
|
205
|
+
expect(result.discovered).toHaveLength(0);
|
|
206
|
+
expect(result.skipped).toHaveLength(3);
|
|
207
|
+
for (const key of ["id", "bin", "protocol"]) {
|
|
208
|
+
expect(result.skipped.some((s) => s.reason.includes(key))).toBe(true);
|
|
209
|
+
}
|
|
210
|
+
expect(collectedLogs.filter((l) => l.level === "warn").length).toBeGreaterThanOrEqual(3);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("protocol 不兼容(2 越界)→ unusable 不装载;protocol 1 兼容", () => {
|
|
214
|
+
makeEnginePkg(tmpRoot, "future", { protocol: 2, capabilities: FULL_CAPABILITIES });
|
|
215
|
+
const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
|
|
216
|
+
expect(result.discovered).toHaveLength(0);
|
|
217
|
+
expect(result.unusable).toHaveLength(1);
|
|
218
|
+
expect(result.unusable[0].id).toBe("future");
|
|
219
|
+
expect(result.unusable[0].reason).toContain("protocol");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("capabilities 缺键取最保守值 + warn;未知键忽略 + warn", () => {
|
|
223
|
+
makeEnginePkg(tmpRoot, "partial-caps", {
|
|
224
|
+
capabilities: { schemaEnforcement: "native", unknownKey: "x" },
|
|
225
|
+
});
|
|
226
|
+
const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
|
|
227
|
+
expect(result.discovered).toHaveLength(1);
|
|
228
|
+
expect(result.discovered[0].descriptor.capabilities).toEqual({
|
|
229
|
+
schemaEnforcement: "native",
|
|
230
|
+
steer: "unsupported",
|
|
231
|
+
conversation: "unsupported",
|
|
232
|
+
personaInjection: "prompt",
|
|
233
|
+
eventGranularity: "coarse",
|
|
234
|
+
sandbox: "none",
|
|
235
|
+
sessionRead: "outcome-only",
|
|
236
|
+
resume: "unsupported",
|
|
237
|
+
interrupt: "kill-only",
|
|
238
|
+
permissionMode: "ignored",
|
|
239
|
+
maxTurns: false,
|
|
240
|
+
});
|
|
241
|
+
const warns = collectedLogs.filter((l) => l.level === "warn").map((l) => l.message);
|
|
242
|
+
expect(warns.some((m) => m.includes("capabilities.steer missing"))).toBe(true);
|
|
243
|
+
expect(warns.some((m) => m.includes("unknown capabilities key(s) ignored: unknownKey"))).toBe(true);
|
|
244
|
+
// 未知键不进 capabilities(词表封闭)
|
|
245
|
+
expect(Object.keys(result.discovered[0].descriptor.capabilities)).toHaveLength(11);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("capabilities 整段缺失 → 全保守 + warn(包仍装载)", () => {
|
|
249
|
+
makeEnginePkg(tmpRoot, "no-caps", {});
|
|
250
|
+
const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
|
|
251
|
+
expect(result.discovered).toHaveLength(1);
|
|
252
|
+
expect(result.discovered[0].descriptor.capabilities.maxTurns).toBe(false);
|
|
253
|
+
expect(result.discovered[0].descriptor.capabilities.steer).toBe("unsupported");
|
|
254
|
+
expect(collectedLogs.some((l) => l.message.includes("most conservative values"))).toBe(true);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("envPrefixes 缺省 [];空串/含 */非法形态/保留前缀 → 丢弃该前缀 + warn,包仍可用", () => {
|
|
258
|
+
makeEnginePkg(tmpRoot, "bad-prefixes", {
|
|
259
|
+
envPrefixes: ["", "HAS*STAR", "BAD-DASH", "XYZ_HOST", "xyz_agent_", "GOOD_", "good_"],
|
|
260
|
+
capabilities: FULL_CAPABILITIES,
|
|
261
|
+
});
|
|
262
|
+
const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
|
|
263
|
+
// 包仍可用(A12②):装载成功
|
|
264
|
+
expect(result.discovered).toHaveLength(1);
|
|
265
|
+
expect(result.skipped).toHaveLength(0);
|
|
266
|
+
const warns = collectedLogs.filter((l) => l.level === "warn").map((l) => l.message);
|
|
267
|
+
// 非法形态逐条拒绝(空串 / 含 * / 非法字符)
|
|
268
|
+
expect(warns.filter((m) => m.includes("dropping the prefix")).length).toBeGreaterThanOrEqual(5);
|
|
269
|
+
// 保留前缀大小写不敏感(XYZ_HOST / xyz_agent_ 均拒)
|
|
270
|
+
expect(warns.some((m) => m.includes("reserved host prefix"))).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("modelCatalog 三态:缺省不注入(undefined);null 等价省略;显式数组原样;dynamic 缺省 true", () => {
|
|
274
|
+
makeEnginePkg(tmpRoot, "mc-omitted", { capabilities: FULL_CAPABILITIES });
|
|
275
|
+
makeEnginePkg(tmpRoot, "mc-null", { modelCatalog: null, capabilities: FULL_CAPABILITIES });
|
|
276
|
+
makeEnginePkg(tmpRoot, "mc-explicit", {
|
|
277
|
+
modelCatalog: { models: [{ id: "m1" }] },
|
|
278
|
+
capabilities: FULL_CAPABILITIES,
|
|
279
|
+
});
|
|
280
|
+
makeEnginePkg(tmpRoot, "mc-no-models", { modelCatalog: { dynamic: false }, capabilities: FULL_CAPABILITIES });
|
|
281
|
+
const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
|
|
282
|
+
const manifestOf = (id: string) => result.discovered.find((d) => d.id === id)!.descriptor.manifest!;
|
|
283
|
+
// 缺省 = 不注入(解析器不得把省略填成 models: []——「无枚举面」语义必须可达)
|
|
284
|
+
expect("modelCatalog" in manifestOf("mc-omitted")).toBe(false);
|
|
285
|
+
expect(manifestOf("mc-null").modelCatalog).toBeNull();
|
|
286
|
+
expect(manifestOf("mc-explicit").modelCatalog).toEqual({ dynamic: true, models: [{ id: "m1" }] });
|
|
287
|
+
// models 缺失 = 无有效枚举面 → null(不是 []——[] 是作者显式「有面但空」声明)
|
|
288
|
+
expect(manifestOf("mc-no-models").modelCatalog).toBeNull();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("displayName 可选(缺省不入快照,消费面回落 id);description 仅形态校验", () => {
|
|
292
|
+
makeEnginePkg(tmpRoot, "plain", { capabilities: FULL_CAPABILITIES });
|
|
293
|
+
const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
|
|
294
|
+
expect("displayName" in result.discovered[0].descriptor.manifest!).toBe(false);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// ── 三级搜索路径 ─────────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
describe("三级搜索路径", () => {
|
|
301
|
+
it("L1 env 根:多根 + org 分组二层布局都命中", () => {
|
|
302
|
+
makeEnginePkg(path.join(tmpRoot, "r1"), "from-env-root", { capabilities: FULL_CAPABILITIES });
|
|
303
|
+
// org 分组:<root>/@scope/<pkg>/package.json
|
|
304
|
+
makeEnginePkg(path.join(tmpRoot, "r2", "@scope"), "scoped-engine", { capabilities: FULL_CAPABILITIES });
|
|
305
|
+
const result = scanEngines(
|
|
306
|
+
scanOpts({ env: { [ENGINE_ROOTS_ENV]: `${path.join(tmpRoot, "r1")}${path.delimiter}${path.join(tmpRoot, "r2")}` } }),
|
|
307
|
+
);
|
|
308
|
+
expect(result.discovered.map((d) => d.id).sort()).toEqual(["from-env-root", "scoped-engine"]);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("L1 宿主根第二通道:HostServices.discoveryRoots().engines 合并扫描", () => {
|
|
312
|
+
makeEnginePkg(path.join(tmpRoot, "hostroot"), "from-host-root", { capabilities: FULL_CAPABILITIES });
|
|
313
|
+
resetCoreForTests();
|
|
314
|
+
const collector = makeLogCollector();
|
|
315
|
+
collectedLogs = collector.logs;
|
|
316
|
+
configureCore({
|
|
317
|
+
...collector.host,
|
|
318
|
+
discoveryRoots: () => ({ engines: [{ dir: path.join(tmpRoot, "hostroot"), source: "host-npm" }] }),
|
|
319
|
+
} as Parameters<typeof configureCore>[0]);
|
|
320
|
+
const result = scanEngines(scanOpts({}));
|
|
321
|
+
expect(result.discovered.map((d) => d.id)).toEqual(["from-host-root"]);
|
|
322
|
+
expect(result.discovered[0].source).toBe("host-npm");
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("L2 node 解析根:nodeModuleRoots 注入命中;deriveNodeModuleRoots 沿上溯链收 node_modules", () => {
|
|
326
|
+
makeEnginePkg(path.join(tmpRoot, "nm"), "from-node-modules", { capabilities: FULL_CAPABILITIES });
|
|
327
|
+
const result = scanEngines(scanOpts({ nodeModuleRoots: [path.join(tmpRoot, "nm")] }));
|
|
328
|
+
expect(result.discovered.map((d) => d.id)).toEqual(["from-node-modules"]);
|
|
329
|
+
|
|
330
|
+
// deriveNodeModuleRoots:entry 位于 <tmp>/a/b/entry.js,<tmp>/node_modules 存在 → 命中
|
|
331
|
+
fs.mkdirSync(path.join(tmpRoot, "a", "b"), { recursive: true });
|
|
332
|
+
fs.mkdirSync(path.join(tmpRoot, "node_modules"));
|
|
333
|
+
fs.writeFileSync(path.join(tmpRoot, "a", "b", "entry.js"), "// entry");
|
|
334
|
+
const derived = deriveNodeModuleRoots(path.join(tmpRoot, "a", "b", "entry.js"));
|
|
335
|
+
expect(derived).toContain(path.join(tmpRoot, "node_modules"));
|
|
336
|
+
// 由内向外:更深的 node_modules 在前
|
|
337
|
+
expect(derived.indexOf(path.join(tmpRoot, "node_modules"))).toBeGreaterThan(
|
|
338
|
+
derived.indexOf(path.join(tmpRoot, "a", "node_modules")),
|
|
339
|
+
);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("L3 显式配置:enabled:false 不装载(A12⑤);command 缺失跳过;config 透传 + cwd 透传", () => {
|
|
343
|
+
makeEnginePkg(tmpRoot, "l3-disabled", { capabilities: FULL_CAPABILITIES });
|
|
344
|
+
const agentDir = path.join(tmpRoot, "agent");
|
|
345
|
+
fs.mkdirSync(path.join(agentDir, "subagents"), { recursive: true });
|
|
346
|
+
fs.writeFileSync(
|
|
347
|
+
path.join(agentDir, "subagents", "config.json"),
|
|
348
|
+
JSON.stringify({
|
|
349
|
+
engines: {
|
|
350
|
+
"l3-disabled": { enabled: false, command: "whatever" },
|
|
351
|
+
"l3-broken": { args: [] },
|
|
352
|
+
"l3-ok": {
|
|
353
|
+
command: path.join(tmpRoot, "l3-disabled", "bin/cli.mjs"),
|
|
354
|
+
args: ["--flag"],
|
|
355
|
+
config: { apiKeyHint: "no-secrets-here" },
|
|
356
|
+
cwd: tmpRoot,
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
}),
|
|
360
|
+
);
|
|
361
|
+
const result = scanEngines(scanOpts({ agentDir }));
|
|
362
|
+
expect(result.discovered.map((d) => d.id)).toEqual(["l3-ok"]);
|
|
363
|
+
expect(result.discovered[0].engineConfig).toEqual({ apiKeyHint: "no-secrets-here" });
|
|
364
|
+
// enabled:false 不进清单也不进 unusable(显式禁用非错误)
|
|
365
|
+
expect(result.discovered.some((d) => d.id === "l3-disabled")).toBe(false);
|
|
366
|
+
expect(result.unusable.some((u) => u.id === "l3-disabled")).toBe(false);
|
|
367
|
+
// command 缺失条目:warn 跳过(readExplicitEngines 丢弃,不影响其他条目)
|
|
368
|
+
expect(
|
|
369
|
+
collectedLogs.some((l) => l.message.includes("engines.l3-broken.command is required")),
|
|
370
|
+
).toBe(true);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("L3 裸命令名经 PATH 解析;解析失败 → unusable 不进清单(投影 = 已发现且可执行)", () => {
|
|
374
|
+
const binDir = path.join(tmpRoot, "pathbin");
|
|
375
|
+
fs.mkdirSync(binDir);
|
|
376
|
+
fs.writeFileSync(path.join(binDir, "l3-on-path"), BIN_CONTENT);
|
|
377
|
+
fs.chmodSync(path.join(binDir, "l3-on-path"), 0o755);
|
|
378
|
+
const agentDir = path.join(tmpRoot, "agent");
|
|
379
|
+
fs.mkdirSync(path.join(agentDir, "subagents"), { recursive: true });
|
|
380
|
+
fs.writeFileSync(
|
|
381
|
+
path.join(agentDir, "subagents", "config.json"),
|
|
382
|
+
JSON.stringify({
|
|
383
|
+
engines: {
|
|
384
|
+
"l3-path": { command: "l3-on-path" },
|
|
385
|
+
"l3-ghost": { command: "definitely-not-on-path-xyz" },
|
|
386
|
+
},
|
|
387
|
+
}),
|
|
388
|
+
);
|
|
389
|
+
const result = scanEngines(
|
|
390
|
+
scanOpts({ agentDir, env: { PATH: binDir } }),
|
|
391
|
+
);
|
|
392
|
+
expect(result.discovered.map((d) => d.id)).toEqual(["l3-path"]);
|
|
393
|
+
expect(result.discovered[0].descriptor.command).toBe(path.join(binDir, "l3-on-path"));
|
|
394
|
+
expect(result.unusable).toHaveLength(1);
|
|
395
|
+
expect(result.unusable[0].id).toBe("l3-ghost");
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// ── L1 env 解析(分隔符 / 非法条目 / 去重)───────────────────────
|
|
400
|
+
|
|
401
|
+
describe("XYZ_AGENT_ENGINE_ROOTS env 解析", () => {
|
|
402
|
+
it("path.delimiter 分隔;空段跳过;非绝对路径丢弃 + warn;去重(大小写敏感)", () => {
|
|
403
|
+
const abs1 = path.join(tmpRoot, "a");
|
|
404
|
+
const abs2 = path.join(tmpRoot, "b");
|
|
405
|
+
const env = {
|
|
406
|
+
[ENGINE_ROOTS_ENV]: [abs1, "relative/path", "", abs1, abs2].join(path.delimiter),
|
|
407
|
+
};
|
|
408
|
+
const roots = parseEngineRootsEnv(env);
|
|
409
|
+
expect(roots).toEqual([abs1, abs2]);
|
|
410
|
+
expect(collectedLogs.some((l) => l.message.includes("not an absolute path"))).toBe(true);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it("env 未设 / 空白 → 空清单(无 warn)", () => {
|
|
414
|
+
expect(parseEngineRootsEnv({})).toEqual([]);
|
|
415
|
+
expect(parseEngineRootsEnv({ [ENGINE_ROOTS_ENV]: " " })).toEqual([]);
|
|
416
|
+
expect(collectedLogs).toHaveLength(0);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// ── 冲突与失败(A12)─────────────────────────────────────────────
|
|
421
|
+
|
|
422
|
+
describe("冲突与失败", () => {
|
|
423
|
+
it("bin 不存在 → unusable;存在但不可执行(0o644)→ unusable,均不进清单", () => {
|
|
424
|
+
if (process.platform === "win32") {
|
|
425
|
+
// Windows 无 X_OK 语义(accessSync 仅查存在性),不可执行反例只在 POSIX 跑
|
|
426
|
+
makeEnginePkg(tmpRoot, "missing-bin", { capabilities: FULL_CAPABILITIES }, { omitBin: true });
|
|
427
|
+
const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
|
|
428
|
+
expect(result.discovered).toHaveLength(0);
|
|
429
|
+
expect(result.unusable).toHaveLength(1);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
makeEnginePkg(tmpRoot, "missing-bin", { capabilities: FULL_CAPABILITIES }, { omitBin: true });
|
|
433
|
+
makeEnginePkg(tmpRoot, "not-executable", { capabilities: FULL_CAPABILITIES }, { binMode: 0o644 });
|
|
434
|
+
const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
|
|
435
|
+
expect(result.discovered).toHaveLength(0);
|
|
436
|
+
expect(result.unusable).toHaveLength(2);
|
|
437
|
+
expect(result.unusable.map((u) => u.id).sort()).toEqual(["missing-bin", "not-executable"]);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("同 id 覆盖:后发现者胜 + debug 留痕(same-id override)", () => {
|
|
441
|
+
makeEnginePkg(path.join(tmpRoot, "first"), "dup", {
|
|
442
|
+
displayName: "First",
|
|
443
|
+
capabilities: FULL_CAPABILITIES,
|
|
444
|
+
});
|
|
445
|
+
makeEnginePkg(path.join(tmpRoot, "second"), "dup", {
|
|
446
|
+
displayName: "Second",
|
|
447
|
+
capabilities: FULL_CAPABILITIES,
|
|
448
|
+
});
|
|
449
|
+
const result = scanEngines(
|
|
450
|
+
scanOpts({
|
|
451
|
+
env: {
|
|
452
|
+
[ENGINE_ROOTS_ENV]: `${path.join(tmpRoot, "first")}${path.delimiter}${path.join(tmpRoot, "second")}`,
|
|
453
|
+
},
|
|
454
|
+
}),
|
|
455
|
+
);
|
|
456
|
+
expect(result.discovered).toHaveLength(1);
|
|
457
|
+
expect(result.discovered[0].descriptor.manifest?.displayName).toBe("Second");
|
|
458
|
+
expect(collectedLogs.some((l) => l.message.includes("same-id override"))).toBe(true);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("坏包不阻断其他引擎(A12②):坏 manifest 与好包共存扫描,好包正常装载", () => {
|
|
462
|
+
// 缺 id 的坏 manifest(makeEnginePkg 的 spread 默认补齐 id,故直写 JSON)
|
|
463
|
+
const brokenDir = path.join(tmpRoot, "broken-id");
|
|
464
|
+
fs.mkdirSync(brokenDir, { recursive: true });
|
|
465
|
+
fs.writeFileSync(
|
|
466
|
+
path.join(brokenDir, "package.json"),
|
|
467
|
+
JSON.stringify({ name: "broken", "xyz-agent": { subagentEngine: { bin: "x", protocol: 1 } } }),
|
|
468
|
+
);
|
|
469
|
+
fs.mkdirSync(path.join(tmpRoot, "torn-json"), { recursive: true });
|
|
470
|
+
fs.writeFileSync(path.join(tmpRoot, "torn-json", "package.json"), "{ torn");
|
|
471
|
+
makeEnginePkg(tmpRoot, "healthy", { capabilities: FULL_CAPABILITIES });
|
|
472
|
+
const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
|
|
473
|
+
expect(result.discovered.map((d) => d.id)).toEqual(["healthy"]);
|
|
474
|
+
expect(result.skipped.length).toBeGreaterThanOrEqual(2);
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// ── 装载与投影 ───────────────────────────────────────────────────
|
|
479
|
+
|
|
480
|
+
describe("装载与注册表", () => {
|
|
481
|
+
it("discoverAndRegisterEngines 装载 cli descriptor:hasEngine true、getEngine 返回 id 命中的 port(构造同步不 spawn)", () => {
|
|
482
|
+
makeEnginePkg(tmpRoot, "loaded", { displayName: "Loaded", capabilities: FULL_CAPABILITIES });
|
|
483
|
+
makeEnginePkg(tmpRoot, "loaded-catalog", {
|
|
484
|
+
capabilities: FULL_CAPABILITIES,
|
|
485
|
+
modelCatalog: { models: [{ id: "m1" }] },
|
|
486
|
+
});
|
|
487
|
+
discoverAndRegisterEngines(scanOpts({ roots: [tmpRoot] }));
|
|
488
|
+
expect(hasEngine("loaded")).toBe(true);
|
|
489
|
+
const port = getEngine("loaded");
|
|
490
|
+
expect(port.id).toBe("loaded");
|
|
491
|
+
// 同步成员直读 manifest 快照(W2 RemoteEngine 形态):无 modelCatalog →
|
|
492
|
+
// listModels 返回 null(无枚举面)、validateModel 成员不实现(构造器摘除)
|
|
493
|
+
expect(port.listModels?.()).toBeNull();
|
|
494
|
+
expect(typeof port.validateModel).toBe("undefined");
|
|
495
|
+
// 显式 modelCatalog → listModels 返回数组(有枚举面)
|
|
496
|
+
const portWithCatalog = getEngine("loaded-catalog");
|
|
497
|
+
expect(portWithCatalog.listModels?.()).toEqual([{ id: "m1" }]);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it("ensureEngineDiscovered:快照命中零扫描;未命中补扫一次后装载", () => {
|
|
501
|
+
makeEnginePkg(tmpRoot, "rescan", { capabilities: FULL_CAPABILITIES });
|
|
502
|
+
const opts = scanOpts({ roots: [tmpRoot] });
|
|
503
|
+
// 未装载:miss → 补扫 → true
|
|
504
|
+
expect(ensureEngineDiscovered("rescan", opts)).toBe(true);
|
|
505
|
+
expect(hasEngine("rescan")).toBe(true);
|
|
506
|
+
// 快照命中:直接 true(再次调用安全)
|
|
507
|
+
expect(ensureEngineDiscovered("rescan", opts)).toBe(true);
|
|
508
|
+
// 确实不存在的 id:补扫后仍 false
|
|
509
|
+
expect(ensureEngineDiscovered("ghost", opts)).toBe(false);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it("engines.json 投影合流:发现 cli ∪ inproc 注册;幂等零写;契约 {v:1, engines} 不变", () => {
|
|
513
|
+
makeEnginePkg(tmpRoot, "projected", { capabilities: FULL_CAPABILITIES });
|
|
514
|
+
registerEngine("pi", () => stubEngine("pi"));
|
|
515
|
+
// syncEnginesFile 读 process.env(生产语义)——L1 根经真实 env 注入,afterEach 恢复
|
|
516
|
+
const prevRoots = process.env[ENGINE_ROOTS_ENV];
|
|
517
|
+
process.env[ENGINE_ROOTS_ENV] = tmpRoot;
|
|
518
|
+
try {
|
|
519
|
+
const agentDir = path.join(tmpRoot, "agent");
|
|
520
|
+
syncEnginesFile(agentDir);
|
|
521
|
+
const filePath = getEnginesFilePath(agentDir);
|
|
522
|
+
const file = JSON.parse(fs.readFileSync(filePath, "utf8")) as SubagentEnginesFile;
|
|
523
|
+
expect(file.v).toBe(1);
|
|
524
|
+
expect(file.engines.sort()).toEqual(["pi", "projected"]);
|
|
525
|
+
expect(typeof file.updatedAt).toBe("number");
|
|
526
|
+
|
|
527
|
+
// 幂等零写(内容不变 mtime 不动)
|
|
528
|
+
const statAfterFirst = fs.statSync(filePath);
|
|
529
|
+
syncEnginesFile(agentDir);
|
|
530
|
+
expect(fs.statSync(filePath).mtimeMs).toBe(statAfterFirst.mtimeMs);
|
|
531
|
+
|
|
532
|
+
// 发现面变化(新增引擎包)触发重写
|
|
533
|
+
makeEnginePkg(tmpRoot, "second-projected", { capabilities: FULL_CAPABILITIES });
|
|
534
|
+
syncEnginesFile(agentDir);
|
|
535
|
+
const updated = JSON.parse(fs.readFileSync(filePath, "utf8")) as SubagentEnginesFile;
|
|
536
|
+
expect(updated.engines.sort()).toEqual(["pi", "projected", "second-projected"]);
|
|
537
|
+
} finally {
|
|
538
|
+
if (prevRoots === undefined) delete process.env[ENGINE_ROOTS_ENV];
|
|
539
|
+
else process.env[ENGINE_ROOTS_ENV] = prevRoots;
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it("syncEnginesFile fail-safe:发现异常不阻塞投影(发现根全部不存在时仍写 inproc 清单)", () => {
|
|
544
|
+
registerEngine("pi", () => stubEngine("pi"));
|
|
545
|
+
const agentDir = path.join(tmpRoot, "agent");
|
|
546
|
+
syncEnginesFile(agentDir);
|
|
547
|
+
const file = JSON.parse(fs.readFileSync(getEnginesFilePath(agentDir), "utf8")) as SubagentEnginesFile;
|
|
548
|
+
expect(file.engines).toEqual(["pi"]);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("零发现零注册时投影空清单(W11 删 inproc 后的终态语义预演)", () => {
|
|
552
|
+
const agentDir = path.join(tmpRoot, "agent");
|
|
553
|
+
syncEnginesFile(agentDir);
|
|
554
|
+
const file = JSON.parse(fs.readFileSync(getEnginesFilePath(agentDir), "utf8")) as SubagentEnginesFile;
|
|
555
|
+
expect(file.engines).toEqual([]);
|
|
556
|
+
expect(listEngines()).toEqual([]);
|
|
557
|
+
});
|
|
558
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// host-task-spec.test.ts —— ExecuteOptions → AgentCallOpts 直译映射单测(fork-from 载体)。
|
|
2
|
+
//
|
|
3
|
+
// 背景(W3 断链修复):fork-from 的 ExecuteOptions.forkFromSessionFile 在 chat 域协议化
|
|
4
|
+
// 改写后曾断链(本 mapper 不映射 → 协议 run 帧无承载位 → fork-from 轮次以无 --fork 的
|
|
5
|
+
// 全新 session 跑,继承历史丢失)。本文件钉死宿主侧映射面:forkFromSessionFile →
|
|
6
|
+
// forkSource 改名透传(三层同名的唯一改名点)+ 缺省不落键(协议负向兼容)。
|
|
7
|
+
// 端到端消费链由两侧专项覆盖:
|
|
8
|
+
// - chat 域执行链观测(subagent-workflow ended-message-and-fork-from.test 的 fake.runs);
|
|
9
|
+
// - pi 引擎接收面(pi-subagent-cli chat-protocol.test 一次性 run + spawn-args.test
|
|
10
|
+
// 的 forkSource → --fork 直测)。
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it } from "vitest";
|
|
13
|
+
|
|
14
|
+
import { executeOptionsToEngineTaskSpec } from "../host-task-spec.ts";
|
|
15
|
+
import type { ExecuteOptions } from "../../types.ts";
|
|
16
|
+
|
|
17
|
+
function baseOpts(): ExecuteOptions {
|
|
18
|
+
return { task: "do work", slug: "map-test" };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("executeOptionsToEngineTaskSpec(ExecuteOptions → AgentCallOpts 直译)", () => {
|
|
22
|
+
it("forkFromSessionFile 存在 → forkSource 透传(fork-from 协议载体的宿主侧源头)", () => {
|
|
23
|
+
const spec = executeOptionsToEngineTaskSpec({
|
|
24
|
+
...baseOpts(),
|
|
25
|
+
forkFromSessionFile: "/sessions/sa-src.jsonl",
|
|
26
|
+
});
|
|
27
|
+
expect(spec.forkSource).toBe("/sessions/sa-src.jsonl");
|
|
28
|
+
expect(spec.prompt).toBe("do work");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("缺省 → 键不落(负向兼容:无 fork-from 的调用帧形状不变)", () => {
|
|
32
|
+
const spec = executeOptionsToEngineTaskSpec(baseOpts());
|
|
33
|
+
expect("forkSource" in spec).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("fork 与 forkSource 互不挤占(独立可选透传;取值优先级语义归引擎侧)", () => {
|
|
37
|
+
const spec = executeOptionsToEngineTaskSpec({
|
|
38
|
+
...baseOpts(),
|
|
39
|
+
fork: true,
|
|
40
|
+
forkFromSessionFile: "/sessions/sa-src.jsonl",
|
|
41
|
+
});
|
|
42
|
+
expect(spec.fork).toBe(true);
|
|
43
|
+
expect(spec.forkSource).toBe("/sessions/sa-src.jsonl");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// model-ref-split.test.ts —— [W3 契约变更④] 无斜杠 canonicalRef 的 core 侧拆分单测。
|
|
2
|
+
//
|
|
3
|
+
// 设计权威源:docs/design/subagent-engine-protocolization.md §3.3 validateModel 行
|
|
4
|
+
// (「core 必须改 resolveIdentityForEngine 与续聊回读对无斜杠 ref 的处理:provider="",
|
|
5
|
+
// id=ref,整串进 name,否则落成 "<ref>/" 畸形」)。拆分单一权威 = splitEngineModelRef,
|
|
6
|
+
// 写入词形 = joinEngineModelRef(provider 空串不拼斜杠);两消费方(chat 域留痕 /
|
|
7
|
+
// 续聊回读)必须同源,否则写入/回读对同一 ref 给出不同 provider/id。
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
|
|
11
|
+
import { joinEngineModelRef, splitEngineModelRef } from "../model-validation.ts";
|
|
12
|
+
|
|
13
|
+
describe("splitEngineModelRef(契约④:无斜杠 ref 不落畸形)", () => {
|
|
14
|
+
it("有斜杠 ref:provider/id 常规拆分,name = 整串", () => {
|
|
15
|
+
expect(splitEngineModelRef("zai/glm-4.6")).toEqual({
|
|
16
|
+
provider: "zai",
|
|
17
|
+
id: "glm-4.6",
|
|
18
|
+
name: "zai/glm-4.6",
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("无斜杠 ref(引擎原样返回):provider=''、id=整串、name=整串——不落 \"<ref>/\" 畸形", () => {
|
|
23
|
+
expect(splitEngineModelRef("glm-4.6")).toEqual({
|
|
24
|
+
provider: "",
|
|
25
|
+
id: "glm-4.6",
|
|
26
|
+
name: "glm-4.6",
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("空串(引擎未实现校验面且无显式 model 的防御形态):provider/id/name 全空串", () => {
|
|
31
|
+
expect(splitEngineModelRef("")).toEqual({ provider: "", id: "", name: "" });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("开头斜杠(异常形态防御):按无斜杠分支处理,不 crash", () => {
|
|
35
|
+
expect(splitEngineModelRef("/glm")).toEqual({ provider: "", id: "/glm", name: "/glm" });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("多斜杠 ref:首个斜杠拆分(provider 不含斜杠、id 保留其余)", () => {
|
|
39
|
+
expect(splitEngineModelRef("prov/org/model")).toEqual({
|
|
40
|
+
provider: "prov",
|
|
41
|
+
id: "org/model",
|
|
42
|
+
name: "prov/org/model",
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("joinEngineModelRef(record.model 写入词形,与拆分往返自洽)", () => {
|
|
48
|
+
it("provider 非空:provider/id 词形", () => {
|
|
49
|
+
expect(joinEngineModelRef({ provider: "zai", id: "glm-4.6", name: "zai/glm-4.6" })).toBe("zai/glm-4.6");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("provider 空串(契约④):只写 id,无头斜杠——续聊回读可还原", () => {
|
|
53
|
+
expect(joinEngineModelRef({ provider: "", id: "glm-4.6", name: "glm-4.6" })).toBe("glm-4.6");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("写入 → 回读往返(join ∘ split 恒等;split ∘ join 语义还原)", () => {
|
|
57
|
+
for (const modelStr of ["zai/glm-4.6", "glm-4.6", ""]) {
|
|
58
|
+
const split = splitEngineModelRef(modelStr);
|
|
59
|
+
const joined = joinEngineModelRef(split);
|
|
60
|
+
expect(joined).toBe(modelStr); // 写出词形 == 原 ref(无任何斜杠畸形增删)
|
|
61
|
+
const reSplit = splitEngineModelRef(joined);
|
|
62
|
+
expect(reSplit).toEqual(split); // 回读还原同一 provider/id/name
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("回归锚(旧行为对照):无斜杠 ref 旧实现落 record.model=\"<ref>/\" 且续聊还原 provider=\"<ref>\"——现两处均消除", () => {
|
|
67
|
+
const ref = "glm-4.6";
|
|
68
|
+
const split = splitEngineModelRef(ref);
|
|
69
|
+
// 写入侧:无尾斜杠
|
|
70
|
+
expect(joinEngineModelRef(split)).not.toContain("/");
|
|
71
|
+
// 回读侧:provider 为空串(旧行为是 "unknown"/整串,给续轮注入虚构 provider)
|
|
72
|
+
expect(splitEngineModelRef(joinEngineModelRef(split)).provider).toBe("");
|
|
73
|
+
});
|
|
74
|
+
});
|