Files
claude-agent-acp/src/tests/diff-statistics.test.ts
nikita-ashihmin 735950a83e perf: supply AIR diff counts from Claude structured patches (#1122)
Claude supplies structured patches for Edit and Write, but ACP diff
blocks previously carried only the reconstructed texts. AIR had to
compare those texts again to display added and removed counts.

Count patch operations in the existing conversion loop and publish
`_meta.jetbrains.air.diffStats` on each diff block. This uses the same
contract as https://github.com/agentclientprotocol/codex-acp/pull/501.
Each block has its own counts. No extra traversal, full file comparison,
or navigation calculation is needed.

Omit statistics when patches are absent or inconsistent, so clients
retain their existing fallback. Exclude EOF markers from the displayed
texts. Document the extension and cover per-block counts, Edit and Write
hooks, EOF changes, and fallback behavior.

Validation:
- `npm run build`
- `npm run check`
- `env -u ANTHROPIC_BASE_URL npm run test:run` (the local endpoint
override affects unrelated provider tests)
2026-09-15 12:39:39 +04:00

132 lines
4.1 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { toolUpdateFromDiffToolResponse } from "../diff.js";
describe("diff statistics", () => {
it("counts each block independently without reading full file content", () => {
const result = toolUpdateFromDiffToolResponse({
filePath: "/file.ts",
get originalFile() {
throw new Error("The patch already contains the changes");
},
get content() {
throw new Error("The patch already contains the changes");
},
structuredPatch: [
{
oldStart: 1,
oldLines: 3,
newStart: 1,
newLines: 2,
lines: [" context", "-first", "-second", "+replacement"],
},
{
oldStart: 1_000_000,
oldLines: 0,
newStart: 1_000_000,
newLines: 2,
lines: ["+first", "+second"],
},
],
});
expect(result.content?.map((block) => block._meta)).toEqual([
{ jetbrains: { air: { version: 1, diffStats: { version: 1, added: 1, removed: 2 } } } },
{ jetbrains: { air: { version: 1, diffStats: { version: 1, added: 2, removed: 0 } } } },
]);
});
it("counts patch operations when an EOF change produces equal displayed texts", () => {
const result = toolUpdateFromDiffToolResponse({
filePath: "/file.ts",
structuredPatch: [
{
oldStart: 1,
oldLines: 1,
newStart: 1,
newLines: 1,
lines: ["-same", "\\ No newline at end of file", "+same"],
},
],
});
expect(result.content).toEqual([
{
type: "diff",
path: "/file.ts",
oldText: "same",
newText: "same",
_meta: {
jetbrains: { air: { version: 1, diffStats: { version: 1, added: 1, removed: 1 } } },
},
},
]);
});
it("publishes zero counts for a block with only context", () => {
const result = toolUpdateFromDiffToolResponse({
filePath: "/file.ts",
structuredPatch: [
{ oldStart: 1, oldLines: 1, newStart: 1, newLines: 1, lines: [" unchanged"] },
],
});
expect(result.content?.[0]._meta).toEqual({
jetbrains: { air: { version: 1, diffStats: { version: 1, added: 0, removed: 0 } } },
});
});
it.each([
{ oldLines: 2, newLines: 1, lines: ["-old", "+new"] },
{ oldLines: 1, newLines: 2, lines: ["-old", "+new"] },
{ oldLines: 1, newLines: 1, lines: ["?unsupported"] },
{ oldLines: "1", newLines: 1, lines: ["-old", "+new"] },
{ oldStart: -1, oldLines: 1, newLines: 1, lines: ["-old", "+new"] },
{ newStart: Number.NaN, oldLines: 1, newLines: 1, lines: ["-old", "+new"] },
{
oldLines: 1,
newLines: 1,
lines: ["\\ No newline at end of file", "-old", "+new"],
},
])("omits statistics for an inconsistent patch: %j", (hunk) => {
const result = toolUpdateFromDiffToolResponse({
filePath: "/file.ts",
structuredPatch: [{ oldStart: 1, newStart: 1, ...hunk }],
});
expect(result.content).toHaveLength(1);
expect(result.content?.[0]).not.toHaveProperty("_meta");
});
it("omits statistics once hunk coordinates stop increasing", () => {
const result = toolUpdateFromDiffToolResponse({
filePath: "/file.ts",
structuredPatch: [
{ oldStart: 10, oldLines: 1, newStart: 10, newLines: 1, lines: ["-old", "+new"] },
{ oldStart: 5, oldLines: 1, newStart: 5, newLines: 1, lines: ["-old", "+new"] },
],
});
expect(result.content?.[0]).toHaveProperty("_meta");
expect(result.content?.[1]).not.toHaveProperty("_meta");
});
it("omits statistics when later hunk coordinates ignore the accumulated line delta", () => {
const result = toolUpdateFromDiffToolResponse({
filePath: "/file.ts",
structuredPatch: [
{
oldStart: 10,
oldLines: 1,
newStart: 10,
newLines: 2,
lines: ["-old", "+new", "+extra"],
},
{ oldStart: 20, oldLines: 1, newStart: 20, newLines: 1, lines: ["-old", "+new"] },
],
});
expect(result.content?.[0]).toHaveProperty("_meta");
expect(result.content?.[1]).not.toHaveProperty("_meta");
});
});