Files
2026-07-13 13:39:12 +08:00

243 lines
8.1 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.
/**
* tests/integration/live-gemini-workload.test.ts
*
* Streaming live Gemini workload test. Reuses shared generators from
* liveGeminiShared.ts. For non-streaming, see live-gemini-nonstream.test.ts.
*/
import test from "node:test";
import assert from "node:assert/strict";
import {
skip,
randomInt,
sendAndValidate,
CASE_BUILDERS,
ensureTestEnvironment,
DELAY_BETWEEN_REQUESTS_MS,
type Message,
} from "./liveGeminiShared.ts";
test.before(async () => {
await ensureTestEnvironment();
});
// --------------------------------------------------------------------------
// Concurrent load — 5 parallel threads × 15 iterations
// --------------------------------------------------------------------------
test("[26] concurrent load — 2 threads × 2 iterations", { skip }, async () => {
const THREAD_COUNT = 2;
const SET_COUNT = 2;
const TOTAL_REQUESTS = THREAD_COUNT * SET_COUNT;
console.log(
`\n Concurrent load: ${THREAD_COUNT} threads × ${SET_COUNT} iterations = ${TOTAL_REQUESTS} requests`
);
const start = performance.now();
// Shared state for parallel-request detection across threads
const requestWindows: { cid: string; start: number; end: number }[] = [];
let parallelViolation: string | null = null;
const threadResults = await Promise.allSettled(
Array.from({ length: THREAD_COUNT }, (_, threadIdx) =>
(async () => {
const results: {
status: number;
duration: number;
tokens: number;
contentLength: number;
correlationId: string;
}[] = [];
for (let set = 1; set <= SET_COUNT; set++) {
if (parallelViolation) break; // another thread detected a violation
const idx = randomInt(0, CASE_BUILDERS.length - 1);
const tc = CASE_BUILDERS[idx];
const label = `t${threadIdx + 1}-i${set}: ${tc.name}`;
const requestStart = Date.now();
const r = await sendAndValidate(label, tc.build);
const requestEnd = Date.now();
// Check for parallel requests with the same correlationId
const cid = r.correlationId;
const myWindow = { cid, start: requestStart, end: requestEnd };
const siblings = requestWindows.filter((w) => w.cid === cid);
for (const s of siblings) {
if (myWindow.start < s.end && s.start < myWindow.end) {
parallelViolation =
`PARALLEL REQUEST DETECTED: cid=${cid.slice(0, 12)}… ` +
`window [${new Date(myWindow.start).toISOString().slice(11, 23)}${new Date(myWindow.end).toISOString().slice(11, 23)}] ` +
`overlaps with [${new Date(s.start).toISOString().slice(11, 23)}${new Date(s.end).toISOString().slice(11, 23)}]`;
break;
}
}
requestWindows.push(myWindow);
results.push({ ...r } as any);
}
return results;
})()
)
);
const totalDuration = performance.now() - start;
const fulfilled = threadResults.filter((r) => r.status === "fulfilled") as PromiseFulfilledResult<
{
status: number;
duration: number;
tokens: number;
contentLength: number;
correlationId: string;
}[]
>[];
const rejected = threadResults.filter((r) => r.status === "rejected") as PromiseRejectedResult[];
const allResults = fulfilled.flatMap((r) => r.value);
const totalTokens = allResults.reduce((sum, r) => sum + r.tokens, 0);
const avgDuration =
allResults.length > 0
? Math.round(allResults.reduce((s, r) => s + r.duration, 0) / allResults.length)
: 0;
console.log(
`\n Concurrent summary: ${fulfilled.length}/${THREAD_COUNT} threads completed | ` +
`${allResults.length}/${TOTAL_REQUESTS} requests succeeded | ` +
`${Math.round(totalDuration)}ms wall clock | ` +
`${avgDuration}ms avg per request | ` +
`${totalTokens} total tokens`
);
if (rejected.length > 0) {
for (const r of rejected) {
const msg = r.reason instanceof Error ? r.message : String(r.reason);
console.log(` THREAD FAILED: ${msg}`);
}
}
if (parallelViolation) {
console.log(`\n !! ${parallelViolation}`);
}
assert.ok(
fulfilled.length === THREAD_COUNT,
`expected all ${THREAD_COUNT} threads to complete, ${rejected.length} failed`
);
assert.ok(
allResults.length === TOTAL_REQUESTS,
`expected ${TOTAL_REQUESTS} total requests, got ${allResults.length}`
);
assert.ok(!parallelViolation, parallelViolation);
});
// --------------------------------------------------------------------------
// Single-thread sequential — 1 thread × 5 iterations
// --------------------------------------------------------------------------
test("[27] single-thread sequential — 1 thread × 5 iterations", { skip }, async () => {
const SET_COUNT = 5;
console.log(`\n Single-thread sequential: 1 thread × ${SET_COUNT} iterations`);
const start = performance.now();
const results: {
status: number;
duration: number;
tokens: number;
contentLength: number;
correlationId: string;
}[] = [];
for (let i = 1; i <= SET_COUNT; i++) {
const idx = randomInt(0, CASE_BUILDERS.length - 1);
const tc = CASE_BUILDERS[idx];
const label = `seq-i${i}: ${tc.name}`;
try {
if (i > 1) await new Promise((r) => setTimeout(r, DELAY_BETWEEN_REQUESTS_MS));
const r = await sendAndValidate(label, tc.build);
results.push(r);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
const ts = new Date().toISOString().slice(11, 23);
console.log(`${ts} ${label.padEnd(45)} FAILED: ${msg}`);
assert.fail(`iteration ${i} failed: ${msg}`);
}
}
const totalDuration = performance.now() - start;
const totalTokens = results.reduce((sum, r) => sum + r.tokens, 0);
const avgDuration =
results.length > 0
? Math.round(results.reduce((s, r) => s + r.duration, 0) / results.length)
: 0;
console.log(
`\n Sequential summary: ${results.length}/${SET_COUNT} succeeded | ` +
`${Math.round(totalDuration)}ms wall clock | ` +
`${avgDuration}ms avg per request | ` +
`${totalTokens} total tokens`
);
// Verify no duplicate correlation IDs (no retries created parallel entries)
const cids = results.map((r) => r.correlationId);
const uniqueCids = new Set(cids);
assert.equal(
uniqueCids.size,
cids.length,
`expected ${cids.length} unique correlation IDs, got ${uniqueCids.size} — duplicates detected`
);
assert.equal(results.length, SET_COUNT, `expected ${SET_COUNT} results, got ${results.length}`);
});
// ── Streaming-specific tests ────────────────────────────────────────────
test("[28] streaming: all payloads return content", { skip }, async () => {
const failures: string[] = [];
for (let i = 0; i < CASE_BUILDERS.length; i++) {
const tc = CASE_BUILDERS[i];
const label = `stream-${String(i + 1).padStart(2, "0")}: ${tc.name}`;
try {
const r = await sendAndValidate(label, tc.build);
if (r.contentLength === 0) {
failures.push(`${tc.name}: 0 bytes content`);
}
if (r.status !== 200) {
failures.push(`${tc.name}: HTTP ${r.status}`);
}
} catch (err) {
failures.push(`${tc.name}: ${err instanceof Error ? err.message : String(err)}`);
}
}
if (failures.length > 0) {
console.log(`\n Streaming failures (${failures.length}):`);
for (const f of failures) console.log(` ${f}`);
}
assert.equal(
failures.length,
0,
`${failures.length}/${CASE_BUILDERS.length} streaming payloads failed`
);
});
test("[29] streaming: correlation IDs are unique per request", { skip }, async () => {
const cids: string[] = [];
const count = 5;
for (let i = 0; i < count; i++) {
const tc = CASE_BUILDERS[i % CASE_BUILDERS.length];
const r = await sendAndValidate(`cid-${i + 1}: ${tc.name}`, tc.build);
cids.push(r.correlationId);
}
const unique = new Set(cids);
assert.equal(
unique.size,
count,
`expected ${count} unique CIDs, got ${unique.size}: ${cids.join(", ")}`
);
});