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

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);
});
});