fix: order workbench sessions by updated desc

This commit is contained in:
lyon
2026-06-20 15:36:41 +08:00
parent ef8765828a
commit 11e2f08e70
3 changed files with 42 additions and 7 deletions
+10 -5
View File
@@ -369,7 +369,7 @@ test("workbench read model exposes session, messages, turn, and trace without wr
assert.equal(sessions.body.sessions[0].turnSummary.durationMs, 3000);
assert.equal(factQueries[0].projectId, undefined);
assert.equal(factQueries[0].limit, 21);
assert.equal(factQueries[0].sessionOrder, "updated_desc");
assert.equal(factQueries[0].sessionsOrder, "updated_desc");
assert.deepEqual(factQueries[0].families, ["sessions"]);
assert.deepEqual(factQueries[1].families, ["messages", "parts", "turns"]);
assert.deepEqual(factQueries[1].sessionIds, [session.id]);
@@ -1032,7 +1032,7 @@ test("workbench session list overlaps include lookup with the page query", async
const runtimeStore = {
async queryWorkbenchFacts(params = {}) {
queries.push({ ...params, pageReleased });
if (params.sessionOrder === "updated_desc") await pageGate;
if (params.sessionsOrder === "updated_desc") await pageGate;
const filtered = filterFacts(facts, params);
return {
facts: filtered,
@@ -1047,7 +1047,7 @@ test("workbench session list overlaps include lookup with the page query", async
try {
const { port } = server.address();
const pending = getJson(port, `/v1/workbench/sessions?includeSessionId=${encodeURIComponent(included.id)}&limit=1`);
await waitForCondition(() => queries.some((query) => query.sessionId === included.id) && queries.some((query) => query.sessionOrder === "updated_desc"));
await waitForCondition(() => queries.some((query) => query.sessionId === included.id) && queries.some((query) => query.sessionsOrder === "updated_desc"));
const includeIndex = queries.findIndex((query) => query.sessionId === included.id);
assert.ok(includeIndex >= 0);
assert.equal(queries[includeIndex].pageReleased, false);
@@ -1057,7 +1057,7 @@ test("workbench session list overlaps include lookup with the page query", async
assert.equal(sessions.status, 200);
assert.equal(sessions.body.sessions[0].sessionId, included.id);
assert.equal(sessions.body.hasMore, true);
assert.equal(queries.find((query) => query.sessionOrder === "updated_desc")?.sessionProjection, "summary");
assert.equal(queries.find((query) => query.sessionsOrder === "updated_desc")?.sessionProjection, "summary");
assert.equal(queries.find((query) => query.sessionId === included.id)?.sessionProjection, undefined);
} finally {
pageReleased = true;
@@ -1621,7 +1621,7 @@ function filterFacts(facts, params = {}) {
.sort((left, right) => Number(left.projectedSeq) - Number(right.projectedSeq)) : [],
checkpoints: families.has("checkpoints") ? facts.checkpoints.filter((record) => matchesFact(record, params, ["traceId", "sessionId", "turnId", "runId", "commandId", "projectionStatus", "projectionHealth"])) : []
};
if (params.sessionOrder === "updated_desc") {
if (workbenchTestFactOrder(params, "sessions") === "updated_desc") {
filtered.sessions.sort((left, right) => String(right.updatedAt ?? "").localeCompare(String(left.updatedAt ?? "")) || String(right.sessionId ?? "").localeCompare(String(left.sessionId ?? "")));
}
const limit = Number.parseInt(params.limit ?? "", 10);
@@ -1629,6 +1629,11 @@ function filterFacts(facts, params = {}) {
return Object.fromEntries(Object.entries(filtered).map(([key, value]) => [key, value.slice(0, limit)]));
}
function workbenchTestFactOrder(params = {}, family = "") {
const value = params[`${family}Order`] ?? params.order;
return value === "updated_desc" ? "updated_desc" : "updated_asc";
}
function matchesFact(record, params, fields) {
return fields.every((field) => {
if (params[field] !== undefined && params[field] !== null && record[field] !== params[field]) return false;
+1 -1
View File
@@ -322,7 +322,7 @@ async function handleWorkbenchSessionList(response, url, options, actor) {
const includeSessionId = safeSessionId(url.searchParams.get("includeSessionId"));
const includeRouteId = includeSessionId ?? safeConversationId(url.searchParams.get("includeSessionId"));
const readModel = createWorkbenchReadModel(options, actor);
const pageQuery = { ownerUserId: actor.role === "admin" ? undefined : actor.id, limit: limit + offset + 1, families: WORKBENCH_SESSION_LIST_PAGE_FAMILIES, sessionOrder: "updated_desc", sessionProjection: "summary" };
const pageQuery = { ownerUserId: actor.role === "admin" ? undefined : actor.id, limit: limit + offset + 1, families: WORKBENCH_SESSION_LIST_PAGE_FAMILIES, sessionsOrder: "updated_desc", sessionProjection: "summary" };
const includeQueryPromise = includeRouteId ? queryWorkbenchSessionInclude(readModel, includeRouteId, perf) : null;
const pageResult = await (perf ? perf.measure("workbench_session_page_query", () => readModel.queryFacts(pageQuery)) : readModel.queryFacts(pageQuery));
if (pageResult.error) return sendJson(response, 503, workbenchProjectionStoreError(pageResult.error));
+31 -1
View File
@@ -919,6 +919,35 @@ test("configured postgres runtime can query thin Workbench session summaries", a
assert.ok(readCall.sql.includes("providerProfile"));
});
test("configured postgres runtime orders Workbench sessions by sessionsOrder updated_desc", async () => {
const queryClient = createFakePostgresClient({ migrationReady: true });
const store = createConfiguredCloudRuntimeStore({
env: {
HWLAB_CLOUD_RUNTIME_ADAPTER: "postgres",
HWLAB_CLOUD_RUNTIME_DURABLE: "true"
},
dbUrl: "postgres://hwlab_redacted@db.example.invalid:5432/hwlab",
queryClient,
now: () => "2026-06-20T10:00:00.000Z"
});
await store.writeWorkbenchFacts({
facts: {
sessions: [
{ sessionId: "ses_order_old", ownerUserId: "usr_order", status: "running", updatedAt: "2026-06-15T10:00:00.000Z" },
{ sessionId: "ses_order_new", ownerUserId: "usr_order", status: "running", updatedAt: "2026-06-20T10:00:00.000Z" },
{ sessionId: "ses_order_middle", ownerUserId: "usr_order", status: "running", updatedAt: "2026-06-18T10:00:00.000Z" }
]
}
});
queryClient.calls.length = 0;
const loaded = await store.queryWorkbenchFacts({ families: ["sessions"], sessionsOrder: "updated_desc", limit: 10 });
const readCall = queryClient.calls.find((call) => call.sql.startsWith("SELECT session_json FROM workbench_sessions"));
assert.deepEqual(loaded.facts.sessions.map((session) => session.sessionId), ["ses_order_new", "ses_order_middle", "ses_order_old"]);
assert.match(readCall?.sql ?? "", /ORDER BY updated_at DESC/u);
});
test("configured postgres runtime queries requested workbench fact families concurrently", async () => {
const expectedReads = 4;
let started = 0;
@@ -1357,9 +1386,10 @@ function workbenchFactRows(map, jsonColumn, sql, params, readErrorCode) {
error.code = readErrorCode;
throw error;
}
const direction = sql.includes("ORDER BY updated_at DESC") ? -1 : 1;
const rows = [...map.values()]
.filter((record) => sqlRecordMatches(record, sql, params))
.sort((left, right) => String(left.updated_at).localeCompare(String(right.updated_at)) || String(left.id ?? left.session_id ?? left.message_id ?? left.part_id ?? left.turn_id ?? left.trace_id).localeCompare(String(right.id ?? right.session_id ?? right.message_id ?? right.part_id ?? right.turn_id ?? right.trace_id)))
.sort((left, right) => direction * (String(left.updated_at).localeCompare(String(right.updated_at)) || String(left.id ?? left.session_id ?? left.message_id ?? left.part_id ?? left.turn_id ?? left.trace_id).localeCompare(String(right.id ?? right.session_id ?? right.message_id ?? right.part_id ?? right.turn_id ?? right.trace_id))))
.map((record) => ({ [jsonColumn]: record[jsonColumn] }));
const limit = sql.includes(" LIMIT $") ? Number(params.at(-1)) : null;
return { rows: Number.isInteger(limit) && limit > 0 ? rows.slice(0, limit) : rows };