Просмотр исходного кода

fix(python-sdk): satisfy runtime packaging gates

fz 1 месяц назад
Родитель
Сommit
acd8dd43fa

+ 1 - 1
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: 82f6d26c79d32c6952f3bc11c96fa1c2ddceecdc
+config-catalog.md: 2e4aad7532b061e8328f25a53c2c3b0c4bb4dfa0
 config-catalog.zh.md: 958d3115447db37de248bbf30b0744308ff8dbb8

+ 1 - 1
docs/config-catalog.md

@@ -2370,7 +2370,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/shell/tool-bash-persistent/src/index.ts:405`](../packages/shell/tool-bash-persistent/src/index.ts)
+Source: [`packages/shell/tool-bash-persistent/src/index.ts:406`](../packages/shell/tool-bash-persistent/src/index.ts)
 
 <a id="deepseek-aidsh-tool-fs"></a>
 

+ 2 - 2
docs/subsystems/terminal.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/subsystems/terminal.md
-terminal.md: eedf6157b256e83d3b57e07bf738429773b87574
-terminal.zh.md: 3cc2c281e8c2090bb7e92808f932be689fd0b710
+terminal.md: c7031143117a514f8579a22fed07a1461babb15e
+terminal.zh.md: 7c469c028e8b3da7c9d5012f9f49c1ae65a9bd03

+ 1 - 1
docs/subsystems/terminal.md

@@ -180,5 +180,5 @@ list(owner: Agent): TerminalSessionSnapshot[]
 
 Types: [Agent](core.md)
 
-Source: [`packages/terminal/terminal/src/index.ts:105`](../../packages/terminal/terminal/src/index.ts)
+Source: [`packages/terminal/terminal/src/index.ts:108`](../../packages/terminal/terminal/src/index.ts)
 <!-- END GENERATED cordis-surface -->

+ 1 - 1
docs/subsystems/terminal.zh.md

@@ -180,5 +180,5 @@ list(owner: Agent): TerminalSessionSnapshot[]
 
 Types: [Agent](core.md)
 
-Source: [`packages/terminal/terminal/src/index.ts:105`](../../packages/terminal/terminal/src/index.ts)
+Source: [`packages/terminal/terminal/src/index.ts:108`](../../packages/terminal/terminal/src/index.ts)
 <!-- END GENERATED cordis-surface -->

+ 25 - 0
packages/fs/tool-fs-search/tests/rg-sidecar.spec.ts

@@ -0,0 +1,25 @@
+import { describe, expect, it, vi } from 'vitest'
+
+const existsSync = vi.hoisted(() => vi.fn(() => true))
+
+vi.mock('node:fs', async (importOriginal) => {
+  const actual = await importOriginal<typeof import('node:fs')>()
+  return { ...actual, existsSync }
+})
+
+vi.mock('@vscode/ripgrep', () => new Proxy({}, {
+  get() {
+    throw new Error('the platform package must not load when the executable sidecar exists')
+  },
+}))
+
+import { resolveRgPath } from '@deepseek-ai/dsh-tool-fs-search'
+
+describe('single-executable ripgrep resolution', () => {
+  it('uses the native sidecar beside the current executable', async () => {
+    const sidecar = `${process.execPath}-rg`
+
+    await expect(resolveRgPath()).resolves.toBe(sidecar)
+    expect(existsSync).toHaveBeenCalledWith(sidecar)
+  })
+})

+ 35 - 1
packages/subprocess/subprocess-local/tests/process-inspector.spec.ts

@@ -140,6 +140,32 @@ describe('Linux process inspector', () => {
     ])
   })
 
+  it('contains cycles in the procfs children index', () => {
+    const fake = fakeInternals()
+    fake.files.set('/proc/10/stat', stat(10, 10, 10, 10, '500'))
+    fake.files.set('/proc/10/task/10/children', '11')
+    fake.files.set('/proc/11/stat', stat(11, 10, 10, 10, '501', 10))
+    fake.files.set('/proc/11/task/11/children', '10')
+
+    expect(createProcessInspector('linux', 'x64', fake.internals).processTree(10)).toEqual([
+      { pid: 11, started: '501' },
+      { pid: 10, started: '500' },
+    ])
+  })
+
+  it('falls back to the PID namespace when a descendant children index is unreadable', () => {
+    const fake = fakeInternals()
+    fake.dirs.set('/proc', ['10', '11', '12'])
+    fake.files.set('/proc/10/stat', stat(10, 10, 10, 10, '500'))
+    fake.files.set('/proc/10/task/10/children', '12 invalid 11')
+    fake.files.set('/proc/11/stat', stat(11, 10, 10, 10, '501', 10))
+
+    expect(createProcessInspector('linux', 'x64', fake.internals).processTree(10)).toEqual([
+      { pid: 11, started: '501' },
+      { pid: 10, started: '500' },
+    ])
+  })
+
   it('keeps readiness inspection local when procfs has no children index', () => {
     const fake = fakeInternals()
     fake.files.set('/proc/10/stat', stat(10, 10, 10, 10, '500'))
@@ -147,11 +173,19 @@ describe('Linux process inspector', () => {
 
     expect(inspector.processTree(10, false)).toEqual([{ pid: 10, started: '500' }])
     expect(inspector.isStdinWaiting(10, false)).toBe(false)
+
+    const readFile = fake.internals.readFile.bind(fake.internals)
+    let statReads = 0
+    fake.internals.readFile = (path) => {
+      if (path === '/proc/10/stat' && statReads++ > 0) throw new Error('process exited')
+      return readFile(path)
+    }
+    expect(inspector.processTree(10, false)).toEqual([])
   })
 
   it('detects read, select, poll, and epoll waits across non-leader threads', () => {
     const fake = fakeInternals()
-    fake.dirs.set('/proc', ['100', '101'])
+    fake.dirs.set('/proc', ['77', '100', '101'])
     fake.files.set('/proc/100/stat', stat(100, 77, 100, 77, '1'))
     fake.files.set('/proc/101/stat', stat(101, 77, 100, 77, '2'))
     fake.dirs.set('/proc/100/task', ['100'])

+ 17 - 1
python/sdk/tests/test_runtime_resolution.py

@@ -51,7 +51,10 @@ def test_runtime_requires_spawn_helper_only_on_macos(
     runtime_dir.mkdir()
     linux = runtime_dir / "dsh-jsonrpc-agent-pkg-linux-x64"
     linux.touch()
-    (runtime_dir / "dsh-jsonrpc-agent-pkg-macos-arm64").touch()
+    Path(f"{linux}-rg").touch()
+    macos = runtime_dir / "dsh-jsonrpc-agent-pkg-macos-arm64"
+    macos.touch()
+    Path(f"{macos}-rg").touch()
     monkeypatch.setattr(runtime, "bundled_package_dir", lambda: tmp_path)
 
     monkeypatch.setattr(runtime, "_current_platform_tag", lambda: "macos-arm64")
@@ -59,3 +62,16 @@ def test_runtime_requires_spawn_helper_only_on_macos(
         runtime.bundled_runtime_path()
     monkeypatch.setattr(runtime, "_current_platform_tag", lambda: "linux-x64")
     assert runtime.bundled_runtime_path() == linux
+
+
+def test_runtime_requires_ripgrep_sidecar(
+    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+    runtime_dir = tmp_path / "runtime"
+    runtime_dir.mkdir()
+    (runtime_dir / "dsh-jsonrpc-agent-pkg-linux-x64").touch()
+    monkeypatch.setattr(runtime, "bundled_package_dir", lambda: tmp_path)
+    monkeypatch.setattr(runtime, "_current_platform_tag", lambda: "linux-x64")
+
+    with pytest.raises(FileNotFoundError, match="ripgrep sidecar"):
+        runtime.bundled_runtime_path()

+ 26 - 0
scripts/cordis-yaml.ts

@@ -0,0 +1,26 @@
+import * as yaml from 'js-yaml'
+
+export interface JsExpr {
+  __jsExpr: string
+}
+
+const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
+  kind: 'scalar',
+  resolve: data => typeof data === 'string',
+  construct: (data: unknown): JsExpr => {
+    if (typeof data !== 'string') throw new TypeError('!!js requires a scalar string')
+    return { __jsExpr: data }
+  },
+})
+const schema = yaml.JSON_SCHEMA.extend(jsExprType)
+
+/** Parse a Cordis config while preserving Loader `!!js` expressions as data. */
+export function loadCordisYaml(source: string): unknown {
+  return yaml.load(source, { schema })
+}
+
+export function isJsExpr(value: unknown): value is JsExpr {
+  return typeof value === 'object'
+    && value !== null
+    && typeof (value as Record<string, unknown>).__jsExpr === 'string'
+}

+ 3 - 22
scripts/verify-cordis-config.ts

@@ -12,13 +12,9 @@
 
 import { globSync, readFileSync } from 'node:fs'
 import { dirname, relative, resolve } from 'node:path'
-import * as yaml from 'js-yaml'
 import ts from 'typescript'
 import { cordisConfigFiles } from './cordis-config-files.ts'
-
-interface JsExpr {
-  __jsExpr: string
-}
+import { isJsExpr, loadCordisYaml } from './cordis-yaml.ts'
 
 interface PackageManifest {
   name?: string
@@ -56,16 +52,6 @@ const CHOOSER_BACKEND_PACKAGES = [
   '@deepseek-ai/dsh-client-ui-directory-picker-browse',
   '@deepseek-ai/dsh-client-ui-directory-picker-native',
 ]
-const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
-  kind: 'scalar',
-  resolve: data => typeof data === 'string',
-  construct: (data: unknown): JsExpr => {
-    if (typeof data !== 'string') throw new TypeError('!!js requires a scalar string')
-    return { __jsExpr: data }
-  },
-})
-const schema = yaml.JSON_SCHEMA.extend(jsExprType)
-
 const errors: string[] = []
 const pluginReferences: PluginReference[] = []
 
@@ -73,7 +59,7 @@ if (import.meta.main) {
   const files = cordisConfigFiles(root)
 
   for (const file of files) {
-    const document: unknown = yaml.load(readFileSync(resolve(root, file), 'utf8'), { schema })
+    const document = loadCordisYaml(readFileSync(resolve(root, file), 'utf8'))
     if (!isUnknownArray(document)) {
       errors.push(`${file}: root must be a Loader entry array`)
       continue
@@ -172,7 +158,7 @@ function validatePresetPlaneSeparation(): string[] {
 
 /** Every entry of one config file, or an empty list when it is not an entry array. */
 function loadEntries(file: string): unknown[] {
-  const document: unknown = yaml.load(readFileSync(resolve(root, file), 'utf8'), { schema })
+  const document = loadCordisYaml(readFileSync(resolve(root, file), 'utf8'))
   return isUnknownArray(document) ? document : []
 }
 
@@ -462,7 +448,6 @@ export function metadataExpressionErrors(entry: Record<string, unknown>, path: s
 function disabledExpressionProblem(expression: string): string | undefined {
   try {
     // Compilation only — the constructor never executes the body.
-    // oxlint-disable-next-line typescript/no-implied-eval
     new Function(`return (${expression})`)
     return undefined
   } catch (error) {
@@ -484,10 +469,6 @@ function collectExpressionPaths(value: unknown, path: string, output: string[]):
   for (const [key, child] of Object.entries(value)) collectExpressionPaths(child, `${path}.${key}`, output)
 }
 
-function isJsExpr(value: unknown): value is JsExpr {
-  return isRecord(value) && typeof value.__jsExpr === 'string'
-}
-
 function isRecord(value: unknown): value is Record<string, unknown> {
   return value !== null && typeof value === 'object'
 }

+ 2 - 16
scripts/verify-runtime-closure.ts

@@ -8,11 +8,7 @@ import { globSync } from 'node:fs'
 import { readFile } from 'node:fs/promises'
 import { basename, dirname, resolve } from 'node:path'
 import { parseArgs } from 'node:util'
-import * as yaml from 'js-yaml'
-
-interface JsExpr {
-  __jsExpr: string
-}
+import { loadCordisYaml } from './cordis-yaml.ts'
 
 interface PackageManifest {
   name?: string
@@ -34,16 +30,6 @@ interface RuntimePlatform {
 
 type RuntimePlatformManifest = Record<string, RuntimePlatform>
 
-const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
-  kind: 'scalar',
-  resolve: data => typeof data === 'string',
-  construct: (data: unknown): JsExpr => {
-    if (typeof data !== 'string') throw new TypeError('!!js requires a scalar string')
-    return { __jsExpr: data }
-  },
-})
-const schema = yaml.JSON_SCHEMA.extend(jsExprType)
-
 const root = resolve(import.meta.dirname, '..')
 const { values } = parseArgs({
   args: process.argv.slice(2),
@@ -107,7 +93,7 @@ async function missingPresetPlugins(
   const failures: string[] = []
   const presetPaths = globSync('apps/cli/config/agent-presets/*/agent.cordis.yml', { cwd: root }).sort()
   for (const presetPath of presetPaths) {
-    const document: unknown = yaml.load(await readFile(resolve(root, presetPath), 'utf8'), { schema })
+    const document = loadCordisYaml(await readFile(resolve(root, presetPath), 'utf8'))
     if (!Array.isArray(document)) {
       failures.push(`${presetPath}: preset root must be a Loader entry array`)
       continue