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.
This commit is contained in:
sjw1231 committed 2026-10-22 15:53:02 +08:00
1 parent 1f14903f28
commit 7fab3cf976
12 files changed
+157 -3

No files matched your search

@@ -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',
+2
View File
@@ -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` | — |
+2
View File
@@ -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` | — |
+10
View File
@@ -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')
@@ -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')
@@ -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"
@@ -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"
@@ -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.
@@ -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.",
@@ -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<string, JsonValue>;
/** 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;
@@ -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<string, JsonValue>;
/** 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;
@@ -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<string, JsonValue>;
/** 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;