02ac02cd66
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
149 lines
5.3 KiB
TypeScript
149 lines
5.3 KiB
TypeScript
import { describe, it, expect, beforeEach } from "vitest";
|
|
import { create } from "zustand";
|
|
import { immer } from "zustand/middleware/immer";
|
|
import { createSessionRuntimeSlice } from "./session-runtime-slice";
|
|
import type { SessionCommit, SessionRuntimeSlice } from "./types";
|
|
|
|
function makeStore() {
|
|
return create<SessionRuntimeSlice>()(
|
|
immer((set, get, store) => createSessionRuntimeSlice(set, get, store)),
|
|
);
|
|
}
|
|
|
|
const SESSION = "sess-1";
|
|
|
|
function commit(overrides: Partial<SessionCommit>): SessionCommit {
|
|
return {
|
|
id: "id",
|
|
session_id: SESSION,
|
|
commit_sha: "sha",
|
|
parent_sha: "parent",
|
|
commit_message: "msg",
|
|
author_name: "test",
|
|
author_email: "test@test",
|
|
files_changed: 0,
|
|
insertions: 0,
|
|
deletions: 0,
|
|
committed_at: "2026-05-28T00:00:00Z",
|
|
created_at: "2026-05-28T00:00:00Z",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("bumpSessionCommitsRefetch", () => {
|
|
let useStore: ReturnType<typeof makeStore>;
|
|
|
|
beforeEach(() => {
|
|
useStore = makeStore();
|
|
});
|
|
|
|
it("increments the trigger counter for the session's env key", () => {
|
|
useStore.getState().bumpSessionCommitsRefetch(SESSION);
|
|
expect(useStore.getState().sessionCommits.refetchTrigger[SESSION]).toBe(1);
|
|
|
|
useStore.getState().bumpSessionCommitsRefetch(SESSION);
|
|
expect(useStore.getState().sessionCommits.refetchTrigger[SESSION]).toBe(2);
|
|
});
|
|
|
|
it("does NOT touch byEnvironmentId — visible commits stay during refetch", () => {
|
|
// Stale-while-revalidate is the whole point: the WS handler bumps so the
|
|
// hook refetches, but the panel keeps showing the previous commits until
|
|
// the new list arrives. If a bump cleared the list, the panel would
|
|
// briefly fall through to its empty state.
|
|
const existing = [commit({ commit_sha: "abc" }), commit({ commit_sha: "def" })];
|
|
useStore.getState().setSessionCommits(SESSION, existing);
|
|
|
|
useStore.getState().bumpSessionCommitsRefetch(SESSION);
|
|
|
|
expect(useStore.getState().sessionCommits.byEnvironmentId[SESSION]).toEqual(existing);
|
|
});
|
|
|
|
it("resolves through environmentIdBySessionId so co-environment sessions share a trigger", () => {
|
|
// sess-1 and sess-2 share env-foo; the trigger lives under env-foo so
|
|
// either session bumping it refetches both.
|
|
useStore.getState().registerSessionEnvironment(SESSION, "env-foo");
|
|
useStore.getState().registerSessionEnvironment("sess-2", "env-foo");
|
|
|
|
useStore.getState().bumpSessionCommitsRefetch(SESSION);
|
|
|
|
const triggers = useStore.getState().sessionCommits.refetchTrigger;
|
|
expect(triggers["env-foo"]).toBe(1);
|
|
expect(triggers[SESSION]).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("setSessionCommits — empty-response guard", () => {
|
|
let useStore: ReturnType<typeof makeStore>;
|
|
|
|
beforeEach(() => {
|
|
useStore = makeStore();
|
|
});
|
|
|
|
it("rejects [] over a populated list by default (race protection)", () => {
|
|
// The default guard protects against a stale fetch response landing
|
|
// after incremental commit_created notifications already populated the
|
|
// store. Without it, an in-flight initial fetch could clobber the live
|
|
// list with [].
|
|
useStore.getState().setSessionCommits(SESSION, [commit({ commit_sha: "abc" })]);
|
|
|
|
useStore.getState().setSessionCommits(SESSION, []);
|
|
|
|
const after = useStore.getState().sessionCommits.byEnvironmentId[SESSION];
|
|
expect(after).toHaveLength(1);
|
|
expect(after[0].commit_sha).toBe("abc");
|
|
});
|
|
|
|
it("accepts [] when allowEmpty:true (authoritative response after reset)", () => {
|
|
// After commits_reset/branch_switched, a refetch can legitimately return
|
|
// []. The caller opts into the empty-accepting path so the panel stops
|
|
// showing the pre-reset list.
|
|
useStore.getState().setSessionCommits(SESSION, [commit({ commit_sha: "abc" })]);
|
|
|
|
useStore.getState().setSessionCommits(SESSION, [], { allowEmpty: true });
|
|
|
|
expect(useStore.getState().sessionCommits.byEnvironmentId[SESSION]).toEqual([]);
|
|
});
|
|
|
|
it("writes the populated list regardless of allowEmpty", () => {
|
|
// Sanity: allowEmpty only changes behaviour for empty arrays. A populated
|
|
// list always overwrites.
|
|
useStore.getState().setSessionCommits(SESSION, [commit({ commit_sha: "old" })]);
|
|
|
|
useStore.getState().setSessionCommits(SESSION, [commit({ commit_sha: "new" })]);
|
|
|
|
const after = useStore.getState().sessionCommits.byEnvironmentId[SESSION];
|
|
expect(after).toHaveLength(1);
|
|
expect(after[0].commit_sha).toBe("new");
|
|
});
|
|
});
|
|
|
|
describe("addSessionCommit — duplicate notifications", () => {
|
|
let useStore: ReturnType<typeof makeStore>;
|
|
|
|
beforeEach(() => {
|
|
useStore = makeStore();
|
|
});
|
|
|
|
it("updates a commit from a live notification without duplicating a fetched snapshot", () => {
|
|
useStore
|
|
.getState()
|
|
.setSessionCommits(SESSION, [
|
|
commit({ commit_sha: "head", parent_sha: "middle" }),
|
|
commit({ commit_sha: "middle", parent_sha: "base", insertions: 1, pushed: true }),
|
|
]);
|
|
|
|
useStore
|
|
.getState()
|
|
.addSessionCommit(
|
|
SESSION,
|
|
commit({ commit_sha: "middle", parent_sha: "base", insertions: 7 }),
|
|
);
|
|
|
|
const commits = useStore.getState().sessionCommits.byEnvironmentId[SESSION];
|
|
expect(commits).toHaveLength(2);
|
|
expect(commits?.map(({ commit_sha }) => commit_sha)).toEqual(["head", "middle"]);
|
|
expect(commits?.[1].insertions).toBe(7);
|
|
expect(commits?.[1].pushed).toBe(true);
|
|
});
|
|
});
|