Skip to content

Commit a619518

Browse files
committed
fix(e2e): keep cross-os response reads timed
1 parent ea0b0ad commit a619518

2 files changed

Lines changed: 80 additions & 17 deletions

File tree

scripts/openclaw-cross-os-release-checks.ts

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2554,6 +2554,7 @@ async function configureDiscordSmoke(params) {
25542554
export async function readBoundedCrossOsResponseText(
25552555
response: Response,
25562556
maxChars = CROSS_OS_FETCH_BODY_MAX_CHARS,
2557+
options: { signal?: AbortSignal | null } = {},
25572558
): Promise<string> {
25582559
if (!response.body) {
25592560
return "";
@@ -2563,10 +2564,11 @@ export async function readBoundedCrossOsResponseText(
25632564
const decoder = new TextDecoder();
25642565
let text = "";
25652566
let truncated = false;
2567+
let aborted = false;
25662568

25672569
try {
25682570
while (text.length <= maxChars) {
2569-
const { done, value } = await reader.read();
2571+
const { done, value } = await readCrossOsResponseChunk(reader, options.signal);
25702572
if (done) {
25712573
text += decoder.decode();
25722574
break;
@@ -2579,8 +2581,11 @@ export async function readBoundedCrossOsResponseText(
25792581
break;
25802582
}
25812583
}
2584+
} catch (error) {
2585+
aborted = options.signal?.aborted === true;
2586+
throw error;
25822587
} finally {
2583-
if (truncated) {
2588+
if (truncated || aborted) {
25842589
await reader.cancel().catch(() => undefined);
25852590
} else {
25862591
reader.releaseLock();
@@ -2590,6 +2595,36 @@ export async function readBoundedCrossOsResponseText(
25902595
return truncated ? `${text}\n[truncated]` : text;
25912596
}
25922597

2598+
function readCrossOsResponseChunk(
2599+
reader: ReadableStreamDefaultReader<Uint8Array>,
2600+
signal?: AbortSignal | null,
2601+
): Promise<ReadableStreamReadResult<Uint8Array>> {
2602+
if (!signal) {
2603+
return reader.read();
2604+
}
2605+
if (signal.aborted) {
2606+
throw crossOsAbortReason(signal);
2607+
}
2608+
return new Promise((resolveRead, rejectRead) => {
2609+
const onAbort = () => rejectRead(crossOsAbortReason(signal));
2610+
signal.addEventListener("abort", onAbort, { once: true });
2611+
reader
2612+
.read()
2613+
.then(resolveRead, rejectRead)
2614+
.finally(() => {
2615+
signal.removeEventListener("abort", onAbort);
2616+
});
2617+
});
2618+
}
2619+
2620+
function crossOsAbortReason(signal: AbortSignal): Error {
2621+
const reason = signal.reason;
2622+
if (reason instanceof Error) {
2623+
return reason;
2624+
}
2625+
return new Error(typeof reason === "string" ? reason : "The operation was aborted.");
2626+
}
2627+
25932628
export function dashboardHtmlMarkerStatus(html: string): {
25942629
app: boolean;
25952630
ready: boolean;
@@ -2647,11 +2682,12 @@ async function waitForDiscordMessage(params) {
26472682
let response;
26482683
let text;
26492684
try {
2685+
const init = buildDiscordFetchInit(params.token);
26502686
response = await fetch(
26512687
`https://discord.com/api/v10/channels/${params.channelId}/messages?limit=20`,
2652-
buildDiscordFetchInit(params.token),
2688+
init,
26532689
);
2654-
text = await readBoundedCrossOsResponseText(response);
2690+
text = await readBoundedCrossOsResponseText(response, undefined, { signal: init.signal });
26552691
} catch {
26562692
await sleep(2_000);
26572693
continue;
@@ -2680,20 +2716,21 @@ export function buildDiscordFetchInit(token, init = {}) {
26802716
}
26812717

26822718
async function postDiscordMessage(params) {
2719+
const init = buildDiscordFetchInit(params.token, {
2720+
method: "POST",
2721+
headers: {
2722+
"Content-Type": "application/json",
2723+
},
2724+
body: JSON.stringify({
2725+
content: params.content,
2726+
flags: 4096,
2727+
}),
2728+
});
26832729
const response = await fetch(
26842730
`https://discord.com/api/v10/channels/${params.channelId}/messages`,
2685-
buildDiscordFetchInit(params.token, {
2686-
method: "POST",
2687-
headers: {
2688-
"Content-Type": "application/json",
2689-
},
2690-
body: JSON.stringify({
2691-
content: params.content,
2692-
flags: 4096,
2693-
}),
2694-
}),
2731+
init,
26952732
);
2696-
const text = await readBoundedCrossOsResponseText(response);
2733+
const text = await readBoundedCrossOsResponseText(response, undefined, { signal: init.signal });
26972734
if (!response.ok) {
26982735
throw new Error(`Failed to post Discord smoke message: ${text}`);
26992736
}
@@ -3525,10 +3562,11 @@ async function runDashboardSmoke(params) {
35253562
attempt += 1;
35263563
logStream.write(`${new Date().toISOString()} attempt=${attempt} url=${dashboardUrl}\n`);
35273564
try {
3565+
const signal = AbortSignal.timeout(CROSS_OS_DASHBOARD_FETCH_TIMEOUT_MS);
35283566
const response = await fetch(dashboardUrl, {
3529-
signal: AbortSignal.timeout(CROSS_OS_DASHBOARD_FETCH_TIMEOUT_MS),
3567+
signal,
35303568
});
3531-
const html = await readBoundedCrossOsResponseText(response);
3569+
const html = await readBoundedCrossOsResponseText(response, undefined, { signal });
35323570
const markers = dashboardHtmlMarkerStatus(html);
35333571
const assetUrls = resolveDashboardAssetUrls(dashboardUrl, html);
35343572
if (response.ok && markers.ready) {

test/scripts/openclaw-cross-os-release-checks.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,31 @@ describe("scripts/openclaw-cross-os-release-checks", () => {
166166
expect(CROSS_OS_FETCH_BODY_MAX_CHARS).toBeGreaterThan(1024);
167167
});
168168

169+
it("keeps cross-OS fetch timeouts active while reading response bodies", async () => {
170+
let canceled = false;
171+
const abortController = new AbortController();
172+
const response = new Response(
173+
new ReadableStream<Uint8Array>({
174+
start(controller) {
175+
controller.enqueue(new TextEncoder().encode("partial"));
176+
},
177+
cancel() {
178+
canceled = true;
179+
},
180+
}),
181+
);
182+
183+
const text = readBoundedCrossOsResponseText(response, 1024, {
184+
signal: abortController.signal,
185+
});
186+
187+
await delay(0);
188+
abortController.abort(new Error("cross-os body timed out"));
189+
190+
await expect(text).rejects.toThrow("cross-os body timed out");
191+
expect(canceled).toBe(true);
192+
});
193+
169194
it("requires dashboard root markers and same-origin asset URLs", () => {
170195
const html = [
171196
"<title>OpenClaw Control</title>",

0 commit comments

Comments
 (0)