fix(coding-agent): tighten btw branchability

This commit is contained in:
KamijoToma
2026-06-18 02:45:03 +08:00
parent 2bbb16c49b
commit 0babeec0ef
3 changed files with 23 additions and 9 deletions
@@ -53,6 +53,7 @@ export class BtwController {
this.#lastQuestion !== undefined &&
this.#lastReplyText !== undefined &&
this.#lastAssistantMessage !== undefined &&
this.#lastLeafId !== null &&
this.#lastLeafId === this.ctx.sessionManager.getLeafId()
);
}
@@ -226,7 +226,7 @@ describe("AgentSession.branchFromBtw", () => {
activeSession.sessionManager.appendMessage({ role: "user", content: "seed", timestamp: Date.now() });
await activeSession.sessionManager.flush();
const abortController = new AbortController();
const execution = new Promise<void>(() => {});
const execution = Promise.withResolvers<void>().promise;
activeSession.trackEvalExecution(execution, abortController).catch(() => undefined);
expect(activeSession.isEvalRunning).toBe(true);
@@ -112,10 +112,8 @@ describe("BtwController", () => {
it("replaces a previous request by aborting it before issuing the next runEphemeralTurn", async () => {
const signals: AbortSignal[] = [];
let firstRelease!: () => void;
const firstPromise = new Promise<RunEphemeralTurnResult>(resolve => {
firstRelease = () => resolve({ replyText: "first", assistantMessage: createAssistantMessage("first") });
});
const first = Promise.withResolvers<RunEphemeralTurnResult>();
const firstPromise = first.promise;
const runEphemeralTurn = vi
.fn<(args: RunEphemeralTurnArgs) => Promise<RunEphemeralTurnResult>>()
.mockImplementationOnce(async args => {
@@ -141,11 +139,11 @@ describe("BtwController", () => {
expect(signals[1]?.aborted).toBe(false);
expect(btwContainer.children).toHaveLength(1);
// Allow the orphaned first request to finish to keep the test clean.
firstRelease();
first.resolve({ replyText: "first", assistantMessage: createAssistantMessage("first") });
});
it("clears the panel when the active request is dismissed via Escape", async () => {
const runEphemeralTurn = vi.fn(async () => new Promise<RunEphemeralTurnResult>(() => {}));
const runEphemeralTurn = vi.fn(async () => Promise.withResolvers<RunEphemeralTurnResult>().promise);
const btwContainer = new Container();
const ctx = makeCtx(makeFakeSession(runEphemeralTurn), btwContainer);
const controller = new BtwController(ctx);
@@ -185,7 +183,7 @@ describe("BtwController", () => {
});
it("does not allow branch while /btw is still running", async () => {
const runEphemeralTurn = vi.fn(async () => new Promise<RunEphemeralTurnResult>(() => {}));
const runEphemeralTurn = vi.fn(async () => Promise.withResolvers<RunEphemeralTurnResult>().promise);
const ctx = makeCtx(makeFakeSession(runEphemeralTurn));
const controller = new BtwController(ctx);
@@ -194,6 +192,21 @@ describe("BtwController", () => {
expect(controller.canBranch()).toBe(false);
});
it("does not allow branch when the completed answer has no originating leaf", async () => {
const assistantMessage = createAssistantMessage("Answer");
const runEphemeralTurn = vi.fn(async () => ({ replyText: "Answer", assistantMessage }));
const ctx = makeCtx(makeFakeSession(runEphemeralTurn)) as InteractiveModeContext & {
setTestLeafId(nextLeafId: string | null): void;
};
ctx.setTestLeafId(null);
const controller = new BtwController(ctx);
await controller.start("Question?");
await drainBtwRequest();
expect(controller.canBranch()).toBe(false);
});
it("allows branch after a complete non-empty reply", async () => {
const assistantMessage = createAssistantMessage("Answer");
const runEphemeralTurn = vi.fn(async () => ({ replyText: "Answer", assistantMessage }));
@@ -221,7 +234,7 @@ describe("BtwController", () => {
});
it("does not allow branch after aborted or errored requests", async () => {
const abortedRun = vi.fn(async () => new Promise<RunEphemeralTurnResult>(() => {}));
const abortedRun = vi.fn(async () => Promise.withResolvers<RunEphemeralTurnResult>().promise);
const abortedController = new BtwController(makeCtx(makeFakeSession(abortedRun)));
await abortedController.start("Question?");
expect(abortedController.handleEscape()).toBe(true);