/** * @file Pins the cause of "after Switch branch the Changes panel still shows * the old branch": `request()` shares one in-flight GET between callers, and * the panel's re-read after a switch joined a poll that had STARTED before * the switch finished — so it got the old branch's answer. A write now * closes every in-flight GET to new joiners (before it is sent and once it * settles), and `gitAction` announces itself (`agenthub:git-changed`) so the * views that never hear the bar's callback read again too. * @author Nguyễn Ngọc Trí Vĩ */ import { describe, it, expect, vi, afterEach } from "vitest"; import { api } from "../api"; import { GIT_CHANGED_EVENT } from "../gitChanged"; afterEach(() => { vi.unstubAllGlobals(); }); function json(body: unknown) { return { ok: true, status: 200, headers: { get: () => "application/json" }, json: async () => body, }; } describe("a git write and the reads around it", () => { it("a read asked after a switch never joins a read started before it", async () => { let releaseOld: (v: unknown) => void = () => {}; const spy = vi.fn((_url: string, init?: RequestInit) => { if (init?.method === "POST") return Promise.resolve(json({ ok: true, action: "switch" })); // The first GET (a poll) hangs until released, answering the OLD branch. if (spy.mock.calls.filter((c) => !c[1]?.method).length === 1) { return new Promise((r) => { releaseOld = () => r(json({ branch: "old" })); }); } return Promise.resolve(json({ branch: "new" })); }); vi.stubGlobal("fetch", spy); const poll = api.lanes.git(3); await api.lanes.gitAction(3, "switch", { branch: "new" }); const after = api.lanes.git(3); releaseOld(undefined); expect(await poll).toEqual({ branch: "old" }); expect(await after).toEqual({ branch: "new" }); expect(spy.mock.calls.filter((c) => !c[1]?.method)).toHaveLength(2); }); it("still shares a read between callers when no write came between them", async () => { const spy = vi.fn(() => Promise.resolve(json({ branch: "x" }))); vi.stubGlobal("fetch", spy); await Promise.all([api.lanes.git(4), api.lanes.git(4)]); expect(spy).toHaveBeenCalledTimes(1); }); it("announces every git write for its workspace, a failed one included", async () => { const seen: unknown[] = []; const on = (e: Event) => seen.push((e as CustomEvent).detail); window.addEventListener(GIT_CHANGED_EVENT, on); try { vi.stubGlobal( "fetch", vi.fn(() => Promise.resolve(json({ ok: true }))) ); await api.lanes.gitAction({ lane: 5, session: "s" }, "shelve", {}); vi.stubGlobal( "fetch", vi.fn(() => Promise.resolve({ ok: false, status: 409, headers: { get: () => "application/json" }, json: async () => ({ error: { code: "ESHELVED", message: "x" } }), }) ) ); await expect(api.lanes.gitAction(5, "switch", { branch: "b" })).rejects.toThrow(); } finally { window.removeEventListener(GIT_CHANGED_EVENT, on); } expect(seen).toEqual([ { laneId: 5, action: "shelve" }, { laneId: 5, action: "switch" }, ]); }); });