






















@@ -3,6 +3,7 @@ import { randomBytes, randomUUID } from "node:crypto";
33import fs from "node:fs/promises";
44import path from "node:path";
55import { afterEach, describe, expect, it } from "vitest";
6+import type { EventFrame } from "../../packages/gateway-protocol/src/index.js";
67import { isLiveTestEnabled } from "../agents/live-test-helpers.js";
78import type { OpenClawConfig } from "../config/config.js";
89import { extractFirstTextBlock } from "../shared/chat-message-content.js";
@@ -43,6 +44,27 @@ function restoreEnv(snapshot: LiveEnvSnapshot): void {
4344restoreLiveEnv(snapshot);
4445}
454647+async function removeLiveTempDir(dir: string): Promise<void> {
48+let lastError: unknown;
49+for (let attempt = 0; attempt < 100; attempt += 1) {
50+try {
51+await fs.rm(dir, { recursive: true, force: true });
52+return;
53+} catch (error) {
54+lastError = error;
55+const code = (error as { code?: unknown } | null)?.code;
56+if (code !== "EBUSY" && code !== "ENOTEMPTY" && code !== "EPERM" && code !== "EACCES") {
57+throw error;
58+}
59+await new Promise((resolve) => {
60+setTimeout(resolve, 100);
61+});
62+}
63+}
64+await fs.rm(dir, { recursive: true, force: true });
65+void lastError;
66+}
67+4668async function writeLiveGatewayConfig(params: {
4769configPath: string;
4870modelKey: string;
@@ -72,6 +94,7 @@ async function writeLiveGatewayConfig(params: {
7294}
73957496async function connectGatewayClient(params: {
97+onEvent?: (event: EventFrame) => void;
7598url: string;
7699token: string;
77100}): Promise<GatewayClient> {
@@ -86,6 +109,7 @@ async function connectGatewayClient(params: {
86109requestTimeoutMs: 60_000,
87110tickWatchTimeoutMs: AGENT_REQUEST_TIMEOUT_MS + 120_000,
88111clientDisplayName: "trajectory-live",
112+onEvent: params.onEvent,
89113});
90114return client;
91115}
@@ -141,79 +165,87 @@ async function waitForPath(filePath: string, timeoutMs = 60_000): Promise<void>
141165throw new Error(`timed out waiting for ${filePath}`);
142166}
143167144-function extractAssistantTexts(messages: unknown[]): string[] {
145-const texts: string[] = [];
146-for (const entry of messages) {
147- if (!entry || typeof entry !== "object") {
148- continue;
149- }
150- if ((entry as { role?: unknown }).role !== "assistant") {
151- continue;
152-}
153-const text = extractFirstTextBlock(entry);
154-if (typeof text === "string" && text.trim().length > 0) {
155-texts.push(text);
168+async function waitForChatFinalText(params: {
169+events: EventFrame[];
170+runId: string;
171+timeoutMs: number;
172+}): Promise<string> {
173+const deadline = Date.now() + params.timeoutMs;
174+while (Date.now() < deadline) {
175+const text = params.events
176+ .map((event) => extractChatFinalText(event, params.runId))
177+ .find(Boolean);
178+if (text) {
179+return text;
156180}
181+await new Promise((resolve) => {
182+setTimeout(resolve, 50);
183+});
157184}
158-return texts;
185+throw new Error(`timed out waiting for chat final for ${params.runId}`);
159186}
160187161-function formatAssistantTextPreview(texts: string[], maxChars = 800): string {
162-const combined = texts.join("\n\n").trim();
163-if (!combined) {
164-return "<none>";
188+function extractChatFinalText(event: EventFrame, runId: string): string | undefined {
189+if (event.event !== "chat") {
190+return undefined;
191+}
192+const payload = event.payload;
193+if (!payload || typeof payload !== "object") {
194+return undefined;
165195}
166-return combined.length > maxChars ? `${combined.slice(0, maxChars)}...` : combined;
196+const record = payload as Record<string, unknown>;
197+if (record.runId !== runId || record.state !== "final") {
198+return undefined;
199+}
200+const message = record.message;
201+if (!message || typeof message !== "object") {
202+return undefined;
203+}
204+const messageRecord = message as Record<string, unknown>;
205+if (typeof messageRecord.text === "string" && messageRecord.text.trim()) {
206+return messageRecord.text;
207+}
208+const content = Array.isArray(messageRecord.content) ? messageRecord.content : [];
209+return content
210+.map((entry) =>
211+entry && typeof entry === "object" ? (entry as Record<string, unknown>).text : undefined,
212+)
213+.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0)
214+.join("\n")
215+.trim();
167216}
168217169-async function waitForAssistantText(params: {
170-client: GatewayClient;
171-contains: string;
172-sessionKey: string;
173-timeoutMs?: number;
174-}): Promise<string> {
175-const timeoutMs = params.timeoutMs ?? 60_000;
218+async function approveTrajectoryExport(client: GatewayClient): Promise<string> {
176219const startedAt = Date.now();
177-while (Date.now() - startedAt < timeoutMs) {
178-const history: { messages?: unknown[] } = await params.client.request("chat.history", {
179-sessionKey: params.sessionKey,
180-limit: 24,
181-});
182-const assistantTexts = extractAssistantTexts(history.messages ?? []);
183-const matched = assistantTexts.find((text) => text.includes(params.contains));
184-if (matched) {
185-return matched;
220+let approval:
221+| {
222+id?: string;
223+request?: {
224+command?: string;
225+};
226+}
227+| undefined;
228+while (Date.now() - startedAt < 60_000) {
229+const approvals = (await client.request(
230+"exec.approval.list",
231+{},
232+{ timeoutMs: 10_000 },
233+)) as Array<{
234+id?: string;
235+request?: {
236+command?: string;
237+};
238+}>;
239+approval = approvals.find((entry) =>
240+entry.request?.command?.includes("sessions export-trajectory"),
241+);
242+if (approval) {
243+break;
186244}
187245await new Promise((resolve) => {
188246setTimeout(resolve, 500);
189247});
190248}
191-192-const finalHistory: { messages?: unknown[] } = await params.client.request("chat.history", {
193-sessionKey: params.sessionKey,
194-limit: 24,
195-});
196-throw new Error(
197-`timed out waiting for assistant text containing ${params.contains}: ${formatAssistantTextPreview(
198- extractAssistantTexts(finalHistory.messages ?? []),
199- )}`,
200-);
201-}
202-203-async function approveTrajectoryExport(client: GatewayClient): Promise<string> {
204-const approvals = (await client.request(
205-"exec.approval.list",
206-{},
207-{ timeoutMs: 10_000 },
208-)) as Array<{
209-id?: string;
210-request?: {
211-command?: string;
212-};
213-}>;
214-const approval = approvals.find((entry) =>
215-entry.request?.command?.includes("sessions export-trajectory"),
216-);
217249expect(typeof approval?.id).toBe("string");
218250expect(approval?.request?.command).toContain("sessions export-trajectory");
219251if (!approval?.id) {
@@ -247,7 +279,7 @@ describeLive("gateway live trajectory export", () => {
247279cleanup.push(async () => {
248280restoreEnv(previousEnv);
249281clearRuntimeConfigSnapshot();
250-await fs.rm(tempDir, { recursive: true, force: true });
282+await removeLiveTempDir(tempDir);
251283});
252284253285const stateDir = path.join(tempDir, "state");
@@ -294,9 +326,13 @@ describeLive("gateway live trajectory export", () => {
294326await server.close();
295327});
296328329+const gatewayEvents: EventFrame[] = [];
297330const client = await connectGatewayClient({
298331url: `ws://127.0.0.1:${port}`,
299332 token,
333+onEvent: (event) => {
334+gatewayEvents.push(event);
335+},
300336});
301337logLiveStep("client-connected");
302338cleanup.push(async () => {
@@ -341,10 +377,9 @@ describeLive("gateway live trajectory export", () => {
341377const finalText =
342378typeof exportResponse?.message === "object"
343379 ? extractFirstTextBlock(exportResponse.message)
344- : await waitForAssistantText({
345- client,
346- sessionKey,
347-contains: "Trajectory exports can include",
380+ : await waitForChatFinalText({
381+events: gatewayEvents,
382+runId: exportRunId,
348383timeoutMs: 60_000,
349384});
350385expect(finalText).toContain("Trajectory exports can include");
@@ -353,9 +388,7 @@ describeLive("gateway live trajectory export", () => {
353388logLiveStep("export:approved", { approvalId });
354389await waitForPath(path.join(bundleDir, "events.jsonl"), 60_000);
355390logLiveStep("export:done", { finalText });
356-if (finalText) {
357-expect(finalText).toContain("Approve once");
358-}
391+expect(finalText).toContain("Approve once");
359392const bundleNames = await listDirectoryNames(bundleDir);
360393for (const expectedName of [
361394"artifacts.json",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。