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
267 lines
8.4 KiB
TypeScript
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);
|
|
});
|
|
});
|