fix(pwsh): pin CI expectations for the mirror design

- jscpd: the executor/tool mirror dsh-bash-local/dsh-tool-bash by design
  (Agent Note), so the mirrored regions carry explicit ignore markers with
  reasons instead of being flagged as duplication.
- pwsh-local: a self-terminated process reports SIGTERM or SIGKILL on
  POSIX (PowerShell's Stop-Process choice), not only SIGTERM.
- gen-tool-catalog.spec: the shipped-tool completeness list gains 'pwsh'.
This commit is contained in:
Huanqi Cao
2026-08-01 19:34:12 +08:00
parent e8b7987e82
commit 30c421ed75
4 changed files with 17 additions and 4 deletions

View File

@@ -22,6 +22,7 @@ import type { BashExecRequest, BashExecSpec, BashProcess, BashProcessRead, BashR
import type { SubprocessCollect, SubprocessHandle, SubprocessOutputReader, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
import { clampTimeout, deadline, timeoutOf } from '@deepseek-ai/dsh-timeout'
/* jscpd:ignore-start -- deliberate call-for-call mirror of dsh-bash-local (Agent Note: pwsh-tool-and-executor). */
/**
* Model-friendly environment overrides for PowerShell: disable colors and
* pagers that would garble tool output. `TERM=dumb` is a POSIX concept and is
@@ -312,5 +313,6 @@ export class PwshLocalExecutor extends BashExecutor {
*/
protected onProcessDone(_proc: BashProcess, _stderr: string): void {}
}
/* jscpd:ignore-end */
export default PwshLocalExecutor

View File

@@ -197,11 +197,12 @@ describe.skipIf(!hasPwsh)('PwshLocalExecutor.run', () => {
const result = await bash.run(bash.resolve({ command: 'Stop-Process -Id $PID' }))
expect(result.timedOut).toBe(false)
expect(result.aborted).toBe(false)
// Windows reports a forced termination without a signal; POSIX reports SIGTERM.
// Windows reports a forced termination without a signal; POSIX reports the
// terminating signal PowerShell chose (SIGTERM, or SIGKILL for the hard kill).
if (process.platform === 'win32') {
expect(result.signal).toBeNull()
} else {
expect(result.signal).toBe('SIGTERM')
expect(['SIGTERM', 'SIGKILL']).toContain(result.signal)
}
})
@@ -347,7 +348,8 @@ describe.skipIf(!hasPwsh)('PwshLocalExecutor.start (background process handles)'
await proc.done
expect(proc.status).toBe('killed')
expect(proc.exitCode).toBeNull()
expect(proc.signal).toBe('SIGTERM')
// PowerShell picks SIGTERM for Stop-Process, SIGKILL for the hard kill.
expect(['SIGTERM', 'SIGKILL']).toContain(proc.signal)
})
it('a background spawn failure settles as killed with the error readable on stderr', async () => {

View File

@@ -58,6 +58,7 @@ interface PwshForegroundResult {
stderr: { text: string; truncated: boolean; spillPath?: string }
}
/* jscpd:ignore-start -- minimal mirror of dsh-tool-bash's validation and execute plumbing (Agent Note). */
function validatePwshArgs(args: PwshToolArgs): void {
if (args.command.trim().length === 0) {
throw new Error('invalid command: expected a non-empty string')
@@ -69,6 +70,7 @@ function validatePwshArgs(args: PwshToolArgs): void {
throw new Error(`invalid timeoutMs: expected a positive number, got ${JSON.stringify(args.timeoutMs)}`)
}
}
/* jscpd:ignore-end */
function pwshDescription(): string {
return 'Execute a PowerShell command (`pwsh -Command`) and return its stdout/stderr. '
@@ -185,6 +187,10 @@ export function apply(ctx: Context, config: Config = {}): void {
workdir: { type: 'string', description: 'Working directory for this command. Defaults to the session workspace; a relative path is resolved against it.' },
},
output: {
// The foreground result wire shape mirrors dsh-tool-bash's by contract —
// consumers of one must accept the other (see the pwsh-tool-and-executor
// Agent Note).
/* jscpd:ignore-start -- deliberate foreground-result schema symmetry with dsh-tool-bash. */
schema: {
type: 'object',
additionalProperties: false,
@@ -217,11 +223,13 @@ export function apply(ctx: Context, config: Config = {}): void {
},
},
},
/* jscpd:ignore-end */
render: (_args, value) => [{
type: 'text',
text: renderPwshOutput(value),
}],
},
/* jscpd:ignore-start -- the foreground execute path mirrors dsh-tool-bash's by design (see the pwsh-tool-and-executor Agent Note). */
async execute(args: PwshToolArgs, exec) {
validatePwshArgs(args)
const workdir = resolveWorkdir(args.workdir, exec)
@@ -239,6 +247,7 @@ export function apply(ctx: Context, config: Config = {}): void {
}
return canonicalPwshResult(result)
},
/* jscpd:ignore-end */
presentCall: (args: PwshToolArgs): TerminalCallView => ({
card: 'terminal',
title: args.command,

View File

@@ -23,7 +23,7 @@ describe('gen-tool-catalog collectToolCatalog', () => {
it('boots every shipped tool package and harvests its model-facing schemas', async () => {
const catalog = await collectToolCatalog()
const names = catalog.flatMap(entry => entry.schemas.map(s => s.name)).sort()
expect(names).toEqual(['ask_user_question', 'bash', 'bash', 'cordis_inspect', 'cordis_mount', 'cordis_unmount', 'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep', 'lsp', 'ralph', 'read', 'run_code', 'session_event_read', 'session_event_search', 'session_event_trace', 'session_search', 'session_trace', 'skill', 'str_replace_editor', 'subagent', 'task_kill', 'task_list', 'task_output', 'terminal_close', 'terminal_list', 'terminal_open', 'terminal_read', 'terminal_send', 'terminal_signal', 'todo_write', 'update_goal', 'web_fetch', 'web_search', 'workflow', 'write'])
expect(names).toEqual(['ask_user_question', 'bash', 'bash', 'cordis_inspect', 'cordis_mount', 'cordis_unmount', 'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep', 'lsp', 'pwsh', 'ralph', 'read', 'run_code', 'session_event_read', 'session_event_search', 'session_event_trace', 'session_search', 'session_trace', 'skill', 'str_replace_editor', 'subagent', 'task_kill', 'task_list', 'task_output', 'terminal_close', 'terminal_list', 'terminal_open', 'terminal_read', 'terminal_send', 'terminal_signal', 'todo_write', 'update_goal', 'web_fetch', 'web_search', 'workflow', 'write'])
// Every tool carries a JSON-Schema `parameters` object (what the model sees).
for (const entry of catalog) {
for (const schema of entry.schemas) {