Files
wehub-resource-sync 426e9eeabd
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Waiting to run
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Waiting to run
Build Agent Image / build-and-push (push) Waiting to run
Chat shell gestures / Chat shell gesture + parity e2e (push) Waiting to run
Cloud Gateway Discord / Test (push) Waiting to run
Cloud Gateway Webhook / Test (push) Waiting to run
Cloud Tests / lint-and-types (push) Waiting to run
Cloud Tests / unit-tests (push) Waiting to run
Cloud Tests / integration-tests (push) Waiting to run
Cloud Tests / e2e-tests (push) Blocked by required conditions
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Apps Worker (Product 2) / Determine environment (push) Waiting to run
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Blocked by required conditions
Deploy Eliza Provisioning Worker / Determine environment (push) Waiting to run
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Blocked by required conditions
Dev Smoke / Classify changed paths (push) Waiting to run
Dev Smoke / bun run dev onboarding chat (push) Blocked by required conditions
Dev Smoke / Vite HMR dependency-level smoke (push) Blocked by required conditions
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Waiting to run
Publish @elizaos/example-code / check_npm (push) Waiting to run
Publish @elizaos/example-code / publish_npm (push) Blocked by required conditions
Publish @elizaos/plugin-elizacloud / verify_version (push) Waiting to run
Publish @elizaos/plugin-elizacloud / publish_npm (push) Blocked by required conditions
Sandbox Live Smoke / Sandbox live smoke (push) Waiting to run
Snap Build & Test / Build Snap (amd64) (push) Waiting to run
Snap Build & Test / Build Snap (arm64) (push) Waiting to run
supply-chain / sbom (push) Waiting to run
supply-chain / vulnerability-scan (push) Waiting to run
Build, Push & Deploy to Phala Cloud / build-and-push (push) Waiting to run
Test Packaging / Validate Packaging Configs (push) Waiting to run
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Waiting to run
Test Packaging / Pack & Test JS Tarballs (push) Waiting to run
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Waiting to run
UI Fixture E2E / ui-fixture-e2e (push) Waiting to run
UI Fixture E2E / fixture-e2e (push) Waiting to run
UI Story Gate / story-gate (push) Waiting to run
vault-ci / test (macos-latest) (push) Waiting to run
vault-ci / test (ubuntu-latest) (push) Waiting to run
vault-ci / test (windows-latest) (push) Waiting to run
vault-ci / app-core wiring tests (push) Waiting to run
verify-patches / verify patches/CHECKSUMS.sha256 (push) Waiting to run
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Waiting to run
Voice Benchmark Smoke / voice bench smoke summary (push) Blocked by required conditions
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Waiting to run
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Waiting to run
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Waiting to run
Test Packaging / Build & Test PyPI Package (push) Waiting to run
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

420 lines
13 KiB
JavaScript
Executable File

