Files
kandev/apps/web/lib/state/slices/session-runtime/migrate-env-keyed-data.test.ts
T
2026-05-17 20:16:15 +01:00

301 lines
11 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
import { createSessionRuntimeSlice } from "./session-runtime-slice";
import { createSessionSlice } from "../session/session-slice";
import type { SessionCommit, SessionRuntimeSlice } from "./types";
import type { SessionSlice } from "../session/types";
import { sessionId as toSessionId, taskId as toTaskId } from "@/lib/types/ids";
function makeStore() {
return create<SessionRuntimeSlice>()(immer(createSessionRuntimeSlice));
}
type CombinedSlice = SessionRuntimeSlice & SessionSlice;
function makeCombinedStore() {
return create<CombinedSlice>()(
immer((set) => ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...(createSessionSlice as any)(set),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...(createSessionRuntimeSlice as any)(set),
})),
);
}
function commit(sha: string): SessionCommit {
return {
id: `commit-${sha}`,
session_id: "sess-1",
commit_sha: sha,
parent_sha: `parent-${sha}`,
author_name: "Test User",
author_email: "test@example.com",
commit_message: `Commit ${sha}`,
committed_at: "2026-05-09T00:00:00Z",
files_changed: 1,
insertions: 1,
deletions: 0,
created_at: "2026-05-09T00:00:00Z",
};
}
describe("registerSessionEnvironment — migrateEnvKeyedData", () => {
it("setUserShells writes directly under the environmentId key (no session translation)", () => {
const store = makeStore();
store.getState().setUserShells("env-1", [{ terminalId: "t1" } as never]);
const state = store.getState();
expect(state.userShells.byEnvironmentId["env-1"]).toEqual([{ terminalId: "t1" }]);
expect(state.userShells.loaded["env-1"]).toBe(true);
});
it("setUserShells with empty environmentId is a no-op", () => {
const store = makeStore();
store.getState().setUserShells("", [{ terminalId: "t1" } as never]);
const state = store.getState();
expect(state.userShells.byEnvironmentId[""]).toBeUndefined();
});
it("migrates data from sessionId key to environmentId key", () => {
const store = makeStore();
// Simulate data arriving under the fallback sessionId key
store.getState().setSessionCommits("sess-1", [{ commit_sha: "abc" } as never]);
store.getState().setGitStatus("sess-1", { branch: "main" } as never);
store.getState().appendShellOutput("sess-1", "hello");
// Register the environment mapping
store.getState().registerSessionEnvironment("sess-1", "env-1");
const state = store.getState();
// Data should now live under the environmentId key
expect(state.sessionCommits.byEnvironmentId["env-1"]).toEqual([{ commit_sha: "abc" }]);
expect(state.gitStatus.byEnvironmentId["env-1"]).toEqual({ branch: "main" });
expect(state.shell.outputs["env-1"]).toBe("hello");
// sessionId key should be cleaned up
expect(state.sessionCommits.byEnvironmentId["sess-1"]).toBeUndefined();
expect(state.gitStatus.byEnvironmentId["sess-1"]).toBeUndefined();
expect(state.shell.outputs["sess-1"]).toBeUndefined();
});
it("does not clobber pre-existing environmentId data", () => {
const store = makeStore();
// Data already stored under the environmentId key
store.getState().registerSessionEnvironment("other-sess", "env-1");
store.getState().setSessionCommits("other-sess", [{ commit_sha: "existing" } as never]);
// Stale data under sessionId from before mapping was known
store.getState().setGitStatus("sess-2", { branch: "stale" } as never);
// A different session wrote commits under its own fallback key
store.setState((draft) => {
draft.sessionCommits.byEnvironmentId["sess-2"] = [{ commit_sha: "orphan" } as never];
});
// Register: sess-2 maps to env-1 (which already has commit data)
store.getState().registerSessionEnvironment("sess-2", "env-1");
const state = store.getState();
// Commits: pre-existing env-1 data preserved, orphaned sess-2 key deleted
expect(state.sessionCommits.byEnvironmentId["env-1"]).toEqual([{ commit_sha: "existing" }]);
expect(state.sessionCommits.byEnvironmentId["sess-2"]).toBeUndefined();
// Git status: no env-1 data existed, so sess-2 data migrated
expect(state.gitStatus.byEnvironmentId["env-1"]).toEqual({ branch: "stale" });
expect(state.gitStatus.byEnvironmentId["sess-2"]).toBeUndefined();
});
it("no-ops when sessionId equals environmentId", () => {
const store = makeStore();
store.getState().setSessionCommits("local-1", [{ commit_sha: "abc" } as never]);
store.getState().registerSessionEnvironment("local-1", "local-1");
const state = store.getState();
// Data stays under the same key
expect(state.sessionCommits.byEnvironmentId["local-1"]).toEqual([{ commit_sha: "abc" }]);
});
it("migrates only sub-keys that have data", () => {
const store = makeStore();
// Only put data in shell outputs and userShells, leave others empty
store.getState().appendShellOutput("sess-3", "output");
store.setState((draft) => {
draft.userShells.byEnvironmentId["sess-3"] = [{ terminalId: "t1" } as never];
});
store.getState().registerSessionEnvironment("sess-3", "env-3");
const state = store.getState();
expect(state.shell.outputs["env-3"]).toBe("output");
expect(state.shell.outputs["sess-3"]).toBeUndefined();
expect(state.userShells.byEnvironmentId["env-3"]).toEqual([{ terminalId: "t1" }]);
expect(state.userShells.byEnvironmentId["sess-3"]).toBeUndefined();
// Other stores should have no data for either key
expect(state.sessionCommits.byEnvironmentId["env-3"]).toBeUndefined();
expect(state.sessionCommits.byEnvironmentId["sess-3"]).toBeUndefined();
});
});
describe("setGitStatus", () => {
it("updates cached status when only branch diff totals change", () => {
const store = makeStore();
store.getState().setGitStatus("sess-1", {
branch: "feature",
remote_branch: "",
ahead: 0,
behind: 0,
files: {},
timestamp: "same-timestamp",
branch_additions: 0,
branch_deletions: 0,
} as never);
store.getState().setGitStatus("sess-1", {
branch: "feature",
remote_branch: "",
ahead: 0,
behind: 0,
files: {},
timestamp: "same-timestamp",
branch_additions: 3,
branch_deletions: 1,
} as never);
expect(store.getState().gitStatus.byEnvironmentId["sess-1"]).toMatchObject({
branch_additions: 3,
branch_deletions: 1,
});
});
});
describe("setTaskSession — cross-slice migration", () => {
it("migrates env-keyed data when task_environment_id is set", () => {
const store = makeCombinedStore();
// Data arrives under fallback sessionId key before the mapping is known
store.getState().setGitStatus("sess-1", { branch: "main" } as never);
store.getState().setSessionCommits("sess-1", [{ commit_sha: "abc" } as never]);
// Session update arrives with task_environment_id
store.getState().setTaskSession({
id: toSessionId("sess-1"),
task_id: toTaskId("task-1"),
state: "RUNNING",
task_environment_id: "env-1",
started_at: "",
updated_at: "",
});
const state = store.getState();
expect(state.environmentIdBySessionId["sess-1"]).toBe("env-1");
expect(state.gitStatus.byEnvironmentId["env-1"]).toEqual({ branch: "main" });
expect(state.gitStatus.byEnvironmentId["sess-1"]).toBeUndefined();
expect(state.sessionCommits.byEnvironmentId["env-1"]).toEqual([{ commit_sha: "abc" }]);
expect(state.sessionCommits.byEnvironmentId["sess-1"]).toBeUndefined();
});
it("does not migrate when task_environment_id is absent", () => {
const store = makeCombinedStore();
store.getState().setGitStatus("sess-2", { branch: "dev" } as never);
store.getState().setTaskSession({
id: toSessionId("sess-2"),
task_id: toTaskId("task-2"),
state: "RUNNING",
started_at: "",
updated_at: "",
});
const state = store.getState();
expect(state.environmentIdBySessionId["sess-2"]).toBeUndefined();
expect(state.gitStatus.byEnvironmentId["sess-2"]).toEqual({ branch: "dev" });
});
});
describe("setTaskSessionsForTask — bulk cross-slice migration", () => {
it("migrates env-keyed data for all sessions with task_environment_id", () => {
const store = makeCombinedStore();
// Data under fallback keys
store.getState().setGitStatus("sess-a", { branch: "a" } as never);
store.getState().appendShellOutput("sess-b", "hello");
store.getState().setTaskSessionsForTask("task-1", [
{
id: toSessionId("sess-a"),
task_id: toTaskId("task-1"),
state: "COMPLETED",
task_environment_id: "env-x",
started_at: "",
updated_at: "",
},
{
id: toSessionId("sess-b"),
task_id: toTaskId("task-1"),
state: "RUNNING",
task_environment_id: "env-y",
started_at: "",
updated_at: "",
},
]);
const state = store.getState();
expect(state.gitStatus.byEnvironmentId["env-x"]).toEqual({ branch: "a" });
expect(state.gitStatus.byEnvironmentId["sess-a"]).toBeUndefined();
expect(state.shell.outputs["env-y"]).toBe("hello");
expect(state.shell.outputs["sess-b"]).toBeUndefined();
});
});
describe("setSessionCommits — stale response protection", () => {
it("does not overwrite existing commits with an empty array", () => {
const store = makeStore();
store.getState().setSessionCommits("sess-1", [commit("abc")]);
store.getState().setSessionCommits("sess-1", []);
const state = store.getState();
expect(state.sessionCommits.byEnvironmentId["sess-1"]).toEqual([commit("abc")]);
});
it("sets an empty array when no existing commits are present", () => {
const store = makeStore();
store.getState().setSessionCommits("sess-1", []);
const state = store.getState();
expect(state.sessionCommits.byEnvironmentId["sess-1"]).toEqual([]);
});
it("overwrites existing commits with a non-empty array", () => {
const store = makeStore();
store.getState().setSessionCommits("sess-1", [commit("abc")]);
store.getState().setSessionCommits("sess-1", [commit("def")]);
const state = store.getState();
expect(state.sessionCommits.byEnvironmentId["sess-1"]).toEqual([commit("def")]);
});
it("allows an empty response after commits are explicitly cleared", () => {
const store = makeStore();
store.getState().setSessionCommits("sess-1", [commit("abc")]);
store.getState().clearSessionCommits("sess-1");
store.getState().setSessionCommits("sess-1", []);
const state = store.getState();
expect(state.sessionCommits.byEnvironmentId["sess-1"]).toEqual([]);
});
});