fix(v02): rebuild terminal user messages (#878)

Co-authored-by: Codex Agent <codex@hwlab.local>
This commit is contained in:
Lyon
2026-06-05 00:41:06 +08:00
committed by GitHub
parent d8771db433
commit 0c10056c22
2 changed files with 131 additions and 5 deletions
+104
View File
@@ -1652,6 +1652,110 @@ test("cloud api terminal workspace sync preserves saved messages and stores runn
}
});
test("cloud api terminal workspace sync rebuilds missing user message from saved preview", async () => {
const codeAgentChatResults = new Map();
const traceId = "trc_issue853_preview_only_trace";
const conversationId = "cnv_issue853_preview_only";
const sessionId = "ses_issue853_preview_only";
const threadId = "thread-issue-853-preview-only";
const userText = "请执行 hwpod profile list,并在最终回复里原样包含 HWLAB-853-FINAL-preview-only";
const env = {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
AGENTRUN_MGR_URL: "http://127.0.0.1:9",
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1"
};
const now = () => "2026-06-04T10:35:00.000Z";
const accessController = createAccessController({ env, now });
const server = createCloudApiServer({ env, accessController, codeAgentChatResults, now });
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
const aliceCreate = await postJson(port, "/v1/admin/users", { username: "alice-issue853-preview-only", password: "alice-pass" }, adminLogin.cookie);
assert.equal(aliceCreate.status, 201);
const aliceLogin = await postJson(port, "/auth/login", { username: "alice-issue853-preview-only", password: "alice-pass" });
const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(workspace.status, 200);
await accessController.recordAgentSessionOwner({
ownerUserId: aliceCreate.body.user.id,
sessionId,
projectId: "prj_device_pod_workbench",
agentId: "hwlab-code-agent",
status: "failed",
conversationId,
threadId,
traceId,
session: {
source: "workbench-status-sync",
sessionStatus: "failed",
lastTraceId: traceId,
messages: [
{ id: "msg_issue853_preview_agent", role: "agent", title: "Code Agent result", text: "HyueAPI 403 INSUFFICIENT_BALANCE", status: "failed", traceId, conversationId, sessionId, threadId, createdAt: "2026-06-04T10:34:59.000Z" }
],
messageCount: 1,
firstUserMessagePreview: userText,
valuesRedacted: true,
secretMaterialStored: false
}
});
codeAgentChatResults.set(traceId, {
status: "failed",
traceId,
conversationId,
sessionId,
threadId,
ownerUserId: aliceCreate.body.user.id,
error: { code: "provider_insufficient_balance", message: "HyueAPI 403 INSUFFICIENT_BALANCE" },
agentRun: { terminalStatus: "failed" },
runnerTrace: {
traceId,
status: "failed",
eventCount: 35,
events: [],
lastEvent: { label: "agentrun:result:failed", status: "failed", type: "result" }
},
session: { sessionId, conversationId, threadId, status: "failed" }
});
const selected = await patchJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}`, {
expectedRevision: 1,
selectedConversationId: conversationId,
selectedAgentSessionId: sessionId,
activeTraceId: traceId,
sessionStatus: "running",
messages: [],
updatedByClient: "test-suite"
}, aliceLogin.cookie);
assert.equal(selected.status, 200);
const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(restored.status, 200);
assert.equal(restored.body.workspace.activeTraceId, null);
const messages = restored.body.workspace.selectedConversation.messages;
assert.equal(messages.length, 2);
assert.equal(messages[0].role, "user");
assert.equal(messages[0].text, userText);
assert.equal(messages[0].traceId, traceId);
assert.equal(messages[1].role, "agent");
assert.equal(messages[1].status, "failed");
assert.equal(messages[1].text, "HyueAPI 403 INSUFFICIENT_BALANCE");
assert.equal(messages[1].runnerTrace.traceId, traceId);
assert.equal(messages[1].runnerTrace.eventCount, 35);
assert.equal(restored.body.workspace.selectedConversation.messageCount, 2);
assert.equal(restored.body.workspace.selectedConversation.firstUserMessagePreview, userText);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("access controller restores AgentRun mapping by Code Agent traceId", async () => {
const accessController = createAccessController({ now: () => "2026-06-01T00:00:00.000Z" });
await accessController.recordAgentSessionOwner({
+27 -5
View File
@@ -2593,13 +2593,14 @@ function isLegacyFinalResponseFallbackText(value) {
return textOr(value, "").trim() === "Code Agent 仍在处理,可以继续 steer 或等待 trace 完成。";
}
function mergeTerminalConversationMessages(existingMessages = [], result = {}, context = {}) {
const messages = Array.isArray(existingMessages) ? existingMessages.map(redactConversationMessage).filter(Boolean) : [];
const baseMessages = Array.isArray(existingMessages) ? existingMessages.map(redactConversationMessage).filter(Boolean) : [];
const traceId = safeTraceIdLocal(result.traceId ?? context.traceId);
const terminalStatus = terminalWorkbenchSessionStatus(result);
const assistantText = textOr(result.reply?.content ?? result.assistantText ?? result.message?.content ?? result.error?.message ?? result.userMessage, "");
const now = textOr(context.now, new Date().toISOString());
const firstUserPreview = firstUserPreviewFromMessages(baseMessages) ?? textOr(context.firstUserMessagePreview, null);
const messages = ensureTerminalUserMessage(baseMessages, { ...context, traceId, now, firstUserPreview });
const existingCount = numberOrNull(context.existingMessageCount) ?? messages.length;
const firstUserPreview = firstUserPreviewFromMessages(messages) ?? textOr(context.firstUserMessagePreview, null);
const runnerTrace = terminalRunnerTraceForConversationMessage(result, { ...context, traceId, terminalStatus });
const next = messages.map((message) => {
if (traceId && message.traceId === traceId && message.role === "agent") {
@@ -2634,12 +2635,33 @@ function mergeTerminalConversationMessages(existingMessages = [], result = {}, c
}));
addedAgentMessage = true;
}
const finalMessages = next.slice(-50);
return {
messages: next.slice(-50),
messageCount: existingCount + (addedAgentMessage ? 1 : 0),
firstUserMessagePreview: firstUserPreviewFromMessages(next) ?? firstUserPreview
messages: finalMessages,
messageCount: Math.max(existingCount + (addedAgentMessage ? 1 : 0), finalMessages.length),
firstUserMessagePreview: firstUserPreviewFromMessages(finalMessages) ?? firstUserPreview
};
}
function ensureTerminalUserMessage(messages = [], context = {}) {
if (messages.some((message) => message?.role === "user")) return messages;
const userText = textOr(context.firstUserPreview, "");
if (!userText) return messages;
const traceId = safeTraceIdLocal(context.traceId);
const userMessage = redactConversationMessage({
id: traceId ? `msg_${traceId.slice(4)}_user` : `msg_terminal_user_${messages.length}`,
role: "user",
title: "用户",
text: userText,
status: "sent",
traceId,
conversationId: context.conversationId,
sessionId: context.sessionId,
threadId: context.threadId,
createdAt: context.now,
updatedAt: context.now
});
return userMessage ? [userMessage, ...messages] : messages;
}
function terminalRunnerTraceForConversationMessage(result = {}, context = {}) {
const source = result.runnerTrace && typeof result.runnerTrace === "object" ? result.runnerTrace : null;
if (!source) return null;