From 7fab3cf976e79dc5dfda5eb5090a378778bfecb2 Mon Sep 17 00:00:00 2001 From: sjw1231 Date: Thu, 22 Oct 2026 15:53:02 +0800 Subject: [PATCH] test: refresh module graph and Web pins for apply_patch Regenerate docs/module-graph for the fs-patch package, add apply_patch to the shipped Web composition tool list, and refresh the Web tool-schema, system-prompt, and collapse-timing pins that CI compares against the default tool set. Cover the symbolic-link preflight refusal through a provider lstat double and the atomic-write parent failure through a regular-file blocker, so Windows coverage no longer depends on POSIX-only tests. --- apps/web/tests/shipped-composition.e2e.ts | 1 + docs/module-graph.md | 2 ++ docs/module-graph.zh.md | 2 ++ packages/fs/fs-local/tests/fsio.spec.ts | 10 ++++++ .../fs/tool-fs/tests/patch/preflight.spec.ts | 15 ++++++++- .../collapse-timing/next-message.expected.md | 2 +- .../web/collapse-timing/verbose.expected.md | 2 +- .../system-prompt.expected.md | 2 ++ .../tool-schemas.expected.json | 28 ++++++++++++++++ .../system-prompt.expected.md | 32 +++++++++++++++++++ .../system-prompt.expected.md | 32 +++++++++++++++++++ .../web/ptc-round/system-prompt.expected.md | 32 +++++++++++++++++++ 12 files changed, 157 insertions(+), 3 deletions(-) diff --git a/apps/web/tests/shipped-composition.e2e.ts b/apps/web/tests/shipped-composition.e2e.ts index ceb4b4644b..f676896f34 100644 --- a/apps/web/tests/shipped-composition.e2e.ts +++ b/apps/web/tests/shipped-composition.e2e.ts @@ -473,6 +473,7 @@ function assertLeanChildRecord(agent: Agent): void { * Agent Note owns the rationale and its sources. */ const EXPECTED_TOOLS = [ + 'apply_patch', 'ask_user_question', 'bash', 'create_goal', diff --git a/docs/module-graph.md b/docs/module-graph.md index 03d9ee8d5e..d66ffc57fe 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -61,6 +61,7 @@ flowchart TD pkg_fs["fs"] pkg_fs_local["fs-local"] pkg_fs_observation_policy["fs-observation-policy"] + pkg_fs_patch["fs-patch"] pkg_fs_sandbox["fs-sandbox"] pkg_tool_fs["tool-fs"] pkg_tool_fs_search["tool-fs-search"] @@ -1475,6 +1476,7 @@ flowchart TD | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions) | `llm` | — | | [`llm`](../packages/llm/llm) | `llm` | — | | [`scope`](../packages/core/scope) | `core` | — | +| [`fs-patch`](../packages/fs/fs-patch) | `fs` | — | | [`api-gateway`](../packages/api/gateway) | `api` | — | | [`api-job-controller`](../packages/api/job-controller) | `api` | — | | [`api-remote-access`](../packages/api/remote-access) | `api` | — | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index 1516a3f04a..2f11bfa9e8 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -63,6 +63,7 @@ flowchart TD pkg_fs["fs"] pkg_fs_local["fs-local"] pkg_fs_observation_policy["fs-observation-policy"] + pkg_fs_patch["fs-patch"] pkg_fs_sandbox["fs-sandbox"] pkg_tool_fs["tool-fs"] pkg_tool_fs_search["tool-fs-search"] @@ -1477,6 +1478,7 @@ flowchart TD | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions) | `llm` | — | | [`llm`](../packages/llm/llm) | `llm` | — | | [`scope`](../packages/core/scope) | `core` | — | +| [`fs-patch`](../packages/fs/fs-patch) | `fs` | — | | [`api-gateway`](../packages/api/gateway) | `api` | — | | [`api-job-controller`](../packages/api/job-controller) | `api` | — | | [`api-remote-access`](../packages/api/remote-access) | `api` | — | diff --git a/packages/fs/fs-local/tests/fsio.spec.ts b/packages/fs/fs-local/tests/fsio.spec.ts index 05913db397..e53e1e4b37 100644 --- a/packages/fs/fs-local/tests/fsio.spec.ts +++ b/packages/fs/fs-local/tests/fsio.spec.ts @@ -1015,6 +1015,16 @@ describe('atomic write failures', () => { } finally { await chmod(locked, 0o755) } }) + it('maps a parent path blocked by a regular file to FS_IO_ERROR on every platform', async () => { + const blocker = join(dir, 'blocker') + await writeFile(blocker, 'file') + const target = join(blocker, 'child') + await expect(writeFileAtomic(target, 'content', undefined, undefined)).rejects.toMatchObject({ + code: 'FS_IO_ERROR', message: expect.stringContaining(`cannot write "${target}": `) as string, cause: expect.any(Error) as Error, + }) + expect(await readFile(blocker, 'utf8')).toBe('file') + }) + it.each([new Error('provider defect'), Object.assign(new Error('invalid errno'), { code: 1 }), 'provider defect'])( 'preserves unexpected failures during staging: %s', async (failure) => { const file = join(dir, 'a') diff --git a/packages/fs/tool-fs/tests/patch/preflight.spec.ts b/packages/fs/tool-fs/tests/patch/preflight.spec.ts index 238d213193..e15a1e0663 100644 --- a/packages/fs/tool-fs/tests/patch/preflight.spec.ts +++ b/packages/fs/tool-fs/tests/patch/preflight.spec.ts @@ -1,7 +1,7 @@ /** Whole-patch admission rejects all writes while preserving prior observation state. */ import { mkdir, symlink, writeFile } from 'node:fs/promises' import { resolve } from 'node:path' -import { FsError } from '@deepseek-ai/dsh-fs' +import { FsError, FsVersion } from '@deepseek-ai/dsh-fs' import type { FsErrorCode } from '@deepseek-ai/dsh-fs' import { expect, it, vi } from 'vitest' import { add, move, patch, remove, setup, update } from './harness.ts' @@ -52,6 +52,19 @@ it.each([ expect(await t.editIntent('a')).toEqual({ version: await t.version('a') }) }) +it.each(['delete', 'move'] as const)('rejects a %s source the provider reports as a symbolic link', async (kind) => { + const t = await setup() + await t.file('a', 'old\n') + vi.spyOn(t.fs, 'lstat').mockResolvedValue({ type: 'symlink', version: FsVersion('link') }) + const a = t.record(kind, 'a', 0, 2, kind === 'move' ? 'b' : undefined) + expect(await t.run(patch(kind === 'delete' ? remove('a') : move('a', 'b')))).toEqual({ phase: 'preflight', refusals: [ + { operation: a, failure: { code: 'PATCH_SYMLINK_SOURCE', + message: `cannot ${kind} "${a.path}": path is a symbolic link — operate on its target path instead` } }, + ] }) + expect(await t.bytes('a')).toBe('old\n') + expect(t.observations).toEqual([]) +}) + it.skipIf(process.platform === 'win32').each(['delete', 'move'] as const)('rejects a symbolic-link %s source without changing its target', async (kind) => { const t = await setup() await t.file('a', 'old\n') diff --git a/snapshots/web/collapse-timing/next-message.expected.md b/snapshots/web/collapse-timing/next-message.expected.md index b8e9c993c2..e6fbbc7e2b 100644 --- a/snapshots/web/collapse-timing/next-message.expected.md +++ b/snapshots/web/collapse-timing/next-message.expected.md @@ -11,7 +11,7 @@ - button "Copy" - status: Completed - button "Completed in {{duration}}" [expanded] -- button "Tools updated 33 added" +- button "Tools updated 34 added" - button "Think Both files have been read. a.txt contains \"alpha\" and b.txt contains \"beta\". I'll now reply with DONE as instructed." - paragraph: DONE - button "Copy" diff --git a/snapshots/web/collapse-timing/verbose.expected.md b/snapshots/web/collapse-timing/verbose.expected.md index de9e56b3e8..f6d474df77 100644 --- a/snapshots/web/collapse-timing/verbose.expected.md +++ b/snapshots/web/collapse-timing/verbose.expected.md @@ -19,7 +19,7 @@ - button "Copy" - status: Completed - button "Completed in {{duration}}" [disabled] [expanded] -- button "Tools updated 33 added" +- button "Tools updated 34 added" - button "Think Both files have been read. a.txt contains \"alpha\" and b.txt contains \"beta\". I'll now reply with DONE as instructed." - paragraph: DONE - button "Copy" diff --git a/snapshots/web/fresh-round-trip/system-prompt.expected.md b/snapshots/web/fresh-round-trip/system-prompt.expected.md index 31efd9a0a5..98dc0b5c74 100644 --- a/snapshots/web/fresh-round-trip/system-prompt.expected.md +++ b/snapshots/web/fresh-round-trip/system-prompt.expected.md @@ -12,6 +12,8 @@ Read an existing file before overwriting it with write (the default fs-observati Read a file before editing it (the default fs-observation-policy requires it), unless you just created or edited it in this session. +Prefer apply_patch over separate edit calls for changes across files or separated regions. Read a file before updating, deleting, or moving it (the default fs-observation-policy requires it), unless you just created or edited it in this session. Operations run in order, stop at the first failure, and are not rolled back; re-read affected files before retrying when needed. + Use the glob tool — not shell find — to discover files by path pattern. Use the grep tool — not shell grep or rg — to search file contents. Use read on a matched file when you need surrounding context. diff --git a/snapshots/web/fresh-round-trip/tool-schemas.expected.json b/snapshots/web/fresh-round-trip/tool-schemas.expected.json index efbb6a4648..013b77c3b7 100644 --- a/snapshots/web/fresh-round-trip/tool-schemas.expected.json +++ b/snapshots/web/fresh-round-trip/tool-schemas.expected.json @@ -1,5 +1,33 @@ { "initial": [ + { + "name": "apply_patch", + "description": "Create, update, delete, or move UTF-8 text files with one Codex-style patch; prefer it over multiple edit calls when a change spans several files or separated regions.", + "parameters": { + "type": "object", + "properties": { + "patch": { + "type": "string", + "description": "Codex patch: start with *** Begin Patch and end with *** End Patch. Use *** Add File: path with + lines; *** Update File: path, optionally followed by *** Move to: path, with @@ or @@ context and space-prefixed context, + added, or - removed lines; *** End of File anchors the last hunk to the file end. Use *** Delete File: path to delete. Paths are relative to the Session working directory. Read existing files before updating, deleting, or moving them." + }, + "sandbox_permissions": { + "type": "string", + "description": "The narrowest wider sandbox mode for a one-shot retry of the exact operation the sandbox just denied; the retry asks the user for approval.", + "enum": [ + "workspace-write", + "danger-full-access" + ] + }, + "justification": { + "type": "string", + "description": "Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access. Use the language of the user’s current request." + } + }, + "required": [ + "patch" + ] + } + }, { "name": "ask_user_question", "description": "Ask the user a concise question when you need confirmation, a choice, or missing information before proceeding.", diff --git a/snapshots/web/official-subagent-bundle/system-prompt.expected.md b/snapshots/web/official-subagent-bundle/system-prompt.expected.md index 22460604c8..8f0382482f 100644 --- a/snapshots/web/official-subagent-bundle/system-prompt.expected.md +++ b/snapshots/web/official-subagent-bundle/system-prompt.expected.md @@ -14,6 +14,8 @@ Read an existing file before overwriting it with write (the default fs-observati Read a file before editing it (the default fs-observation-policy requires it), unless you just created or edited it in this session. +Prefer apply_patch over separate edit calls for changes across files or separated regions. Read a file before updating, deleting, or moving it (the default fs-observation-policy requires it), unless you just created or edited it in this session. Operations run in order, stop at the first failure, and are not rolled back; re-read affected files before retrying when needed. + Use the glob tool — not shell find — to discover files by path pattern. Use the grep tool — not shell grep or rg — to search file contents. Use read on a matched file when you need surrounding context. @@ -47,6 +49,15 @@ Program-only SDK bindings: type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue } interface ToolArgsMap { + /** Create, update, delete, or move UTF-8 text files with one Codex-style patch; prefer it over multiple edit calls when a change spans several files or separated regions. */ + apply_patch: { + /** Codex patch: start with *** Begin Patch and end with *** End Patch. Use *** Add File: path with + lines; *** Update File: path, optionally followed by *** Move to: path, with @@ or @@ context and space-prefixed context, + added, or - removed lines; *** End of File anchors the last hunk to the file end. Use *** Delete File: path to delete. Paths are relative to the Session working directory. Read existing files before updating, deleting, or moving them. */ + patch: string; + /** The narrowest wider sandbox mode for a one-shot retry of the exact operation the sandbox just denied; the retry asks the user for approval. */ + sandbox_permissions?: "workspace-write" | "danger-full-access"; + /** Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access. Use the language of the user’s current request. */ + justification?: string; + } & Record; /** Ask the user a concise question when you need confirmation, a choice, or missing information before proceeding. */ ask_user_question: { /** Questions to ask the user before continuing. */ @@ -375,6 +386,27 @@ interface ToolArgsMap { } interface ToolOutputMap { + apply_patch: { + phase: "commit"; + cancelled: boolean; + operations: ({ + index: number; + kind: "add" | "update" | "delete" | "move"; + path: string; + destination?: string; + patchLine: number; + status: "completed" | "failed" | "unknown" | "skipped"; + code?: string; + message?: string; + hunkLine?: number; + changes?: ({ + path: string; + before: string | null; + after: string | null; + })[]; + })[]; + affectedPaths: string[]; + }; ask_user_question: { answers: { id: string; diff --git a/snapshots/web/optional-string-editor/system-prompt.expected.md b/snapshots/web/optional-string-editor/system-prompt.expected.md index 26cee5dcf4..1dca11dda8 100644 --- a/snapshots/web/optional-string-editor/system-prompt.expected.md +++ b/snapshots/web/optional-string-editor/system-prompt.expected.md @@ -14,6 +14,8 @@ Read an existing file before overwriting it with write (the default fs-observati Read a file before editing it (the default fs-observation-policy requires it), unless you just created or edited it in this session. +Prefer apply_patch over separate edit calls for changes across files or separated regions. Read a file before updating, deleting, or moving it (the default fs-observation-policy requires it), unless you just created or edited it in this session. Operations run in order, stop at the first failure, and are not rolled back; re-read affected files before retrying when needed. + Use the glob tool — not shell find — to discover files by path pattern. Use the grep tool — not shell grep or rg — to search file contents. Use read on a matched file when you need surrounding context. @@ -47,6 +49,15 @@ Program-only SDK bindings: type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue } interface ToolArgsMap { + /** Create, update, delete, or move UTF-8 text files with one Codex-style patch; prefer it over multiple edit calls when a change spans several files or separated regions. */ + apply_patch: { + /** Codex patch: start with *** Begin Patch and end with *** End Patch. Use *** Add File: path with + lines; *** Update File: path, optionally followed by *** Move to: path, with @@ or @@ context and space-prefixed context, + added, or - removed lines; *** End of File anchors the last hunk to the file end. Use *** Delete File: path to delete. Paths are relative to the Session working directory. Read existing files before updating, deleting, or moving them. */ + patch: string; + /** The narrowest wider sandbox mode for a one-shot retry of the exact operation the sandbox just denied; the retry asks the user for approval. */ + sandbox_permissions?: "workspace-write" | "danger-full-access"; + /** Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access. Use the language of the user’s current request. */ + justification?: string; + } & Record; /** Ask the user a concise question when you need confirmation, a choice, or missing information before proceeding. */ ask_user_question: { /** Questions to ask the user before continuing. */ @@ -383,6 +394,27 @@ interface ToolArgsMap { } interface ToolOutputMap { + apply_patch: { + phase: "commit"; + cancelled: boolean; + operations: ({ + index: number; + kind: "add" | "update" | "delete" | "move"; + path: string; + destination?: string; + patchLine: number; + status: "completed" | "failed" | "unknown" | "skipped"; + code?: string; + message?: string; + hunkLine?: number; + changes?: ({ + path: string; + before: string | null; + after: string | null; + })[]; + })[]; + affectedPaths: string[]; + }; ask_user_question: { answers: { id: string; diff --git a/snapshots/web/ptc-round/system-prompt.expected.md b/snapshots/web/ptc-round/system-prompt.expected.md index f664b6d8b3..3b1dc53336 100644 --- a/snapshots/web/ptc-round/system-prompt.expected.md +++ b/snapshots/web/ptc-round/system-prompt.expected.md @@ -14,6 +14,8 @@ Read an existing file before overwriting it with write (the default fs-observati Read a file before editing it (the default fs-observation-policy requires it), unless you just created or edited it in this session. +Prefer apply_patch over separate edit calls for changes across files or separated regions. Read a file before updating, deleting, or moving it (the default fs-observation-policy requires it), unless you just created or edited it in this session. Operations run in order, stop at the first failure, and are not rolled back; re-read affected files before retrying when needed. + Use the glob tool — not shell find — to discover files by path pattern. Use the grep tool — not shell grep or rg — to search file contents. Use read on a matched file when you need surrounding context. @@ -47,6 +49,15 @@ Program-only SDK bindings: type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue } interface ToolArgsMap { + /** Create, update, delete, or move UTF-8 text files with one Codex-style patch; prefer it over multiple edit calls when a change spans several files or separated regions. */ + apply_patch: { + /** Codex patch: start with *** Begin Patch and end with *** End Patch. Use *** Add File: path with + lines; *** Update File: path, optionally followed by *** Move to: path, with @@ or @@ context and space-prefixed context, + added, or - removed lines; *** End of File anchors the last hunk to the file end. Use *** Delete File: path to delete. Paths are relative to the Session working directory. Read existing files before updating, deleting, or moving them. */ + patch: string; + /** The narrowest wider sandbox mode for a one-shot retry of the exact operation the sandbox just denied; the retry asks the user for approval. */ + sandbox_permissions?: "workspace-write" | "danger-full-access"; + /** Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access. Use the language of the user’s current request. */ + justification?: string; + } & Record; /** Ask the user a concise question when you need confirmation, a choice, or missing information before proceeding. */ ask_user_question: { /** Questions to ask the user before continuing. */ @@ -366,6 +377,27 @@ interface ToolArgsMap { } interface ToolOutputMap { + apply_patch: { + phase: "commit"; + cancelled: boolean; + operations: ({ + index: number; + kind: "add" | "update" | "delete" | "move"; + path: string; + destination?: string; + patchLine: number; + status: "completed" | "failed" | "unknown" | "skipped"; + code?: string; + message?: string; + hunkLine?: number; + changes?: ({ + path: string; + before: string | null; + after: string | null; + })[]; + })[]; + affectedPaths: string[]; + }; ask_user_question: { answers: { id: string;