Files
elizaos--eliza/plugins/plugin-finances/test/subscriptions-service.real-db.test.ts
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

385 lines
13 KiB
TypeScript

/**
* Real-DB integration tests for the subscriptions back-end.
*
* Boots a REAL PGLite-backed AgentRuntime via {@link createRealTestRuntime},
* registers `financesPlugin` so the SQL plugin materializes the `app_finances`
* tables (including the subscription audit / candidate / cancellation tables),
* then exercises {@link SubscriptionsService} against that live database.
*
* The two cross-domain runtime-service seams (Gmail + browser bridge) are
* mocked: the service takes them as injectable options, so these tests stay
* hermetic (no Google, no browser companion) while every DB read/write is a
* real round-trip. The `agent_browser` cancellation path mocks the
* `computeruse` runtime service.
*/
import type { AgentRuntime } from "@elizaos/core";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import {
createRealTestRuntime,
type RealTestRuntimeResult,
} from "../../../packages/test/helpers/real-runtime.ts";
import { browserPlugin } from "../../plugin-browser/src/plugin.ts";
import {
__resetBrowserWorkspaceStateForTests,
executeBrowserWorkspaceCommand,
} from "../../plugin-browser/src/workspace/browser-workspace.ts";
import financesPlugin from "../src/plugin.ts";
import type { SubscriptionsBrowserGateway } from "../src/services/browser-bridge-seam.ts";
import type { SubscriptionsGmailGateway } from "../src/services/gmail-seam.ts";
import { SubscriptionsService } from "../src/services/subscriptions-service.ts";
function gmailMessage(overrides: Record<string, unknown>) {
const now = new Date().toISOString();
return {
id: "msg-1",
externalId: "msg-1",
agentId: "agent",
provider: "google" as const,
side: "owner" as const,
threadId: "thread-1",
subject: "Your receipt",
from: "billing@fixture-streaming.example",
fromEmail: "billing@fixture-streaming.example",
replyTo: null,
to: [],
cc: [],
snippet: "Thanks for your monthly plan receipt",
receivedAt: now,
isUnread: false,
isImportant: false,
likelyReplyNeeded: false,
triageScore: 40,
triageReason: "Recent Gmail message.",
labels: [],
htmlLink: null,
metadata: {},
syncedAt: now,
updatedAt: now,
...overrides,
};
}
const emptyGmail: SubscriptionsGmailGateway = {
async searchSubscriptionMessages() {
return [];
},
};
const noCompanionBrowser: SubscriptionsBrowserGateway = {
async listBrowserCompanions() {
return [];
},
async createBrowserSession() {
throw new Error("createBrowserSession should not be called");
},
async getBrowserSession() {
throw new Error("getBrowserSession should not be called");
},
};
const GOOGLE_PLAY_SUBSCRIPTIONS_URL =
"https://play.google.com/store/account/subscriptions";
const googlePlayFixtureRoutes = [
{
url: GOOGLE_PLAY_SUBSCRIPTIONS_URL,
title: "Google Play Subscriptions",
body: [
"<main>",
"<h1>Google Play</h1>",
"<h2>Subscriptions</h2>",
`<form method="get" action="${GOOGLE_PLAY_SUBSCRIPTIONS_URL}">`,
'<input type="hidden" name="confirm" value="1" />',
'<button data-lifeops-action="cancel-subscription" type="submit">Cancel subscription</button>',
"</form>",
"</main>",
].join(""),
},
{
url: `${GOOGLE_PLAY_SUBSCRIPTIONS_URL}?canceled=1`,
title: "Google Play Cancellation Complete",
body: [
"<main>",
"<h1>Google Play</h1>",
"<p>subscription canceled</p>",
"</main>",
].join(""),
},
{
url: `${GOOGLE_PLAY_SUBSCRIPTIONS_URL}?confirm=1`,
title: "Google Play Confirm Cancellation",
body: [
"<main>",
"<h1>Google Play</h1>",
"<p>Confirm cancellation</p>",
`<form method="get" action="${GOOGLE_PLAY_SUBSCRIPTIONS_URL}">`,
'<input type="hidden" name="canceled" value="1" />',
'<button data-lifeops-action="confirm-cancellation" type="submit">Confirm cancellation</button>',
"</form>",
"</main>",
].join(""),
},
] as const;
async function seedGooglePlayWorkspaceFixture(): Promise<void> {
delete process.env.ELIZA_BROWSER_WORKSPACE_URL;
delete process.env.ELIZA_BROWSER_WORKSPACE_TOKEN;
__resetBrowserWorkspaceStateForTests();
const opened = await executeBrowserWorkspaceCommand({
show: true,
subaction: "open",
title: "Google Play Fixture",
url: "about:blank",
});
const tabId = opened.tab?.id;
if (!tabId) {
throw new Error(
"browser workspace did not create a Google Play fixture tab",
);
}
for (const route of googlePlayFixtureRoutes) {
await executeBrowserWorkspaceCommand({
id: tabId,
networkAction: "route",
responseBody: [
"<!doctype html>",
"<html>",
`<head><title>${route.title}</title></head>`,
`<body>${route.body}</body>`,
"</html>",
].join(""),
responseStatus: 200,
subaction: "network",
url: route.url,
});
}
}
describe("SubscriptionsService — real PGLite", () => {
let runtime: AgentRuntime;
let testResult: RealTestRuntimeResult;
beforeAll(async () => {
testResult = await createRealTestRuntime({
characterName: "subscriptions-real-db-tests",
plugins: [financesPlugin, browserPlugin],
});
runtime = testResult.runtime;
}, 180_000);
afterAll(async () => {
await testResult?.cleanup();
});
it("audits subscriptions from mocked Gmail evidence and persists the audit", async () => {
const gmail: SubscriptionsGmailGateway = {
async searchSubscriptionMessages() {
return [
// Scores against the `fixture_streaming` playbook (alias + keyword +
// domain markers in the blob).
gmailMessage({
id: "msg-fixture",
subject: "Fixture Streaming monthly plan receipt",
snippet: "Your $9.99 monthly plan from fixture-streaming.example",
from: "fixture streaming <billing@fixture-streaming.example>",
fromEmail: "billing@fixture-streaming.example",
}),
];
},
};
const service = new SubscriptionsService(runtime, {
gmailGateway: gmail,
browserGateway: noCompanionBrowser,
});
const summary = await service.auditSubscriptions({ queryWindowDays: 90 });
expect(summary.audit.source).toBe("gmail");
expect(summary.audit.status).toBe("completed");
const fixture = summary.candidates.find(
(c) => c.serviceSlug === "fixture_streaming",
);
expect(fixture).toBeTruthy();
expect(fixture?.cadence).toBe("monthly");
expect(fixture?.annualCostEstimateUsd).toBeCloseTo(9.99 * 12, 2);
// Round-trip: the latest audit reads back from the real DB.
const latest = await service.getLatestSubscriptionAudit();
expect(latest?.audit.id).toBe(summary.audit.id);
expect(
latest?.candidates.some((c) => c.serviceSlug === "fixture_streaming"),
).toBe(true);
});
it("falls back to a manual audit when Gmail throws and a serviceQuery is given", async () => {
const gmail: SubscriptionsGmailGateway = {
async searchSubscriptionMessages() {
throw new Error("Google Gmail is not connected.");
},
};
const service = new SubscriptionsService(runtime, {
gmailGateway: gmail,
browserGateway: noCompanionBrowser,
});
const summary = await service.auditSubscriptions({
serviceQuery: "Fixture Streaming",
});
expect(summary.audit.source).toBe("manual");
expect(
summary.candidates.some((c) => c.serviceSlug === "fixture_streaming"),
).toBe(true);
});
it("returns unsupported_surface for an unknown service (no playbook)", async () => {
const service = new SubscriptionsService(runtime, {
gmailGateway: emptyGmail,
browserGateway: noCompanionBrowser,
});
const summary = await service.cancelSubscription({
serviceName: "Totally Unknown SaaS",
confirmed: true,
});
expect(summary.cancellation.status).toBe("unsupported_surface");
// Status read-back from the real DB resolves the latest cancellation.
const status = await service.getSubscriptionCancellationStatus({
serviceSlug: summary.cancellation.serviceSlug,
});
expect(status?.cancellation.id).toBe(summary.cancellation.id);
});
it("drives an agent_browser cancellation through a mocked computeruse service", async () => {
// The fixture_streaming playbook has a confirmable click flow whose
// cancellation marker is "subscription canceled". A get_dom probe that
// returns that marker drives the flow to completed.
const computeruse = {
async executeBrowserAction(params: {
action: string;
}): Promise<Record<string, unknown>> {
if (params.action === "get_dom") {
return { success: true, content: "subscription canceled" };
}
if (params.action === "screenshot") {
return { success: true, screenshot: "x".repeat(10) };
}
return { success: true, message: "ok" };
},
};
const originalGetService = runtime.getService.bind(runtime);
// biome-ignore lint/suspicious/noExplicitAny: test seam to inject computeruse
(runtime as any).getService = (name: string) =>
name === "computeruse" ? computeruse : originalGetService(name);
try {
const service = new SubscriptionsService(runtime, {
gmailGateway: emptyGmail,
browserGateway: noCompanionBrowser,
});
const summary = await service.cancelSubscription({
serviceSlug: "fixture_streaming",
executor: "agent_browser",
confirmed: true,
});
expect(
summary.cancellation.status,
JSON.stringify(summary.cancellation, null, 2),
).toBe("completed");
// Round-trips through the real DB.
const status = await service.getSubscriptionCancellationStatus({
cancellationId: summary.cancellation.id,
});
expect(status?.cancellation.status).toBe("completed");
} finally {
// biome-ignore lint/suspicious/noExplicitAny: restore original
(runtime as any).getService = originalGetService;
}
});
it("drives an agent_browser cancellation through BrowserService workspace when computeruse is absent", async () => {
await seedGooglePlayWorkspaceFixture();
type RuntimeGetService = AgentRuntime["getService"];
const mutableRuntime = runtime as AgentRuntime & {
getService: RuntimeGetService;
};
const originalGetService = mutableRuntime.getService.bind(runtime);
mutableRuntime.getService = ((serviceType: string) => {
if (serviceType === "computeruse") {
return null;
}
return originalGetService(serviceType);
}) as RuntimeGetService;
try {
const service = new SubscriptionsService(runtime, {
gmailGateway: emptyGmail,
browserGateway: noCompanionBrowser,
});
const summary = await service.cancelSubscription({
serviceSlug: "google_play",
executor: "agent_browser",
confirmed: true,
});
if (summary.cancellation.status !== "completed") {
throw new Error(JSON.stringify(summary.cancellation, null, 2));
}
expect(summary.cancellation.artifactCount).toBeGreaterThanOrEqual(1);
expect(summary.cancellation.evidenceSummary).toBe(
"subscription canceled",
);
expect(summary.cancellation.metadata.artifacts).toEqual([
expect.objectContaining({
kind: "screenshot",
label: "google-play-cancelled",
}),
]);
} finally {
mutableRuntime.getService = originalGetService as RuntimeGetService;
__resetBrowserWorkspaceStateForTests();
}
}, 30_000);
it("creates a user_browser session via the mocked browser gateway", async () => {
const created: Array<Record<string, unknown>> = [];
const browser: SubscriptionsBrowserGateway = {
async listBrowserCompanions() {
return [
{
id: "companion-1",
browser: "chrome",
profileId: "default",
connectionState: "connected",
} as never,
];
},
async createBrowserSession(request) {
created.push(request as unknown as Record<string, unknown>);
return {
id: "session-1",
status: "running",
} as never;
},
async getBrowserSession() {
return { id: "session-1", status: "running" } as never;
},
};
const service = new SubscriptionsService(runtime, {
gmailGateway: emptyGmail,
browserGateway: browser,
});
const summary = await service.cancelSubscription({
serviceSlug: "fixture_streaming",
executor: "user_browser",
confirmed: true,
});
expect(summary.cancellation.status).toBe("running");
expect(summary.cancellation.browserSessionId).toBe("session-1");
expect(created).toHaveLength(1);
expect(created[0]?.title).toContain("Fixture Streaming");
});
});