diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 8be8bd031..3f50b7bd4 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1978,4 +1978,4 @@ _Dedicated to Peter's shoulder ([@steipete](https://twitter.com/steipete))_ ## [0.9.4] - 2025-11-26 -Initial release with multi-provider LLM support. \ No newline at end of file +Initial release with multi-provider LLM support. diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 851a76915..02a02546d 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,7 +1,6 @@ # Changelog ## [Unreleased] - ### Added - Added `new_segment` option to `init_experiment` to force a new segment even when contract fields match, enabling re-initialization with unchanged parameters @@ -15,6 +14,9 @@ ### Changed +- Changed chunk edit operation format from flat `{ "op": "splice", ... }` to keyed `{ "splice": { ... } }` format for improved clarity and schema validation +- Updated chunk edit schema to use discriminated union types with operation-specific field requirements (insert ops no longer require CRC, mutation ops require CRC) +- Improved chunk selector sanitization to strip filename prefixes and normalize checksums to uppercase, handling model output variations - Changed `log_experiment` revert behavior to only restore run-modified files instead of reverting entire working tree, preserving pre-existing uncommitted changes - Changed `init_experiment` error message for pending unlogged runs to include command, metric, and pass/fail status for better context - Changed `init_experiment` to detect when contract fields match current state and skip re-initialization (no-op) unless `new_segment=true` diff --git a/packages/coding-agent/src/patch/index.ts b/packages/coding-agent/src/patch/index.ts index db7762062..a0ae30848 100644 --- a/packages/coding-agent/src/patch/index.ts +++ b/packages/coding-agent/src/patch/index.ts @@ -104,45 +104,84 @@ const patchEditSchema = Type.Object({ diff: Type.Optional(Type.String({ description: "Diff hunks (update) or full content (create)" })), }); -const CHUNK_EDIT_OPS_BASE = [ - "append_child", - "prepend_child", - "append_sibling", - "prepend_sibling", - "replace", - "delete", -] as const; - -const chunkEditOperationSchemaBaseProps = { - sel: Type.Optional(Type.String({ description: "Chunk path selector" })), - crc: Type.Optional(Type.String({ description: "Chunk checksum for staleness validation" })), +const chunkEditSelProp = { sel: Type.Optional(Type.String({ description: "Chunk path selector" })) } as const; +const chunkEditCrcProp = { crc: Type.String({ description: "Chunk checksum for staleness validation" }) } as const; +const chunkEditContentProp = { + content: Type.Union([Type.String(), Type.Array(Type.String())], { description: "Inserted or replacement content" }), +} as const; +const chunkEditOptionalContentProp = { content: Type.Optional( - Type.Union([Type.String(), Type.Array(Type.String())], { description: "Inserted or replacement content" }), + Type.Union([Type.String(), Type.Array(Type.String())], { + description: "Replacement content (empty string to delete lines)", + }), ), } as const; - -const chunkEditOperationSchemaWithoutSplice = Type.Object({ - op: StringEnum(CHUNK_EDIT_OPS_BASE, { description: "Chunk edit operation" }), - ...chunkEditOperationSchemaBaseProps, -}); - -const chunkEditOperationSchemaWithSplice = Type.Object({ - op: StringEnum([...CHUNK_EDIT_OPS_BASE, "splice"], { description: "Chunk edit operation" }), - ...chunkEditOperationSchemaBaseProps, +const chunkEditBegEndProps = { + beg: Type.Integer({ + description: + "Start line: absolute file line (1-indexed, same as read gutter). With end, inclusive when beg ≤ end; when beg = end + 1, zero-width insertion.", + }), + end: Type.Integer({ + description: + "End line: absolute file line (1-indexed). Inclusive when beg ≤ end; for zero-width insertion use end = beg − 1.", + }), +} as const; +const chunkEditOptionalBegEndProps = { beg: Type.Optional( Type.Integer({ description: - "Start line: absolute file line (1-indexed, same as read gutter). Used by splice and replace with line range. With end, inclusive when beg \u2264 end; when beg = end + 1, zero-width insertion.", - }), - ), - end: Type.Optional( - Type.Integer({ - description: - "End line: absolute file line (1-indexed). Inclusive when beg \u2264 end; for zero-width insertion use end = beg \u2212 1.", + "Start line for line-scoped replace. When both beg and end are provided, replaces only those file lines (same as splice).", }), ), + end: Type.Optional(Type.Integer({ description: "End line for line-scoped replace." })), +} as const; + +// Insert ops (no CRC) +const appendChildOp = Type.Object({ append_child: Type.Object({ ...chunkEditSelProp, ...chunkEditContentProp }) }); +const prependChildOp = Type.Object({ prepend_child: Type.Object({ ...chunkEditSelProp, ...chunkEditContentProp }) }); +const appendSiblingOp = Type.Object({ append_sibling: Type.Object({ ...chunkEditSelProp, ...chunkEditContentProp }) }); +const prependSiblingOp = Type.Object({ + prepend_sibling: Type.Object({ ...chunkEditSelProp, ...chunkEditContentProp }), }); +// Mutation ops (CRC required) +const replaceOp = Type.Object({ + replace: Type.Object({ + ...chunkEditSelProp, + ...chunkEditCrcProp, + ...chunkEditContentProp, + ...chunkEditOptionalBegEndProps, + }), +}); +const deleteOp = Type.Object({ delete: Type.Object({ ...chunkEditSelProp, ...chunkEditCrcProp }) }); +const spliceOp = Type.Object({ + splice: Type.Object({ + ...chunkEditSelProp, + ...chunkEditCrcProp, + ...chunkEditBegEndProps, + ...chunkEditOptionalContentProp, + }), +}); + +const chunkEditOperationSchemaWithoutSplice = Type.Union([ + appendChildOp, + prependChildOp, + appendSiblingOp, + prependSiblingOp, + replaceOp, + deleteOp, +]); + +const chunkEditOperationSchemaWithSplice = Type.Union([ + appendChildOp, + prependChildOp, + appendSiblingOp, + prependSiblingOp, + replaceOp, + deleteOp, + spliceOp, +]); + const chunkEditOperationSchema = chunkSplicesEnabled() ? chunkEditOperationSchemaWithSplice : chunkEditOperationSchemaWithoutSplice; @@ -382,6 +421,44 @@ function isChunkParams(params: ReplaceParams | PatchParams | HashlineParams | Ch return "operations" in params; } +const CHUNK_OP_KEYS = [ + "append_child", + "prepend_child", + "append_sibling", + "prepend_sibling", + "replace", + "delete", + "splice", +] as const; +type ChunkOpKey = (typeof CHUNK_OP_KEYS)[number]; + +interface ParsedChunkOp { + op: ChunkOpKey; + sel?: string; + crc?: string; + content?: string | string[]; + beg?: number; + end?: number; +} + +/** Normalize keyed `{ splice: { ... } }` format to flat internal format. Also tolerates legacy `{ op: "splice", ... }`. */ +function parseKeyedChunkOp(raw: Record): ParsedChunkOp { + // Legacy flat format: { op: "splice", sel: "...", ... } + if (typeof raw.op === "string" && CHUNK_OP_KEYS.includes(raw.op as ChunkOpKey)) { + return raw as unknown as ParsedChunkOp; + } + + // Keyed format: { splice: { sel: "...", ... } } + for (const key of CHUNK_OP_KEYS) { + if (key in raw && typeof raw[key] === "object" && raw[key] !== null) { + const inner = raw[key] as Record; + return { op: key, ...inner } as unknown as ParsedChunkOp; + } + } + + throw new Error(`Unknown chunk edit operation shape: ${JSON.stringify(Object.keys(raw))}`); +} + /** * Edit tool implementation. * @@ -535,9 +612,10 @@ export class EditTool implements AgentTool { : undefined; const normalizedOperations: ChunkEditOperation[] = operations.map(operation => { - const anchorSelector = operation.sel ?? defaultSelector; - const anchorCrc = operation.crc ?? (operation.sel === undefined ? defaultCrc : undefined); - const content = normalizeChunkContentInput(operation.content); + const opEntry = parseKeyedChunkOp(operation as Record); + const anchorSelector = opEntry.sel ?? defaultSelector; + const anchorCrc = opEntry.crc ?? (opEntry.sel === undefined ? defaultCrc : undefined); + const content = normalizeChunkContentInput(opEntry.content); const requireChecksum = (op: string): void => { if (anchorCrc) return; @@ -559,31 +637,31 @@ export class EditTool implements AgentTool { ); }; - switch (operation.op) { + switch (opEntry.op) { case "append_child": case "prepend_child": case "append_sibling": case "prepend_sibling": if (!content) { - throw new Error(`Content required for ${operation.op} on ${anchorSelector ?? ""}`); + throw new Error(`Content required for ${opEntry.op} on ${anchorSelector ?? ""}`); } - return { op: operation.op, sel: operation.sel, crc: operation.crc, content } as ChunkEditOperation; + return { op: opEntry.op, sel: opEntry.sel, crc: opEntry.crc, content } as ChunkEditOperation; case "replace": requireChecksum("replace"); if (content.length === 0) { - return { op: "delete", sel: operation.sel, crc: anchorCrc } as ChunkEditOperation; + return { op: "delete", sel: opEntry.sel, crc: anchorCrc } as ChunkEditOperation; } return { op: "replace", - sel: operation.sel, + sel: opEntry.sel, crc: anchorCrc, content, - beg: (operation as { beg?: number }).beg, - end: (operation as { end?: number }).end, + beg: opEntry.beg, + end: opEntry.end, } as ChunkEditOperation; case "delete": requireChecksum("delete"); - return { op: "delete", sel: operation.sel, crc: anchorCrc } as ChunkEditOperation; + return { op: "delete", sel: opEntry.sel, crc: anchorCrc } as ChunkEditOperation; case "splice": if (!chunkSplicesEnabled()) { throw new Error( @@ -591,19 +669,19 @@ export class EditTool implements AgentTool { ); } requireChecksum("splice"); - if (operation.beg == null || operation.end == null) { + if (opEntry.beg == null || opEntry.end == null) { throw new Error(`beg and end required for splice on ${anchorSelector}`); } return { op: "splice", - sel: operation.sel, + sel: opEntry.sel, crc: anchorCrc, - beg: operation.beg, - end: operation.end, + beg: opEntry.beg, + end: opEntry.end, content, } as ChunkEditOperation; default: - throw new Error(`Unknown chunk edit operation: ${(operation as { op: string }).op}`); + throw new Error(`Unknown chunk edit operation: ${opEntry.op}`); } }); diff --git a/packages/coding-agent/src/prompts/tools/chunk-edit.md b/packages/coding-agent/src/prompts/tools/chunk-edit.md index 56f4b2b56..42d02df94 100644 --- a/packages/coding-agent/src/prompts/tools/chunk-edit.md +++ b/packages/coding-agent/src/prompts/tools/chunk-edit.md @@ -4,7 +4,7 @@ Read the file first with `read(path="file.ts")`. Use chunk paths exactly as show Successful edit responses already include the updated root chunk rendering for that file. Do not immediately re-read the file just to refresh checksums unless the file may have changed externally. -**Checksum scope:** Each chunk has its own CRC over that chunk’s source span. Editing non-overlapping lines elsewhere in the file does not change unrelated chunks’ checksums. There is no separate whole-file guard. +**Checksum scope:** Each chunk has its own CRC over that chunk's source span. Editing non-overlapping lines elsewhere in the file does not change unrelated chunks' checksums. There is no separate whole-file guard. {{#if chunkSplices}} @@ -15,43 +15,42 @@ Successful edit responses already include the updated root chunk rendering for t - `splice` is almost always the right choice for small, targeted fixes - `replace` with `beg`/`end` also works for line-level fixes (same behavior as `splice`) {{/if}} -|op|effect| -|---|---| -|`append_child`|insert as last child inside a container (before closing delimiter)| -|`prepend_child`|insert as first child inside a container (after opening delimiter)| -|`append_sibling`|insert after the selected chunk| -|`prepend_sibling`|insert before the selected chunk| -|`replace`|without `beg`/`end`: rewrite the **entire** chunk from first line to last line; supply full replacement content. With `beg`/`end`: replace only those **file lines** within the chunk (same as `splice` but semantically a replacement). `splice` is almost always the right choice for small, targeted fixes| -|`delete`|remove the entire chunk| +|operation|format|effect| +|---|---|---| +|`append_child`|`{ "append_child": { "sel": "…", "content": "…" } }`|insert as last child inside a container (before closing delimiter)| +|`prepend_child`|`{ "prepend_child": { "sel": "…", "content": "…" } }`|insert as first child inside a container (after opening delimiter)| +|`append_sibling`|`{ "append_sibling": { "sel": "…", "content": "…" } }`|insert after the selected chunk| +|`prepend_sibling`|`{ "prepend_sibling": { "sel": "…", "content": "…" } }`|insert before the selected chunk| +|`replace`|`{ "replace": { "sel": "…", "crc": "…", "content": "…" } }`|without `beg`/`end`: rewrite the **entire** chunk from first line to last line; supply full replacement content. With `beg`/`end`: replace only those **file lines** within the chunk (same as `splice` but semantically a replacement). `splice` is almost always the right choice for small, targeted fixes| +|`delete`|`{ "delete": { "sel": "…", "crc": "…" } }`|remove the entire chunk| {{#if chunkSplices}} -|`splice`|when `beg` ≤ `end`, replace **file** lines `beg`–`end` (inclusive) within the selected chunk; empty `content` deletes them. When `beg` = `end` + 1, **zero-width splice**: insert in the gap after file line `end` and before file line `beg` (no lines removed)| +|`splice`|`{ "splice": { "sel": "…", "crc": "…", "beg": N, "end": N } }`|when `beg` ≤ `end`, replace **file** lines `beg`–`end` (inclusive) within the selected chunk; empty `content` deletes them. When `beg` = `end` + 1, **zero-width splice**: insert in the gap after file line `end` and before file line `beg` (no lines removed)| {{/if}} -- `op` is a single keyword only (e.g. `"splice"`). Do not put other fields like `sel` inside `op`. -- `sel` is the chunk path (a separate field from `op`) +- `sel` is the chunk path - `crc` is the chunk checksum used for staleness validation on `replace`, `delete`{{#if chunkSplices}}, and `splice`{{/if}}; it guards the selected chunk, not unrelated file changes elsewhere in the file {{#if chunkSplices}} -- `beg`/`end` are **absolute file line numbers** (same as the gutter in `read`). They must fall within the selected chunk’s file line span (or form a valid zero-width gap on its boundary). +- `beg`/`end` are **absolute file line numbers** (same as the gutter in `read`). They must fall within the selected chunk's file line span (or form a valid zero-width gap on its boundary). {{/if}} - `path="file.ts:chunk_path"` sets a default `sel` for operations that omit it - top-level `crc` sets the default checksum for the path-level selector -- **Auto-indent (flat):** The tool adds the target insertion column’s base indent to every **non-empty** line of your `content`. It does not parse or understand block nesting; put any deeper relative indentation in the `content` yourself (extra leading spaces/tabs on inner lines). +- **Auto-indent (flat):** The tool adds the target insertion column's base indent to every **non-empty** line of your `content`. It does not parse or understand block nesting; put any deeper relative indentation in the `content` yourself (extra leading spaces/tabs on inner lines). - Chunk paths are always fully qualified: `class_Server.fn_start`, not bare `fn_start` -- Batch ops observe earlier edits in the same request. If op 1 changes a chunk’s checksum, span, or path, op 2 must use the **post-op-1** checksum/span/path. +- Batch ops observe earlier edits in the same request. If op 1 changes a chunk's checksum, span, or path, op 2 must use the **post-op-1** checksum/span/path. - `replace`/`delete` operate on the selected chunk range, including leading comments or attributes that the parser attached to that chunk. {{#if chunkSplices}} -**Zero-width `splice` (insert only, `beg` = `end` + 1), using file line numbers `S` = chunk’s first line, `E` = chunk’s last line:** +**Zero-width `splice` (insert only, `beg` = `end` + 1), using file line numbers `S` = chunk's first line, `E` = chunk's last line:** -Use zero-width splice for separator gaps between neighboring chunks. Gap edits are independent from either chunk’s checksum: inserting into the gap does not update the following chunk’s `crc`, and later `replace`/`delete` operations only affect that chunk’s own selected range. +Use zero-width splice for separator gaps between neighboring chunks. Gap edits are independent from either chunk's checksum: inserting into the gap does not update the following chunk's `crc`, and later `replace`/`delete` operations only affect that chunk's own selected range. |Goal|`beg`|`end`| |---|---|---| |Insert before file line L|L|L − 1| |Insert after file line L|L + 1|L| -|Insert before the chunk’s first line S|S|S − 1| -|Insert after the chunk’s last line E|E + 1|E| +|Insert before the chunk's first line S|S|S − 1| +|Insert after the chunk's last line E|E + 1|E| {{/if}} @@ -97,10 +96,11 @@ All examples reference this `read` output (`beg`/`end` match the gutter): "path": "server.ts", "operations": [ { - "op": "replace", - "sel": "class_Server.fn_start", - "crc": "HTST", - "content": "start(): void {\n log(\"starting\");\n this.tryBind();\n}" + "replace": { + "sel": "class_Server.fn_start", + "crc": "HTST", + "content": "start(): void {\n log(\"starting\");\n this.tryBind();\n}" + } } ] ``` @@ -112,12 +112,13 @@ All examples reference this `read` output (`beg`/`end` match the gutter): "path": "server.ts", "operations": [ { - "op": "splice", - "sel": "class_Server.fn_start", - "crc": "HTST", - "beg": 13, - "end": 13, - "content": " warn(\"booting on \" + this.port);" + "splice": { + "sel": "class_Server.fn_start", + "crc": "HTST", + "beg": 13, + "end": 13, + "content": " warn(\"booting on \" + this.port);" + } } ] ``` @@ -128,12 +129,13 @@ All examples reference this `read` output (`beg`/`end` match the gutter): "path": "server.ts", "operations": [ { - "op": "splice", - "sel": "class_Server.fn_start", - "crc": "HTST", - "beg": 13, - "end": 12, - "content": "const startedAt = Date.now();" + "splice": { + "sel": "class_Server.fn_start", + "crc": "HTST", + "beg": 13, + "end": 12, + "content": "const startedAt = Date.now();" + } } ] ``` @@ -144,9 +146,10 @@ All examples reference this `read` output (`beg`/`end` match the gutter): "path": "server.ts", "operations": [ { - "op": "delete", - "sel": "class_Server.fn_tryBind", - "crc": "VNWR" + "delete": { + "sel": "class_Server.fn_tryBind", + "crc": "VNWR" + } } ] ``` @@ -158,17 +161,19 @@ All examples reference this `read` output (`beg`/`end` match the gutter): "path": "server.ts", "operations": [ { - "op": "splice", - "sel": "class_Server.fn_start", - "crc": "HTST", - "beg": 13, - "end": 13, - "content": " warn(\"booting on \" + this.port);" + "splice": { + "sel": "class_Server.fn_start", + "crc": "HTST", + "beg": 13, + "end": 13, + "content": " warn(\"booting on \" + this.port);" + } }, { - "op": "delete", - "sel": "class_Server.fn_tryBind", - "crc": "VNWR" + "delete": { + "sel": "class_Server.fn_tryBind", + "crc": "VNWR" + } } ] ``` @@ -179,15 +184,17 @@ All examples reference this `read` output (`beg`/`end` match the gutter): "path": "server.ts", "operations": [ { - "op": "replace", - "sel": "class_Server.fn_start", - "crc": "HTST", - "content": "start(): void {\n warn(\"booting on \" + this.port);\n for (let i = 0; i < MAX_RETRIES; i++) {\n this.tryBind();\n }\n}" + "replace": { + "sel": "class_Server.fn_start", + "crc": "HTST", + "content": "start(): void {\n warn(\"booting on \" + this.port);\n for (let i = 0; i < MAX_RETRIES; i++) {\n this.tryBind();\n }\n}" + } }, { - "op": "delete", - "sel": "class_Server.fn_tryBind", - "crc": "VNWR" + "delete": { + "sel": "class_Server.fn_tryBind", + "crc": "VNWR" + } } ] ``` @@ -205,8 +212,8 @@ All examples reference this `read` output (`beg`/`end` match the gutter): {{#if chunkSplices}} - For `splice`, copy `beg`/`end` from the **file line numbers** in `read` output (the same values as elision `sel=L…` ranges). - **Do NOT batch multiple `splice` operations on the same chunk** in one edit call. Each splice changes the chunk's checksum, and you cannot predict the new checksum. Instead: either combine the changes into a single splice with a wider `beg`/`end` range covering all affected lines, or make separate edit calls (one splice per call). -- Each `splice` operation requires its own `crc` field (not just a top-level `crc`). Put `"crc": "XXXX"` inside the operation object. {{/if}} - When restoring a deleted statement, check surrounding lines carefully — insert at the exact position relative to existing code. Do not duplicate adjacent lines. - Use only chunk paths that appear in the `read` output. If you need deeper chunks, `read` the parent chunk first to discover its children. + diff --git a/packages/coding-agent/src/tools/chunk-tree.ts b/packages/coding-agent/src/tools/chunk-tree.ts index 2de784903..c95bd3cca 100644 --- a/packages/coding-agent/src/tools/chunk-tree.ts +++ b/packages/coding-agent/src/tools/chunk-tree.ts @@ -725,6 +725,7 @@ function resolveAnchorChunk(state: MutableChunkState, selector: string | undefin /** * Normalize a chunk selector from model output: + * - Strip `filename.ext:` prefix (model puts `path:chunk` format in `sel`) * - Strip leading `:` (model artifact) * - Strip trailing `#XXXX` hash suffix (model confuses checksum with path) * - Convert literal `"null"` string to empty @@ -733,10 +734,14 @@ function sanitizeChunkSelector(selector: string | undefined): string | undefined if (selector === undefined || selector === null) return undefined; let s = typeof selector === "string" ? selector : String(selector); if (s === "null" || s === "undefined") return undefined; + // Strip filename prefix: "pirate.ts:expression_2" -> "expression_2" + // Models sometimes put the path:chunk format in the sel field instead of path. + const colonIdx = chunkReadPathSeparatorIndex(s); + if (colonIdx !== -1) s = s.slice(colonIdx + 1); // Strip leading colon (":error" -> "error") if (s.startsWith(":")) s = s.slice(1); // Strip trailing #XXXX checksum suffix ("imports#YKXY" -> "imports") - s = s.replace(/#[A-Z]{4}$/, ""); + s = s.replace(/#[A-Z]{4}$/i, ""); return s.trim() || undefined; } @@ -754,11 +759,12 @@ function resolveAnchorWithCrc( * Normalize a CRC value from model output: * - Convert literal `"null"` / `"undefined"` strings to undefined * - Treat empty strings as undefined + * - Normalize to uppercase (models sometimes emit lowercase checksums) */ function sanitizeCrc(crc: string | undefined): string | undefined { if (crc === undefined || crc === null) return undefined; if (crc === "null" || crc === "undefined" || crc === "") return undefined; - return crc; + return crc.toUpperCase(); } function validateCrc(chunk: ChunkNode, crc: string | undefined): void { diff --git a/packages/coding-agent/test/tools/chunk-mode.test.ts b/packages/coding-agent/test/tools/chunk-mode.test.ts index c6b095627..4c21c41bd 100644 --- a/packages/coding-agent/test/tools/chunk-mode.test.ts +++ b/packages/coding-agent/test/tools/chunk-mode.test.ts @@ -208,14 +208,15 @@ describe("chunk mode tools", () => { crc: checksum, operations: [ { - op: "replace", - content: ` private handleError(err: Error): string { + replace: { + content: ` private handleError(err: Error): string { return \`normalized:\${err.message}\`; } `, + }, }, ], - }); + } as never); const editText = getText(editResult); const updatedSource = await Bun.file(filePath).text(); @@ -256,20 +257,22 @@ describe("chunk mode tools", () => { crc: checksum, operations: [ { - op: "splice", - beg: 63, - end: 63, - content: "return err.message.toUpperCase() + total;", + splice: { + beg: 63, + end: 63, + content: "return err.message.toUpperCase() + total;", + }, }, { - op: "splice", - crc: checksum2, - beg: 3, - end: 3, - content: "let total = 1;", + splice: { + crc: checksum2, + beg: 3, + end: 3, + content: "let total = 1;", + }, }, ], - }); + } as never); const updatedSource = await Bun.file(filePath).text(); expect(updatedSource).toContain("let total = 1;"); @@ -306,9 +309,10 @@ describe("chunk mode tools", () => { path: filePath, operations: [ { - op: "append_child", - sel: "class_Server", - content: 'status(): string {\n return "ok";\n}\n', + append_child: { + sel: "class_Server", + content: 'status(): string {\n return "ok";\n}\n', + }, }, ], } as never); @@ -350,20 +354,22 @@ describe("chunk mode tools", () => { path: filePath, operations: [ { - op: "splice", - sel: chunkPath, - crc: checksum, - beg: 4, - end: 3, - content: "const end = Date.now();", + splice: { + sel: chunkPath, + crc: checksum, + beg: 4, + end: 3, + content: "const end = Date.now();", + }, }, { - op: "splice", - sel: chunkPath, - crc: checksum2, - beg: 3, - end: 2, - content: "const start = Date.now();", + splice: { + sel: chunkPath, + crc: checksum2, + beg: 3, + end: 2, + content: "const start = Date.now();", + }, }, ], } as never); @@ -386,10 +392,11 @@ describe("chunk mode tools", () => { path: filePath, operations: [ { - op: "replace", - sel: "fn_main", - crc: checksum, - content: "", + replace: { + sel: "fn_main", + crc: checksum, + content: "", + }, }, ], }); @@ -427,12 +434,13 @@ describe("chunk mode tools", () => { path: filePath, operations: [ { - op: "splice", - sel: "class_Server.fn_handleError", - crc: checksum, - beg: 5, - end: 2, - content: " let total = 1;", + splice: { + sel: "class_Server.fn_handleError", + crc: checksum, + beg: 5, + end: 2, + content: " let total = 1;", + }, }, ], }), @@ -453,14 +461,16 @@ describe("chunk mode tools", () => { path: filePath, operations: [ { - op: "append_child", - sel: "class_Server", - content: ' status(): string {\n return "ok";\n }', + append_child: { + sel: "class_Server", + content: ' status(): string {\n return "ok";\n }', + }, }, { - op: "delete", - sel: "class_Server.fn_handleError", - crc: "ZZZZ", + delete: { + sel: "class_Server.fn_handleError", + crc: "ZZZZ", + }, }, ], }), @@ -482,10 +492,11 @@ describe("chunk mode tools", () => { path: filePath, operations: [ { - op: "replace", - sel: "class_Server.fn_handleError", - crc: checksum, - content: " private handleError(err: Error): string {\n if (err) {\n", + replace: { + sel: "class_Server.fn_handleError", + crc: checksum, + content: " private handleError(err: Error): string {\n if (err) {\n", + }, }, ], }), @@ -507,18 +518,20 @@ describe("chunk mode tools", () => { path: filePath, operations: [ { - op: "replace", - sel: "class_Server.fn_handleError", - crc: checksum, - content: " private handleError(err: Error): string {\n return err.message;\n }", + replace: { + sel: "class_Server.fn_handleError", + crc: checksum, + content: " private handleError(err: Error): string {\n return err.message;\n }", + }, }, { - op: "splice", - sel: "class_Server.fn_handleError", - crc: checksum, - beg: 63, - end: 63, - content: " return err.message.toUpperCase();", + splice: { + sel: "class_Server.fn_handleError", + crc: checksum, + beg: 63, + end: 63, + content: " return err.message.toUpperCase();", + }, }, ], }), @@ -541,13 +554,14 @@ describe("chunk mode tools", () => { path: filePath, operations: [ { - op: "splice", - sel: "class_Server.fn_handleError", - // no crc! - beg: 3, - end: 3, - content: " let total = 1;", - }, + splice: { + sel: "class_Server.fn_handleError", + // no crc! + beg: 3, + end: 3, + content: " let total = 1;", + }, + } as never, ], }), ).rejects.toThrow(/Checksum required/); @@ -567,10 +581,11 @@ describe("chunk mode tools", () => { path: filePath, operations: [ { - op: "replace", - sel: "main", - crc: checksum, - content: 'function main(): void {\n console.log("started");\n}\n', + replace: { + sel: "main", + crc: checksum, + content: 'function main(): void {\n console.log("started");\n}\n', + }, }, ], });