@zhushanwen/subagent-core 0.4.0 → 0.5.1
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/dist/{chunk-4KN4TTG7.js → chunk-43ONBFZX.js} +1 -1
- package/dist/{chunk-X7SZ5HLQ.js → chunk-V3VHZ2VX.js} +20 -27
- package/dist/execution/engine/engines/zcode/constants.cjs +29 -41
- package/dist/execution/engine/engines/zcode/constants.d.cts +65 -53
- package/dist/execution/engine/engines/zcode/constants.d.ts +65 -53
- package/dist/execution/engine/engines/zcode/constants.js +17 -27
- package/dist/execution/engine/engines/zcode/reader.d.cts +2 -1
- package/dist/execution/engine/engines/zcode/reader.d.ts +2 -1
- package/dist/execution/engine/engines/zcode/reader.js +2 -2
- package/dist/index.cjs +16418 -14070
- package/dist/index.d.cts +5136 -4016
- package/dist/index.d.ts +5136 -4016
- package/dist/index.js +16343 -14048
- package/dist/{types-DpUO16pj.d.cts → types-Dv4QhSJ_.d.cts} +215 -112
- package/dist/{types-DpUO16pj.d.ts → types-Dv4QhSJ_.d.ts} +215 -112
- package/package.json +3 -3
- package/src/__tests__/agent-opts-resolver-schema-prompt.test.ts +0 -1
- package/src/__tests__/append-system-prompt-assembly.test.ts +8 -7
- package/src/__tests__/fr4-get-state-handshake.test.ts +2 -2
- package/src/__tests__/m2-append-content-probe.test.ts +0 -1
- package/src/__tests__/manifest-store.test.ts +10 -9
- package/src/__tests__/record-store-cache.test.ts +0 -2
- package/src/__tests__/record-store-index.test.ts +1 -2
- package/src/__tests__/review-fix-loop-script.test.ts +4 -2
- package/src/__tests__/review-fix-loop-utils.test.ts +10 -10
- package/src/__tests__/robustness-low-batch1.test.ts +6 -5
- package/src/__tests__/robustness-medium-batch1.test.ts +2 -2
- package/src/__tests__/robustness-medium-batch2.test.ts +17 -6
- package/src/__tests__/robustness-medium-batch4.test.ts +2 -2
- package/src/__tests__/session-runner.test.ts +1 -1
- package/src/core/__tests__/host-services.test.ts +2 -2
- package/src/core/__tests__/logger.test.ts +1 -1
- package/src/core/error-message.ts +9 -0
- package/src/core/host-services.ts +21 -5
- package/src/core/notify-ports.ts +18 -5
- package/src/execution/__tests__/agent-profile.test.ts +4 -4
- package/src/execution/__tests__/agent-registry.test.ts +370 -0
- package/src/execution/__tests__/agent-result-mapper.test.ts +18 -2
- package/src/execution/__tests__/agents-assembly.test.ts +2 -2
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +6 -6
- package/src/execution/__tests__/chat-engine-routing.test.ts +68 -33
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +4 -4
- package/src/execution/__tests__/cold-resurrect.test.ts +283 -0
- package/src/execution/__tests__/delivery-methods.test.ts +75 -62
- package/src/execution/__tests__/descendant-sweep-guards.test.ts +220 -0
- package/src/execution/__tests__/descendant-sweep.test.ts +6 -6
- package/src/execution/__tests__/dialog-queue.test.ts +2 -2
- package/src/execution/__tests__/engine-model-validation.test.ts +741 -0
- package/src/execution/__tests__/epipe-fallback.test.ts +29 -24
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +21 -91
- package/src/execution/__tests__/execute-nesting.test.ts +34 -188
- package/src/execution/__tests__/execution-record.test.ts +217 -60
- package/src/execution/__tests__/execution-runtime-face.test.ts +5 -5
- package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +3 -3
- package/src/execution/__tests__/finalize-record.test.ts +173 -17
- package/src/execution/__tests__/gc-timer.test.ts +55 -2
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +12 -12
- package/src/execution/__tests__/get-state-handshake.test.ts +1 -1
- package/src/execution/__tests__/helpers/mock-extension-api.ts +1 -1
- package/src/execution/__tests__/helpers/session-runner-mocks.ts +113 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +150 -3
- package/src/execution/__tests__/helpers/subagent-service-mocks.ts +137 -0
- package/src/execution/__tests__/keep-alive-no-progress.test.ts +50 -148
- package/src/execution/__tests__/kill-all-escalation.test.ts +2 -2
- package/src/execution/__tests__/lifecycle-predicates.test.ts +1 -2
- package/src/execution/__tests__/manifest-parentid.test.ts +4 -4
- package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +1 -1
- package/src/execution/__tests__/model-resolver.test.ts +2 -2
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +23 -121
- package/src/execution/__tests__/nested-visibility.test.ts +2 -0
- package/src/execution/__tests__/notify-ledger.test.ts +973 -18
- package/src/execution/__tests__/output-collector.test.ts +117 -6
- package/src/execution/__tests__/pi-invocation.test.ts +1 -1
- package/src/execution/__tests__/record-store.test.ts +10 -9
- package/src/execution/__tests__/records-cwd-isolation.test.ts +2 -2
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +29 -132
- package/src/execution/__tests__/recursive-visibility-env.test.ts +55 -179
- package/src/execution/__tests__/rpc-mode.test.ts +2 -2
- package/src/execution/__tests__/run-and-finalize-anchoring.test.ts +387 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +15 -9
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +4 -4
- package/src/execution/__tests__/run-spawn-edges.test.ts +17 -15
- package/src/execution/__tests__/run-spawn-integration.test.ts +2 -2
- package/src/execution/__tests__/run-spawn-resume.test.ts +2 -2
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +2 -2
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +4 -4
- package/src/execution/__tests__/service-kill-escalation.test.ts +7 -5
- package/src/execution/__tests__/session-context-resolver.test.ts +0 -2
- package/src/execution/__tests__/session-file-gc.test.ts +35 -0
- package/src/execution/__tests__/session-pending.test.ts +257 -14
- package/src/execution/__tests__/session-reconstructor.test.ts +366 -15
- package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +1 -1
- package/src/execution/__tests__/session-runner-close-prune.test.ts +2 -2
- package/src/execution/__tests__/session-runner-dispatch.test.ts +489 -0
- package/src/execution/__tests__/session-runner-epipe.test.ts +3 -3
- package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +5 -4
- package/src/execution/__tests__/session-runner-lifecycle-helpers.test.ts +290 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +7 -5
- package/src/execution/__tests__/settled-watchdog.test.ts +282 -47
- package/src/execution/__tests__/spawn-args.test.ts +11 -10
- package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +26 -23
- package/src/execution/__tests__/spawn-event-adapter.test.ts +1 -1
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +5 -4
- package/src/execution/__tests__/spawned-children.test.ts +2 -1
- package/src/execution/__tests__/start-sync-model-guard.test.ts +4 -4
- package/src/execution/__tests__/stdin-writer.test.ts +6 -4
- package/src/execution/__tests__/stream-sink-retirement.test.ts +6 -6
- package/src/execution/__tests__/subagent-actions-core.test.ts +42 -22
- package/src/execution/__tests__/subagent-service-message-close.test.ts +29 -26
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +8 -8
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +27 -22
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +30 -29
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +45 -20
- package/src/execution/__tests__/subagent-service.test.ts +37 -30
- package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +88 -12
- package/src/execution/__tests__/subprocess-agent-runner-timeout.test.ts +75 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +22 -20
- package/src/execution/__tests__/temp-prompt.test.ts +1 -1
- package/src/execution/__tests__/timeout-integration.test.ts +6 -5
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +1 -1
- package/src/execution/__tests__/turn-limiter.test.ts +1 -1
- package/src/execution/__tests__/ui-request-handler-factory.test.ts +12 -6
- package/src/execution/__tests__/ui-request-handler.test.ts +4 -3
- package/src/execution/__tests__/ui-request-queue.test.ts +142 -2
- package/src/execution/__tests__/worktree-manager.test.ts +14 -5
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +4 -3
- package/src/execution/agent-registry.ts +74 -30
- package/src/execution/agent-result-mapper.ts +3 -0
- package/src/execution/cold-resurrect.ts +199 -0
- package/src/execution/engine/__tests__/common/capability-gate.test.ts +91 -0
- package/src/execution/engine/__tests__/common/data-dir.test.ts +1 -1
- package/src/execution/engine/__tests__/common/journal-wiring.test.ts +84 -0
- package/src/execution/engine/__tests__/common/kill-chain.test.ts +34 -7
- package/src/execution/engine/__tests__/common/persona-router.test.ts +4 -5
- package/src/execution/engine/__tests__/common/pool-manager.test.ts +211 -51
- package/src/execution/engine/__tests__/common/session-view-service-zcode-dbpath.test.ts +149 -0
- package/src/execution/engine/__tests__/common/session-view-service.test.ts +499 -0
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +22 -112
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +15 -28
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +12 -12
- package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +1 -1
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +21 -82
- package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +4 -7
- package/src/execution/engine/__tests__/engine-discovery.test.ts +15 -3
- package/src/execution/engine/__tests__/model-prompt.test.ts +15 -3
- package/src/execution/engine/__tests__/registry.test.ts +4 -2
- package/src/execution/engine/__tests__/routing.test.ts +115 -6
- package/src/execution/engine/common/capability-gate.ts +92 -0
- package/src/execution/engine/common/journal-wiring.ts +97 -0
- package/src/execution/engine/common/kill-chain.ts +42 -10
- package/src/execution/engine/common/nesting-guard.ts +66 -0
- package/src/execution/engine/common/persona-router.ts +18 -8
- package/src/execution/engine/common/pool-manager.ts +332 -78
- package/src/execution/engine/common/session-view-service.ts +506 -0
- package/src/execution/engine/common/session-view-types.ts +125 -0
- package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +174 -28
- package/src/execution/engine/engines/pi/__tests__/spawn-opts-direct.test.ts +257 -0
- package/src/execution/{argv-mirror.ts → engine/engines/pi/argv-mirror.ts} +43 -21
- package/src/execution/{get-state-handshake.ts → engine/engines/pi/get-state-handshake.ts} +1 -1
- package/src/execution/{output-collector.ts → engine/engines/pi/output-collector.ts} +108 -19
- package/src/execution/engine/engines/pi/pi-engine.ts +434 -38
- package/src/execution/{pi-invocation.ts → engine/engines/pi/pi-invocation.ts} +2 -2
- package/src/execution/engine/engines/pi/reader.ts +11 -0
- package/src/execution/engine/engines/pi/registration.ts +4 -2
- package/src/execution/{session-runner.ts → engine/engines/pi/session-runner.ts} +617 -412
- package/src/execution/{spawn-event-adapter.ts → engine/engines/pi/spawn-event-adapter.ts} +75 -34
- package/src/execution/{stdin-writer.ts → engine/engines/pi/stdin-writer.ts} +10 -9
- package/src/execution/{temp-prompt.ts → engine/engines/pi/temp-prompt.ts} +2 -2
- package/src/execution/{turn-limiter.ts → engine/engines/pi/turn-limiter.ts} +1 -1
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +54 -27
- package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +321 -0
- package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +13 -12
- package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +7 -210
- package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +6 -110
- package/src/execution/engine/engines/zcode/__tests__/session-channel-dispose-harvest.test.ts +247 -0
- package/src/execution/engine/engines/zcode/__tests__/session-channel-turn-timers.test.ts +451 -0
- package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +20 -8
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +77 -110
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +25 -319
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-dispose.test.ts +210 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-retry.test.ts +353 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-status.test.ts +282 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-timeout.test.ts +372 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +48 -148
- package/src/execution/engine/engines/zcode/appserver-launcher.ts +167 -0
- package/src/execution/engine/engines/zcode/connection.ts +24 -11
- package/src/execution/engine/engines/zcode/constants.ts +75 -71
- package/src/execution/engine/engines/zcode/golden-sample.ts +8 -46
- package/src/execution/engine/engines/zcode/parser.ts +12 -346
- package/src/execution/engine/engines/zcode/preparer.ts +12 -143
- package/src/execution/engine/engines/zcode/session-channel.ts +351 -39
- package/src/execution/engine/engines/zcode/zcode-engine.ts +665 -760
- package/src/execution/engine/host-task-spec.ts +32 -36
- package/src/execution/engine/model-validation.ts +177 -0
- package/src/execution/engine/port.ts +24 -6
- package/src/execution/engine/routing.ts +75 -1
- package/src/execution/engine/types.ts +30 -103
- package/src/execution/execution-record.ts +222 -144
- package/src/execution/finalize-record.ts +131 -93
- package/src/execution/idle-gc.ts +28 -0
- package/src/execution/lifecycle-predicates.ts +1 -1
- package/src/execution/notify-host.ts +212 -0
- package/src/execution/record-store.ts +119 -73
- package/src/execution/round-settlement.ts +93 -0
- package/src/execution/session-file-gc.ts +96 -62
- package/src/execution/session-reconstructor.ts +256 -143
- package/src/execution/sessions-index.ts +117 -47
- package/src/execution/settled-watchdog.ts +264 -52
- package/src/execution/subagent-actions-core.ts +22 -22
- package/src/execution/subagent-service.ts +815 -963
- package/src/execution/subprocess-agent-runner.ts +144 -97
- package/src/execution/types.ts +9 -0
- package/src/execution/ui-request-handler-factory.ts +102 -74
- package/src/execution/ui-request-queue.ts +50 -19
- package/src/index.ts +442 -316
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +1 -1
- package/src/orchestration/__tests__/agent-call-stream.test.ts +2 -2
- package/src/orchestration/__tests__/args-validator.test.ts +14 -0
- package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +1 -1
- package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +11 -9
- package/src/orchestration/__tests__/execute-agent-call.test.ts +151 -11
- package/src/orchestration/__tests__/file-run-store.test.ts +7 -6
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +2 -2
- package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +1 -1
- package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +3 -3
- package/src/orchestration/__tests__/lifecycle.test.ts +7 -5
- package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +1 -1
- package/src/orchestration/__tests__/run-snapshot.test.ts +8 -5
- package/src/orchestration/__tests__/script-lint.test.ts +17 -0
- package/src/orchestration/__tests__/test-mocks.ts +2 -0
- package/src/orchestration/__tests__/worker-exit-without-result.test.ts +7 -6
- package/src/orchestration/__tests__/worker-message-pump-finalize-run.test.ts +229 -0
- package/src/orchestration/__tests__/{error-recovery-handlers.test.ts → worker-message-pump-handlers.test.ts} +27 -19
- package/src/orchestration/__tests__/{error-recovery-postmessage-defense.test.ts → worker-message-pump-postmessage-defense.test.ts} +8 -8
- package/src/orchestration/__tests__/{error-recovery-serialize-failed-result.test.ts → worker-message-pump-serialize-failed-result.test.ts} +2 -2
- package/src/orchestration/__tests__/{error-recovery-workflow-call.test.ts → worker-message-pump-workflow-call.test.ts} +1 -1
- package/src/orchestration/__tests__/worker-pending-timeout-abort.test.ts +1 -1
- package/src/orchestration/__tests__/worker-script-builder.test.ts +1 -1
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +1 -1
- package/src/orchestration/agent-opts-resolver.ts +1 -1
- package/src/orchestration/args-validator.ts +53 -31
- package/src/orchestration/execute-agent-call.ts +19 -73
- package/src/orchestration/launcher.ts +128 -81
- package/src/orchestration/lifecycle.ts +43 -45
- package/src/orchestration/models/ports.ts +4 -4
- package/src/orchestration/models/run-runtime.ts +2 -2
- package/src/orchestration/models/trace.ts +1 -1
- package/src/orchestration/models/types.ts +111 -22
- package/src/orchestration/models/workflow-run.ts +1 -1
- package/src/orchestration/script-lint.ts +91 -58
- package/src/orchestration/{error-recovery.ts → worker-message-pump.ts} +157 -150
- package/src/shared/__tests__/meta-parser.test.ts +151 -0
- package/src/shared/__tests__/resource-discovery.test.ts +258 -215
- package/src/shared/atomic-write.ts +12 -9
- package/src/shared/meta-parser.ts +158 -84
- package/src/shared/model-ref.ts +15 -2
- package/src/shared/resource-discovery.ts +9 -183
- package/src/shared/schema-jsonify.ts +1 -1
- package/workflows/review-fix-loop-utils.cjs +151 -89
- package/workflows/review-fix-loop-utils.d.cts +287 -0
- package/src/execution/__tests__/execute-options-mapper.test.ts +0 -200
- package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +0 -164
- package/src/execution/engine/engines/pi/task-spec-mapper.ts +0 -100
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +0 -39
- package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +0 -150
- package/src/execution/engine/engines/zcode/__tests__/preparer-appserver.test.ts +0 -387
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +0 -710
- package/src/execution/engine/engines/zcode/appserver-home.ts +0 -442
- package/src/execution/engine/engines/zcode/appserver-probe.ts +0 -141
- package/src/execution/engine/engines/zcode/launcher.ts +0 -161
- package/src/execution/execute-options-mapper.ts +0 -115
- package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +0 -288
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// 统一资源发现模块测试(ADR-031)。
|
|
4
4
|
// 验证:扫描源覆盖、优先级合并、manifest 校验、约定目录 fallback。
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
|
+
import * as fsp from "node:fs/promises";
|
|
6
7
|
import * as os from "node:os";
|
|
7
8
|
import * as path from "node:path";
|
|
8
9
|
|
|
@@ -25,11 +26,22 @@ vi.mock("node:os", async (importOriginal) => {
|
|
|
25
26
|
return { ...actual, homedir: () => mockHomeDir };
|
|
26
27
|
});
|
|
27
28
|
|
|
29
|
+
// manifestCache 计数(原 resource-discovery-manifest-cache.test.ts 手法迁编):
|
|
30
|
+
// 透传 actual 实现 + vi.fn 计数,行为零变化——async readPackageManifest 的
|
|
31
|
+
// readFile 计数断言依赖此包装(ESM 下 node:fs/promises 不可直接 spyOn)。
|
|
32
|
+
vi.mock("node:fs/promises", async (importOriginal) => {
|
|
33
|
+
const actual = await importOriginal<typeof import("node:fs/promises")>();
|
|
34
|
+
return {
|
|
35
|
+
...actual,
|
|
36
|
+
readFile: vi.fn(
|
|
37
|
+
(...args: Parameters<typeof actual.readFile>) => actual.readFile(...args),
|
|
38
|
+
),
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
|
|
28
42
|
import {
|
|
29
43
|
discoverResources,
|
|
30
|
-
discoverResourcesSync,
|
|
31
44
|
findWorkspaceRoot,
|
|
32
|
-
processPackageSync,
|
|
33
45
|
getCachedFile,
|
|
34
46
|
getCachedFileContent,
|
|
35
47
|
__testResetShadowDedup,
|
|
@@ -37,6 +49,7 @@ import {
|
|
|
37
49
|
isMachineSource,
|
|
38
50
|
getCachedParsed,
|
|
39
51
|
clearFileCache,
|
|
52
|
+
type ResourceSource,
|
|
40
53
|
} from "../resource-discovery.ts";
|
|
41
54
|
// logger 断言目标随源切换(u0-data-discovery):resource-discovery 的 logger 是
|
|
42
55
|
// core facade(同 component 单例引用),spyOn facade 实例即拦截模块内 logger 调用
|
|
@@ -68,7 +81,7 @@ function writeFile(dir: string, name: string, content: string): string {
|
|
|
68
81
|
return filePath;
|
|
69
82
|
}
|
|
70
83
|
|
|
71
|
-
function writePackageJson(pkgDir: string, pi: Record<string, unknown>): void {
|
|
84
|
+
function writePackageJson(pkgDir: string, pi: Record<string, unknown> | undefined): void {
|
|
72
85
|
fs.mkdirSync(pkgDir, { recursive: true });
|
|
73
86
|
fs.writeFileSync(
|
|
74
87
|
path.join(pkgDir, "package.json"),
|
|
@@ -77,6 +90,19 @@ function writePackageJson(pkgDir: string, pi: Record<string, unknown>): void {
|
|
|
77
90
|
);
|
|
78
91
|
}
|
|
79
92
|
|
|
93
|
+
/** 定值 mtime(整数 ms,消除浮点 mtime 与 utimesSync 恢复的精度差)。 */
|
|
94
|
+
const T1 = new Date("2026-01-01T00:00:00Z");
|
|
95
|
+
const T2 = new Date("2026-01-01T00:00:10Z");
|
|
96
|
+
|
|
97
|
+
function setMtime(filePath: string, t: Date): void {
|
|
98
|
+
fs.utimesSync(filePath, t, t);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** node:fs/promises readFile 包装计数(async manifestCache 读者)。 */
|
|
102
|
+
function asyncReadCount(): number {
|
|
103
|
+
return vi.mocked(fsp.readFile).mock.calls.length;
|
|
104
|
+
}
|
|
105
|
+
|
|
80
106
|
// ============================================================
|
|
81
107
|
// findWorkspaceRoot
|
|
82
108
|
// ============================================================
|
|
@@ -85,7 +111,7 @@ describe("findWorkspaceRoot", () => {
|
|
|
85
111
|
it("returns cwd when no marker found", () => {
|
|
86
112
|
const ws = tmpWorkspace();
|
|
87
113
|
expect(findWorkspaceRoot(ws)).toBe(ws);
|
|
88
|
-
fs.rmSync(ws, { recursive: true, force: true });
|
|
114
|
+
fs.rmSync(ws, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
89
115
|
});
|
|
90
116
|
|
|
91
117
|
it("finds .git root", () => {
|
|
@@ -94,140 +120,7 @@ describe("findWorkspaceRoot", () => {
|
|
|
94
120
|
const sub = path.join(ws, "sub", "deep");
|
|
95
121
|
fs.mkdirSync(sub, { recursive: true });
|
|
96
122
|
expect(findWorkspaceRoot(sub)).toBe(ws);
|
|
97
|
-
fs.rmSync(ws, { recursive: true, force: true });
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
// ============================================================
|
|
102
|
-
// discoverResourcesSync — 扫描源覆盖 + 优先级
|
|
103
|
-
// ============================================================
|
|
104
|
-
|
|
105
|
-
describe("discoverResourcesSync", () => {
|
|
106
|
-
let ws: string;
|
|
107
|
-
let agentDir: string;
|
|
108
|
-
|
|
109
|
-
beforeEach(() => {
|
|
110
|
-
ws = tmpWorkspace();
|
|
111
|
-
agentDir = path.join(ws, ".fake-agent");
|
|
112
|
-
});
|
|
113
|
-
afterEach(() => {
|
|
114
|
-
fs.rmSync(ws, { recursive: true, force: true });
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("discovers agents from project .pi/agents/", () => {
|
|
118
|
-
writeFile(path.join(ws, ".pi", "agents"), "worker.md", "body");
|
|
119
|
-
const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
|
|
120
|
-
// 按 source 过滤断言——用户全局目录(~/.agents/agents/)可能有真实 agent 文件,
|
|
121
|
-
// 测试不假设环境为空(2026-08:环境新增 tech-design-review.md 暴露此脆弱性)
|
|
122
|
-
const project = result.filter((r) => r.source === "project-pi");
|
|
123
|
-
expect(project.map((r) => path.basename(r.path))).toEqual(["worker.md"]);
|
|
124
|
-
expect(project[0]?.available).toBe(true);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("discovers workflows from project .pi/workflows/", () => {
|
|
128
|
-
writeFile(path.join(ws, ".pi", "workflows"), "build.js", "const meta={name:'build'};");
|
|
129
|
-
const result = discoverResourcesSync({ kind: "workflows", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "workflows") });
|
|
130
|
-
expect(result.map((r) => path.basename(r.path))).toEqual(["build.js"]);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("project .agents overrides project .pi on name clash (priority)", () => {
|
|
134
|
-
writeFile(path.join(ws, ".pi", "agents"), "worker.md", "pi-body");
|
|
135
|
-
writeFile(path.join(ws, ".agents", "agents"), "worker.md", "agents-body");
|
|
136
|
-
const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
|
|
137
|
-
const project = result.filter((r) => r.source === "project-agents");
|
|
138
|
-
expect(project).toHaveLength(1);
|
|
139
|
-
expect(project[0]?.source).toBe("project-agents");
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it("includes tmp source for workflows when includeTmp=true", () => {
|
|
143
|
-
writeFile(path.join(ws, ".pi", "workflows", ".tmp"), "temp.js", "const meta={name:'temp'};");
|
|
144
|
-
const result = discoverResourcesSync({
|
|
145
|
-
kind: "workflows",
|
|
146
|
-
workspaceRoot: ws,
|
|
147
|
-
hostRoots: hostRootsFor(agentDir, "workflows"),
|
|
148
|
-
includeTmp: true,
|
|
149
|
-
});
|
|
150
|
-
expect(result.map((r) => path.basename(r.path))).toEqual(["temp.js"]);
|
|
151
|
-
expect(result[0]?.source).toBe("project-pi-tmp");
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("excludes tmp source when includeTmp omitted", () => {
|
|
155
|
-
writeFile(path.join(ws, ".pi", "workflows", ".tmp"), "temp.js", "x");
|
|
156
|
-
const result = discoverResourcesSync({ kind: "workflows", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "workflows") });
|
|
157
|
-
expect(result).toEqual([]);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("ignores _ prefix and .chain.md files", () => {
|
|
161
|
-
const dir = path.join(ws, ".pi", "agents");
|
|
162
|
-
writeFile(dir, "real.md", "body");
|
|
163
|
-
writeFile(dir, "_skip.md", "ignored");
|
|
164
|
-
writeFile(dir, "trace.chain.md", "ignored");
|
|
165
|
-
const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
|
|
166
|
-
const project = result.filter((r) => r.source === "project-pi");
|
|
167
|
-
expect(project.map((r) => path.basename(r.path))).toEqual(["real.md"]);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it("nonexistent directories are silently skipped", () => {
|
|
171
|
-
const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
|
|
172
|
-
// 用户全局目录可能有真实文件——只断言 project 源为空
|
|
173
|
-
expect(result.filter((r) => r.source === "project-pi" || r.source === "project-agents")).toEqual([]);
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
// ============================================================
|
|
178
|
-
// processPackageSync — manifest 校验
|
|
179
|
-
// ============================================================
|
|
180
|
-
|
|
181
|
-
describe("processPackageSync", () => {
|
|
182
|
-
let pkgDir: string;
|
|
183
|
-
|
|
184
|
-
beforeEach(() => {
|
|
185
|
-
pkgDir = tmpWorkspace();
|
|
186
|
-
});
|
|
187
|
-
afterEach(() => {
|
|
188
|
-
fs.rmSync(pkgDir, { recursive: true, force: true });
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it("loads from manifest directory declaration", () => {
|
|
192
|
-
writeFile(path.join(pkgDir, "agents"), "worker.md", "body");
|
|
193
|
-
writePackageJson(pkgDir, { agents: ["./agents"] });
|
|
194
|
-
const result = processPackageSync(pkgDir, "agents");
|
|
195
|
-
expect(result).toHaveLength(1);
|
|
196
|
-
expect(result[0]?.available).toBe(true);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it("loads from manifest file declaration", () => {
|
|
200
|
-
writeFile(pkgDir, "worker.md", "body");
|
|
201
|
-
writePackageJson(pkgDir, { agents: ["./worker.md"] });
|
|
202
|
-
const result = processPackageSync(pkgDir, "agents");
|
|
203
|
-
expect(result).toHaveLength(1);
|
|
204
|
-
expect(result[0]?.available).toBe(true);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it("manifest path not exists → available=false, no fallback", () => {
|
|
208
|
-
writePackageJson(pkgDir, { agents: ["./nonexistent"] });
|
|
209
|
-
const result = processPackageSync(pkgDir, "agents");
|
|
210
|
-
expect(result).toHaveLength(1);
|
|
211
|
-
expect(result[0]?.available).toBe(false);
|
|
212
|
-
// 不 fallback 到约定目录
|
|
213
|
-
writeFile(path.join(pkgDir, "agents"), "hidden.md", "body");
|
|
214
|
-
const result2 = processPackageSync(pkgDir, "agents");
|
|
215
|
-
expect(result2).toHaveLength(1);
|
|
216
|
-
expect(result2[0]?.available).toBe(false);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it("no manifest → fallback to convention dir", () => {
|
|
220
|
-
writeFile(path.join(pkgDir, "agents"), "worker.md", "body");
|
|
221
|
-
// 无 package.json 或无 pi.agents
|
|
222
|
-
const result = processPackageSync(pkgDir, "agents");
|
|
223
|
-
expect(result).toHaveLength(1);
|
|
224
|
-
expect(result[0]?.available).toBe(true);
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it("no manifest and no convention dir → empty", () => {
|
|
228
|
-
writePackageJson(pkgDir, { extensions: ["./index.ts"] });
|
|
229
|
-
const result = processPackageSync(pkgDir, "agents");
|
|
230
|
-
expect(result).toEqual([]);
|
|
123
|
+
fs.rmSync(ws, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
231
124
|
});
|
|
232
125
|
});
|
|
233
126
|
|
|
@@ -242,7 +135,7 @@ describe("discoverResources (async)", () => {
|
|
|
242
135
|
ws = tmpWorkspace();
|
|
243
136
|
});
|
|
244
137
|
afterEach(() => {
|
|
245
|
-
fs.rmSync(ws, { recursive: true, force: true });
|
|
138
|
+
fs.rmSync(ws, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
246
139
|
__testResetShadowDedup();
|
|
247
140
|
});
|
|
248
141
|
|
|
@@ -287,72 +180,10 @@ describe("user-extension-paths (XYZ_EXTENSION_PATHS)", () => {
|
|
|
287
180
|
afterEach(() => {
|
|
288
181
|
if (savedEnv === undefined) delete process.env.XYZ_EXTENSION_PATHS;
|
|
289
182
|
else process.env.XYZ_EXTENSION_PATHS = savedEnv;
|
|
290
|
-
fs.rmSync(ws, { recursive: true, force: true });
|
|
183
|
+
fs.rmSync(ws, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
291
184
|
__testResetShadowDedup();
|
|
292
185
|
});
|
|
293
186
|
|
|
294
|
-
it("discovers agents from XYZ_EXTENSION_PATHS via pi.agents manifest", () => {
|
|
295
|
-
const pkgDir = path.join(ws, "my-ext");
|
|
296
|
-
writePackageJson(pkgDir, { agents: ["./agents"] });
|
|
297
|
-
writeFile(path.join(pkgDir, "agents"), "custom.md", "body");
|
|
298
|
-
process.env.XYZ_EXTENSION_PATHS = pkgDir;
|
|
299
|
-
const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
|
|
300
|
-
const ext = result.filter((r) => r.source === "user-extension-paths");
|
|
301
|
-
expect(ext.map((r) => path.basename(r.path))).toEqual(["custom.md"]);
|
|
302
|
-
expect(ext[0]?.source).toBe("user-extension-paths");
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it("discovers agents via convention dir (no manifest)", () => {
|
|
306
|
-
const pkgDir = path.join(ws, "my-ext");
|
|
307
|
-
writeFile(path.join(pkgDir, "agents"), "conv.md", "body");
|
|
308
|
-
process.env.XYZ_EXTENSION_PATHS = pkgDir;
|
|
309
|
-
const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
|
|
310
|
-
const ext = result.filter((r) => r.source === "user-extension-paths");
|
|
311
|
-
expect(ext.map((r) => path.basename(r.path))).toEqual(["conv.md"]);
|
|
312
|
-
expect(ext[0]?.source).toBe("user-extension-paths");
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
it("multiple paths separated by delimiter", () => {
|
|
316
|
-
const pkg1 = path.join(ws, "ext1");
|
|
317
|
-
const pkg2 = path.join(ws, "ext2");
|
|
318
|
-
writeFile(path.join(pkg1, "agents"), "a1.md", "body");
|
|
319
|
-
writeFile(path.join(pkg2, "agents"), "a2.md", "body");
|
|
320
|
-
process.env.XYZ_EXTENSION_PATHS = `${pkg1}${path.delimiter}${pkg2}`;
|
|
321
|
-
const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
|
|
322
|
-
const ext = result.filter((r) => r.source === "user-extension-paths");
|
|
323
|
-
expect(ext.map((r) => path.basename(r.path)).sort()).toEqual(["a1.md", "a2.md"]);
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
it("overrides npm on name clash (priority: user-extension-paths > npm)", () => {
|
|
327
|
-
const npmPkg = path.join(agentDir, "npm", "node_modules", "test-pkg");
|
|
328
|
-
writePackageJson(npmPkg, { agents: ["./agents"] });
|
|
329
|
-
writeFile(path.join(npmPkg, "agents"), "shared.md", "npm-body");
|
|
330
|
-
const devPkg = path.join(ws, "dev-ext");
|
|
331
|
-
writePackageJson(devPkg, { agents: ["./agents"] });
|
|
332
|
-
writeFile(path.join(devPkg, "agents"), "shared.md", "dev-body");
|
|
333
|
-
process.env.XYZ_EXTENSION_PATHS = devPkg;
|
|
334
|
-
const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
|
|
335
|
-
const shared = result.find((r) => path.basename(r.path) === "shared.md");
|
|
336
|
-
expect(shared?.source).toBe("user-extension-paths");
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
it("project-agents overrides user-extension-paths (project wins)", () => {
|
|
340
|
-
const devPkg = path.join(ws, "dev-ext");
|
|
341
|
-
writePackageJson(devPkg, { agents: ["./agents"] });
|
|
342
|
-
writeFile(path.join(devPkg, "agents"), "x.md", "dev-body");
|
|
343
|
-
writeFile(path.join(ws, ".agents", "agents"), "x.md", "project-body");
|
|
344
|
-
process.env.XYZ_EXTENSION_PATHS = devPkg;
|
|
345
|
-
const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
|
|
346
|
-
const x = result.find((r) => path.basename(r.path) === "x.md");
|
|
347
|
-
expect(x?.source).toBe("project-agents");
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
it("empty/unset env → no user-extension-paths source", () => {
|
|
351
|
-
delete process.env.XYZ_EXTENSION_PATHS;
|
|
352
|
-
const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
|
|
353
|
-
expect(result.filter((r) => r.source === "user-extension-paths")).toEqual([]);
|
|
354
|
-
});
|
|
355
|
-
|
|
356
187
|
it("async discoverResources also scans user-extension-paths", async () => {
|
|
357
188
|
const pkgDir = path.join(ws, "my-ext");
|
|
358
189
|
writePackageJson(pkgDir, { agents: ["./agents"] });
|
|
@@ -388,7 +219,7 @@ describe("user-extension-paths (XYZ_EXTENSION_PATHS)", () => {
|
|
|
388
219
|
expect(x?.source).toBe("project-agents");
|
|
389
220
|
});
|
|
390
221
|
|
|
391
|
-
it("async: 4 源混合 fixture——输出序 + 优先级
|
|
222
|
+
it("async: 4 源混合 fixture——输出序 + 优先级", async () => {
|
|
392
223
|
// 源优先级低→高:npm < user-extension-paths < project-pi < project-agents
|
|
393
224
|
const npmPkg = path.join(agentDir, "npm", "node_modules", "test-pkg");
|
|
394
225
|
writePackageJson(npmPkg, { agents: ["./agents"] });
|
|
@@ -415,8 +246,6 @@ describe("user-extension-paths (XYZ_EXTENSION_PATHS)", () => {
|
|
|
415
246
|
expect(asyncResult.find((r) => path.basename(r.path) === "shared.md")?.source).toBe(
|
|
416
247
|
"project-agents",
|
|
417
248
|
);
|
|
418
|
-
// 快照等价:源级并行 async 版与串行 sync 版输出逐项一致(含序与 source 标签)
|
|
419
|
-
expect(asyncResult).toEqual(discoverResourcesSync(config));
|
|
420
249
|
});
|
|
421
250
|
|
|
422
251
|
it("async: 同名遮蔽时机器源×机器源降 debug 不产生 warn(D8d 分级)", async () => {
|
|
@@ -521,7 +350,7 @@ describe("user-extension-paths (XYZ_EXTENSION_PATHS)", () => {
|
|
|
521
350
|
warnSpy.mockRestore();
|
|
522
351
|
}
|
|
523
352
|
} finally {
|
|
524
|
-
fs.rmSync(userAgentsDir, { recursive: true, force: true });
|
|
353
|
+
fs.rmSync(userAgentsDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
525
354
|
}
|
|
526
355
|
});
|
|
527
356
|
|
|
@@ -557,7 +386,7 @@ describe("user-extension-paths (XYZ_EXTENSION_PATHS)", () => {
|
|
|
557
386
|
warnSpy.mockRestore();
|
|
558
387
|
}
|
|
559
388
|
} finally {
|
|
560
|
-
fs.rmSync(userAgentsDir, { recursive: true, force: true });
|
|
389
|
+
fs.rmSync(userAgentsDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
561
390
|
}
|
|
562
391
|
});
|
|
563
392
|
|
|
@@ -617,7 +446,7 @@ describe("user-extension-paths (XYZ_EXTENSION_PATHS)", () => {
|
|
|
617
446
|
__testResetShadowDedup();
|
|
618
447
|
}
|
|
619
448
|
} finally {
|
|
620
|
-
fs.rmSync(userAgentsDir, { recursive: true, force: true });
|
|
449
|
+
fs.rmSync(userAgentsDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
621
450
|
}
|
|
622
451
|
});
|
|
623
452
|
});
|
|
@@ -637,7 +466,7 @@ describe("m5: 统一 mtime 缓存层", () => {
|
|
|
637
466
|
expect(first?.content).toBe("---\nname: x\ndescription: y\n---\nbody");
|
|
638
467
|
expect(second).toBe(first); // 同引用 = 命中缓存(未重 read)
|
|
639
468
|
} finally {
|
|
640
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
469
|
+
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
641
470
|
}
|
|
642
471
|
});
|
|
643
472
|
|
|
@@ -651,7 +480,7 @@ describe("m5: 统一 mtime 缓存层", () => {
|
|
|
651
480
|
// 内容一致性(命中语义由 TC4a 的对象引用断言覆盖——此处验证两 API 一致)
|
|
652
481
|
expect(a?.content).toBe(b);
|
|
653
482
|
} finally {
|
|
654
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
483
|
+
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
655
484
|
}
|
|
656
485
|
});
|
|
657
486
|
|
|
@@ -667,7 +496,7 @@ describe("m5: 统一 mtime 缓存层", () => {
|
|
|
667
496
|
fs.rmSync(f);
|
|
668
497
|
expect(getCachedFileContent(f)).toBeNull();
|
|
669
498
|
} finally {
|
|
670
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
499
|
+
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
671
500
|
}
|
|
672
501
|
});
|
|
673
502
|
});
|
|
@@ -691,7 +520,7 @@ describe("getCachedParsed(mtime 级解析缓存)", () => {
|
|
|
691
520
|
expect(second).toBe("OK");
|
|
692
521
|
expect(parse).toHaveBeenCalledTimes(1); // 第二次命中缓存,不重 parse
|
|
693
522
|
} finally {
|
|
694
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
523
|
+
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
695
524
|
}
|
|
696
525
|
});
|
|
697
526
|
|
|
@@ -707,7 +536,7 @@ describe("getCachedParsed(mtime 级解析缓存)", () => {
|
|
|
707
536
|
fs.rmSync(f);
|
|
708
537
|
expect(getCachedParsed(f, parse)).toBeNull(); // 删除 → null
|
|
709
538
|
} finally {
|
|
710
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
539
|
+
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
711
540
|
}
|
|
712
541
|
});
|
|
713
542
|
|
|
@@ -722,7 +551,7 @@ describe("getCachedParsed(mtime 级解析缓存)", () => {
|
|
|
722
551
|
getCachedParsed(f, parse);
|
|
723
552
|
expect(parse).toHaveBeenCalledTimes(2); // 缓存被清 → 重新 parse
|
|
724
553
|
} finally {
|
|
725
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
554
|
+
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
726
555
|
}
|
|
727
556
|
});
|
|
728
557
|
|
|
@@ -744,7 +573,221 @@ describe("getCachedParsed(mtime 级解析缓存)", () => {
|
|
|
744
573
|
expect(w1).toEqual({ kind: "workflow", len: 14 });
|
|
745
574
|
expect(a2).toEqual({ kind: "agent", content: "shared-content" });
|
|
746
575
|
} finally {
|
|
747
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
576
|
+
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// ── manifestCache(async readPackageManifest)缓存语义 ──
|
|
582
|
+
// 原 resource-discovery-manifest-cache.test.ts 的 async 侧语义迁编(C3 sync 轨删除:
|
|
583
|
+
// sync 读者消亡,双读者共享用例随之前提失效;读计数断言改走 node:fs/promises 包装)。
|
|
584
|
+
// readPackageManifest 是 module 私有,经 XYZ_EXTENSION_PATHS → discoverResources 路径驱动;
|
|
585
|
+
// 其余源(user-agents / npm / project)无 package.json 可读,asyncReadCount 即
|
|
586
|
+
// pkgDir/package.json 的精确读计数。
|
|
587
|
+
|
|
588
|
+
describe("manifestCache(async readPackageManifest)", () => {
|
|
589
|
+
let ws: string;
|
|
590
|
+
let pkgDir: string;
|
|
591
|
+
let savedEnv: string | undefined;
|
|
592
|
+
|
|
593
|
+
beforeEach(() => {
|
|
594
|
+
clearFileCache();
|
|
595
|
+
vi.mocked(fsp.readFile).mockClear();
|
|
596
|
+
ws = tmpWorkspace();
|
|
597
|
+
pkgDir = path.join(ws, "my-ext");
|
|
598
|
+
savedEnv = process.env.XYZ_EXTENSION_PATHS;
|
|
599
|
+
});
|
|
600
|
+
afterEach(() => {
|
|
601
|
+
if (savedEnv === undefined) delete process.env.XYZ_EXTENSION_PATHS;
|
|
602
|
+
else process.env.XYZ_EXTENSION_PATHS = savedEnv;
|
|
603
|
+
fs.rmSync(ws, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
function extConfig(kind: "agents" | "workflows") {
|
|
607
|
+
return { kind, workspaceRoot: ws, hostRoots: hostRootsFor(path.join(ws, ".fake-agent"), kind) };
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
it("命中:同 mtime 二次 discoverResources 不重读(async readFile 计数不增)", async () => {
|
|
611
|
+
writePackageJson(pkgDir, { agents: ["./agents"] });
|
|
612
|
+
writeFile(path.join(pkgDir, "agents"), "a.md", "body");
|
|
613
|
+
setMtime(path.join(pkgDir, "package.json"), T1);
|
|
614
|
+
process.env.XYZ_EXTENSION_PATHS = pkgDir;
|
|
615
|
+
|
|
616
|
+
const r1 = await discoverResources(extConfig("agents"));
|
|
617
|
+
const ext1 = r1.filter((r) => r.source === "user-extension-paths");
|
|
618
|
+
expect(ext1.map((r) => path.basename(r.path))).toEqual(["a.md"]);
|
|
619
|
+
const first = asyncReadCount();
|
|
620
|
+
expect(first).toBeGreaterThanOrEqual(1); // manifest 首读确实发生
|
|
621
|
+
|
|
622
|
+
const r2 = await discoverResources(extConfig("agents"));
|
|
623
|
+
// 同 mtime:manifestCache 命中,readFile 计数不增
|
|
624
|
+
expect(asyncReadCount()).toBe(first);
|
|
625
|
+
expect(r2.filter((r) => r.source === "user-extension-paths")).toHaveLength(1);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it("kind 共享一次 parse:同 package.json 的 agents/workflows 两 kind 只读一次", async () => {
|
|
629
|
+
writePackageJson(pkgDir, { agents: ["./agents"], workflows: ["./w.js"] });
|
|
630
|
+
writeFile(path.join(pkgDir, "agents"), "a.md", "body");
|
|
631
|
+
writeFile(pkgDir, "w.js", "const meta={name:'w'};");
|
|
632
|
+
setMtime(path.join(pkgDir, "package.json"), T1);
|
|
633
|
+
process.env.XYZ_EXTENSION_PATHS = pkgDir;
|
|
634
|
+
|
|
635
|
+
const rAgents = await discoverResources(extConfig("agents"));
|
|
636
|
+
expect(rAgents.filter((r) => r.source === "user-extension-paths").map((r) => path.basename(r.path)))
|
|
637
|
+
.toEqual(["a.md"]);
|
|
638
|
+
const afterFirst = asyncReadCount();
|
|
639
|
+
expect(afterFirst).toBeGreaterThanOrEqual(1);
|
|
640
|
+
|
|
641
|
+
const rWorkflows = await discoverResources(extConfig("workflows"));
|
|
642
|
+
expect(rWorkflows.filter((r) => r.source === "user-extension-paths").map((r) => path.basename(r.path)))
|
|
643
|
+
.toEqual(["w.js"]);
|
|
644
|
+
// 两 kind 共享同一条目:第二次调用命中缓存,不重 read
|
|
645
|
+
expect(asyncReadCount()).toBe(afterFirst);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it("失效:package.json 重写(mtime 变)→ 重读新 manifest(计数递增 + 新内容生效)", async () => {
|
|
649
|
+
writePackageJson(pkgDir, { agents: ["./agents"] });
|
|
650
|
+
writeFile(path.join(pkgDir, "agents"), "a.md", "body");
|
|
651
|
+
setMtime(path.join(pkgDir, "package.json"), T1);
|
|
652
|
+
process.env.XYZ_EXTENSION_PATHS = pkgDir;
|
|
653
|
+
await discoverResources(extConfig("agents"));
|
|
654
|
+
const first = asyncReadCount();
|
|
655
|
+
|
|
656
|
+
// 重写 manifest 声明不存在的路径:不需显式设 mtime(新写入 mtime 必 ≠ 定值 T1)
|
|
657
|
+
writePackageJson(pkgDir, { agents: ["./gone.md"] });
|
|
658
|
+
const r2 = await discoverResources(extConfig("agents"));
|
|
659
|
+
// mtime 变 → 重读(计数 +1)→ 新声明生效:路径不存在 → available=false 占位
|
|
660
|
+
expect(asyncReadCount()).toBe(first + 1);
|
|
661
|
+
const ext2 = r2.filter((r) => r.source === "user-extension-paths");
|
|
662
|
+
expect(ext2).toHaveLength(1);
|
|
663
|
+
expect(path.basename(ext2[0]?.path ?? "")).toBe("gone.md");
|
|
664
|
+
expect(ext2[0]?.available).toBe(false);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// ── 失败/边缘路径(原 resource-discovery-manifest-cache.test.ts 迁编,impl-plan
|
|
668
|
+
// 偏差 #9:缺则将对应用例改写为 async 形态并入,不丢缓存行为覆盖)。原版直接
|
|
669
|
+
// 断言 readPackageManifestSync 返回值;async 形态观察面 = readFile 计数 +
|
|
670
|
+
// user-extension-paths 结果(manifest undefined → processPackage 约定目录
|
|
671
|
+
// fallback,pkgDir 无 {kind}/ 目录 → 零资源即 undefined 的行为面)。 ──
|
|
672
|
+
|
|
673
|
+
it("坏 JSON → undefined 且不缓存:mtime 未变二次 discover 仍重读(计数递增)", async () => {
|
|
674
|
+
// 起点即坏 JSON(无缓存条目)——若坏结果被缓存,二次调用不会重新 readFile。
|
|
675
|
+
// (good→bad 且 mtime 未变的场景由「命中」用例路径覆盖:mtime 相等直接命中原好条目)
|
|
676
|
+
writeFile(pkgDir, "package.json", "{ broken json");
|
|
677
|
+
setMtime(path.join(pkgDir, "package.json"), T1);
|
|
678
|
+
process.env.XYZ_EXTENSION_PATHS = pkgDir;
|
|
679
|
+
|
|
680
|
+
const r1 = await discoverResources(extConfig("agents"));
|
|
681
|
+
// 坏 JSON → manifest undefined(约定目录 fallback 零资源),不抛
|
|
682
|
+
expect(r1.filter((r) => r.source === "user-extension-paths")).toHaveLength(0);
|
|
683
|
+
const first = asyncReadCount();
|
|
684
|
+
expect(first).toBeGreaterThanOrEqual(1); // 首读确实发生
|
|
685
|
+
|
|
686
|
+
const r2 = await discoverResources(extConfig("agents"));
|
|
687
|
+
expect(r2.filter((r) => r.source === "user-extension-paths")).toHaveLength(0);
|
|
688
|
+
// 二次调用仍重新 readFile = 坏结果未入缓存(毒条目永不入缓存)
|
|
689
|
+
expect(asyncReadCount()).toBe(first + 1);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it("坏 JSON 不驱逐已有好条目:恢复原内容 + 原 mtime → 命中原条目(readFile 不增)", async () => {
|
|
693
|
+
writePackageJson(pkgDir, { agents: ["./agents"] });
|
|
694
|
+
writeFile(path.join(pkgDir, "agents"), "a.md", "body");
|
|
695
|
+
const pkgJson = path.join(pkgDir, "package.json");
|
|
696
|
+
setMtime(pkgJson, T1);
|
|
697
|
+
process.env.XYZ_EXTENSION_PATHS = pkgDir;
|
|
698
|
+
await discoverResources(extConfig("agents"));
|
|
699
|
+
const first = asyncReadCount();
|
|
700
|
+
expect(first).toBeGreaterThanOrEqual(1);
|
|
701
|
+
|
|
702
|
+
// 改坏且 mtime 变(T2)→ undefined(坏 JSON 路径:不缓存、不驱逐 {T1, pi} 好条目)
|
|
703
|
+
writeFile(pkgDir, "package.json", "{ broken");
|
|
704
|
+
setMtime(pkgJson, T2);
|
|
705
|
+
await discoverResources(extConfig("agents"));
|
|
706
|
+
expect(asyncReadCount()).toBe(first + 1);
|
|
707
|
+
|
|
708
|
+
// 恢复原内容 + utimesSync 恢复原 mtime → 若好条目仍在则命中(无第 3 次 read)
|
|
709
|
+
writePackageJson(pkgDir, { agents: ["./agents"] });
|
|
710
|
+
setMtime(pkgJson, T1);
|
|
711
|
+
const r3 = await discoverResources(extConfig("agents"));
|
|
712
|
+
expect(
|
|
713
|
+
r3.filter((r) => r.source === "user-extension-paths").map((r) => path.basename(r.path)),
|
|
714
|
+
).toEqual(["a.md"]);
|
|
715
|
+
expect(asyncReadCount()).toBe(first + 1); // 无第 3 次 read = 命中保留的原条目
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it("stat 失败(package.json 删除)→ 驱逐条目:重建同内容同 mtime 必重读", async () => {
|
|
719
|
+
writePackageJson(pkgDir, { agents: ["./agents"] });
|
|
720
|
+
writeFile(path.join(pkgDir, "agents"), "a.md", "body");
|
|
721
|
+
const pkgJson = path.join(pkgDir, "package.json");
|
|
722
|
+
setMtime(pkgJson, T1);
|
|
723
|
+
process.env.XYZ_EXTENSION_PATHS = pkgDir;
|
|
724
|
+
await discoverResources(extConfig("agents"));
|
|
725
|
+
const first = asyncReadCount();
|
|
726
|
+
expect(first).toBeGreaterThanOrEqual(1);
|
|
727
|
+
|
|
728
|
+
fs.rmSync(pkgJson);
|
|
729
|
+
await discoverResources(extConfig("agents"));
|
|
730
|
+
expect(asyncReadCount()).toBe(first); // stat 失败路径不触发 readFile
|
|
731
|
+
|
|
732
|
+
// 重建同内容 + 恢复同 mtime:条目已被驱逐 → 必重 readFile(计数 +1)
|
|
733
|
+
writePackageJson(pkgDir, { agents: ["./agents"] });
|
|
734
|
+
setMtime(pkgJson, T1);
|
|
735
|
+
const r3 = await discoverResources(extConfig("agents"));
|
|
736
|
+
expect(
|
|
737
|
+
r3.filter((r) => r.source === "user-extension-paths").map((r) => path.basename(r.path)),
|
|
738
|
+
).toEqual(["a.md"]);
|
|
739
|
+
expect(asyncReadCount()).toBe(first + 1);
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
it("无 pi 字段(合法解析结果)也缓存:二次 discover 不重读", async () => {
|
|
743
|
+
writePackageJson(pkgDir, undefined);
|
|
744
|
+
setMtime(path.join(pkgDir, "package.json"), T1);
|
|
745
|
+
process.env.XYZ_EXTENSION_PATHS = pkgDir;
|
|
746
|
+
|
|
747
|
+
const r1 = await discoverResources(extConfig("agents"));
|
|
748
|
+
expect(r1.filter((r) => r.source === "user-extension-paths")).toHaveLength(0);
|
|
749
|
+
const first = asyncReadCount();
|
|
750
|
+
expect(first).toBeGreaterThanOrEqual(1);
|
|
751
|
+
|
|
752
|
+
const r2 = await discoverResources(extConfig("agents"));
|
|
753
|
+
expect(r2.filter((r) => r.source === "user-extension-paths")).toHaveLength(0);
|
|
754
|
+
expect(asyncReadCount()).toBe(first); // 合法解析 + 无 manifest → undefined 条目入缓存
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
// [review 修复] 非对象 JSON 结构守卫:JSON.parse 对 "42" / '"str"' / "null" 等
|
|
758
|
+
// 合法 JSON 产出非对象值,parsePiField 显式守卫归 undefined(「无 manifest」语义),
|
|
759
|
+
// 不依赖 primitive 装箱 / null 抛 TypeError 落 catch 的巧合。
|
|
760
|
+
it("非对象 JSON(42 / null / string / pi 字段非对象或数组)→ undefined 不抛,按合法解析结果缓存", async () => {
|
|
761
|
+
const cases: Array<{ raw: string; label: string }> = [
|
|
762
|
+
{ raw: "42", label: "number" },
|
|
763
|
+
{ raw: "null", label: "null" },
|
|
764
|
+
{ raw: '"just-a-string"', label: "string" },
|
|
765
|
+
{ raw: '{"name":"p","pi":42}', label: "pi-field-not-object" },
|
|
766
|
+
// 数组能骗过 typeof "object" 守卫(装箱对象)——非合法 pi 容器,显式归 undefined
|
|
767
|
+
{ raw: '{"name":"p","pi":[]}', label: "pi-field-array" },
|
|
768
|
+
];
|
|
769
|
+
// 每 case 独立子目录(缓存 key = package.json 绝对路径),互不命中;
|
|
770
|
+
// asyncReadCount 是跨 case 累积计数,断言用本 case 增量
|
|
771
|
+
for (const { raw, label } of cases) {
|
|
772
|
+
const caseDir = path.join(ws, label);
|
|
773
|
+
writeFile(caseDir, "package.json", raw);
|
|
774
|
+
setMtime(path.join(caseDir, "package.json"), T1);
|
|
775
|
+
process.env.XYZ_EXTENSION_PATHS = caseDir;
|
|
776
|
+
|
|
777
|
+
const caseStart = asyncReadCount();
|
|
778
|
+
const rAgents = await discoverResources(extConfig("agents"));
|
|
779
|
+
expect(
|
|
780
|
+
rAgents.filter((r) => r.source === "user-extension-paths"),
|
|
781
|
+
`case ${label}`,
|
|
782
|
+
).toHaveLength(0);
|
|
783
|
+
expect(asyncReadCount() - caseStart, `case ${label}`).toBe(1); // 首读恰一次
|
|
784
|
+
const rWorkflows = await discoverResources(extConfig("workflows"));
|
|
785
|
+
expect(
|
|
786
|
+
rWorkflows.filter((r) => r.source === "user-extension-paths"),
|
|
787
|
+
`case ${label}`,
|
|
788
|
+
).toHaveLength(0);
|
|
789
|
+
// 合法解析 + 无 manifest → 缓存 undefined 条目(两 kind 共享,零重读)
|
|
790
|
+
expect(asyncReadCount() - caseStart, `case ${label}`).toBe(1);
|
|
748
791
|
}
|
|
749
792
|
});
|
|
750
793
|
});
|