183 lines
5.6 KiB
TypeScript
183 lines
5.6 KiB
TypeScript
|
|
import test from "node:test";
|
||
|
|
import assert from "node:assert/strict";
|
||
|
|
|
||
|
|
import type {
|
||
|
|
AIPanelView,
|
||
|
|
AISession,
|
||
|
|
} from "../../infrastructure/ai/types.ts";
|
||
|
|
import { createEmptyDraft } from "./aiDraftState.ts";
|
||
|
|
import {
|
||
|
|
pruneInactiveScopedSessions,
|
||
|
|
pruneInactiveScopedTransientState,
|
||
|
|
} from "./aiScopeCleanup.ts";
|
||
|
|
|
||
|
|
function createSession(id: string, scope: AISession["scope"], externalSessionId?: string): AISession {
|
||
|
|
return {
|
||
|
|
id,
|
||
|
|
title: id,
|
||
|
|
agentId: "catty",
|
||
|
|
scope,
|
||
|
|
messages: [],
|
||
|
|
externalSessionId,
|
||
|
|
createdAt: 1,
|
||
|
|
updatedAt: 1,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
test("pruneInactiveScopedTransientState removes closed workspace and terminal scope state", () => {
|
||
|
|
const activeSessionIdMap = {
|
||
|
|
"terminal:open-terminal": "session-open",
|
||
|
|
"terminal:closed-terminal": "session-closed-terminal",
|
||
|
|
"workspace:open-workspace": "session-open-workspace",
|
||
|
|
"workspace:closed-workspace": "session-closed-workspace",
|
||
|
|
};
|
||
|
|
const draftsByScope = {
|
||
|
|
"terminal:open-terminal": createEmptyDraft("catty"),
|
||
|
|
"terminal:closed-terminal": createEmptyDraft("catty"),
|
||
|
|
"workspace:open-workspace": createEmptyDraft("catty"),
|
||
|
|
"workspace:closed-workspace": createEmptyDraft("catty"),
|
||
|
|
};
|
||
|
|
const panelViewByScope = {
|
||
|
|
"terminal:open-terminal": { mode: "draft" },
|
||
|
|
"terminal:closed-terminal": { mode: "session", sessionId: "session-closed-terminal" },
|
||
|
|
"workspace:open-workspace": { mode: "draft" },
|
||
|
|
"workspace:closed-workspace": { mode: "session", sessionId: "session-closed-workspace" },
|
||
|
|
} satisfies Record<string, AIPanelView>;
|
||
|
|
|
||
|
|
const next = pruneInactiveScopedTransientState(
|
||
|
|
activeSessionIdMap,
|
||
|
|
draftsByScope,
|
||
|
|
panelViewByScope,
|
||
|
|
new Set(["open-terminal", "open-workspace"]),
|
||
|
|
);
|
||
|
|
|
||
|
|
assert.deepEqual(next.activeSessionIdMap, {
|
||
|
|
"terminal:open-terminal": "session-open",
|
||
|
|
"workspace:open-workspace": "session-open-workspace",
|
||
|
|
});
|
||
|
|
assert.deepEqual(next.draftsByScope, {
|
||
|
|
"terminal:open-terminal": draftsByScope["terminal:open-terminal"],
|
||
|
|
"workspace:open-workspace": draftsByScope["workspace:open-workspace"],
|
||
|
|
});
|
||
|
|
assert.deepEqual(next.panelViewByScope, {
|
||
|
|
"terminal:open-terminal": panelViewByScope["terminal:open-terminal"],
|
||
|
|
"workspace:open-workspace": panelViewByScope["workspace:open-workspace"],
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test("pruneInactiveScopedSessions reports inactive targets without deleting persisted history", () => {
|
||
|
|
const sessions = [
|
||
|
|
createSession("terminal-restorable", {
|
||
|
|
type: "terminal",
|
||
|
|
targetId: "closed-restorable",
|
||
|
|
hostIds: ["host-1"],
|
||
|
|
}, "ext-1"),
|
||
|
|
createSession("terminal-local", {
|
||
|
|
type: "terminal",
|
||
|
|
targetId: "closed-local",
|
||
|
|
hostIds: ["local-shell"],
|
||
|
|
}, "ext-2"),
|
||
|
|
createSession("workspace-closed", {
|
||
|
|
type: "workspace",
|
||
|
|
targetId: "closed-workspace",
|
||
|
|
}, "ext-3"),
|
||
|
|
createSession("terminal-open", {
|
||
|
|
type: "terminal",
|
||
|
|
targetId: "open-terminal",
|
||
|
|
hostIds: ["host-2"],
|
||
|
|
}, "ext-4"),
|
||
|
|
];
|
||
|
|
|
||
|
|
const next = pruneInactiveScopedSessions(
|
||
|
|
sessions,
|
||
|
|
new Set(["open-terminal"]),
|
||
|
|
);
|
||
|
|
|
||
|
|
assert.deepEqual(next.orphanedSessionIds, [
|
||
|
|
"terminal-restorable",
|
||
|
|
"terminal-local",
|
||
|
|
"workspace-closed",
|
||
|
|
]);
|
||
|
|
assert.equal(next.sessions, sessions);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("pruneInactiveScopedSessions preserves inactive workspace and local terminal history after restart", () => {
|
||
|
|
const sessions = [
|
||
|
|
createSession("terminal-local", {
|
||
|
|
type: "terminal",
|
||
|
|
targetId: "closed-local",
|
||
|
|
hostIds: ["local-shell"],
|
||
|
|
}, "ext-local"),
|
||
|
|
createSession("workspace-closed", {
|
||
|
|
type: "workspace",
|
||
|
|
targetId: "closed-workspace",
|
||
|
|
}, "ext-workspace"),
|
||
|
|
];
|
||
|
|
|
||
|
|
const next = pruneInactiveScopedSessions(
|
||
|
|
sessions,
|
||
|
|
new Set(),
|
||
|
|
);
|
||
|
|
|
||
|
|
assert.deepEqual(next.orphanedSessionIds, [
|
||
|
|
"terminal-local",
|
||
|
|
"workspace-closed",
|
||
|
|
]);
|
||
|
|
assert.equal(next.sessions, sessions);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("pruneInactiveScopedSessions preserves original sessions when orphaned restorable chats are already detached", () => {
|
||
|
|
const sessions = [
|
||
|
|
createSession("terminal-restorable", {
|
||
|
|
type: "terminal",
|
||
|
|
targetId: "closed-restorable",
|
||
|
|
hostIds: ["host-1"],
|
||
|
|
}),
|
||
|
|
createSession("terminal-open", {
|
||
|
|
type: "terminal",
|
||
|
|
targetId: "open-terminal",
|
||
|
|
hostIds: ["host-2"],
|
||
|
|
}, "ext-4"),
|
||
|
|
];
|
||
|
|
|
||
|
|
const next = pruneInactiveScopedSessions(
|
||
|
|
sessions,
|
||
|
|
new Set(["open-terminal"]),
|
||
|
|
);
|
||
|
|
|
||
|
|
assert.deepEqual(next.orphanedSessionIds, ["terminal-restorable"]);
|
||
|
|
assert.equal(next.sessions, sessions);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("pruneInactiveScopedSessions treats sessions displayed elsewhere as in-use, not orphaned", () => {
|
||
|
|
// terminal-restorable's original scope (terminal-closed-A) is gone, but
|
||
|
|
// the user resumed it into terminal-open-B from history. The session's
|
||
|
|
// externalSessionId must be preserved and it must not appear in the
|
||
|
|
// orphaned list, otherwise the active chat loses external agent continuity.
|
||
|
|
const resumedElsewhere = createSession("terminal-restorable", {
|
||
|
|
type: "terminal",
|
||
|
|
targetId: "terminal-closed-A",
|
||
|
|
hostIds: ["host-1"],
|
||
|
|
}, "ext-resumed");
|
||
|
|
|
||
|
|
const trulyOrphaned = createSession("terminal-stale", {
|
||
|
|
type: "terminal",
|
||
|
|
targetId: "terminal-closed-C",
|
||
|
|
hostIds: ["host-2"],
|
||
|
|
}, "ext-stale");
|
||
|
|
|
||
|
|
const sessions = [resumedElsewhere, trulyOrphaned];
|
||
|
|
|
||
|
|
const next = pruneInactiveScopedSessions(
|
||
|
|
sessions,
|
||
|
|
new Set(["terminal-open-B"]),
|
||
|
|
new Set(["terminal-restorable"]),
|
||
|
|
);
|
||
|
|
|
||
|
|
// Only the one not being displayed anywhere should show up as orphaned.
|
||
|
|
assert.deepEqual(next.orphanedSessionIds, ["terminal-stale"]);
|
||
|
|
// The resumed session must retain its externalSessionId.
|
||
|
|
const resumedNext = next.sessions.find((s) => s.id === "terminal-restorable");
|
||
|
|
assert.equal(resumedNext?.externalSessionId, "ext-resumed");
|
||
|
|
});
|