@zhushanwen/pi-network-proxy 0.2.1 → 0.2.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-network-proxy",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "LLM network proxy: rule-based per-hostname traffic splitting via a globalThis.fetch wrapper (http/https/socks5 proxy), with request counters.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -43,7 +43,7 @@
43
43
  "@zhushanwen/pi-ext-guards": "0.5.0",
44
44
  "@zhushanwen/pi-extension-logger": "0.6.2",
45
45
  "@zhushanwen/pi-file-lock": "0.4.1",
46
- "@zhushanwen/pi-llm-shared": "0.11.3"
46
+ "@zhushanwen/pi-llm-shared": "0.12.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@vitest/coverage-v8": "^4.1.9",
@@ -9,12 +9,13 @@ import { tmpdir } from "node:os";
9
9
  import { join } from "node:path";
10
10
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
11
11
 
12
+ // mock 只覆盖本包模块图实际消费的导出(getLogger / setPiHandle;createLogger 无消费方,
13
+ // 推测性 mock 会在 logger 包改表面时静默失配——缺导出是显式 undefined, loud fail)
12
14
  const { loggerMock } = vi.hoisted(() => ({
13
15
  loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
14
16
  }));
15
17
  vi.mock("@zhushanwen/pi-extension-logger", () => ({
16
18
  getLogger: () => loggerMock,
17
- createLogger: () => loggerMock,
18
19
  setPiHandle: vi.fn(),
19
20
  }));
20
21
 
@@ -55,6 +56,19 @@ function writeConfigFile(content: string): void {
55
56
  writeFileSync(join(agentDir, "config", "network-proxy-ext-config.json"), content);
56
57
  }
57
58
 
