426e9eeabd
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
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
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) Has been cancelled
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) Has been cancelled
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) Has been cancelled
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) Has been cancelled
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) Has been cancelled
161 lines
5.9 KiB
TypeScript
161 lines
5.9 KiB
TypeScript
// Unit tests for the visual-QA analyzer: colour fractions, dominant palette,
|
|
// change-metric, and the pure expectation evaluator, all on synthetic
|
|
// sharp-generated fixtures so they run deterministically in CI without a real
|
|
// screenshot. OCR must remain packaged: the analyzer may prefer a system
|
|
// tesseract binary, but the repo dependency fallback is part of the contract.
|
|
import { mkdtempSync, readFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import sharp from "sharp";
|
|
import { afterAll, describe, expect, it } from "vitest";
|
|
import {
|
|
analyzeScreenshot,
|
|
changeMetric,
|
|
colorFractions,
|
|
dominantPalette,
|
|
evaluateExpectation,
|
|
} from "./visual-qa.mjs";
|
|
|
|
const dir = mkdtempSync(join(tmpdir(), "visual-qa-"));
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const appPackageJson = JSON.parse(
|
|
readFileSync(resolve(__dirname, "../../package.json"), "utf8"),
|
|
);
|
|
const rootPackageJson = JSON.parse(
|
|
readFileSync(resolve(__dirname, "../../../../package.json"), "utf8"),
|
|
);
|
|
const rootLockfile = readFileSync(
|
|
resolve(__dirname, "../../../../bun.lock"),
|
|
"utf8",
|
|
);
|
|
const solid = async (name: string, r: number, g: number, b: number) => {
|
|
const p = join(dir, name);
|
|
await sharp({
|
|
create: { width: 120, height: 120, channels: 3, background: { r, g, b } },
|
|
})
|
|
.png()
|
|
.toFile(p);
|
|
return p;
|
|
};
|
|
|
|
afterAll(() => {
|
|
// best-effort temp cleanup; leaving fixtures on failure aids debugging
|
|
});
|
|
|
|
describe("colorFractions", () => {
|
|
it("reads a solid blue frame as overwhelmingly blue, not orange", async () => {
|
|
const c = await colorFractions(await solid("blue.png", 30, 60, 200));
|
|
expect(c.blue_fraction).toBeGreaterThan(0.9);
|
|
expect(c.orange_fraction).toBe(0);
|
|
});
|
|
it("reads a brand-orange frame as orange, not blue", async () => {
|
|
const c = await colorFractions(await solid("orange.png", 220, 110, 40));
|
|
expect(c.orange_fraction).toBeGreaterThan(0.9);
|
|
expect(c.blue_fraction).toBe(0);
|
|
});
|
|
it("reads a grey frame as near-neutral", async () => {
|
|
const c = await colorFractions(await solid("grey.png", 180, 180, 182));
|
|
expect(c.neutral_fraction).toBeGreaterThan(0.9);
|
|
expect(c.blue_fraction).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("dominantPalette", () => {
|
|
it("returns the fill colour as the dominant bucket", async () => {
|
|
const pal = await dominantPalette(await solid("red.png", 240, 16, 16));
|
|
expect(pal[0].fraction).toBeGreaterThan(0.9);
|
|
expect(pal[0].rgb[0]).toBeGreaterThan(200);
|
|
});
|
|
});
|
|
|
|
describe("changeMetric", () => {
|
|
it("reports ~0 change for identical frames and ~full change for different ones", async () => {
|
|
const grey = await solid("g1.png", 180, 180, 180);
|
|
const greySame = await solid("g2.png", 180, 180, 180);
|
|
const blue = await solid("b1.png", 20, 40, 200);
|
|
expect((await changeMetric(grey, greySame)).changed_fraction).toBe(0);
|
|
const diff = await changeMetric(grey, blue);
|
|
expect(diff.changed_fraction).toBeGreaterThan(0.9);
|
|
expect(diff.changed_bbox_norm).not.toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("evaluateExpectation (pure gate logic)", () => {
|
|
const neutral = { blue_fraction: 0, orange_fraction: 0, neutral_fraction: 1 };
|
|
it("passes when required text is present and no blue", () => {
|
|
const r = evaluateExpectation({
|
|
text: "Sign in to Eliza Cloud\nAsk me anything",
|
|
colors: neutral,
|
|
expect: {
|
|
require_text: ["Eliza", "Sign in"],
|
|
forbid_text: ["undefined"],
|
|
},
|
|
});
|
|
expect(r.verdict).toBe("pass");
|
|
});
|
|
it("fails when required text is missing", () => {
|
|
const r = evaluateExpectation({
|
|
text: "some other screen",
|
|
colors: neutral,
|
|
expect: { require_text: ["Sign in to Eliza Cloud"] },
|
|
});
|
|
expect(r.verdict).toBe("fail");
|
|
expect(r.checks.find((c) => c.name.startsWith("require_text"))?.ok).toBe(
|
|
false,
|
|
);
|
|
});
|
|
it("fails when forbidden text (a broken-pipeline tell) is present", () => {
|
|
const r = evaluateExpectation({
|
|
text: "Balance: undefined\nStartup failed: NaN",
|
|
colors: neutral,
|
|
expect: { forbid_text: ["undefined", "Startup failed", "NaN"] },
|
|
});
|
|
expect(r.verdict).toBe("fail");
|
|
expect(
|
|
r.checks.filter((c) => c.name.startsWith("forbid_text") && !c.ok),
|
|
).toHaveLength(3);
|
|
});
|
|
it("fails the brand rule when blue exceeds the ceiling", () => {
|
|
const r = evaluateExpectation({
|
|
text: "",
|
|
colors: { blue_fraction: 0.4, orange_fraction: 0, neutral_fraction: 0.6 },
|
|
expect: { max_blue_fraction: 0.02 },
|
|
});
|
|
expect(r.verdict).toBe("fail");
|
|
expect(r.checks.find((c) => c.name === "brand:no_blue")?.ok).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("analyzeScreenshot end to end", () => {
|
|
it("keeps the packaged tesseract.js fallback available for required OCR", async () => {
|
|
expect(
|
|
appPackageJson.dependencies?.["tesseract.js"] ??
|
|
appPackageJson.devDependencies?.["tesseract.js"],
|
|
).toBeTruthy();
|
|
expect(
|
|
rootPackageJson.dependencies?.["tesseract.js"] ??
|
|
rootPackageJson.devDependencies?.["tesseract.js"],
|
|
).toBeTruthy();
|
|
expect(rootLockfile).toContain('"tesseract.js": ["tesseract.js@');
|
|
});
|
|
|
|
it("flags a blue screen as a brand:no_blue failure with a real palette", async () => {
|
|
const report = await analyzeScreenshot(
|
|
await solid("bluescreen.png", 20, 40, 210),
|
|
{
|
|
expect: { state: "synthetic-blue", max_blue_fraction: 0.02 },
|
|
},
|
|
);
|
|
expect(report.verdict).toBe("fail");
|
|
expect(report.color_fractions.blue_fraction).toBeGreaterThan(0.9);
|
|
expect(report.dominant_palette[0].fraction).toBeGreaterThan(0.9);
|
|
// OCR must either produce text or name the engine failure; it must never
|
|
// fabricate an empty read as a successful "no text on screen" result.
|
|
expect(typeof report.ocr_text).toBe("string");
|
|
expect(
|
|
report.ocr_note === null || typeof report.ocr_note === "string",
|
|
).toBe(true);
|
|
});
|
|
});
|