122 lines
4.8 KiB
TypeScript
122 lines
4.8 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.
|
|
const runs = { count: 0 };
|
|
function fakeSdk(events: any[]) {
|
|
return {
|
|
Agent: class {
|
|
private listeners: ((e: any) => void)[] = [];
|
|
constructor(_cfg: unknown) {}
|
|
subscribe(fn: (e: any) => void) {
|
|
this.listeners.push(fn);
|
|
return () => {
|
|
this.listeners = this.listeners.filter((l) => l !== fn);
|
|
};
|
|
}
|
|
// The real SDK's abort() is a no-op before run() creates its AbortController,
|
|
// so this fake does NOT cooperate with a pre-abort — proving runEditTurn's own
|
|
// short-circuit, not the fake's leniency.
|
|
abort() {}
|
|
async run(_input: string) {
|
|
runs.count += 1;
|
|
for (const e of events) for (const l of this.listeners) l(e);
|
|
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 macOS automation prompt (#59)", () => {
|
|
// The claude-code provider spawns the bundled `claude` CLI, which on macOS
|
|
// performs IDE auto-connect when launched inside VS Code — sending Apple Events
|
|
// that raise the "control other applications" prompt. The extension only
|
|
// consumes the text result and never needs that connection, so we disable it.
|
|
it("spawns the agent with IDE auto-connect + auto-install disabled, preserving the rest of the environment", async () => {
|
|
vi.resetModules();
|
|
const cfgs: any[] = [];
|
|
vi.doMock("@cline/sdk", () => ({
|
|
Agent: class {
|
|
constructor(cfg: any) {
|
|
cfgs.push(cfg);
|
|
}
|
|
subscribe() {
|
|
return () => {};
|
|
}
|
|
abort() {}
|
|
async run() {
|
|
return { status: "completed", outputText: "X", runId: "r" };
|
|
}
|
|
},
|
|
}));
|
|
process.env.__WGL_TEST_SENTINEL__ = "keep-me";
|
|
await runEditTurn("do it", "old");
|
|
const cfg = cfgs[0];
|
|
// mK("0") === true in the claude binary → hard-disables IDE auto-connect,
|
|
// so no osascript / Apple Event fires.
|
|
expect(cfg.options?.env?.CLAUDE_CODE_AUTO_CONNECT_IDE).toBe("0");
|
|
// Belt-and-suspenders: skip the IDE-extension auto-install (deep-link path).
|
|
expect(cfg.options?.env?.CLAUDE_CODE_IDE_SKIP_AUTO_INSTALL).toBe("1");
|
|
// The full process environment must survive (login under HOME, proxy/CA vars).
|
|
expect(cfg.options?.env?.__WGL_TEST_SENTINEL__).toBe("keep-me");
|
|
delete process.env.__WGL_TEST_SENTINEL__;
|
|
vi.doUnmock("@cline/sdk");
|
|
vi.resetModules();
|
|
});
|
|
});
|
|
|
|
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 pre-aborted AbortSignal short-circuits before run() and throws (INV-47)", async () => {
|
|
vi.resetModules();
|
|
vi.doMock("@cline/sdk", () => fakeSdk([]));
|
|
runs.count = 0;
|
|
const ac = new AbortController();
|
|
ac.abort();
|
|
await expect(runEditTurn("do it", "old", { signal: ac.signal })).rejects.toThrow(/aborted/);
|
|
expect(runs.count).toBe(0); // the turn never ran — no proposal could be produced
|
|
vi.doUnmock("@cline/sdk");
|
|
vi.resetModules();
|
|
});
|
|
});
|