fix(ui): make mock sessions archive flow usable (#110556)

* fix(ui): complete mock sessions archive flow

* fix(ui): scope archive filtering to stateful fixtures
This commit is contained in:
Peter Steinberger
2026-07-18 09:27:01 +01:00
committed by GitHub
parent 66f4ccabc5
commit 6032dec3ba
3 changed files with 185 additions and 34 deletions
+20 -1
View File
@@ -1013,6 +1013,23 @@ async function createChatPickerScenario(): Promise<ControlUiMockGatewayScenario>
labelPrefix: "Long running session",
}),
];
const archivedSessions = [
sessionRow("agent:main:archived-launch-notes", "Archived launch notes", baseTime - 86_400_000, {
archived: true,
totalTokens: 42_000,
}),
sessionRow(
"agent:main:discord:channel:archived-lounge",
"#archived-lounge",
baseTime - 172_800_000,
{
archived: true,
channel: "discord",
kind: "group",
totalTokens: 18_000,
},
),
];
const telegramSessions = buildSessionRows({
baseTime: baseTime - 30_000,
count: TOTAL_TELEGRAM_SESSIONS,
@@ -1046,6 +1063,7 @@ async function createChatPickerScenario(): Promise<ControlUiMockGatewayScenario>
// (raw persists, hash advances) because config.get ships a raw fixture.
"config.get": configMocks.get,
"config.schema": configMocks.schema,
"sessions.patch": { ok: true },
"sessions.diff": buildSessionDiffMock(),
"plugins.list": buildPluginCatalogMock(),
"channels.status": buildChannelsStatusMock(baseTime),
@@ -1355,11 +1373,12 @@ async function createChatPickerScenario(): Promise<ControlUiMockGatewayScenario>
...searchPrefixes("claude-sonnet-4-6"),
...searchPrefixes("anthropic"),
]),
...buildSessionListCases(sessions),
...buildSessionListCases([...sessions, ...archivedSessions]),
],
},
},
models: modelProviders.models,
sessionArchiveFiltering: true,
sessionKey: "agent:main:main",
};
}
@@ -149,3 +149,115 @@ describe("mock gateway stateful config", () => {
socket.close();
});
});
describe("mock gateway stateful sessions", () => {
it("keeps archive filtering opt-in for static session fixtures", async () => {
const script = createControlUiMockGatewayInitScript({
methodResponses: {
"sessions.list": {
count: 1,
defaults: {},
path: "",
sessions: [{ key: "agent:main:research", archived: false }],
ts: 0,
},
"sessions.patch": { ok: true },
},
});
window.sessionStorage.clear();
// oxlint-disable-next-line typescript/no-implied-eval -- Executes the generated init script standalone, proving it captures no module closures.
new Function(script)();
const socket = new WebSocket("ws://mock-gateway");
const frames: ResponseFrame[] = [];
socket.addEventListener("message", (event) => {
frames.push(JSON.parse(String((event as MessageEvent).data)) as ResponseFrame);
});
await flushMockTimers();
socket.send(
JSON.stringify({
type: "req",
id: "patch-1",
method: "sessions.patch",
params: { key: "agent:main:research", archived: true },
}),
);
await flushMockTimers();
socket.send(JSON.stringify({ type: "req", id: "list-1", method: "sessions.list", params: {} }));
await flushMockTimers();
expect(frames.find((frame) => frame.id === "list-1")?.payload).toMatchObject({
count: 1,
sessions: [{ key: "agent:main:research", archived: false }],
});
socket.close();
});
it("moves archive patches between active and archived session lists", async () => {
const script = createControlUiMockGatewayInitScript({
methodResponses: {
"sessions.list": {
count: 2,
defaults: {},
path: "",
sessions: [
{ key: "agent:main:research", archived: false },
{ key: "agent:main:launch-notes", archived: true },
],
ts: 0,
},
"sessions.patch": { ok: true },
},
sessionArchiveFiltering: true,
});
window.sessionStorage.clear();
// oxlint-disable-next-line typescript/no-implied-eval -- Executes the generated init script standalone, proving it captures no module closures.
new Function(script)();
const socket = new WebSocket("ws://mock-gateway");
const frames: ResponseFrame[] = [];
socket.addEventListener("message", (event) => {
frames.push(JSON.parse(String((event as MessageEvent).data)) as ResponseFrame);
});
await flushMockTimers();
const request = async (id: string, method: string, params: unknown) => {
socket.send(JSON.stringify({ type: "req", id, method, params }));
await flushMockTimers();
const response = frames.find((frame) => frame.type === "res" && frame.id === id);
if (!response) {
throw new Error(`No mock response for ${method}`);
}
return response.payload as Record<string, unknown>;
};
const keys = (payload: Record<string, unknown>) =>
(payload.sessions as Array<{ key: string }>).map((row) => row.key);
expect(keys(await request("list-1", "sessions.list", {}))).toEqual(["agent:main:research"]);
expect(keys(await request("list-2", "sessions.list", { archived: true }))).toEqual([
"agent:main:launch-notes",
]);
expect(
await request("patch-3", "sessions.patch", {
key: "agent:main:research",
archived: true,
}),
).toEqual({ ok: true });
expect(keys(await request("list-4", "sessions.list", {}))).toEqual([]);
expect(keys(await request("list-5", "sessions.list", { archived: true }))).toEqual([
"agent:main:research",
"agent:main:launch-notes",
]);
await request("patch-6", "sessions.patch", {
key: "agent:main:launch-notes",
archived: false,
});
expect(keys(await request("list-7", "sessions.list", {}))).toEqual(["agent:main:launch-notes"]);
expect(keys(await request("list-8", "sessions.list", { archived: true }))).toEqual([
"agent:main:research",
]);
socket.close();
});
});
+53 -33
View File
@@ -67,6 +67,8 @@ export type ControlUiMockGatewayScenario = {
inFlightRun?: { runId: string; text?: string; plan?: unknown } | null;
/** Session run state served alongside history (hasActiveRun/activeRunIds). */
sessionInfo?: Record<string, unknown> | null;
/** Partition sessions.list fixtures by archived state after applying patches. */
sessionArchiveFiltering?: boolean;
models?: Array<{
id: string;
name: string;
@@ -260,6 +262,7 @@ function normalizeScenario(
inFlightRun: scenario.inFlightRun ?? null,
models: scenario.models ?? [{ id: "gpt-5.5", name: "gpt-5.5", provider: "openai" }],
sessionInfo: scenario.sessionInfo ?? null,
sessionArchiveFiltering: scenario.sessionArchiveFiltering ?? false,
sessionKey,
sessionGroups: scenario.sessionGroups ?? [],
terminalEnabled: scenario.terminalEnabled ?? false,
@@ -578,36 +581,48 @@ function installControlUiMockGateway(input: {
patch[key] = params[key];
}
}
if (scenario.sessionArchiveFiltering && hasOwn(params, "archived")) {
patch.archived = params.archived;
}
sessionPatches.set(params.key, patch);
}
function applySessionPatches(response: unknown): unknown {
function applySessionPatches(response: unknown, params: unknown): unknown {
if (!isRecord(response) || !Array.isArray(response.sessions)) {
return response;
}
const showArchived = isRecord(params) && params.archived === true;
const sessions = response.sessions.map((row) => {
if (!isRecord(row) || typeof row.key !== "string") {
return row;
}
const patch = sessionPatches.get(row.key);
const next = patch ? { ...row, ...patch } : { ...row };
// Replay group renames/deletes over static fixtures: the real gateway
// rewrites member categories server-side before the next sessions.list.
let category = typeof next.category === "string" ? next.category : undefined;
for (const rename of groupsState.renames) {
if (category === rename.from) {
category = rename.to ?? undefined;
}
}
if (category === undefined) {
delete next.category;
} else {
next.category = category;
}
return next;
});
if (!scenario.sessionArchiveFiltering) {
return { ...response, sessions };
}
const filteredSessions = sessions.filter(
(row) => isRecord(row) && (row.archived === true) === showArchived,
);
return {
...response,
sessions: response.sessions.map((row) => {
if (!isRecord(row) || typeof row.key !== "string") {
return row;
}
const patch = sessionPatches.get(row.key);
const next = patch ? { ...row, ...patch } : { ...row };
// Replay group renames/deletes over static fixtures: the real gateway
// rewrites member categories server-side before the next sessions.list.
let category = typeof next.category === "string" ? next.category : undefined;
for (const rename of groupsState.renames) {
if (category === rename.from) {
category = rename.to ?? undefined;
}
}
if (category === undefined) {
delete next.category;
} else {
next.category = category;
}
return next;
}),
count: filteredSessions.length,
sessions: filteredSessions,
};
}
@@ -700,7 +715,9 @@ function installControlUiMockGateway(input: {
}
const configured = configuredResponse(method, params);
if (configured.found) {
return method === "sessions.list" ? applySessionPatches(configured.value) : configured.value;
return method === "sessions.list"
? applySessionPatches(configured.value, params)
: configured.value;
}
switch (method) {
case "connect":
@@ -841,17 +858,20 @@ function installControlUiMockGateway(input: {
case "models.list":
return { models: scenario.models };
case "sessions.list":
return applySessionPatches({
count: 1,
defaults: {
contextTokens: null,
model: "gpt-5.5",
modelProvider: "openai",
return applySessionPatches(
{
count: 1,
defaults: {
contextTokens: null,
model: "gpt-5.5",
modelProvider: "openai",
},
path: "",
sessions: [sessionRow()],
ts: Date.now(),
},
path: "",
sessions: [sessionRow()],
ts: Date.now(),
});
params,
);
case "sessions.groups.list":
return groupsPayload();
case "sessions.groups.put": {