mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-09-24 15:49:09 +00:00
2df408a571
Merge seed only wrote activeSessionIdMap, so unsent draft demoted the panel to draft mode and send created a new session while the old chat still showed. Prefer persisted workspace active over hasDraft, and materialize panelView.
271 lines
8.0 KiB
TypeScript
271 lines
8.0 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import type {
|
|
AIPanelView,
|
|
AISession,
|
|
} from "../../infrastructure/ai/types.ts";
|
|
import {
|
|
applyDraftEntrySelection,
|
|
applyHistorySessionSelection,
|
|
normalizePanelView,
|
|
panelViewsEqual,
|
|
resolveDisplayedPanelView,
|
|
resolveDisplayedSession,
|
|
shouldForceDraftViewSync,
|
|
} from "./aiPanelViewState.ts";
|
|
|
|
function createSession(id: string): AISession {
|
|
return {
|
|
id,
|
|
title: `Session ${id}`,
|
|
messages: [],
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
agentId: "catty",
|
|
scope: {
|
|
type: "terminal",
|
|
targetId: "terminal-1",
|
|
},
|
|
};
|
|
}
|
|
|
|
test("panelViewsEqual treats draft views as equal even when refs differ", () => {
|
|
assert.equal(
|
|
panelViewsEqual({ mode: "draft" }, { mode: "draft" }),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("panelViewsEqual distinguishes session targets", () => {
|
|
assert.equal(
|
|
panelViewsEqual(
|
|
{ mode: "session", sessionId: "session-1" },
|
|
{ mode: "session", sessionId: "session-2" },
|
|
),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("draft view never falls back to most recent history", () => {
|
|
const panelView: AIPanelView = { mode: "draft" };
|
|
const sessions = [createSession("session-2"), createSession("session-1")];
|
|
|
|
assert.equal(resolveDisplayedSession(panelView, sessions), null);
|
|
});
|
|
|
|
test("session view returns the selected session", () => {
|
|
const selectedSession = createSession("session-2");
|
|
const panelView: AIPanelView = { mode: "session", sessionId: selectedSession.id };
|
|
const sessions = [createSession("session-1"), selectedSession];
|
|
|
|
assert.equal(resolveDisplayedSession(panelView, sessions), selectedSession);
|
|
});
|
|
|
|
test("missing session target resolves to null instead of history fallback", () => {
|
|
const panelView: AIPanelView = { mode: "session", sessionId: "missing-session" };
|
|
const sessions = [createSession("session-2"), createSession("session-1")];
|
|
|
|
assert.equal(resolveDisplayedSession(panelView, sessions), null);
|
|
});
|
|
|
|
test("missing session target normalizes back to draft view", () => {
|
|
const panelView: AIPanelView = { mode: "session", sessionId: "missing-session" };
|
|
const sessions = [createSession("session-2"), createSession("session-1")];
|
|
|
|
assert.deepEqual(normalizePanelView(panelView, sessions), { mode: "draft" });
|
|
});
|
|
|
|
test("shouldForceDraftViewSync keeps explicit session while it is still in scoped history", () => {
|
|
const explicit: AIPanelView = { mode: "session", sessionId: "just-created" };
|
|
const normalized: AIPanelView = { mode: "draft" };
|
|
// Predicate mirrors scoped historySessions (not the global store).
|
|
const scopedHistory = new Set(["just-created"]);
|
|
|
|
assert.equal(
|
|
shouldForceDraftViewSync(explicit, normalized, (id) => scopedHistory.has(id)),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("shouldForceDraftViewSync forces draft when explicit session is gone", () => {
|
|
const explicit: AIPanelView = { mode: "session", sessionId: "deleted" };
|
|
const normalized: AIPanelView = { mode: "draft" };
|
|
|
|
assert.equal(
|
|
shouldForceDraftViewSync(explicit, normalized, () => false),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("shouldForceDraftViewSync demotes when session is only in global store, not scoped history", () => {
|
|
const explicit: AIPanelView = { mode: "session", sessionId: "other-scope" };
|
|
const normalized: AIPanelView = { mode: "draft" };
|
|
// Global store still has it; scoped history (what normalize uses) does not.
|
|
const globalStore = new Set(["other-scope"]);
|
|
const scopedHistory = new Set<string>();
|
|
|
|
assert.equal(
|
|
shouldForceDraftViewSync(explicit, normalized, (id) => scopedHistory.has(id)),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
shouldForceDraftViewSync(explicit, normalized, (id) => globalStore.has(id)),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("shouldForceDraftViewSync is a no-op when views already match", () => {
|
|
const view: AIPanelView = { mode: "session", sessionId: "session-1" };
|
|
|
|
assert.equal(
|
|
shouldForceDraftViewSync(view, view, () => true),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("missing explicit panel view resumes the most recent matching history when no draft exists", () => {
|
|
const sessions = [createSession("session-2"), createSession("session-1")];
|
|
|
|
assert.deepEqual(
|
|
resolveDisplayedPanelView(undefined, false, sessions, undefined, "workspace"),
|
|
{ mode: "session", sessionId: "session-2" },
|
|
);
|
|
});
|
|
|
|
test("missing explicit panel view restores the persisted active session instead of the newest", () => {
|
|
const sessions = [createSession("session-2"), createSession("session-1")];
|
|
|
|
assert.deepEqual(
|
|
resolveDisplayedPanelView(undefined, false, sessions, "session-1", "workspace"),
|
|
{ mode: "session", sessionId: "session-1" },
|
|
);
|
|
});
|
|
|
|
test("persisted session id that no longer exists in history falls back to newest", () => {
|
|
const sessions = [createSession("session-2"), createSession("session-1")];
|
|
|
|
assert.deepEqual(
|
|
resolveDisplayedPanelView(undefined, false, sessions, "deleted-session", "workspace"),
|
|
{ mode: "session", sessionId: "session-2" },
|
|
);
|
|
});
|
|
|
|
test("null persisted session id falls back to newest history entry", () => {
|
|
const sessions = [createSession("session-2"), createSession("session-1")];
|
|
|
|
assert.deepEqual(
|
|
resolveDisplayedPanelView(undefined, false, sessions, null, "workspace"),
|
|
{ mode: "session", sessionId: "session-2" },
|
|
);
|
|
});
|
|
|
|
test("terminal scope without explicit view always starts from draft even when history exists", () => {
|
|
const sessions = [createSession("session-2"), createSession("session-1")];
|
|
|
|
assert.deepEqual(
|
|
resolveDisplayedPanelView(undefined, false, sessions, "session-1", "terminal"),
|
|
{ mode: "draft" },
|
|
);
|
|
});
|
|
|
|
test("missing explicit panel view prefers the draft when unsent input exists without an active chat", () => {
|
|
const sessions = [createSession("session-2"), createSession("session-1")];
|
|
|
|
assert.deepEqual(
|
|
resolveDisplayedPanelView(undefined, true, sessions, null, "workspace"),
|
|
{ mode: "draft" },
|
|
);
|
|
});
|
|
|
|
test("workspace unsent draft keeps the persisted active chat after merge seed", () => {
|
|
// Merge often seeds activeSessionIdMap without writing panelView. Typing a
|
|
// follow-up must not demote to draft or send will createSession().
|
|
const sessions = [createSession("session-2"), createSession("session-1")];
|
|
|
|
assert.deepEqual(
|
|
resolveDisplayedPanelView(undefined, true, sessions, "session-1", "workspace"),
|
|
{ mode: "session", sessionId: "session-1" },
|
|
);
|
|
});
|
|
|
|
test("explicit new-chat draft still wins over a stale persisted id", () => {
|
|
const sessions = [createSession("session-2"), createSession("session-1")];
|
|
|
|
assert.deepEqual(
|
|
resolveDisplayedPanelView(
|
|
{ mode: "draft" },
|
|
true,
|
|
sessions,
|
|
"session-1",
|
|
"workspace",
|
|
),
|
|
{ mode: "draft" },
|
|
);
|
|
});
|
|
|
|
test("draft state is used when there is no implicit history to resume", () => {
|
|
assert.deepEqual(
|
|
resolveDisplayedPanelView(undefined, true, [], null, "workspace"),
|
|
{ mode: "draft" },
|
|
);
|
|
});
|
|
|
|
test("history selection switches to the chosen session without touching draft state", () => {
|
|
const calls: string[] = [];
|
|
|
|
applyHistorySessionSelection("session-2", {
|
|
showSessionView: (sessionId) => {
|
|
calls.push(`view:${sessionId}`);
|
|
},
|
|
setActiveSessionId: (sessionId) => {
|
|
calls.push(`active:${sessionId}`);
|
|
},
|
|
closeHistory: () => {
|
|
calls.push("close-history");
|
|
},
|
|
});
|
|
|
|
assert.deepEqual(calls, [
|
|
"view:session-2",
|
|
"active:session-2",
|
|
"close-history",
|
|
]);
|
|
});
|
|
|
|
test("draft entry ensures a draft exists before switching the panel to draft mode", () => {
|
|
const calls: string[] = [];
|
|
|
|
applyDraftEntrySelection({
|
|
ensureDraft: () => {
|
|
calls.push("ensure-draft");
|
|
},
|
|
showDraftView: () => {
|
|
calls.push("show-draft");
|
|
},
|
|
});
|
|
|
|
assert.deepEqual(calls, [
|
|
"ensure-draft",
|
|
"show-draft",
|
|
]);
|
|
});
|
|
|
|
test("draft entry can preserve the current session view while ensuring draft state", () => {
|
|
const calls: string[] = [];
|
|
|
|
applyDraftEntrySelection({
|
|
ensureDraft: () => {
|
|
calls.push("ensure-draft");
|
|
},
|
|
showDraftView: () => {
|
|
calls.push("show-draft");
|
|
},
|
|
preserveSessionView: true,
|
|
});
|
|
|
|
assert.deepEqual(calls, [
|
|
"ensure-draft",
|
|
]);
|
|
});
|