refactor(patch): restructured chunk edit schema to use explicit op enum and anchor field

- Refactored chunk edit schema to use explicit `op` enum field (replace, delete, append, prepend, after, before) instead of multiple boolean/string flags.
- Renamed chunk edit parameters from `after`/`before` field names to `anchor` for sibling-relative insert operations.
- Simplified chunk edit validation logic by consolidating nested conditionals into switch statement on operation type.
- Fixed `log_experiment` to correctly identify run-modified files by removing pre-run dirty path filtering.
- Simplified chunk edit and read prompt documentation to emphasize read-first workflow with consolidated examples.
This commit is contained in:
can1357
2026-04-07 05:50:54 +02:00
parent a30ee4a111
commit 87a3b6ca03
8 changed files with 235 additions and 289 deletions
+10
View File
@@ -21,6 +21,15 @@
### Changed
- Simplified chunk edit schema to use explicit `op` field (replace, delete, append, prepend, after, before) instead of separate boolean flags for improved clarity
- Updated chunk edit prompt documentation to consolidate operation guidance with streamlined rules, ops reference table, and simplified examples
- Refactored chunk edit preview formatting to use `op` field and `anchor` parameter for sibling-relative inserts (after/before operations)
- Renamed chunk edit parameter from `after`/`before` field names to use `anchor` field for specifying named children in sibling-relative insert operations
- Simplified chunk read and edit prompt documentation to reduce verbosity and focus on essential guidance
- Refactored chunk edit schema to use explicit `op` field (replace, delete, append, prepend, after, before) instead of boolean flags for improved clarity and consistency
- Updated chunk edit prompt documentation to simplify operation guidance with consolidated rules, ops reference, and streamlined examples
- Changed chunk edit preview formatting to use `op` field and `anchor` parameter for sibling-relative inserts (after/before operations)
- Renamed chunk edit parameter from `after`/`before` field names to use `anchor` field for specifying named children in sibling-relative insert operations
- Updated streaming edit preview to display chunk edits alongside hashline edits with operation-specific formatting
- Improved chunk edit preview labels to show operation type (delete, append, prepend, insert, replace) with target and line ranges
- Simplified chunk read path parsing to extract only the selector string without redundant CRC field
@@ -72,6 +81,7 @@
### Fixed
- Fixed `log_experiment` to correctly identify run-modified files by removing pre-run dirty path filtering, ensuring all uncommitted changes are properly validated
- Fixed chunk edit operations to extract and use embedded CRC checksums when models write selectors like `fn_foo#ABCD` instead of separate `sel` and `crc` fields
- Fixed shell command error handling to properly check for errors before processing chunks in bash executor and config resolution
- Fixed `log_experiment` to correctly identify and revert only files modified by the run, leaving pre-existing uncommitted changes intact
@@ -209,7 +209,7 @@ export function createLogExperimentTool(
const preRunDirtyPaths = pendingRun.preRunDirtyPaths;
let keepScopeValidation: { committablePaths: string[] } | undefined;
if (params.status === "keep") {
const scopeValidation = await validateKeepPaths(options, workDir, state, preRunDirtyPaths);
const scopeValidation = await validateKeepPaths(options, workDir, state);
if (typeof scopeValidation === "string") {
return {
content: [{ type: "text", text: `Error: ${scopeValidation}` }],
@@ -605,7 +605,6 @@ async function validateKeepPaths(
options: AutoresearchToolFactoryOptions,
workDir: string,
state: ExperimentState,
preRunDirtyPaths: string[],
): Promise<{ committablePaths: string[] } | string> {
if (state.scopePaths.length === 0) {
return "Files in Scope is empty for the current segment. Re-run init_experiment after fixing autoresearch.md.";
@@ -624,7 +623,6 @@ async function validateKeepPaths(
}
const workDirPrefix = await readGitWorkDirPrefix(options, workDir);
const preRunSet = new Set(preRunDirtyPaths);
const committablePaths: string[] = [];
for (const entry of parseWorkDirDirtyPathsWithStatus(statusText, workDirPrefix)) {
if (isAutoresearchLocalStatePath(entry.path)) {
@@ -634,10 +632,6 @@ async function validateKeepPaths(
committablePaths.push(entry.path);
continue;
}
// Skip files that were already dirty before the run
if (preRunSet.has(entry.path)) {
continue;
}
if (state.offLimits.some(spec => pathMatchesContractPath(entry.path, spec))) {
return `cannot keep this run because ${entry.path} is listed under Off Limits in autoresearch.md`;
}
+75 -94
View File
@@ -102,49 +102,30 @@ const patchEditSchema = Type.Object({
diff: Type.Optional(Type.String({ description: "Diff hunks (update) or full content (create)" })),
});
const pTarget = {
target: Type.String({ description: "Chunk selector path with optional #CRC suffix copied from read output" }),
} as const;
const CHUNK_OP_VALUES = ["replace", "delete", "append", "prepend", "after", "before"] as const;
const pContent = {
content: Type.Optional(
Type.Union([Type.String(), Type.Array(Type.String())], { description: "Replacement content" }),
),
} as const;
const pDelete = {
delete: Type.Optional(Type.Boolean({ description: "Delete the target chunk when true" })),
} as const;
const pPosition = {
append: Type.Optional(Type.Boolean({ description: "Insert content as the last child of target" })),
prepend: Type.Optional(Type.Boolean({ description: "Insert content as the first child of target" })),
after: Type.Optional(Type.String({ description: "Insert content after this named child within target" })),
before: Type.Optional(Type.String({ description: "Insert content before this named child within target" })),
} as const;
const pRangesOpt = {
line: Type.Optional(
Type.Integer({
const chunkToolEditSchema = Type.Object({
target: Type.String({
description:
"Chunk path from read output, with #CRC suffix for replace/delete (e.g. 'class_X.fn_y#A14F'). Use parent path without #CRC for insert ops.",
}),
op: Type.Optional(
StringEnum(CHUNK_OP_VALUES, {
description:
"Start line for line-scoped replace (1-indexed file line from read gutter). Omit for whole-chunk replace.",
"Edit op (default: replace). 'delete' removes target. 'append'/'prepend' insert as last/first child. 'after'/'before' insert at sibling position; require 'anchor'.",
}),
),
end_line: Type.Optional(
Type.Integer({ description: "End line for line-scoped replace. Omit for single-line edit." }),
content: Type.Optional(
Type.String({ description: "New content (required for replace/append/prepend/after/before)." }),
),
} as const;
const chunkToolEditSchema = Type.Object(
{
...pTarget,
...pContent,
...pDelete,
...pPosition,
...pRangesOpt,
},
{ additionalProperties: false },
);
line: Type.Optional(
Type.Integer({ description: "Absolute file line for line-scoped replace (omit for whole-chunk replace)." }),
),
end_line: Type.Optional(Type.Integer({ description: "End line (inclusive) for replacing a line range." })),
anchor: Type.Optional(
Type.String({ description: "Named child to insert relative to (required for op=after/before)." }),
),
});
const chunkEditParamsSchema = Type.Object(
{
@@ -584,75 +565,75 @@ export class EditTool implements AgentTool<TInput> {
for (const edit of edits) {
const { selector, crc } = parseChunkTarget(edit.target);
const op = edit.op ?? "replace";
const hasContent = edit.content !== undefined;
const positionModes =
Number(Boolean(edit.append)) +
Number(Boolean(edit.prepend)) +
Number(edit.after !== undefined) +
Number(edit.before !== undefined);
const content = flattenContent(edit.content);
if (positionModes > 1) {
throw new Error(`Edit target ${describeChunkTarget(selector)} must choose only one insertion mode.`);
}
switch (op) {
case "delete": {
if (hasContent || edit.line !== undefined || edit.end_line !== undefined) {
throw new Error(
`Delete edit on ${describeChunkTarget(selector)} cannot include content or line ranges.`,
);
}
normalizedOperations.push({
op: "delete",
sel: selector,
crc: await assertChecksum("delete", crc, selector),
});
break;
}
if (edit.delete) {
if (hasContent || edit.line !== undefined || edit.end_line !== undefined || positionModes > 0) {
throw new Error(
`Delete edit on ${describeChunkTarget(selector)} cannot include content, line ranges, or insert positions.`,
);
}
normalizedOperations.push({
op: "delete",
sel: selector,
crc: await assertChecksum("delete", crc, selector),
});
continue;
}
if (positionModes > 0) {
if (edit.line !== undefined || edit.end_line !== undefined) {
throw new Error(`Insert edit on ${describeChunkTarget(selector)} cannot include line ranges.`);
}
if (!hasContent) {
throw new Error(`Content required for insert edit on ${describeChunkTarget(selector)}.`);
}
const content = flattenContent(edit.content);
if (edit.append) {
case "append": {
if (!hasContent) throw new Error(`Content required for append on ${describeChunkTarget(selector)}.`);
normalizedOperations.push({ op: "append_child", sel: selector, content });
continue;
break;
}
if (edit.prepend) {
case "prepend": {
if (!hasContent) throw new Error(`Content required for prepend on ${describeChunkTarget(selector)}.`);
normalizedOperations.push({ op: "prepend_child", sel: selector, content });
continue;
break;
}
if (edit.after !== undefined) {
case "after": {
if (!hasContent)
throw new Error(`Content required for after-insert on ${describeChunkTarget(selector)}.`);
if (!edit.anchor)
throw new Error(`'anchor' required for op=after on ${describeChunkTarget(selector)}.`);
normalizedOperations.push({
op: "append_sibling",
sel: joinChunkPath(selector, edit.after),
sel: joinChunkPath(selector, edit.anchor),
content,
});
continue;
break;
}
case "before": {
if (!hasContent)
throw new Error(`Content required for before-insert on ${describeChunkTarget(selector)}.`);
if (!edit.anchor)
throw new Error(`'anchor' required for op=before on ${describeChunkTarget(selector)}.`);
normalizedOperations.push({
op: "prepend_sibling",
sel: joinChunkPath(selector, edit.anchor),
content,
});
break;
}
default: {
if (!hasContent) {
throw new Error(`Content required for replace edit on ${describeChunkTarget(selector)}.`);
}
normalizedOperations.push({
op: "replace",
sel: selector,
crc: await assertChecksum("replace", crc, selector),
content,
line: edit.line,
endLine: edit.end_line,
});
break;
}
normalizedOperations.push({
op: "prepend_sibling",
sel: joinChunkPath(selector, edit.before ?? ""),
content,
});
continue;
}
if (!hasContent) {
throw new Error(`Content required for replace edit on ${describeChunkTarget(selector)}.`);
}
normalizedOperations.push({
op: "replace",
sel: selector,
crc: await assertChecksum("replace", crc, selector),
content: flattenContent(edit.content),
line: edit.line,
endLine: edit.end_line,
});
}
const chunkResult = applyChunkEdits({
+19 -19
View File
@@ -198,27 +198,27 @@ function formatStreamingHashlineEdits(edits: Partial<HashlineToolEdit | ChunkToo
? edit.content
: "";
const target = edit.target ?? "?";
const op = edit.op ?? "replace";
if (edit.delete) {
return { srcLabel: `\u2022 delete ${target}`, dst: "" };
switch (op) {
case "delete":
return { srcLabel: `\u2022 delete ${target}`, dst: "" };
case "append":
return { srcLabel: `\u2022 append child ${target}`, dst: contentLines };
case "prepend":
return { srcLabel: `\u2022 prepend child ${target}`, dst: contentLines };
case "after":
return { srcLabel: `\u2022 insert after ${target}/${edit.anchor ?? "?"}`, dst: contentLines };
case "before":
return { srcLabel: `\u2022 insert before ${target}/${edit.anchor ?? "?"}`, dst: contentLines };
default: {
if (edit.line != null) {
const range = edit.end_line != null ? `${edit.line}\u2026${edit.end_line}` : `${edit.line}`;
return { srcLabel: `\u2022 replace ${target} L${range}`, dst: contentLines };
}
return { srcLabel: `\u2022 replace ${target}`, dst: contentLines };
}
}
if (edit.append) {
return { srcLabel: `\u2022 append child ${target}`, dst: contentLines };
}
if (edit.prepend) {
return { srcLabel: `\u2022 prepend child ${target}`, dst: contentLines };
}
if (edit.after) {
return { srcLabel: `\u2022 insert after ${target}.${edit.after}`, dst: contentLines };
}
if (edit.before) {
return { srcLabel: `\u2022 insert before ${target}.${edit.before}`, dst: contentLines };
}
if (edit.line != null) {
const range = edit.end_line != null ? `${edit.line}\u2026${edit.end_line}` : `${edit.line}`;
return { srcLabel: `\u2022 replace ${target} L${range}`, dst: contentLines };
}
return { srcLabel: `\u2022 replace ${target}`, dst: contentLines };
}
}
function formatMetadataLine(lineCount: number | null, language: string | undefined, uiTheme: Theme): string {
@@ -1,132 +1,37 @@
Edits files by addressing syntax-aware chunks from `read` output.
Edits files via syntax-aware chunks. Run `read(path="file.ts")` first — it returns chunks with anchors `[name#CRC]` where `#CRC` is a 4-char hex checksum. Copy the full chunk path AND `#CRC` verbatim into `target`.
Read the file first with `read(path="file.ts")`. Copy `target` directly from the latest chunk anchors. The `#CRC` suffix in `target` is the chunk checksum.
<rules>
- **MUST** `read` first. NEVER invent chunk names or CRCs — copy them from the latest read output or edit response.
- `target` **MUST** be the **fully-qualified** path (e.g. `class_X.fn_y.if_2`, not `if_2`), ending with `#CRC` for replace/delete.
- Prefer `line`/`end_line` (absolute file line numbers from the read gutter) for small fixes over whole-chunk replace.
- `content` must include the destination block's inner indentation.
- Successful edits return refreshed anchors — use them for follow-ups, don't re-read just for new CRCs.
</rules>
Successful edit responses include the updated chunk tree with checksums. Do not re-read just to refresh checksums unless the file changed externally.
<ops>
|op|fields|effect|
|---|---|---|
|`replace` (default)|`target#CRC`, `content`, opt. `line`/`end_line`|rewrite chunk or a line range within it|
|`delete`|`target#CRC`|remove chunk|
|`append` / `prepend`|`target`, `content`|insert as last/first child of target|
|`after` / `before`|`target`, `anchor` (child name), `content`|insert at sibling position|
**Checksum scope:** Each chunk has its own CRC over its source span. Editing non-overlapping lines elsewhere does not change unrelated chunks' checksums.
For file-root edits, `target` is the file CRC alone (e.g. `"#VSKB"`).
</ops>
<operations>
**Choosing the right edit shape:**
- To rewrite an entire chunk → `{ "target": "chunk#CRC", "content": "…" }`
- To fix a single line → add `"line": 13`
- To fix a contiguous range → add `"line": 13, "end_line": 17`
- To delete a chunk → `{ "target": "chunk#CRC", "delete": true }`
- To append/prepend inside a container → add `"append": true` or `"prepend": true`
- To insert relative to a named child → use `"after": "child_name"` or `"before": "child_name"` on the parent target
|shape|effect|
|---|---|
|`{ "target": "chunk#CRC", "content": "…" }`|replace the target chunk|
|`{ "target": "chunk#CRC", "line": 13, "content": "…" }`|replace one line within the target chunk|
|`{ "target": "chunk#CRC", "line": 13, "end_line": 17, "content": "…" }`|replace an inclusive line range within the target chunk|
|`{ "target": "chunk#CRC", "delete": true }`|delete the target chunk|
|`{ "target": "chunk", "append": true, "content": "…" }`|append as last child of the target chunk|
|`{ "target": "chunk", "prepend": true, "content": "…" }`|prepend as first child of the target chunk|
|`{ "target": "parent", "after": "child", "content": "…" }`|insert after the named child within `parent`|
|`{ "target": "parent", "before": "child", "content": "…" }`|insert before the named child within `parent`|
- `line`/`end_line` are **absolute file line numbers** from the `read` gutter. `line` alone = single line. `line` + `end_line` = inclusive range. `line` with `end_line` = `line`-1 = zero-width insert.
- `path` is always just the file path. Do not embed `:chunk` selectors in `path`.
- `target` is the chunk path, optionally followed by `#CRC` copied from the anchor. Example: `"class_Server.fn_start#HTST"`.
- Insert edits usually omit the checksum because they do not rewrite the target chunk itself.
- For file-root replace/delete, use the file header checksum as `"target": "#VSKB"`.
- Content must already include the intended inner indentation for the destination block.
- Batch edits observe earlier edits. If edit 1 changes checksum/span/path, edit 2 must use the post-edit anchor values.
- `replace`/`delete` include leading comments/attributes attached to the chunk.
</operations>
<examples>
All examples reference this `read` output:
<example>
Given read output:
```
| server.ts·40L·ts·#VSKB
5| class Server {
| {{anchor "class_Server" "XKQZ"}}
12| start(): void {
| {{anchor "fn_start" "HTST"}}
13| log("booting on " + this.port);
14| for (let i = 0; i < MAX_RETRIES; i++) {
15| this.tryBind();
16| }
17| }
19| private tryBind(): boolean {
| {{anchor "fn_tryBind" "VNWR"}}
20| // TODO: add backoff
21| return bind(this.port);
22| }
14| this.tryBind();
15| }
```
<example name="replace a method">
```
"path": "server.ts",
"edits": [
{
"target": "{{sel "class_Server.fn_start"}}#HTST",
"content": "start(): void {\n log(\"starting\");\n this.tryBind();\n}"
}
]
Fix the typo on line 13:
```json
{"path":"server.ts","edits":[{"target":"{{sel "class_Server.fn_start"}}#HTST","line":13,"content":" warn(\"booting on \" + this.port);"}]}
```
</example>
<example name="replace a single line">
```
"path": "server.ts",
"edits": [
{
"target": "{{sel "class_Server.fn_start"}}#HTST",
"line": 13,
"content": " warn(\"booting on \" + this.port);"
}
]
```
</example>
<example name="delete a chunk">
```
"path": "server.ts",
"edits": [
{
"target": "{{sel "class_Server.fn_tryBind"}}#VNWR",
"delete": true
}
]
```
</example>
<example name="append a child method">
```
"path": "server.ts",
"edits": [
{
"target": "{{sel "class_Server"}}",
"append": true,
"content": "status(): string {\n return \"ok\";\n}"
}
]
```
</example>
<example name="insert after a named child">
```
"path": "server.ts",
"edits": [
{
"target": "{{sel "class_Server"}}",
"after": "fn_start",
"content": "reset(): void {\n this.port = 0;\n}"
}
]
```
</example>
</examples>
<critical>
- **MUST** include `path` in every edit call.
- **MUST** read the latest chunk output before editing.
- **MUST** include a `#CRC` suffix in `target` for every replace or delete edit.
- **MUST** use updated chunk output from the edit response for follow-up edits.
- **MUST** use the smallest correct target; do not rewrite siblings unnecessarily.
- **MUST** use the **fully qualified** chunk path. Copy the complete path shown in read output — e.g. `fn_handleTerraform.try.if_2`, never just `if_2`. Short/leaf names are ambiguous.
- For line-scoped replace edits, use file line numbers from the `read` gutter.
- Multiple line-scoped edits on the same chunk in one batch are fine — the engine auto-updates the checksum between operations.
</critical>
</output>
@@ -1,55 +1,16 @@
Reads files using syntax-aware chunks.
<instruction>
## Parameters
- `path` -- file path or URL; may include `:selector` suffix as an alternative to `sel`
- `sel` -- optional selector (see table below)
- `timeout` -- seconds, for URLs only
- `path` — file path or URL; may include `:selector` suffix
- `sel` — optional selector: `class_Foo`, `class_Foo.fn_bar`, `L50`, `L50-L120`, or `raw`
- `timeout` — seconds, for URLs only
## Selectors
Each anchor `[name#CCCC]` in the output is a chunk ID. Copy `name#CCCC` into the edit tool's `target` field.
Line numbers in the gutter are absolute — use them for `line`/`end_line` in edits.
|Input|Behavior|
|---|---|
|_(omitted)_|Render the file root chunk|
|`class_Foo`|Read a chunk by path|
|`class_Foo.fn_bar`|Read a nested chunk path|
|`L50` or `L50-L120`|Absolute file line range|
|`raw`|Read full raw file content (no chunk rendering)|
Each anchor line shows `[name#CCCC]` — `#CCCC` is the edit checksum. Copy it when editing with `chunk-edit`.
If `path:chunk` and `sel` are both provided, `sel` wins. Missing chunk paths return `[Chunk not found]`.
Code rows use **absolute file line numbers** in the gutter. `chunk-edit` `line`/`end_line` use those same numbers.
## Examples
`read(path="src/math.ts")`
```text
| src/math.ts·120L·ts·#A744
|
5| export function sum(values: readonly number[]): number {
| {{anchor "fn_sum" "3286"}}
6| return values.reduce((total, value) => total + value, 0);
7| }
10| export class Calculator {
| {{anchor "class_Calculator" "5D36"}}
11| multiply(left: number, right: number): number {
| {{anchor "fn_multiply" "B592"}}
12| return left * right;
13| }
14| }
```
## Language Support
Chunk trees: JavaScript, TypeScript, TSX, Python, Rust, Go. Others use blank-line fallback.
Chunk trees: JS, TS, TSX, Python, Rust, Go. Others use blank-line fallback.
</instruction>
<critical>
- **MUST** use `read` instead of shell commands for file reading.
- **MUST** copy the current checksum before editing a chunk with `chunk-edit`.
- **MUST NOT** assume chunk names; always read the current output first.
- **MUST** `read` before editing — never invent chunk names or CRCs.
</critical>
</output>
@@ -621,6 +621,101 @@ describe("autoresearch tools", () => {
expect(runJson.status).toBe("keep");
});
it("commits in-scope changes when those paths were already dirty before the run", async () => {
const dir = makeTempDir();
tempDirs.push(dir);
writeAutoresearchWorkspace(dir, {
contract: [
"# Autoresearch",
"",
"## Benchmark",
"- command: bash autoresearch.sh",
"- primary metric: runtime_ms",
"- metric unit: ms",
"- direction: lower",
"",
"## Files in Scope",
"- src/in-scope.ts",
"",
"## Off Limits",
"",
"## Constraints",
"- keep behavior stable",
"",
].join("\n"),
});
fs.mkdirSync(path.join(dir, "src"), { recursive: true });
fs.writeFileSync(path.join(dir, "src", "in-scope.ts"), "export const value = 1;\n");
await $`git init`.cwd(dir).quiet();
await $`git config user.email test@example.com`.cwd(dir).quiet();
await $`git config user.name Test User`.cwd(dir).quiet();
await $`git add .`.cwd(dir).quiet();
await $`git commit -m initial`.cwd(dir).quiet();
await $`git checkout -b autoresearch/test-keep-prerun-dirty`.cwd(dir).quiet();
fs.writeFileSync(path.join(dir, "src", "in-scope.ts"), "export const value = 3;\n");
fs.writeFileSync(path.join(dir, "autoresearch.program.md"), "# Strategy\n\n- focus on in-scope edits\n");
await Bun.write(
path.join(dir, ".autoresearch", "runs", "0001", "run.json"),
JSON.stringify({
command: "bash autoresearch.sh",
exitCode: 0,
parsedMetrics: { runtime_ms: 9 },
parsedPrimary: 9,
runNumber: 1,
}),
);
const runtime = createSessionRuntime();
runtime.state.metricName = "runtime_ms";
runtime.state.metricUnit = "ms";
runtime.state.scopePaths = ["src/in-scope.ts"];
runtime.state.constraints = ["keep behavior stable"];
const runDirectory = path.join(dir, ".autoresearch", "runs", "0001");
runtime.lastRunArtifactDir = runDirectory;
runtime.lastRunNumber = 1;
runtime.lastRunDuration = 1.2;
runtime.lastRunSummary = {
checksDurationSeconds: 0,
checksPass: null,
checksTimedOut: false,
command: "bash autoresearch.sh",
durationSeconds: 1.2,
parsedAsi: null,
parsedMetrics: { runtime_ms: 9 },
parsedPrimary: 9,
passed: true,
preRunDirtyPaths: ["src/in-scope.ts"],
runDirectory,
runNumber: 1,
};
const tool = createLogExperimentTool({
dashboard: createDashboardStub(),
getRuntime: () => runtime,
pi: createGitApi(),
});
const result = await tool.execute(
"call-keep-prerun-dirty",
{
commit: "initial",
metric: 9,
status: "keep",
description: "Improve in scope after prior WIP",
asi: { hypothesis: "refine further" },
},
undefined,
undefined,
createContext(dir),
);
expect(firstTextBlockText(result.content)).toContain("Logged run #1: keep");
const committedPaths = await $`git show --name-only --pretty=format: HEAD`.cwd(dir).text();
expect(committedPaths).toContain("src/in-scope.ts");
expect(committedPaths).toContain("autoresearch.program.md");
});
it("rejects keep when an out-of-scope file is dirty", async () => {
const dir = makeTempDir();
tempDirs.push(dir);
@@ -296,7 +296,7 @@ describe("chunk mode tools", () => {
await editTool.execute("chunk-edit-string-content", {
path: filePath,
edits: [{ target: "class_Server", append: true, content: 'status(): string {\n return "ok";\n}\n' }],
edits: [{ target: "class_Server", op: "append", content: 'status(): string {\n return "ok";\n}\n' }],
} as never);
const updatedSource = await Bun.file(filePath).text();
@@ -415,8 +415,8 @@ describe("chunk mode tools", () => {
editTool.execute("chunk-edit-batch-rollback", {
path: filePath,
edits: [
{ target: "class_Server", append: true, content: ' status(): string {\n return "ok";\n }' },
{ target: "class_Server.fn_handleError#ZZZZ", delete: true },
{ target: "class_Server", op: "append", content: ' status(): string {\n return "ok";\n }' },
{ target: "class_Server.fn_handleError#ZZZZ", op: "delete" },
],
}),
).rejects.toThrow(/No changes were saved/);
@@ -491,7 +491,7 @@ describe("chunk mode tools", () => {
line: 3,
end_line: 3,
content: " let total = 1;",
} as never,
},
],
}),
).rejects.toThrow(/Checksum required/);