/**
* Tests for the GPU job queue (#14549): the pure state machine in
* queue-lib.mjs (job validation, claim order, backpressure, unreachable→drain
* transitions, result records), and the real worker process end-to-end — a
* spawned queue-worker.mjs consuming a temp jobs dir against a live in-test
* HTTP stub standing in for the llama-server endpoint (the GPU model itself is
* exercised on the owner-gated Linux acceptance run), including the
* drain-to-skip path against an unreachable port. Run with
* `bun test docker/certification`.
*/
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { spawn } from "node:child_process";
import fs from "node:fs/promises";
import http from "node:http";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
claimOrder,
createWorkerState,
decideEnqueue,
IMAGE_PLACEHOLDER,
makeJobId,
onServiceOk,
onServiceUnreachable,
parseJob,
QueueJobInvalidError,
resolveImagePlaceholders,
resultRecord,
shouldSkipJob,
} from "./queue-lib.mjs";
import { enqueueJob, QueueBackpressureError } from "./queue-worker.mjs";
const here = path.dirname(fileURLToPath(import.meta.url));
const workerPath = path.join(here, "queue-worker.mjs");
describe("parseJob", () => {
const models = ["ocr", "vlm"];
test("accepts a well-formed job", () => {
const job = parseJob(
JSON.stringify({ id: "a-1", model: "ocr", request: { messages: [] } }),
models,
);
expect(job.id).toBe("a-1");
});
test("rejects malformed JSON, missing fields, unknown models, unsafe paths", () => {
expect(() => parseJob("{nope", models)).toThrow(QueueJobInvalidError);
expect(() => parseJob('"str"', models)).toThrow(/not an object/);
expect(() => parseJob("{}", models)).toThrow(/missing id/);
expect(() =>
parseJob(
JSON.stringify({ id: "a/../b", model: "ocr", request: {} }),
models,
),
).toThrow(/unsafe characters/);
expect(() =>
parseJob(JSON.stringify({ id: "a", model: "nope", request: {} }), models),
).toThrow(/model must be one of ocr\|vlm/);
expect(() =>
parseJob(JSON.stringify({ id: "a", model: "ocr" }), models),
).toThrow(/missing request/);
expect(() =>
parseJob(
JSON.stringify({
id: "a",
model: "ocr",
request: {},
imagePath: "../etc/passwd",
}),
models,
),
).toThrow(/without \.\./);
expect(() =>
parseJob(
JSON.stringify({
id: "a",
model: "ocr",
request: {},
imagePath: "/abs",
}),
models,
),
).toThrow(/relative/);
});
});
describe("claim order and ids", () => {
test("claims oldest-first by timestamp-prefixed id, json files only", () => {
const names = [
"20260706T2-b.json",
"20260706T1-a.json",
"junk.tmp",
"20260706T3-c.json",
];
expect(claimOrder(names)).toEqual([
"20260706T1-a.json",
"20260706T2-b.json",
"20260706T3-c.json",
]);
});
test("makeJobId sorts by enqueue time", () => {
const early = makeJobId(Date.UTC(2026, 6, 6, 10, 0, 0), "aaa");
const late = makeJobId(Date.UTC(2026, 6, 6, 10, 0, 1), "aaa");
expect(early < late).toBe(true);
expect(early).toMatch(/^[0-9]+-aaa$/);
});
});
describe("backpressure", () => {
test("accepts under the cap, refuses at the cap with a reason", () => {
expect(decideEnqueue(0, 2)).toEqual({ accept: true });
expect(decideEnqueue(1, 2)).toEqual({ accept: true });
expect(decideEnqueue(2, 2).accept).toBe(false);
expect(decideEnqueue(2, 2).reason).toMatch(/2 pending >= max 2/);
});
});
describe("unreachable → drain state machine", () => {
test("stamps the outage start, drains only past the window, resets on contact", () => {
const drainAfterMs = 1000;
let state = createWorkerState();
expect(shouldSkipJob(state)).toBe(false);
state = onServiceUnreachable(state, 10_000, drainAfterMs);
expect(state.unreachableSince).toBe(10_000);
expect(state.draining).toBe(false);
state = onServiceUnreachable(state, 10_500, drainAfterMs);
expect(state.unreachableSince).toBe(10_000); // outage start is preserved
expect(state.draining).toBe(false);
state = onServiceUnreachable(state, 11_000, drainAfterMs);
expect(state.draining).toBe(true);
expect(shouldSkipJob(state)).toBe(true);
// Draining latches until the service actually answers again.
state = onServiceUnreachable(state, 11_001, drainAfterMs);
expect(state.draining).toBe(true);
state = onServiceOk();
expect(state).toEqual({ unreachableSince: null, draining: false });
});
});
describe("resultRecord", () => {
const job = { id: "j1", model: "ocr" };
test("ok carries duration + response; failed/skipped carry a reason", () => {
const ok = resultRecord(
job,
{ status: "ok", durationMs: 42, response: { x: 1 } },
"t",
);
expect(ok).toEqual({
schema: 1,
id: "j1",
model: "ocr",
status: "ok",
completedAt: "t",
durationMs: 42,
response: { x: 1 },
});
const skipped = resultRecord(
job,
{ status: "skipped", reason: "drained" },
"t",
);
expect(skipped.reason).toBe("drained");
expect(skipped.response).toBeUndefined();
expect(() => resultRecord(job, { status: "wat" }, "t")).toThrow(
/unknown outcome/,
);
});
});
describe("resolveImagePlaceholders", () => {
test("replaces only the placeholder url, without mutating the input", () => {
const request = {
messages: [
{
role: "user",
content: [
{ type: "image_url", image_url: { url: IMAGE_PLACEHOLDER } },
{ type: "text", text: `keep ${IMAGE_PLACEHOLDER} in text` },
{ type: "image_url", image_url: { url: "data:already/inline" } },
],
},
],
};
const resolved = resolveImagePlaceholders(
request,
"data:image/png;base64,AA==",
);
expect(resolved.messages[0].content[0].image_url.url).toBe(
"data:image/png;base64,AA==",
);
expect(resolved.messages[0].content[1].text).toContain(IMAGE_PLACEHOLDER);
expect(resolved.messages[0].content[2].image_url.url).toBe(
"data:already/inline",
);
expect(request.messages[0].content[0].image_url.url).toBe(
IMAGE_PLACEHOLDER,
);
});
});
/** Spawn the real worker in --once mode and wait for exit. */
function runWorkerOnce(jobsRoot, serviceUrl, extraArgs = []) {
return new Promise((resolve, reject) => {
const child = spawn(
process.execPath,
[
workerPath,
"--jobs",
jobsRoot,
"--service",
`ocr=${serviceUrl}`,
"--service",
`vlm=${serviceUrl}`,
"--poll-ms",
"50",
"--once",
...extraArgs,
],
{ stdio: ["ignore", "pipe", "pipe"] },
);
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk;
});
child.stderr.on("data", (chunk) => {
stderr += chunk;
});
child.on("error", reject);
child.on("exit", (code) => resolve({ code, stdout, stderr }));
});
}
describe("worker end-to-end (real process, stub llama-server)", () => {
let server;
let baseUrl;
const seen = [];
beforeAll(async () => {
server = http.createServer((req, res) => {
if (req.url === "/health") {
res.writeHead(200, { "content-type": "application/json" });
res.end('{"status":"ok"}');
return;
}
let body = "";
req.on("data", (chunk) => {
body += chunk;
});
req.on("end", () => {
const parsed = JSON.parse(body);
seen.push({ url: req.url, body: parsed });
if (parsed.messages?.[0]?.content === "explode") {
res.writeHead(500, { "content-type": "text/plain" });
res.end("kaboom");
return;
}
res.writeHead(200, { "content-type": "application/json" });
res.end(
JSON.stringify({
choices: [
{ message: { role: "assistant", content: "stub-answer" } },
],
usage: { prompt_tokens: 10, completion_tokens: 2 },
}),
);
});
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
baseUrl = `http://127.0.0.1:${server.address().port}`;
});
afterAll(() => {
server.close();
});
test("consumes enqueued jobs, calls the service, writes ok/failed results beside them", async () => {
const jobsRoot = await fs.mkdtemp(path.join(os.tmpdir(), "eliza-queue-"));
const okId = await enqueueJob(jobsRoot, {
model: "ocr",
request: { messages: [{ role: "user", content: "read this" }] },
});
const failId = await enqueueJob(jobsRoot, {
model: "vlm",
request: { messages: [{ role: "user", content: "explode" }] },
});
// Invalid job file dropped straight into pending/ (bypasses enqueueJob).
await fs.writeFile(
path.join(jobsRoot, "pending", "zzz-invalid.json"),
"{nope",
"utf8",
);
const { code, stderr } = await runWorkerOnce(jobsRoot, baseUrl);
expect(stderr).toBe("");
expect(code).toBe(0);
const okResult = JSON.parse(
await fs.readFile(path.join(jobsRoot, "results", `${okId}.json`), "utf8"),
);
expect(okResult.status).toBe("ok");
expect(okResult.response.choices[0].message.content).toBe("stub-answer");
expect(okResult.durationMs).toBeGreaterThanOrEqual(0);
const failResult = JSON.parse(
await fs.readFile(
path.join(jobsRoot, "results", `${failId}.json`),
"utf8",
),
);
expect(failResult.status).toBe("failed");
expect(failResult.reason).toMatch(/http 500: kaboom/);
const invalidResult = JSON.parse(
await fs.readFile(
path.join(jobsRoot, "results", "zzz-invalid.json"),
"utf8",
),
);
expect(invalidResult.status).toBe("failed");
expect(invalidResult.reason).toMatch(/not valid JSON/);
// Consumed jobs moved to done/, pending drained.
expect((await fs.readdir(path.join(jobsRoot, "pending"))).length).toBe(0);
expect((await fs.readdir(path.join(jobsRoot, "done"))).sort().length).toBe(
3,
);
await fs.rm(jobsRoot, { recursive: true, force: true });
}, 30_000);
test("inlines imagePath as a data URI at the queue:image placeholder", async () => {
const jobsRoot = await fs.mkdtemp(
path.join(os.tmpdir(), "eliza-queue-img-"),
);
await fs.mkdir(path.join(jobsRoot, "images"), { recursive: true });
// 1x1 PNG.
const png = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
"base64",
);
await fs.writeFile(path.join(jobsRoot, "images", "shot.png"), png);
seen.length = 0;
const id = await enqueueJob(jobsRoot, {
model: "ocr",
imagePath: "images/shot.png",
request: {
messages: [
{
role: "user",
content: [
{ type: "image_url", image_url: { url: IMAGE_PLACEHOLDER } },
],
},
],
},
});
const { code } = await runWorkerOnce(jobsRoot, baseUrl);
expect(code).toBe(0);
const result = JSON.parse(
await fs.readFile(path.join(jobsRoot, "results", `${id}.json`), "utf8"),
);
expect(result.status).toBe("ok");
const sent = seen[0].body.messages[0].content[0].image_url.url;
expect(sent).toBe(`data:image/png;base64,${png.toString("base64")}`);
await fs.rm(jobsRoot, { recursive: true, force: true });
}, 30_000);
test("enqueueJob applies max-pending backpressure", async () => {
const jobsRoot = await fs.mkdtemp(
path.join(os.tmpdir(), "eliza-queue-bp-"),
);
const limits = { maxPending: 2 };
await enqueueJob(jobsRoot, { model: "ocr", request: {} }, limits);
await enqueueJob(jobsRoot, { model: "ocr", request: {} }, limits);
await expect(
enqueueJob(jobsRoot, { model: "ocr", request: {} }, limits),
).rejects.toThrow(QueueBackpressureError);
await fs.rm(jobsRoot, { recursive: true, force: true });
});
});
describe("worker drain-to-skip (unreachable service, real process)", () => {
test("past the drain window every pending job becomes an honest skipped record", async () => {
const jobsRoot = await fs.mkdtemp(
path.join(os.tmpdir(), "eliza-queue-drain-"),
);
const ids = [];
for (let i = 0; i < 3; i++) {
ids.push(
await enqueueJob(jobsRoot, { model: "ocr", request: { messages: [] } }),
);
}
// Port 1 on localhost: connection refused immediately, nobody listens.
const { code } = await runWorkerOnce(jobsRoot, "http://127.0.0.1:1", [
"--drain-after-ms",
"300",
"--request-timeout-ms",
"500",
]);
expect(code).toBe(0);
for (const id of ids) {
const result = JSON.parse(
await fs.readFile(path.join(jobsRoot, "results", `${id}.json`), "utf8"),
);
expect(result.status).toBe("skipped");
expect(result.reason).toMatch(/unreachable past 300ms — drained/);
}
expect((await fs.readdir(path.join(jobsRoot, "pending"))).length).toBe(0);
await fs.rm(jobsRoot, { recursive: true, force: true });
}, 30_000);
});