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
224 lines
7.4 KiB
TypeScript
224 lines
7.4 KiB
TypeScript
/**
|
|
* `APP_BLOCK` handler test.
|
|
*
|
|
* Closes the gap from `docs/audits/lifeops-2026-05-09/03-coverage-gap-matrix.md`
|
|
* line 438: the app-block handler had no executable test.
|
|
*
|
|
* Covers the three subactions (`block`, `unblock`, `status`) by mocking the
|
|
* `app-blocker/engine.ts` surface — the single side-effect path the handler
|
|
* dispatches into. The planner / arg-resolver path is bypassed by passing
|
|
* parameters explicitly through `options.parameters`, so this is a true
|
|
* handler-level integration: arg resolution → access gate → engine dispatch →
|
|
* ActionResult shape.
|
|
*
|
|
* Owner-only access is exercised via `entityId === agentId` so the
|
|
* `getAppBlockerAccess` helper takes the isAgentSelf shortcut.
|
|
*/
|
|
|
|
import type { Memory, UUID } from "@elizaos/core";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
interface BlockCall {
|
|
packageNames?: string[];
|
|
appTokens?: string[];
|
|
durationMinutes?: number | null;
|
|
}
|
|
|
|
const stubState = {
|
|
active: false,
|
|
blockedCount: 0,
|
|
blockedPackageNames: [] as string[],
|
|
endsAt: null as string | null,
|
|
platform: "android" as "android" | "ios",
|
|
permissionStatus: "granted" as "granted" | "denied",
|
|
};
|
|
const stubCalls = { block: [] as BlockCall[], unblock: 0 };
|
|
|
|
// The app-blocker engine moved to @elizaos/plugin-blocker (LifeOps decomposition);
|
|
// keep the real access helpers (the owner isAgentSelf shortcut this test relies on)
|
|
// and override only the four engine side-effect functions.
|
|
vi.mock(
|
|
"@elizaos/plugin-blocker/services/app-blocker/index",
|
|
async (importOriginal) => ({
|
|
...(await importOriginal<
|
|
typeof import("@elizaos/plugin-blocker/services/app-blocker/index")
|
|
>()),
|
|
async getAppBlockerStatus() {
|
|
return {
|
|
available: true,
|
|
permissionStatus: stubState.permissionStatus,
|
|
active: stubState.active,
|
|
blockedCount: stubState.blockedCount,
|
|
blockedPackageNames: stubState.blockedPackageNames,
|
|
endsAt: stubState.endsAt,
|
|
platform: stubState.platform,
|
|
engine: "test-stub",
|
|
};
|
|
},
|
|
async getInstalledApps() {
|
|
return [
|
|
{ displayName: "Twitter", packageName: "com.twitter.android" },
|
|
{ displayName: "Instagram", packageName: "com.instagram.android" },
|
|
];
|
|
},
|
|
async startAppBlock(options: BlockCall) {
|
|
stubCalls.block.push(options);
|
|
const blockedCount =
|
|
(options.packageNames?.length ?? 0) + (options.appTokens?.length ?? 0);
|
|
const endsAt =
|
|
typeof options.durationMinutes === "number"
|
|
? new Date(
|
|
Date.now() + options.durationMinutes * 60_000,
|
|
).toISOString()
|
|
: null;
|
|
stubState.active = true;
|
|
stubState.blockedCount = blockedCount;
|
|
stubState.blockedPackageNames = options.packageNames ?? [];
|
|
stubState.endsAt = endsAt;
|
|
return { success: true, blockedCount, endsAt };
|
|
},
|
|
async stopAppBlock() {
|
|
stubCalls.unblock += 1;
|
|
stubState.active = false;
|
|
stubState.blockedCount = 0;
|
|
stubState.blockedPackageNames = [];
|
|
stubState.endsAt = null;
|
|
return { success: true };
|
|
},
|
|
}),
|
|
);
|
|
|
|
// Audit F folded the standalone `APP_BLOCK` action into a handler function;
|
|
// the BLOCK umbrella in `./block.ts` is the only registered action.
|
|
const { runAppBlockHandler } = await import("../src/actions/app-block.js");
|
|
const { createMinimalRuntimeStub } = await import("./first-run-helpers.js");
|
|
|
|
function ownerMessage(agentId: UUID, text: string): Memory {
|
|
return {
|
|
id: `msg-${Math.random().toString(36).slice(2, 8)}` as UUID,
|
|
entityId: agentId,
|
|
roomId: agentId,
|
|
agentId,
|
|
content: { text, source: "test" },
|
|
createdAt: Date.now(),
|
|
} as Memory;
|
|
}
|
|
|
|
function resetStub(initial: Partial<typeof stubState> = {}) {
|
|
stubState.active = initial.active ?? false;
|
|
stubState.blockedCount = initial.blockedCount ?? 0;
|
|
stubState.blockedPackageNames = initial.blockedPackageNames ?? [];
|
|
stubState.endsAt = initial.endsAt ?? null;
|
|
stubState.platform = initial.platform ?? "android";
|
|
stubState.permissionStatus = initial.permissionStatus ?? "granted";
|
|
stubCalls.block.length = 0;
|
|
stubCalls.unblock = 0;
|
|
}
|
|
|
|
describe("runAppBlockHandler", () => {
|
|
it("block subaction dispatches startAppBlock with the supplied package list and duration", async () => {
|
|
const runtime = createMinimalRuntimeStub();
|
|
resetStub({ platform: "android" });
|
|
|
|
const result = await runAppBlockHandler(
|
|
runtime,
|
|
ownerMessage(runtime.agentId, "block twitter for 60 minutes"),
|
|
undefined,
|
|
{
|
|
parameters: {
|
|
subaction: "block",
|
|
intent: "block twitter for 60 minutes",
|
|
packageNames: ["com.twitter.android"],
|
|
durationMinutes: 60,
|
|
},
|
|
},
|
|
);
|
|
|
|
if (!result?.success) {
|
|
throw new Error(
|
|
"expected APP_BLOCK to succeed; got: " +
|
|
JSON.stringify({ text: result?.text, data: result?.data }),
|
|
);
|
|
}
|
|
expect(result?.success).toBe(true);
|
|
expect(stubCalls.block).toHaveLength(1);
|
|
expect(stubCalls.block[0]?.packageNames).toEqual(["com.twitter.android"]);
|
|
expect(stubCalls.block[0]?.durationMinutes).toBe(60);
|
|
expect(result?.text ?? "").toMatch(/Started blocking 1 app/);
|
|
expect(
|
|
(result?.data as { blockedCount?: number } | undefined)?.blockedCount,
|
|
).toBe(1);
|
|
});
|
|
|
|
it("status subaction returns active=false when nothing is blocked", async () => {
|
|
const runtime = createMinimalRuntimeStub();
|
|
resetStub({ active: false });
|
|
|
|
const result = await runAppBlockHandler(
|
|
runtime,
|
|
ownerMessage(runtime.agentId, "is anything blocked"),
|
|
undefined,
|
|
{ parameters: { subaction: "status" } },
|
|
);
|
|
|
|
expect(result?.success).toBe(true);
|
|
expect((result?.data as { active?: boolean } | undefined)?.active).toBe(
|
|
false,
|
|
);
|
|
expect(result?.text ?? "").toMatch(/No app block/);
|
|
});
|
|
|
|
it("unblock subaction clears the active block", async () => {
|
|
const runtime = createMinimalRuntimeStub();
|
|
resetStub({
|
|
active: true,
|
|
blockedCount: 2,
|
|
blockedPackageNames: ["com.twitter.android", "com.instagram.android"],
|
|
endsAt: new Date(Date.now() + 30 * 60_000).toISOString(),
|
|
});
|
|
|
|
const result = await runAppBlockHandler(
|
|
runtime,
|
|
ownerMessage(runtime.agentId, "unblock my apps"),
|
|
undefined,
|
|
{ parameters: { subaction: "unblock" } },
|
|
);
|
|
|
|
expect(result?.success).toBe(true);
|
|
expect(stubCalls.unblock).toBe(1);
|
|
expect(result?.text ?? "").toMatch(/Removed the app block/);
|
|
});
|
|
|
|
it("status subaction surfaces blockedPackageNames + endsAt when active", async () => {
|
|
const runtime = createMinimalRuntimeStub();
|
|
const endsAt = new Date(Date.now() + 60 * 60_000).toISOString();
|
|
resetStub({
|
|
active: true,
|
|
blockedCount: 1,
|
|
blockedPackageNames: ["com.twitter.android"],
|
|
endsAt,
|
|
});
|
|
|
|
const result = await runAppBlockHandler(
|
|
runtime,
|
|
ownerMessage(runtime.agentId, "what is blocked right now"),
|
|
undefined,
|
|
{ parameters: { subaction: "status" } },
|
|
);
|
|
|
|
expect(result?.success).toBe(true);
|
|
const data = result?.data as
|
|
| {
|
|
active?: boolean;
|
|
blockedCount?: number;
|
|
blockedPackageNames?: string[];
|
|
endsAt?: string;
|
|
}
|
|
| undefined;
|
|
expect(data?.active).toBe(true);
|
|
expect(data?.blockedCount).toBe(1);
|
|
expect(data?.blockedPackageNames).toEqual(["com.twitter.android"]);
|
|
expect(data?.endsAt).toBe(endsAt);
|
|
});
|
|
});
|