@zhushanwen/pi-ext-guards 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/__tests__/predicates.test.ts +109 -0
- package/src/index.ts +53 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-ext-guards",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Shared runtime-environment guards for Pi extensions — oncePerProcess(key, fn) dedupes cross-session side-effect operations against pi factory re-invocation and handler accumulation (shared library, not a Pi extension, zero dependencies).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// src/__tests__/predicates.test.ts
|
|
2
|
+
//
|
|
3
|
+
// isRecord / isEnoentError / isSubagentProcess 单测(ext-simplify-17 D3/D2/D4 新导出):
|
|
4
|
+
// - isRecord 排数组严版:数组必须 false(钉值,防回退到允数组宽版)
|
|
5
|
+
// - isEnoentError scheduler 严版:仅 Error 子类 + code === "ENOENT" 才 true,
|
|
6
|
+
// 非 Error 对象带 code 是宽版才接受的形态,严版必须 false
|
|
7
|
+
// - isSubagentProcess 三态 + env 注入隔离 + 旧键宁缺勿污钉值(D4 重锚定)
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
|
|
11
|
+
import { isEnoentError, isRecord, isSubagentProcess } from "../index.ts";
|
|
12
|
+
|
|
13
|
+
describe("isRecord", () => {
|
|
14
|
+
it("对象 true", () => {
|
|
15
|
+
expect(isRecord({})).toBe(true);
|
|
16
|
+
expect(isRecord({ key: "value" })).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("null false", () => {
|
|
20
|
+
expect(isRecord(null)).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("undefined false", () => {
|
|
24
|
+
expect(isRecord(undefined)).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("数组 false(排数组钉值——Record 语义不含数组)", () => {
|
|
28
|
+
expect(isRecord([])).toBe(false);
|
|
29
|
+
expect(isRecord([{ a: 1 }])).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("字符串 false", () => {
|
|
33
|
+
expect(isRecord("text")).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("isEnoentError", () => {
|
|
38
|
+
it("Error 带 code=ENOENT true(Node fs 错误的构造形态)", () => {
|
|
39
|
+
expect(isEnoentError(Object.assign(new Error("no such file"), { code: "ENOENT" }))).toBe(
|
|
40
|
+
true,
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("普通 Error false(无 code 属性)", () => {
|
|
45
|
+
expect(isEnoentError(new Error("x"))).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("code 为其他值 false", () => {
|
|
49
|
+
expect(isEnoentError(Object.assign(new Error("denied"), { code: "EACCES" }))).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("非 Error 对象带 code=ENOENT false(严版:宽版才接受的形态)", () => {
|
|
53
|
+
expect(isEnoentError({ code: "ENOENT" })).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("undefined false", () => {
|
|
57
|
+
expect(isEnoentError(undefined)).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("isSubagentProcess", () => {
|
|
62
|
+
it("XYZ_AGENT_SUBAGENT=1 命中(引擎 spawn 链恒注入的现行标记)", () => {
|
|
63
|
+
expect(isSubagentProcess({ XYZ_AGENT_SUBAGENT: "1" })).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("值非 \"1\" 不命中(标记必须是引擎注入的字面 \"1\")", () => {
|
|
67
|
+
expect(isSubagentProcess({ XYZ_AGENT_SUBAGENT: "0" })).toBe(false);
|
|
68
|
+
expect(isSubagentProcess({ XYZ_AGENT_SUBAGENT: "" })).toBe(false);
|
|
69
|
+
expect(isSubagentProcess({ XYZ_AGENT_SUBAGENT: "true" })).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("缺失不命中(顶层会话 → 消费方不降级)", () => {
|
|
73
|
+
expect(isSubagentProcess({})).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("env 参数注入隔离:传入的 env 与宿主 process.env 无关(宿主即使带标记,空 env 也不命中)", () => {
|
|
77
|
+
const saved = process.env.XYZ_AGENT_SUBAGENT;
|
|
78
|
+
try {
|
|
79
|
+
process.env.XYZ_AGENT_SUBAGENT = "1";
|
|
80
|
+
// 显式传 env:宿主的标记不渗透,判据只看参数
|
|
81
|
+
expect(isSubagentProcess({})).toBe(false);
|
|
82
|
+
expect(isSubagentProcess({ XYZ_AGENT_SUBAGENT: "1" })).toBe(true);
|
|
83
|
+
} finally {
|
|
84
|
+
if (saved === undefined) delete process.env.XYZ_AGENT_SUBAGENT;
|
|
85
|
+
else process.env.XYZ_AGENT_SUBAGENT = saved;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("缺省参数读 process.env(不传 env 时以宿主环境为判据)", () => {
|
|
90
|
+
const saved = process.env.XYZ_AGENT_SUBAGENT;
|
|
91
|
+
try {
|
|
92
|
+
process.env.XYZ_AGENT_SUBAGENT = "1";
|
|
93
|
+
expect(isSubagentProcess()).toBe(true);
|
|
94
|
+
delete process.env.XYZ_AGENT_SUBAGENT;
|
|
95
|
+
expect(isSubagentProcess()).toBe(false);
|
|
96
|
+
} finally {
|
|
97
|
+
if (saved === undefined) delete process.env.XYZ_AGENT_SUBAGENT;
|
|
98
|
+
else process.env.XYZ_AGENT_SUBAGENT = saved;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("旧 PI_SUBAGENT_* 身份键不再命中([HISTORICAL] D4 重锚前两键任一存在即命中;引擎协议化后无写入方,宁缺勿污)", () => {
|
|
103
|
+
expect(isSubagentProcess({ PI_SUBAGENT_ROOT_SESSION_ID: "s1" })).toBe(false);
|
|
104
|
+
expect(isSubagentProcess({ PI_SUBAGENT_SELF_RECORD_ID: "r1" })).toBe(false);
|
|
105
|
+
expect(isSubagentProcess({ PI_SUBAGENT_ROOT_SESSION_ID: "s1", PI_SUBAGENT_SELF_RECORD_ID: "r1" })).toBe(
|
|
106
|
+
false,
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// @zhushanwen/pi-ext-guards —— pi 运行环境守卫共享库(不是 Pi extension,零依赖纯函数,
|
|
4
4
|
// 无 pi SDK peerDep——供任意 @zhushanwen/pi-* 包与子代理核心消费)。
|
|
5
5
|
//
|
|
6
|
-
// 背景与设计依据:docs/
|
|
6
|
+
// 背景与设计依据:docs/architecture/file-lock-unification-and-reaper-sink.md §2.2 P3 / §3.2 D3。
|
|
7
7
|
// pi 的 extension 缓存按 cwd 失效:switch_session 时 cwd 不变则 factory 被二次调用且
|
|
8
8
|
// handler 累积注册——session_start handler 的真实派发语义是「每 session × factory
|
|
9
9
|
// 调用次数」,不是「每 session 一次」。handler 体内的跨 session 副作用操作(写非本
|
|
@@ -83,6 +83,58 @@ export function toErrorMessage(e: unknown): string {
|
|
|
83
83
|
return e instanceof Error ? e.message : String(e);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* 判断 value 是否为非 null、非数组的 object(`Record` 语义本义:可按字符串键安全取值)。
|
|
88
|
+
*
|
|
89
|
+
* canonical 来自 ext-simplify-17 D3(排数组严版,设计文档
|
|
90
|
+
* docs/architecture/ext-simplify-17-shared-extraction.md §3.1)。
|
|
91
|
+
*/
|
|
92
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
93
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 判断 err 是否为 Node fs 的 ENOENT 错误(`Error` 实例且 `code === "ENOENT"`)。
|
|
98
|
+
*
|
|
99
|
+
* canonical 来自 ext-simplify-17 D2(scheduler 严版,`"code" in err` 收窄避免
|
|
100
|
+
* `as Record` 强转——taste/no-unsafe-cast)。
|
|
101
|
+
*/
|
|
102
|
+
export function isEnoentError(err: unknown): boolean {
|
|
103
|
+
return err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── subagent 进程判据(isSubagentProcess,ext-simplify-17 D4)────────────
|
|
107
|
+
//
|
|
108
|
+
// 背景:extension 需要知道「当前 pi 进程是否引擎链上的 subagent 子进程」来决定降级
|
|
109
|
+
// (base-tool-enhance background 降级——bte background 生命周期设计的 D14 项,
|
|
110
|
+
// commit 8599abd0c,该设计未在现行 docs 留档 / smart-context R6 不注册工具,
|
|
111
|
+
// 规则定义见 smart-context src/index.ts R 规则族注释)。判据锚
|
|
112
|
+
// XYZ_AGENT_SUBAGENT=1——subagent-engine-sdk env.ts 的 buildEngineChildEnv 对每个引擎
|
|
113
|
+
// 子进程恒注入(L0 层最后写入),再经引擎内二次 spawn(buildOutboundChildEnv 全量
|
|
114
|
+
// 继承)传到 subagent 内的 bash。旧判据(PI_SUBAGENT_ROOT_SESSION_ID /
|
|
115
|
+
// PI_SUBAGENT_SELF_RECORD_ID 两键)在引擎协议化后已无写入方(20260914 探针证实,
|
|
116
|
+
// 报告 .tmp/dev-flow/ext-simplify-17-d4-probe.md),判据失效态由 D4 重锚收敛于此。
|
|
117
|
+
|
|
118
|
+
/** subagent 进程标记 env 键(引擎 spawn 链恒注入 "1",subagent-engine-sdk env.ts L0)。 */
|
|
119
|
+
export const SUBAGENT_MARKER_ENV = "XYZ_AGENT_SUBAGENT";
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 判断当前进程是否引擎链上的 subagent 子进程。
|
|
123
|
+
*
|
|
124
|
+
* 判据 = `env[SUBAGENT_MARKER_ENV] === "1"`(canonical 来自 ext-simplify-17 D4,
|
|
125
|
+
* 设计文档 docs/architecture/ext-simplify-17-shared-extraction.md §3.1)。口径
|
|
126
|
+
* 「宁缺勿污」:只有引擎 spawn 链上的真 subagent 才命中——人工 export 旧
|
|
127
|
+
* PI_SUBAGENT_ROOT_SESSION_ID / PI_SUBAGENT_SELF_RECORD_ID 身份键不再算(两键现
|
|
128
|
+
* 无写入方,见分节注释)。
|
|
129
|
+
*
|
|
130
|
+
* @param env env 快照;缺省读 process.env,显式传入即与宿主进程环境完全隔离(测试注入用)
|
|
131
|
+
*/
|
|
132
|
+
export function isSubagentProcess(
|
|
133
|
+
env: Record<string, string | undefined> = process.env,
|
|
134
|
+
): boolean {
|
|
135
|
+
return env[SUBAGENT_MARKER_ENV] === "1";
|
|
136
|
+
}
|
|
137
|
+
|
|
86
138
|
// ── stale ctx 守卫(guardStaleCtx,崩溃韧性 D1)────────────────────────
|
|
87
139
|
//
|
|
88
140
|
// 背景(docs/design/crash-resilience.md §3.3 D1 / §2.2 事件 E1):pi 的 extension API
|