59
+ /**
60
+ * 标准前导:写配置文件 → 加载 extension(afterEach 的 resetModules 后每次 import
61
+ * 都是新模块图实例)→ 装配 fake pi 并执行工厂。config = 配置文件内容(默认 enabled)。
62
+ * 工厂在 helper 内执行:非法配置用例的「不抛」契约由 await 拒绝等价保证(抛即用例红)。
63
+ */
64
+ async function loadExtension(content = JSON.stringify(ENABLED_CONFIG)): Promise<{ emit: (event: "turn_start" | "turn_end") => void }> {
65
+ writeConfigFile(content);
66
+ const { default: networkProxyExtension } = await import("../index.ts");
67
+ const { pi, emit } = createFakePi();
68
+ networkProxyExtension(pi);
69
+ return { emit };
70
+ }
71
+
58
72
  beforeEach(() => {
59
73
  agentDir = mkdtempSync(join(tmpdir(), "network-proxy-index-"));
60
74
  vi.stubEnv("PI_CODING_AGENT_DIR", agentDir);
@@ -76,9 +90,7 @@ afterEach(() => {
76
90
 
77
91
  describe("networkProxyExtension 工厂加载序列", () => {
78
92
  it("enabled 且校验过:包装 globalThis.fetch(未命中规则时委托 fake 原 fetch)", async () => {
79
- writeConfigFile(JSON.stringify(ENABLED_CONFIG));
80
- const { default: networkProxyExtension } = await import("../index.ts");
81
- networkProxyExtension(createFakePi().pi);
93
+ await loadExtension();
82
94
  expect(globalThis.fetch).not.toBe(fakeFetch);
83
95
  const response = await globalThis.fetch("https://open.bigmodel.cn/api");
84
96
  expect(await response.text()).toBe("fake-original");
@@ -87,26 +99,21 @@ describe("networkProxyExtension 工厂加载序列", () => {
87
99
  });
88
100
 
89
101
  it("enabled=false:不包装(globalThis.fetch 保持原实现),零 error", async () => {
90
- writeConfigFile(JSON.stringify({ ...ENABLED_CONFIG, enabled: false }));
91
- const { default: networkProxyExtension } = await import("../index.ts");
92
- networkProxyExtension(createFakePi().pi);
102
+ await loadExtension(JSON.stringify({ ...ENABLED_CONFIG, enabled: false }));
93
103
  expect(globalThis.fetch).toBe(fakeFetch);
94
104
  expect(loggerMock.error).not.toHaveBeenCalled();
95
105
  });
96
106
 
97
107
  it("非法配置(坏 JSON):不抛、不包装(全部直连),error 含恢复动作(配置文件路径)", async () => {
98
- writeConfigFile("{ not valid json");
99
- const { default: networkProxyExtension } = await import("../index.ts");
100
- expect(() => networkProxyExtension(createFakePi().pi)).not.toThrow();
108
+ // 不抛契约由 loadExtension 的 await 保证(工厂抛错即本用例红)
109
+ await loadExtension("{ not valid json");
101
110
  expect(globalThis.fetch).toBe(fakeFetch);
102
111
  const messages = loggerMock.error.mock.calls.map((c) => String(c[0]));
103
112
  expect(messages.some((m) => m.includes("network-proxy-ext-config.json"))).toBe(true);
104
113
  });
105
114
 
106
115
  it("非法字段(端口越界):loadConfig 回落 defaults(enabled=false → 不包装)+ error 可归因", async () => {
107
- writeConfigFile(JSON.stringify({ ...ENABLED_CONFIG, proxy: { ...ENABLED_CONFIG.proxy, port: 99999 } }));
108
- const { default: networkProxyExtension } = await import("../index.ts");
109
- expect(() => networkProxyExtension(createFakePi().pi)).not.toThrow();
116
+ await loadExtension(JSON.stringify({ ...ENABLED_CONFIG, proxy: { ...ENABLED_CONFIG.proxy, port: 99999 } }));
110
117
  expect(globalThis.fetch).toBe(fakeFetch);
111
118
  expect(loggerMock.error.mock.calls.length).toBeGreaterThan(0);
112
119
  });
@@ -114,10 +121,7 @@ describe("networkProxyExtension 工厂加载序列", () => {
114
121
 
115
122
  describe("turn 粒度热生效", () => {
116
123
  it("turn_start 重读配置:指纹变化换新包装,未变零动作", async () => {
117
- writeConfigFile(JSON.stringify(ENABLED_CONFIG));
118
- const { default: networkProxyExtension } = await import("../index.ts");
119
- const { pi, emit } = createFakePi();
120
- networkProxyExtension(pi);
124
+ const { emit } = await loadExtension();
121
125
  const first = globalThis.fetch;
122
126
 
123
127
  // 未变配置:同包装函数(不重建)
@@ -131,10 +135,7 @@ describe("turn 粒度热生效", () => {
131
135
  });
132
136
 
133
137
  it("pi 重装 globalThis.fetch(configureHttpDispatcher)后 turn_start 重新包装覆盖者", async () => {
134
- writeConfigFile(JSON.stringify(ENABLED_CONFIG));
135
- const { default: networkProxyExtension } = await import("../index.ts");
136
- const { pi, emit } = createFakePi();
137
- networkProxyExtension(pi);
138
+ const { emit } = await loadExtension();
138
139
 
139
140
  let piReinstalledCalls = 0;
140
141
  const piReinstalled: FetchLike = async () => {
@@ -151,12 +152,9 @@ describe("turn 粒度热生效", () => {
151
152
  });
152
153
 
153
154
  it("turn_end flush 计数增量到 stats JSONL(recordDispatch → turn_end → 落盘行)", async () => {
154
- writeConfigFile(JSON.stringify(ENABLED_CONFIG));
155
- const { default: networkProxyExtension } = await import("../index.ts");
155
+ const { emit } = await loadExtension();
156
156
  // resetModules 后 index.ts 与此处 import 的 stats.ts 是同一模块图刷新的同一实例
157
157
  const { recordDispatch, statsFilePath } = await import("../stats.ts");
158
- const { pi, emit } = createFakePi();
159
- networkProxyExtension(pi);
160
158
 
161
159
  recordDispatch("api.anthropic.com", true);
162
160
  recordDispatch("open.bigmodel.cn", false);
@@ -169,10 +167,7 @@ describe("turn 粒度热生效", () => {
169
167
  });
170
168
 
171
169
  it("enabled true→false:turn_start 后恢复原 fetch(禁用生效)", async () => {
172
- writeConfigFile(JSON.stringify(ENABLED_CONFIG));
173
- const { default: networkProxyExtension } = await import("../index.ts");
174
- const { pi, emit } = createFakePi();
175
- networkProxyExtension(pi);
170
+ const { emit } = await loadExtension();
176
171
  expect(globalThis.fetch).not.toBe(fakeFetch);
177
172
 
178
173
  writeConfigFile(JSON.stringify({ ...ENABLED_CONFIG, enabled: false }));
@@ -49,11 +49,12 @@ type FetchInit = Parameters<FetchLike>[1];
49
49
  * 经代理发出时的 init:保留调用方全部选项,附上本次请求的代理分发器。
50
50
  * Omit 掉 Node 全局 RequestInit 自带的 dispatcher(undici-types 类型身份),避免与 npm
51
51
  * undici 的 Dispatcher 形成 `Dispatcher & Dispatcher` 冲突;运行时由本字段整体覆盖。
52
+ * 签名自包含(不引用文件内私有别名 FetchInit)——导出签名的被引用类型必须可命名。
52
53
  */
53
- export type ProxyFetchInit = Omit<NonNullable<FetchInit>, "dispatcher"> & { dispatcher: Dispatcher };
54
+ export type ProxyFetchInit = Omit<NonNullable<Parameters<FetchLike>[1]>, "dispatcher"> & { dispatcher: Dispatcher };
54
55
 
55
56
  /** 代理 fetch 注入接缝:生产环境 = undici.fetch;单测用 fake 避免真实网络。 */
56
- export type ProxyFetch = (input: FetchInput, init: ProxyFetchInit) => Promise<Response>;
57
+ export type ProxyFetch = (input: Parameters<FetchLike>[0], init: ProxyFetchInit) => Promise<Response>;
57
58
 
58
59
  /**
59
60
  * 跨模块环境身份标记(Symbol.for 进程级注册表,jiti reload 后新旧环境共享):