Преглед изворни кода

test(snapshot): isolate a replayed dsh from the machine's proxy environment

A replay must not depend on the runner's network policy, the same reason it
pins its home and sessions root. The harness now honors the proxy environment,
so a runner exporting one sent the web-fetch scenario's fixture request to a
proxy that could not resolve the fixture host and recorded that proxy's error
page. Clear the proxy names in both test-support spawners, from the one list
dsh-http-proxy owns.
Yichen Jiang пре 2 недеља
родитељ
комит
aa0ed35ebc

+ 2 - 2
docs/config-catalog.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/config-catalog.md
-config-catalog.md: 6911366cd70f7fcf0bf0c0ed62d79e0bcd55e680
-config-catalog.zh.md: 69a069624732ca5111c14a3939fa9c43e26d961c
+config-catalog.md: ab3b85626ac2e4910240dc612b2416c5f8381ad7
+config-catalog.zh.md: 75c19554ecbdf7e66e9451b0c3a9c87dfbde8236

+ 1 - 1
docs/config-catalog.md

@@ -955,7 +955,7 @@ export interface ProxyConfig {
 }
 ```
 
-Source: [`packages/net/http-proxy/src/index.ts:49`](../packages/net/http-proxy/src/index.ts)
+Source: [`packages/net/http-proxy/src/index.ts:50`](../packages/net/http-proxy/src/index.ts)
 
 <a id="deepseek-aidsh-invariants"></a>
 

+ 1 - 1
docs/config-catalog.zh.md

@@ -957,7 +957,7 @@ export interface ProxyConfig {
 }
 ```
 
-来源:[`packages/net/http-proxy/src/index.ts:47`](../packages/net/http-proxy/src/index.ts)
+来源:[`packages/net/http-proxy/src/index.ts:50`](../packages/net/http-proxy/src/index.ts)
 
 <a id="deepseek-aidsh-invariants"></a>
 

+ 2 - 2
docs/module-graph.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/module-graph.md
-module-graph.md: 4aa7db428e91719bdd6aaee385c41af903922726
-module-graph.zh.md: 7d390651a8887946b9354b80964e185b5c2c8223
+module-graph.md: 773ffbcf209f922922a255e5b28e7758a9448c96
+module-graph.zh.md: f119320d79353a0d85f845cffdf0ef23d57bf8ab

+ 4 - 2
docs/module-graph.md

@@ -511,6 +511,7 @@ flowchart TD
   pkg_settings --> pkg_brand
   pkg_settings --> pkg_invariants
   pkg_settings --> pkg_session
+  pkg_session_snapshot --> pkg_http_proxy
   pkg_session_snapshot --> pkg_invariants
   pkg_session_snapshot --> pkg_session
   pkg_agent --> pkg_invariants
@@ -700,6 +701,7 @@ flowchart TD
   pkg_terminal --> pkg_brand
   pkg_terminal --> pkg_invariants
   pkg_loader_smoke --> pkg_agent
+  pkg_loader_smoke --> pkg_http_proxy
   pkg_loader_smoke --> pkg_invariants
   pkg_loader_smoke --> pkg_llm
   pkg_loader_smoke --> pkg_session
@@ -1824,7 +1826,7 @@ flowchart TD
 | [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
 | [`session-projection`](../packages/session/session-projection) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
-| [`session-snapshot`](../packages/test-support/session-snapshot) | `test-support` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
+| [`session-snapshot`](../packages/test-support/session-snapshot) | `test-support` | [`http-proxy`](../packages/net/http-proxy), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) |
 | [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
 | [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`spill`](../packages/spill/spill) |
@@ -1862,7 +1864,7 @@ flowchart TD
 | [`bash-local`](../packages/shell/bash-local) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`pwsh-local`](../packages/shell/pwsh-local) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
