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
249 lines
8.2 KiB
JavaScript
249 lines
8.2 KiB
JavaScript
/**
|
|
* Unit coverage for the device-e2e bundle assembler.
|
|
*
|
|
* The real runners need phones/simulators, so this test pins the pure filesystem
|
|
* contract: output directory selection, inline-ready artifact collection,
|
|
* summary writing, and JUnit generation on both passing and failed steps.
|
|
*/
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import {
|
|
captureFailureForensics,
|
|
collectBundleArtifacts,
|
|
createDeviceE2eBundle,
|
|
defaultDeviceE2eOutputDir,
|
|
finalizeDeviceE2eBundle,
|
|
finishBundleStep,
|
|
formatFailureForensicsBlock,
|
|
parseOutputDirArg,
|
|
recordBundleArtifact,
|
|
runBundledCommand,
|
|
startBundleStep,
|
|
} from "./lib/device-e2e-bundle.mjs";
|
|
|
|
const tempDirs = [];
|
|
const ONE_BY_ONE_PNG = Buffer.from(
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=",
|
|
"base64",
|
|
);
|
|
|
|
function tempRoot() {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "device-e2e-bundle-"));
|
|
tempDirs.push(dir);
|
|
return dir;
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const dir of tempDirs.splice(0)) {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe("device-e2e bundle assembly", () => {
|
|
it("parses --output and builds the default per-lane directory", () => {
|
|
expect(parseOutputDirArg(["node", "runner", "--output", "/tmp/out"])).toBe(
|
|
"/tmp/out",
|
|
);
|
|
expect(parseOutputDirArg(["node", "runner"])).toBeUndefined();
|
|
expect(
|
|
defaultDeviceE2eOutputDir({
|
|
appDir: "/repo/packages/app",
|
|
lane: "android",
|
|
date: new Date("2026-07-05T01:02:03.004Z"),
|
|
}),
|
|
).toBe(
|
|
"/repo/packages/app/device-e2e-output/android-2026-07-05T01-02-03-004Z",
|
|
);
|
|
});
|
|
|
|
it("writes summary, junit, and inline copies for existing JPG/MP4 artifacts", () => {
|
|
const root = tempRoot();
|
|
const bundle = createDeviceE2eBundle({
|
|
appDir: root,
|
|
lane: "android",
|
|
outputDir: path.join(root, "bundle"),
|
|
device: { serial: "device-1" },
|
|
build: { buildId: "build-1", commit: "abc123" },
|
|
});
|
|
|
|
const sourceDir = path.join(root, "source");
|
|
fs.mkdirSync(sourceDir, { recursive: true });
|
|
const jpg = path.join(sourceDir, "screen.jpg");
|
|
const mp4 = path.join(sourceDir, "walkthrough.mp4");
|
|
fs.writeFileSync(jpg, "jpg");
|
|
fs.writeFileSync(mp4, "mp4");
|
|
|
|
const step = startBundleStep(bundle, "route coverage");
|
|
recordBundleArtifact(bundle, jpg, "screenshot", step);
|
|
recordBundleArtifact(bundle, mp4, "video", step);
|
|
finishBundleStep(bundle, step, "passed");
|
|
|
|
const bundleRoot = finalizeDeviceE2eBundle(bundle, "passed");
|
|
const summary = JSON.parse(
|
|
fs.readFileSync(path.join(bundleRoot, "summary.json"), "utf8"),
|
|
);
|
|
|
|
expect(summary.result).toBe("passed");
|
|
expect(summary.device.serial).toBe("device-1");
|
|
expect(summary.build.buildId).toBe("build-1");
|
|
expect(summary.steps).toHaveLength(1);
|
|
expect(fs.existsSync(path.join(bundleRoot, "junit.xml"))).toBe(true);
|
|
expect(fs.existsSync(path.join(bundleRoot, "inline", "screen.jpg"))).toBe(
|
|
true,
|
|
);
|
|
expect(
|
|
fs.existsSync(path.join(bundleRoot, "inline", "walkthrough.mp4")),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("collects logs from source directories and records failed steps in junit", () => {
|
|
const root = tempRoot();
|
|
const bundle = createDeviceE2eBundle({
|
|
appDir: root,
|
|
lane: "ios-sim",
|
|
outputDir: path.join(root, "bundle"),
|
|
});
|
|
const logDir = path.join(root, "logs");
|
|
fs.mkdirSync(logDir, { recursive: true });
|
|
fs.writeFileSync(path.join(logDir, "runner.log"), "failed\n");
|
|
|
|
const step = startBundleStep(bundle, "local chat");
|
|
finishBundleStep(bundle, step, "failed", new Error("chat failed"));
|
|
collectBundleArtifacts(bundle, [logDir]);
|
|
finalizeDeviceE2eBundle(bundle, "failed");
|
|
|
|
const junit = fs.readFileSync(path.join(bundle.root, "junit.xml"), "utf8");
|
|
const summary = JSON.parse(
|
|
fs.readFileSync(path.join(bundle.root, "summary.json"), "utf8"),
|
|
);
|
|
expect(junit).toContain('failures="1"');
|
|
expect(junit).toContain("chat failed");
|
|
expect(summary.result).toBe("failed");
|
|
expect(summary.artifacts.some((a) => a.path.endsWith("runner.log"))).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("converts PNG screenshots into inline JPG artifacts", () => {
|
|
const root = tempRoot();
|
|
const bundle = createDeviceE2eBundle({
|
|
appDir: root,
|
|
lane: "android",
|
|
outputDir: path.join(root, "bundle"),
|
|
});
|
|
const png = path.join(bundle.rawDir, "screen.png");
|
|
fs.writeFileSync(png, ONE_BY_ONE_PNG);
|
|
recordBundleArtifact(bundle, png, "screenshot");
|
|
|
|
finalizeDeviceE2eBundle(bundle, "passed");
|
|
|
|
const summary = JSON.parse(
|
|
fs.readFileSync(path.join(bundle.root, "summary.json"), "utf8"),
|
|
);
|
|
expect(fs.existsSync(path.join(bundle.inlineDir, "screen.jpg"))).toBe(true);
|
|
expect(summary.artifacts.some((a) => a.path === "inline/screen.jpg")).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("writes a failed summary and runner log when a bundled command fails", () => {
|
|
const root = tempRoot();
|
|
const bundle = createDeviceE2eBundle({
|
|
appDir: root,
|
|
lane: "android",
|
|
outputDir: path.join(root, "bundle"),
|
|
});
|
|
|
|
expect(() =>
|
|
runBundledCommand(
|
|
bundle,
|
|
"failing command",
|
|
process.execPath,
|
|
["-e", "console.error('nope'); process.exit(7)"],
|
|
{ cwd: root },
|
|
),
|
|
).toThrow(/exited with code 7/);
|
|
finalizeDeviceE2eBundle(bundle, "failed");
|
|
|
|
const summary = JSON.parse(
|
|
fs.readFileSync(path.join(bundle.root, "summary.json"), "utf8"),
|
|
);
|
|
expect(summary.result).toBe("failed");
|
|
expect(summary.steps[0]).toMatchObject({
|
|
name: "failing command",
|
|
status: "failed",
|
|
});
|
|
expect(
|
|
fs.readFileSync(path.join(bundle.logsDir, "runner.log"), "utf8"),
|
|
).toContain("nope");
|
|
});
|
|
|
|
it("records step failure forensics and formats a compact stderr block", () => {
|
|
const root = tempRoot();
|
|
const bundle = createDeviceE2eBundle({
|
|
appDir: root,
|
|
lane: "android",
|
|
outputDir: path.join(root, "bundle"),
|
|
});
|
|
const step = startBundleStep(bundle, "Android route coverage");
|
|
const error = new Error("route failed");
|
|
|
|
captureFailureForensics(
|
|
bundle,
|
|
step,
|
|
({ failureDir }) => {
|
|
const cause = path.join(failureDir, "failure-cause.txt");
|
|
const log = path.join(failureDir, "logcat.txt");
|
|
const screen = path.join(failureDir, "screen.png");
|
|
fs.writeFileSync(cause, "route failed\n");
|
|
fs.writeFileSync(log, "log tail\n");
|
|
fs.writeFileSync(screen, ONE_BY_ONE_PNG);
|
|
return [cause, log, screen];
|
|
},
|
|
error,
|
|
);
|
|
finishBundleStep(bundle, step, "failed", error);
|
|
finalizeDeviceE2eBundle(bundle, "failed");
|
|
|
|
const summary = JSON.parse(
|
|
fs.readFileSync(path.join(bundle.root, "summary.json"), "utf8"),
|
|
);
|
|
const block = formatFailureForensicsBlock(bundle, error);
|
|
|
|
expect(summary.steps[0].failureDir).toBe("failure/android-route-coverage");
|
|
expect(summary.steps[0].artifacts).toEqual([
|
|
"failure/android-route-coverage/failure-cause.txt",
|
|
"failure/android-route-coverage/logcat.txt",
|
|
"failure/android-route-coverage/screen.png",
|
|
]);
|
|
expect(block).toContain("DEVICE E2E FAILURE FORENSICS");
|
|
expect(block).toContain("step: Android route coverage");
|
|
expect(block).toContain("screen.png");
|
|
});
|
|
|
|
it("keeps the original failed step when forensic capture fails", () => {
|
|
const root = tempRoot();
|
|
const bundle = createDeviceE2eBundle({
|
|
appDir: root,
|
|
lane: "ios-sim",
|
|
outputDir: path.join(root, "bundle"),
|
|
});
|
|
const step = startBundleStep(bundle, "boot iOS Simulator");
|
|
|
|
captureFailureForensics(bundle, step, () => {
|
|
throw new Error("simulator disconnected");
|
|
});
|
|
finishBundleStep(bundle, step, "failed", new Error("boot failed"));
|
|
finalizeDeviceE2eBundle(bundle, "failed");
|
|
|
|
const summary = JSON.parse(
|
|
fs.readFileSync(path.join(bundle.root, "summary.json"), "utf8"),
|
|
);
|
|
expect(summary.result).toBe("failed");
|
|
expect(summary.steps[0].error).toBe("boot failed");
|
|
expect(summary.warnings[0]).toContain("simulator disconnected");
|
|
});
|
|
});
|