Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

267 lines
8.4 KiB
TypeScript

/**
* Tests for the Capacitor BackgroundRunner JS at
* `packages/app/ios/App/App/runners/eliza-tasks.js` (Android copy is identical).
*
* The runner executes inside Capacitor's sandboxed JSContext at OS wake time.
* We can't spin up that runtime here, so the test loads the source as text,
* installs test `addEventListener` + `fetch` + `console` globals, evaluates it
* in a fresh scope to extract the `handleWake` function the file installs on
* globalThis, then exercises that function directly.
*
* The same file is read from BOTH the iOS and Android paths; the test that
* asserts they're identical (`runner-mirror.test.ts`) covers content drift.
*/
import { beforeEach, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as vm from 'node:vm';
// Canonical runner JS source lives under app-core platforms/ — `cap:sync`
// copies it into packages/app/ios + packages/app/android, both of which are
// gitignored. The byte-identical Android copy is under the platforms tree
// too. Test against the canonical iOS copy and assert the Android one
// matches via the runner-mirror test below.
const RUNNER_PATH = path.join(
__dirname,
'../../../../packages/app-core/platforms/ios/App/App/runners/eliza-tasks.js'
);
interface FetchCall {
url: string;
init: { method?: string; headers?: Record<string, string>; body?: string };
}
interface RunnerSandbox {
handleWake: (args: unknown) => Promise<unknown>;
fetchCalls: FetchCall[];
setFetchResponse: (response: {
ok: boolean;
status: number;
json: () => Promise<unknown>;
}) => void;
}
function loadRunner(): RunnerSandbox {
const source = fs.readFileSync(RUNNER_PATH, 'utf8');
const fetchCalls: FetchCall[] = [];
let fetchResponse: {
ok: boolean;
status: number;
json: () => Promise<unknown>;
} = {
ok: true,
status: 200,
json: async () => ({ ranTasks: 0, durationMs: 0, lastWakeFiredAt: 0 }),
};
const sandbox: Record<string, unknown> = {
console: { log: () => {}, error: () => {}, warn: () => {} },
setTimeout,
clearTimeout,
Promise,
Date,
Number,
JSON,
Buffer,
Error,
Math,
fetch: async (
url: string,
init: {
method?: string;
headers?: Record<string, string>;
body?: string;
} = {}
) => {
fetchCalls.push({ url, init });
return fetchResponse;
},
addEventListener: () => {},
};
// Mirror globalThis so the runner's `globalThis.handleWake = ...` assignment
// lands in this sandbox.
sandbox.globalThis = sandbox;
const context = vm.createContext(sandbox);
vm.runInContext(source, context, { filename: RUNNER_PATH });
return {
handleWake: sandbox.handleWake as (args: unknown) => Promise<unknown>,
fetchCalls,
setFetchResponse: (response) => {
fetchResponse = response;
},
};
}
const baseArgs = {
kind: 'refresh' as const,
deadlineSec: 30,
deviceSecret: 'secret-abc',
agentBase: 'http://127.0.0.1:31337',
};
describe('runners/eliza-tasks.js — handleWake', () => {
let runner: RunnerSandbox;
beforeEach(() => {
runner = loadRunner();
});
test('exports handleWake on globalThis after evaluation', () => {
expect(typeof runner.handleWake).toBe('function');
});
test('rejects on invalid kind', async () => {
await expect(runner.handleWake({ ...baseArgs, kind: 'bogus' })).rejects.toMatchObject({
delivered: false,
error: expect.stringContaining('kind'),
});
});
test('rejects on invalid deadlineSec', async () => {
await expect(runner.handleWake({ ...baseArgs, deadlineSec: 0 })).rejects.toMatchObject({
delivered: false,
error: expect.stringContaining('deadlineSec'),
});
await expect(runner.handleWake({ ...baseArgs, deadlineSec: -5 })).rejects.toMatchObject({
delivered: false,
error: expect.stringContaining('deadlineSec'),
});
});
test('rejects on missing deviceSecret', async () => {
await expect(runner.handleWake({ ...baseArgs, deviceSecret: '' })).rejects.toMatchObject({
delivered: false,
error: expect.stringContaining('deviceSecret'),
});
});
test('rejects on missing agentBase', async () => {
await expect(runner.handleWake({ ...baseArgs, agentBase: '' })).rejects.toMatchObject({
delivered: false,
error: expect.stringContaining('agentBase'),
});
});
test('POSTs to /api/internal/wake with bearer auth + correct body', async () => {
runner.setFetchResponse({
ok: true,
status: 200,
json: async () => ({ ranTasks: 3, durationMs: 120, lastWakeFiredAt: 999 }),
});
const result = (await runner.handleWake(baseArgs)) as {
delivered: boolean;
ranTasks: number;
durationMs: number;
lastWakeFiredAt: number;
};
expect(runner.fetchCalls.length).toBe(1);
const call = runner.fetchCalls[0];
expect(call.url).toBe('http://127.0.0.1:31337/api/internal/wake');
expect(call.init.method).toBe('POST');
expect(call.init.headers?.authorization).toBe('Bearer secret-abc');
expect(call.init.headers?.['content-type']).toContain('application/json');
const body = JSON.parse(call.init.body ?? '{}') as { kind: string; deadlineMs: number };
expect(body.kind).toBe('refresh');
expect(typeof body.deadlineMs).toBe('number');
// 30s deadline - ~2.5s buffer = ~27.5s; deadlineMs is absolute, must be in the future.
expect(body.deadlineMs).toBeGreaterThan(Date.now());
expect(result).toMatchObject({
delivered: true,
ranTasks: 3,
durationMs: 120,
lastWakeFiredAt: 999,
});
});
test('trims trailing slash from agentBase before composing URL', async () => {
await runner.handleWake({ ...baseArgs, agentBase: 'http://127.0.0.1:31337/' });
expect(runner.fetchCalls[0].url).toBe('http://127.0.0.1:31337/api/internal/wake');
});
test('rejects with delivered:false when the wake POST returns non-2xx', async () => {
runner.setFetchResponse({ ok: false, status: 401, json: async () => ({}) });
await expect(runner.handleWake(baseArgs)).rejects.toMatchObject({
delivered: false,
error: expect.stringContaining('401'),
});
});
test('rejects with delivered:false when the wake POST throws', async () => {
const source = fs.readFileSync(RUNNER_PATH, 'utf8');
const sandbox: Record<string, unknown> = {
console: { log: () => {}, error: () => {}, warn: () => {} },
setTimeout,
Promise,
Date,
Number,
JSON,
Error,
Math,
fetch: () => {
throw new Error('network down');
},
addEventListener: () => {},
};
sandbox.globalThis = sandbox;
const ctx = vm.createContext(sandbox);
vm.runInContext(source, ctx, { filename: RUNNER_PATH });
const fn = sandbox.handleWake as (args: unknown) => Promise<unknown>;
await expect(fn(baseArgs)).rejects.toMatchObject({
delivered: false,
error: expect.stringContaining('network down'),
});
});
test('rejects when work exceeds the OS deadline (minus buffer)', async () => {
// deadlineSec=3 → hardDeadlineMs = max(1000, 3000 - 2500) = 1000ms.
// Fetch never resolves → deadline wins.
const source = fs.readFileSync(RUNNER_PATH, 'utf8');
const sandbox: Record<string, unknown> = {
console: { log: () => {}, error: () => {}, warn: () => {} },
setTimeout,
Promise,
Date,
Number,
JSON,
Error,
Math,
fetch: () => new Promise(() => {}), // never resolves
addEventListener: () => {},
};
sandbox.globalThis = sandbox;
const ctx = vm.createContext(sandbox);
vm.runInContext(source, ctx, { filename: RUNNER_PATH });
const fn = sandbox.handleWake as (args: unknown) => Promise<unknown>;
await expect(fn({ ...baseArgs, deadlineSec: 3 })).rejects.toMatchObject({
delivered: false,
error: expect.stringContaining('deadline'),
});
});
});
describe('runners/eliza-tasks.js — mirroring', () => {
test('iOS and Android canonical copies are byte-identical', () => {
const ios = fs.readFileSync(
path.join(
__dirname,
'../../../../packages/app-core/platforms/ios/App/App/runners/eliza-tasks.js'
),
'utf8'
);
const android = fs.readFileSync(
path.join(
__dirname,
'../../../../packages/app-core/platforms/android/app/src/main/assets/runners/eliza-tasks.js'
),
'utf8'
);
expect(android).toBe(ios);
});
});