+| [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`http-proxy`](../packages/net/http-proxy), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
 | [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
 | [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval) |
 | [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) |

+ 4 - 2
docs/module-graph.zh.md

@@ -513,6 +513,7 @@ flowchart TD
   pkg_settings --> pkg_brand
   pkg_settings --> pkg_invariants
   pkg_settings --> pkg_session
+  pkg_session_snapshot --> pkg_http_proxy
   pkg_session_snapshot --> pkg_invariants
   pkg_session_snapshot --> pkg_session
   pkg_agent --> pkg_invariants
@@ -702,6 +703,7 @@ flowchart TD
   pkg_terminal --> pkg_brand
   pkg_terminal --> pkg_invariants
   pkg_loader_smoke --> pkg_agent
+  pkg_loader_smoke --> pkg_http_proxy
   pkg_loader_smoke --> pkg_invariants
   pkg_loader_smoke --> pkg_llm
   pkg_loader_smoke --> pkg_session
@@ -1826,7 +1828,7 @@ flowchart TD
 | [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
 | [`session-projection`](../packages/session/session-projection) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
-| [`session-snapshot`](../packages/test-support/session-snapshot) | `test-support` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
+| [`session-snapshot`](../packages/test-support/session-snapshot) | `test-support` | [`http-proxy`](../packages/net/http-proxy), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) |
 | [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
 | [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`spill`](../packages/spill/spill) |
@@ -1864,7 +1866,7 @@ flowchart TD
 | [`bash-local`](../packages/shell/bash-local) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`pwsh-local`](../packages/shell/pwsh-local) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
+| [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`http-proxy`](../packages/net/http-proxy), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
 | [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
 | [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval) |
 | [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) |

+ 1 - 0
packages/net/http-proxy/src/index.ts

@@ -27,6 +27,7 @@ export {
   resolveProxyPolicy,
   DIRECT_POLICY,
   LOOPBACK_NO_PROXY,
+  PROXY_ENV_NAMES,
   type ProxyConfig,
   type ProxyDiagnostic,
   type ProxyPolicy,

+ 1 - 11
packages/net/http-proxy/src/install.ts

@@ -9,18 +9,8 @@
  */
 
 import type { Agent, Dispatcher } from 'undici'
-import { DIRECT_POLICY, proxyForUrl, type ProxyPolicy } from './policy.ts'
+import { DIRECT_POLICY, POLICY_ENV_NAMES, proxyForUrl, type ProxyPolicy } from './policy.ts'
 
-/**
- * The environment names each policy field owns, lowercase first. Both casings are written together:
- * undici reads the lowercase name first, so leaving a stale uppercase value behind would let it
- * shadow the resolved one on Windows, where the two names are the same variable.
- */
-const POLICY_ENV_NAMES = {
-  httpProxy: ['http_proxy', 'HTTP_PROXY'],
-  httpsProxy: ['https_proxy', 'HTTPS_PROXY'],
-  noProxy: ['no_proxy', 'NO_PROXY'],
-} as const
 
 /** The active policy, or `undefined` until one is installed. Process-wide, like the dispatcher it tracks. */
 let active: ProxyPolicy | undefined

+ 21 - 0
packages/net/http-proxy/src/policy.ts

@@ -20,6 +20,27 @@ import type { LaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environm
  */
 export const LOOPBACK_NO_PROXY: readonly string[] = ['localhost', '127.0.0.1', '::1', '[::1]']
 
+/**
+ * The environment names each policy field owns, lowercase first — undici reads the lowercase name
+ * first, so both casings are always written or cleared together.
+ */
+export const POLICY_ENV_NAMES = {
+  httpProxy: ['http_proxy', 'HTTP_PROXY'],
+  httpsProxy: ['https_proxy', 'HTTPS_PROXY'],
+  noProxy: ['no_proxy', 'NO_PROXY'],
+} as const
+
+/**
+ * Every environment name that carries proxy configuration, including the `ALL_PROXY` fallback this
+ * package resolves but never writes back. A caller that must isolate a child from the machine's
+ * network policy clears exactly these.
+ */
+export const PROXY_ENV_NAMES: readonly string[] = [
+  ...Object.values(POLICY_ENV_NAMES).flat(),
+  'all_proxy',
+  'ALL_PROXY',
+]
+
 /** Proxy URL schemes this package routes through. Everything else is reported, never silently dropped. */
 const SUPPORTED_PROTOCOLS = new Set(['http:', 'https:'])
 

+ 6 - 4
packages/test-support/loader-smoke/package.json

@@ -36,18 +36,20 @@
     "tsx": "^4.22.4"
   },
   "peerDependencies": {
+    "@deepseek-ai/cordis": "workspace:^",
     "@deepseek-ai/dsh-agent": "workspace:^",
+    "@deepseek-ai/dsh-http-proxy": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@deepseek-ai/dsh-llm": "workspace:^",
-    "@deepseek-ai/dsh-session": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^"
+    "@deepseek-ai/dsh-session": "workspace:^"
   },
   "devDependencies": {
+    "@deepseek-ai/cordis": "workspace:^",
     "@deepseek-ai/dsh-agent": "workspace:^",
     "@deepseek-ai/dsh-app-boot": "workspace:^",
+    "@deepseek-ai/dsh-http-proxy": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@deepseek-ai/dsh-llm": "workspace:^",
-    "@deepseek-ai/dsh-session": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^"
+    "@deepseek-ai/dsh-session": "workspace:^"
   }
 }

+ 14 - 1
packages/test-support/loader-smoke/src/index.ts

@@ -11,6 +11,7 @@
  * @module @deepseek-ai/dsh-loader-smoke
  */
 
+import { PROXY_ENV_NAMES } from '@deepseek-ai/dsh-http-proxy'
 import { mkdtemp, rm } from 'node:fs/promises'
 import { tmpdir } from 'node:os'
 import { join } from 'node:path'
@@ -30,6 +31,14 @@ export const LOADER_SMOKE_TEST_TIMEOUT_MS = DEFAULT_PROCESS_TIMEOUT_MS + 15_000
 /** Which artifact an example bin is booted from: unbuilt `src` via tsx, or built `lib` via plain Node. */
 export type ExampleMode = 'src' | 'lib'
 
+/**
+ * Proxy names cleared from every smoke child.
+ * @returns an environment overlay removing each name that carries proxy configuration.
+ */
+function clearedProxyEnv(): NodeJS.ProcessEnv {
+  return Object.fromEntries(PROXY_ENV_NAMES.map(name => [name, undefined]))
+}
+
 /** Environment variable selecting the mode; CI sets it to `lib`, dev leaves it unset (`src`). */
 export const EXAMPLE_MODE_ENV = 'DSH_EXAMPLE_MODE'
 
@@ -109,7 +118,11 @@ function toLibBin(srcBin: string): string {
 export function resolveExampleLaunch(options: ExampleLaunchOptions): ExampleLaunch {
   const mode = options.mode ?? resolveExampleMode()
   const configArgs = options.configArgs ?? []
-  const env: NodeJS.ProcessEnv = { ...options.env }
+  // A smoke launches a real `dsh` against local fixtures, so it must not inherit the machine's
+  // network policy: the harness honors the proxy environment, and a runner that exports one would
+  // send a fixture-server request to a proxy that cannot resolve the fixture host. `undefined`
+  // removes the name from the child rather than setting it empty.
+  const env: NodeJS.ProcessEnv = { ...clearedProxyEnv(), ...options.env }
 
   if (mode === 'src') {
     if (options.tsconfigPath === undefined) {

+ 3 - 0
packages/test-support/loader-smoke/tsconfig.json

@@ -19,6 +19,9 @@
     },
     {
       "path": "../../runtime-diagnostics/invariants"
+    },
+    {
+      "path": "../../net/http-proxy"
     }
   ]
 }

+ 5 - 3
packages/test-support/session-snapshot/package.json

@@ -39,14 +39,17 @@
     "vitest": "^4.1.8"
   },
   "peerDependencies": {
+    "@deepseek-ai/cordis": "workspace:^",
+    "@deepseek-ai/dsh-http-proxy": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@deepseek-ai/dsh-session": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^"
+    "@deepseek-ai/dsh-session": "workspace:^"
   },
   "devDependencies": {
+    "@deepseek-ai/cordis": "workspace:^",
     "@deepseek-ai/dsh-agent": "workspace:^",
     "@deepseek-ai/dsh-agent-loop": "workspace:^",
     "@deepseek-ai/dsh-compaction": "workspace:^",
+    "@deepseek-ai/dsh-http-proxy": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@deepseek-ai/dsh-llm": "workspace:^",
     "@deepseek-ai/dsh-sandbox": "workspace:^",
@@ -55,7 +58,6 @@
     "@deepseek-ai/dsh-subagent": "workspace:^",
     "@deepseek-ai/dsh-tools": "workspace:^",
     "@deepseek-ai/dsh-user-questions": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^",
     "@types/js-yaml": "^4.0.9"
   }
 }

+ 15 - 0
packages/test-support/session-snapshot/src/harness.ts

@@ -35,8 +35,17 @@ import {
   type AgentUnderTest,
   type LaunchedAcpTestAgent,
 } from './launcher.ts'
+import { PROXY_ENV_NAMES } from '@deepseek-ai/dsh-http-proxy'
 import { captureWorkspaceSnapshot, type WorkspaceSnapshotEntry } from './workspace.ts'
 
+/**
+ * Proxy names removed from every replayed child.
+ * @returns an environment overlay removing each name that carries proxy configuration.
+ */
+function clearedProxyEnv(): NodeJS.ProcessEnv {
+  return Object.fromEntries(PROXY_ENV_NAMES.map(name => [name, undefined]))
+}
+
 export type { AgentUnderTest } from './launcher.ts'
 
 const DEFAULT_WAIT_TIMEOUT_MS = 10_000
@@ -257,6 +266,12 @@ export async function runScenario(input: InputScript, opts: RunOptions): Promise
     })
     const env: NodeJS.ProcessEnv = {
       ...opts.env,
+      // A replay must not depend on the machine's network policy, the same reason it pins its home
+      // and sessions root. The harness honors the proxy environment, so a runner that exports one
+      // would send a scenario's fixture-server request to a proxy that cannot resolve the fixture
+      // host and record that proxy's error page as the expected output. `undefined` removes the
+      // name from the child rather than setting it empty.
+      ...clearedProxyEnv(),
       DSH_SNAPSHOT: opts.mode,
       DSH_SNAPSHOT_FILE: opts.fixtureFile,
       DSH_SNAPSHOT_SESSIONS_ROOT: sessionsRoot,

+ 3 - 0
packages/test-support/session-snapshot/tsconfig.json

@@ -19,6 +19,9 @@
     },
     {
       "path": "../../core/session"
+    },
+    {
+      "path": "../../net/http-proxy"
     }
   ]
 }

