Merge pull request #1546 from pikasTech/fix/1519-session-status
fix(workbench): prefer terminal turn result for session status
This commit is contained in:
@@ -456,6 +456,85 @@ test("workbench read model projects completed current turn for idle session summ
|
||||
}
|
||||
});
|
||||
|
||||
test("workbench read model lets terminal result override stale running session state", async () => {
|
||||
const traceStore = createCodeAgentTraceStore();
|
||||
const results = createCodeAgentChatResultStore();
|
||||
const traceId = "trc_workbench_stale_running_result_completed";
|
||||
const finalText = "OK";
|
||||
const session = {
|
||||
id: "ses_workbench_stale_running_result_completed",
|
||||
projectId: "prj_hwpod_workbench",
|
||||
agentId: "hwlab-code-agent",
|
||||
status: "running",
|
||||
ownerUserId: ACTOR.id,
|
||||
conversationId: "cnv_workbench_stale_running_result_completed",
|
||||
threadId: "thread-workbench-stale-running-result-completed",
|
||||
lastTraceId: traceId,
|
||||
updatedAt: "2026-06-18T16:31:04.000Z",
|
||||
session: {
|
||||
sessionStatus: "running",
|
||||
lastTraceId: traceId,
|
||||
messages: [
|
||||
{ role: "user", text: "reply OK", traceId, status: "sent" },
|
||||
{ role: "agent", text: "", traceId, status: "running" }
|
||||
]
|
||||
}
|
||||
};
|
||||
traceStore.append(traceId, { type: "backend", status: "running", label: "runner:created", terminal: false });
|
||||
results.set(traceId, {
|
||||
status: "completed",
|
||||
traceId,
|
||||
ownerUserId: ACTOR.id,
|
||||
conversationId: session.conversationId,
|
||||
sessionId: session.id,
|
||||
threadId: session.threadId,
|
||||
finalResponse: finalText,
|
||||
agentRun: { runId: "run_workbench_stale_running_completed", commandId: "cmd_workbench_stale_running_completed", status: "completed" }
|
||||
});
|
||||
const accessController = {
|
||||
store: {
|
||||
async listAgentSessionsForUser() { return [session]; },
|
||||
async getAgentSession(sessionId) { return sessionId === session.id ? session : null; },
|
||||
async getAgentSessionByTraceId(requestTraceId) { return requestTraceId === traceId ? session : null; }
|
||||
},
|
||||
async ensureBootstrap() {},
|
||||
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
||||
};
|
||||
const server = createCloudApiServer({ accessController, traceStore, codeAgentChatResults: results });
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
|
||||
try {
|
||||
const { port } = server.address();
|
||||
const sessions = await getJson(port, `/v1/workbench/sessions?includeSessionId=${encodeURIComponent(session.id)}`);
|
||||
assert.equal(sessions.status, 200);
|
||||
assert.equal(sessions.body.sessions[0].status, "completed");
|
||||
assert.equal(sessions.body.sessions[0].running, false);
|
||||
assert.equal(sessions.body.sessions[0].terminal, true);
|
||||
assert.equal(sessions.body.sessions[0].turnSummary.status, "completed");
|
||||
|
||||
const detail = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(session.id)}`);
|
||||
assert.equal(detail.status, 200);
|
||||
assert.equal(detail.body.session.status, "completed");
|
||||
assert.equal(detail.body.session.running, false);
|
||||
assert.equal(detail.body.session.terminal, true);
|
||||
|
||||
const messages = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(session.id)}/messages?limit=10`);
|
||||
assert.equal(messages.status, 200);
|
||||
assert.equal(messages.body.messages[1].status, "completed");
|
||||
assert.equal(messages.body.messages[1].text, finalText);
|
||||
|
||||
const turn = await getJson(port, `/v1/workbench/turns/${encodeURIComponent(traceId)}`);
|
||||
assert.equal(turn.status, 200);
|
||||
assert.equal(turn.body.status, "completed");
|
||||
assert.equal(turn.body.turn.status, "completed");
|
||||
assert.equal(turn.body.turn.running, false);
|
||||
assert.equal(turn.body.turn.terminal, true);
|
||||
assert.equal(turn.body.turn.assistantText, finalText);
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
||||
}
|
||||
});
|
||||
|
||||
test("workbench realtime stream accepts session authority without project or workspace", async () => {
|
||||
const traceStore = createCodeAgentTraceStore();
|
||||
const results = createCodeAgentChatResultStore();
|
||||
|
||||
@@ -277,7 +277,7 @@ async function handleWorkbenchTurnSnapshot(response, url, options, actor, rawTur
|
||||
}
|
||||
const resultVisible = result && (session || actor.role === "admin" || Boolean(result.ownerUserId));
|
||||
const trace = await readModel.traceSnapshot(traceId);
|
||||
const status = workbenchTurnProjectionStatus(trace);
|
||||
const status = workbenchTurnStatusFromFacts({ trace, result });
|
||||
const found = Boolean(resultVisible || session);
|
||||
if (!found) return sendJson(response, 404, workbenchError("workbench_turn_not_found", "Workbench turn is not visible to the current actor.", { turnId, traceId }));
|
||||
const projection = readModel.projectionDiagnostics({ traceId, result, trace });
|
||||
@@ -629,7 +629,7 @@ async function visibleTraceContext(options, actor, traceId) {
|
||||
const resultVisible = result && (session || actor.role === "admin" || Boolean(result.ownerUserId));
|
||||
if (!resultVisible && !session) return { visible: false };
|
||||
const trace = await readModel.traceSnapshot(traceId);
|
||||
const status = workbenchTurnProjectionStatus(trace);
|
||||
const status = workbenchTurnStatusFromFacts({ trace, result });
|
||||
return { visible: true, turnId: traceId, traceId, status, result, session, trace };
|
||||
}
|
||||
|
||||
@@ -666,7 +666,8 @@ function currentWorkbenchTurnProjection(session, options = {}) {
|
||||
const traceId = safeTraceId(session?.lastTraceId) ?? null;
|
||||
if (!traceId) return null;
|
||||
const trace = options.trace?.traceId === traceId ? options.trace : traceSnapshotSync(options, traceId);
|
||||
const status = workbenchTurnProjectionStatus(trace);
|
||||
const result = traceScopedResult(options, traceId);
|
||||
const status = workbenchTurnStatusFromFacts({ trace, result });
|
||||
return {
|
||||
turnId: traceId,
|
||||
traceId,
|
||||
@@ -677,6 +678,20 @@ function currentWorkbenchTurnProjection(session, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function traceScopedResult(options, traceId) {
|
||||
const result = options.result?.traceId === traceId || (!options.result?.traceId && options.result) ? options.result : options.codeAgentChatResults?.get?.(traceId) ?? null;
|
||||
return result && typeof result === "object" ? result : null;
|
||||
}
|
||||
|
||||
function workbenchTurnStatusFromFacts({ trace = null, result = null } = {}) {
|
||||
const resultStatus = normalizeStatus(result?.status ?? result?.terminalStatus);
|
||||
if (TERMINAL_STATUSES.has(resultStatus) && !RUNNING_STATUSES.has(resultStatus)) return resultStatus;
|
||||
const traceStatus = workbenchTurnProjectionStatus(trace);
|
||||
if (traceStatus !== "unknown") return traceStatus;
|
||||
if (RUNNING_STATUSES.has(resultStatus)) return resultStatus;
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function workbenchTurnProjectionStatus(trace) {
|
||||
const status = normalizeStatus(trace?.status);
|
||||
if (status === "missing") return "unknown";
|
||||
|
||||
Reference in New Issue
Block a user