Files
vscode-cowriting-plugin/test/liveTurn.test.ts
T
2026-06-26 04:33:40 -07:00

83 lines
3.1 KiB
TypeScript

import { describe, it, expect, vi } from "vitest";
import { extractReplacement, runEditTurn } from "../src/liveTurn";
// A fake Agent that replays a scripted event list to subscribers, supports abort,
// and resolves agent.run() with a completed (or aborted) result.
function fakeSdk(events: any[], opts?: { abortAware?: boolean }) {
return {
Agent: class {
private listeners: ((e: any) => void)[] = [];
private aborted = false;
constructor(_cfg: unknown) {}
subscribe(fn: (e: any) => void) {
this.listeners.push(fn);
return () => {
this.listeners = this.listeners.filter((l) => l !== fn);
};
}
abort() {
this.aborted = true;
}
async run(_input: string) {
for (const e of events) for (const l of this.listeners) l(e);
if (opts?.abortAware && this.aborted) {
return { status: "aborted", outputText: "", runId: "r1", error: { message: "aborted" } };
}
return { status: "completed", outputText: "EDITED", runId: "r1" };
}
},
};
}
describe("extractReplacement", () => {
it("returns plain text untouched", () => {
expect(extractReplacement("Hello world.", "x")).toBe("Hello world.");
});
it("strips a wrapping fence", () => {
expect(extractReplacement("```\nHello world.\n```", "x")).toBe("Hello world.");
});
it("strips a language-tagged fence", () => {
expect(extractReplacement("```markdown\nHello.\n```\n", "x")).toBe("Hello.");
});
it("leaves inner fences alone", () => {
const inner = "before\n```js\ncode\n```\nafter";
expect(extractReplacement(inner, "x")).toBe(inner);
});
it("keeps the fence when the original selection was itself fence-wrapped", () => {
const sel = "```python\nold()\n```";
const out = "```python\nnew()\n```";
expect(extractReplacement(out, sel)).toBe(out);
});
it("still strips when the selection was plain but the model added a fence", () => {
expect(extractReplacement("```\nplain\n```", "plain old text")).toBe("plain");
});
});
describe("runEditTurn progress + cancel", () => {
it("emits progress snapshots and returns the replacement unchanged (INV-44)", async () => {
vi.resetModules();
vi.doMock("@cline/sdk", () =>
fakeSdk([
{ type: "assistant-text-delta", text: "ED", accumulatedText: "ED" },
{ type: "usage-updated", usage: { inputTokens: 10, outputTokens: 5, cacheReadTokens: 0, cacheWriteTokens: 0 } },
]),
);
const seen: string[] = [];
const turn = await runEditTurn("do it", "old", { onProgress: (s) => seen.push(s.phase) });
expect(turn.replacement).toBe("EDITED");
expect(seen).toContain("writing");
vi.doUnmock("@cline/sdk");
vi.resetModules();
});
it("a fired AbortSignal aborts the agent and the turn throws (INV-47)", async () => {
vi.resetModules();
vi.doMock("@cline/sdk", () => fakeSdk([], { abortAware: true }));
const ac = new AbortController();
ac.abort();
await expect(runEditTurn("do it", "old", { signal: ac.signal })).rejects.toThrow(/aborted/);
vi.doUnmock("@cline/sdk");
vi.resetModules();
});
});