+ 6 - 0
pnpm-lock.yaml

@@ -9510,6 +9510,9 @@ importers:
       '@deepseek-ai/dsh-app-boot':
         specifier: workspace:^
         version: link:../../boot/app-boot
+      '@deepseek-ai/dsh-http-proxy':
+        specifier: workspace:^
+        version: link:../../net/http-proxy
       '@deepseek-ai/dsh-invariants':
         specifier: workspace:^
         version: link:../../runtime-diagnostics/invariants
@@ -9550,6 +9553,9 @@ importers:
       '@deepseek-ai/dsh-compaction':
         specifier: workspace:^
         version: link:../../compaction/compaction
+      '@deepseek-ai/dsh-http-proxy':
+        specifier: workspace:^
+        version: link:../../net/http-proxy
       '@deepseek-ai/dsh-invariants':
         specifier: workspace:^
         version: link:../../runtime-diagnostics/invariants

+ 1 - 1
scripts/run-gates.spec.ts

@@ -104,7 +104,7 @@ describe('gate graph validation', () => {
     expect(ids).toEqual([
       'rescope-vendor', 'publint', 'constraints', 'application-entrypoints',
       'dsh-package-licenses', 'package-invariants', 'built-package-invariants', 'node-next-types',
-      'optional-dependency-imports', 'client-packages', 'client-ui-i18n', 'cordis-config',
+      'optional-dependency-imports', 'client-packages', 'client-ui-i18n', 'no-bare-dispatcher', 'cordis-config',
       'runtime-closure', 'vendored-links',
     ])
     expect(defaultConcurrency('hygiene', ids.length, 8)).toEqual({