binhong87_obsidian-note-agent/tests/agent/approval-queue.test.ts
Bin-Home 83ffaf38bc feat: non-blocking edit mode with batched diff review
- Agent loop no longer awaits per-write approval; queues pending writes
  and returns { status: "queued" } to the LLM so multi-step edits finish
  in one turn. Diffs render inline under the assistant message that
  produced them, with per-block Approve/Reject and a bulk Apply All /
  Reject All footer.
- ApprovalQueue rewritten as a synchronous pending-writes store with an
  injected commit callback; clear() drops unresolved writes on view
  close or new conversation; scheduled (no-UI) runs auto-approveAll
  after the loop drains.
- history-trimmer keeps assistant-with-tool_calls + its tool responses
  as one atomic group so the provider never sees a tool message without
  its preceding tool_calls (fixes DeepSeek 400 on large tool bursts).
- Add pulsing thinking-dots indicator in MessageList whenever the turn
  is busy and streamBuf is empty, covering network latency, tool-call
  argument accumulation, and inter-iteration gaps.
- Drop [openai] console logs (errors/warns retained); remove superfluous
  aria-labels and title tooltips from chat UI.
- Add CLAUDE.md.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
2026-04-23 21:10:51 +08:00

62 lines
2.5 KiB
TypeScript

import { describe, it, expect, vi } from "vitest";
import { ApprovalQueue } from "../../src/agent/approval-queue";
describe("ApprovalQueue", () => {
it("enqueue is synchronous, stores entry, fires onChange", () => {
const commit = vi.fn(async () => {});
const q = new ApprovalQueue({ commit });
const changes: number[] = [];
q.onChange(list => changes.push(list.length));
q.enqueue({ toolCallId: "1", tool: "edit_note", args: { path: "a.md" }, diff: "" });
expect(q.list()).toHaveLength(1);
expect(changes).toEqual([1]);
expect(commit).not.toHaveBeenCalled();
});
it("approve calls commit, removes entry, fires onChange", async () => {
const commit = vi.fn(async () => {});
const q = new ApprovalQueue({ commit });
q.enqueue({ toolCallId: "1", tool: "edit_note", args: { path: "a.md" }, diff: "" });
await q.approve("1");
expect(commit).toHaveBeenCalledWith(expect.objectContaining({ toolCallId: "1" }));
expect(q.list()).toHaveLength(0);
});
it("reject removes entry without calling commit", () => {
const commit = vi.fn(async () => {});
const q = new ApprovalQueue({ commit });
q.enqueue({ toolCallId: "2", tool: "edit_note", args: {}, diff: "" });
q.reject("2");
expect(commit).not.toHaveBeenCalled();
expect(q.list()).toHaveLength(0);
});
it("approveAll drains queue and calls commit for each entry", async () => {
const commit = vi.fn(async () => {});
const q = new ApprovalQueue({ commit });
q.enqueue({ toolCallId: "a", tool: "edit_note", args: {}, diff: "" });
q.enqueue({ toolCallId: "b", tool: "create_note", args: {}, diff: "" });
await q.approveAll();
expect(commit).toHaveBeenCalledTimes(2);
expect(q.list()).toHaveLength(0);
});
it("rejectAll drains queue without calling commit", () => {
const commit = vi.fn(async () => {});
const q = new ApprovalQueue({ commit });
q.enqueue({ toolCallId: "a", tool: "edit_note", args: {}, diff: "" });
q.enqueue({ toolCallId: "b", tool: "create_note", args: {}, diff: "" });
q.rejectAll();
expect(commit).not.toHaveBeenCalled();
expect(q.list()).toHaveLength(0);
});
it("clear drops all entries without calling commit", () => {
const commit = vi.fn(async () => {});
const q = new ApprovalQueue({ commit });
q.enqueue({ toolCallId: "x", tool: "edit_note", args: {}, diff: "" });
q.clear();
expect(commit).not.toHaveBeenCalled();
expect(q.list()).toHaveLength(0);
});
});