f6eb39eb73
- switch takes changes: carry (plain git switch, 409 ECARRYCONFLICT with files) or shelve (shelf first, re-applied if the switch fails) - branch-create switch: true runs git switch -c, the changes come along - shelve / unshelve / shelf-drop + GET /git/shelves: only agenthub-shelf: stash entries, addressed by sha, repo root only, never mid-operation; a clean restore drops the shelf, a conflicted one keeps it - 409 EAGENTRUNNING until confirm: true while the workspace's agent is working - git bar: Bring my changes / Shelve and switch choice, Create & switch, restore offer after a switch; Changes panel: Shelve (n) and the Shelves list - fix: after a switch the Changes panel showed the old branch — the post-write re-read joined an in-flight GET started before the write (request() dedupe); writes now close in-flight GETs, and every git write fires agenthub:git-changed so every view of the workspace re-reads
91 lines
3.2 KiB
TypeScript
91 lines
3.2 KiB
TypeScript
/**
|
|
* @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ĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
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" },
|
|
]);
|
|
});
|
|
});
|