diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index ef4931f765..e5b36bf91c 100644 --- a/docs/config-catalog.i18n.yaml +++ b/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 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 82f6d26c79..2e4aad7532 100644 --- a/docs/config-catalog.md +++ b/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) diff --git a/docs/subsystems/terminal.i18n.yaml b/docs/subsystems/terminal.i18n.yaml index 859104d415..96f05cc50a 100644 --- a/docs/subsystems/terminal.i18n.yaml +++ b/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 diff --git a/docs/subsystems/terminal.md b/docs/subsystems/terminal.md index eedf6157b2..c703114311 100644 --- a/docs/subsystems/terminal.md +++ b/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) diff --git a/docs/subsystems/terminal.zh.md b/docs/subsystems/terminal.zh.md index 3cc2c281e8..7c469c028e 100644 --- a/docs/subsystems/terminal.zh.md +++ b/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) diff --git a/packages/fs/tool-fs-search/tests/rg-sidecar.spec.ts b/packages/fs/tool-fs-search/tests/rg-sidecar.spec.ts new file mode 100644 index 0000000000..49cca6bfa8 --- /dev/null +++ b/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() + 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) + }) +}) diff --git a/packages/subprocess/subprocess-local/tests/process-inspector.spec.ts b/packages/subprocess/subprocess-local/tests/process-inspector.spec.ts index 84cae7564a..2402681461 100644 --- a/packages/subprocess/subprocess-local/tests/process-inspector.spec.ts +++ b/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']) diff --git a/python/sdk/tests/test_runtime_resolution.py b/python/sdk/tests/test_runtime_resolution.py index 778203f4d1..14e90f3283 100644 --- a/python/sdk/tests/test_runtime_resolution.py +++ b/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() diff --git a/scripts/cordis-yaml.ts b/scripts/cordis-yaml.ts new file mode 100644 index 0000000000..60cd492787 --- /dev/null +++ b/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).__jsExpr === 'string' +} diff --git a/scripts/verify-cordis-config.ts b/scripts/verify-cordis-config.ts index f2e0aef46c..9a94a281e8 100644 --- a/scripts/verify-cordis-config.ts +++ b/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, 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 { return value !== null && typeof value === 'object' } diff --git a/scripts/verify-runtime-closure.ts b/scripts/verify-runtime-closure.ts index c0015fe11a..1287542dc2 100644 --- a/scripts/verify-runtime-closure.ts +++ b/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 -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