mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
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:
committed by
GitHub
parent
66f4ccabc5
commit
6032dec3ba
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user