Files
2026-06-12 01:17:11 +08:00

109 lines
4.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import test from "node:test";
import type { ActivityRef } from "../src/api/client.ts";
function buildMutableActivityRef(initial: number): { ref: ActivityRef; update: (at: number) => void } {
const ref: ActivityRef = {
lastActivityAt: initial,
lastActivityIso: new Date(initial).toISOString(),
waitingFor: "test",
lastEventLabel: null
};
return {
ref,
update: (at: number) => {
ref.lastActivityAt = at;
ref.lastActivityIso = new Date(at).toISOString();
}
};
}
function bindSignalFetch(): typeof fetch {
return (async (_input, init) => {
return await new Promise<Response>((_, reject) => {
const signal = init?.signal;
if (signal) {
if (signal.aborted) reject(new DOMException("aborted", "AbortError"));
else signal.addEventListener("abort", () => reject(new DOMException("aborted", "AbortError")), { once: true });
}
});
}) as typeof fetch;
}
test("fetchJson 总超时:未传 activityRef 时 timeoutMs 走总窗口", { timeout: 5000 }, async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = bindSignalFetch();
try {
const { fetchJson } = await import("../src/api/client.ts");
const startedAt = Date.now();
const result = await fetchJson("/v1/agent/chat", { timeoutMs: 200, timeoutName: "test-total" });
const elapsed = Date.now() - startedAt;
assert.equal(result.ok, false, "fetch should fail when no activity is supplied");
assert.match(result.error ?? "", /超时/u, `expected timeout error, got: ${result.error}`);
assert.ok(elapsed < 1500, `fetchJson should abort near 200ms but took ${elapsed}ms`);
} finally {
globalThis.fetch = originalFetch;
}
});
test("fetchJson inactivityactivityRef 每 100ms 推一次活动,stop 后 ~2s abort", { timeout: 15000 }, async () => {
const { ref, update } = buildMutableActivityRef(Date.now());
const originalFetch = globalThis.fetch;
globalThis.fetch = bindSignalFetch();
const interval = setInterval(() => {
update(Date.now());
}, 100);
try {
const { fetchJson } = await import("../src/api/client.ts");
const startedAt = Date.now();
const promise = fetchJson("/v1/agent/chat", { timeoutMs: 2000, timeoutName: "test-inactivity", activityRef: () => ref });
setTimeout(() => clearInterval(interval), 1800);
const result = await promise;
const elapsed = Date.now() - startedAt;
assert.equal(result.ok, false, "fetch should fail once activity stops");
assert.match(result.error ?? "", /无新活动/u, `expected inactivity-timeout error, got: ${result.error}`);
assert.ok(elapsed >= 1800, `should not abort before 1800ms with continuous activity, got ${elapsed}ms`);
assert.ok(elapsed < 6000, `should abort ~1-2s after activity stops, got ${elapsed}ms`);
} finally {
clearInterval(interval);
globalThis.fetch = originalFetch;
}
});
test("fetchJson 无 total timeout10s 持续活动窗口内不会被总上限打掉", { timeout: 30000 }, async () => {
// HWLAB #795 PR feedback + #802: the per-request inactivity window is the
// SOLE abort signal. There is no client-side total-timeout ceiling on
// waitForAgentResult / pollRunnerTrace. A continuous 200ms-cadence
// activity stream must keep a fetchJson request alive well past the
// 3s inactivity window set on the test, and the test only ends when
// the activity stream stops.
const { ref, update } = buildMutableActivityRef(Date.now());
const originalFetch = globalThis.fetch;
globalThis.fetch = bindSignalFetch();
const interval = setInterval(() => {
update(Date.now());
}, 200);
try {
const { fetchJson } = await import("../src/api/client.ts");
const startedAt = Date.now();
const promise = fetchJson("/v1/agent/chat/result/abc", { timeoutMs: 3000, timeoutName: "trace-poll", activityRef: () => ref });
setTimeout(() => clearInterval(interval), 10_000);
const result = await promise;
const elapsed = Date.now() - startedAt;
assert.equal(result.ok, false, "fetch should fail once activity stops");
assert.ok(
elapsed >= 10_000,
`continuous 200ms activity must keep the request alive for the full 10s; a total cap or wall-clock shrink would abort earlier; got ${elapsed}ms`
);
assert.match(result.error ?? "", /无新活动/u, `expected inactivity-timeout error, got: ${result.error}`);
} finally {
clearInterval(interval);
globalThis.fetch = originalFetch;
}
});