/** * Shared test helpers for plugin-cloud-apps. * * Only the SDK client is mocked: {@link FakeElizaCloudClient} stands in for * `@elizaos/cloud-sdk`'s `ElizaCloudClient`, and its `listApps` / `getApp` * methods delegate to per-test functions installed via {@link setListApps} / * {@link setGetApp}. The actions/provider/formatters under test all run for real. */ import { mock } from "bun:test"; import type { ActivateAppFrontendResponse, AdCampaignAttributionResponse, AppBackupSnapshot, AppDeployStatusResponse, AppDto, AppEarningsResponse, AppMonetizationResponse, AppResponse, BuyAppDomainInput, BuyAppDomainResponse, CampaignDaypartingResponse, CampaignPerformanceReportResponse, CheckAppDomainInput, CheckAppDomainResponse, CreateAdSlotInput, CreateAdSlotResponse, CreateAppInput, CreateAppResponse, CreateBookingInput, CreateBookingResponse, CreateCampaignReportShareInput, CreateCampaignReportShareResponse, CreateInfluencerProfileInput, CreateInfluencerProfileResponse, CreatePressReleaseInput, CreatePressReleaseResponse, DeleteAppResponse, DeployAppFrontendInput, DeployAppFrontendResponse, DeployAppInput, DeployAppResponse, DuplicateAdCampaignInput, DuplicateAdCampaignResponse, ExportAppBackupResponse, ListAdSlotsResponse, ListAppDomainsResponse, ListAppFrontendDeploymentsResponse, ListAppsResponse, ListInfluencersResponse, ListPressReleasesResponse, PressReleaseDto, RegenerateAppApiKeyResponse, SubmitPressReleaseInput, SubmitPressReleaseResponse, UpdateAppInput, UpdateAppMonetizationInput, UpdateCampaignDaypartingInput, WithdrawAppEarningsRequest, WithdrawAppEarningsResponse, } from "@elizaos/cloud-sdk"; import type { IAgentRuntime, Memory, Task, UUID } from "@elizaos/core"; /** Per-request options the SDK's poll-friendly app getters accept. */ type SdkRequestOptions = { signal?: AbortSignal; timeoutMs?: number }; type ListAppsFn = () => Promise; type GetAppFn = ( id: string, options?: SdkRequestOptions, ) => Promise; type CreateAppFn = (input: CreateAppInput) => Promise; type CreateAdSlotFn = ( input: CreateAdSlotInput, ) => Promise; type ListAdSlotsFn = () => Promise; type UpdateAdCampaignDaypartingFn = ( campaignId: string, input: UpdateCampaignDaypartingInput, ) => Promise; type DuplicateAdCampaignFn = ( campaignId: string, input?: DuplicateAdCampaignInput, ) => Promise; type GetAdCampaignAttributionFn = ( campaignId: string, ) => Promise; type GetAdCampaignPerformanceReportFn = ( campaignId: string, ) => Promise; type CreateAdCampaignReportShareFn = ( campaignId: string, input?: CreateCampaignReportShareInput, ) => Promise; type ListFrontendDeploymentsFn = ( appId: string, ) => Promise; type ActivateFrontendFn = ( appId: string, deploymentId: string, ) => Promise; type DeployAppFrontendFn = ( id: string, input: DeployAppFrontendInput, ) => Promise; type CreateBookingFn = ( input: CreateBookingInput, ) => Promise; type CreateInfluencerProfileFn = ( input: CreateInfluencerProfileInput, ) => Promise; type ListInfluencersFn = (niche?: string) => Promise; type CreatePressReleaseFn = ( input: CreatePressReleaseInput, ) => Promise; type ListPressReleasesFn = () => Promise; type SubmitPressReleaseFn = ( releaseId: string, input?: SubmitPressReleaseInput, ) => Promise; type ExportAppBackupFn = (appId: string) => Promise; type DeployAppFn = ( id: string, input?: DeployAppInput, ) => Promise; type GetAppDeployStatusFn = ( id: string, options?: SdkRequestOptions, ) => Promise; type DeleteAppFn = (id: string) => Promise; type UpdateAppFn = (id: string, patch: UpdateAppInput) => Promise; type UpdateMonetizationFn = ( id: string, settings: UpdateAppMonetizationInput, ) => Promise; type GetAppEarningsFn = ( id: string, options?: { days?: number }, ) => Promise; type WithdrawAppEarningsFn = ( id: string, request: WithdrawAppEarningsRequest, ) => Promise; type RegenerateAppApiKeyFn = ( id: string, ) => Promise; type CheckAppDomainFn = ( id: string, input: CheckAppDomainInput, ) => Promise; type BuyAppDomainFn = ( id: string, input: BuyAppDomainInput, ) => Promise; type ListAppDomainsFn = (id: string) => Promise; type CloudAppsTestRuntime = Pick< IAgentRuntime, | "agentId" | "getSetting" | "getTasks" | "createTask" | "updateTask" | "deleteTask" >; interface SdkState { listApps: ListAppsFn; getApp: GetAppFn; createApp: CreateAppFn; deployApp: DeployAppFn; createAdSlot: CreateAdSlotFn; listAdSlots: ListAdSlotsFn; updateAdCampaignDayparting: UpdateAdCampaignDaypartingFn; duplicateAdCampaign: DuplicateAdCampaignFn; getAdCampaignAttribution: GetAdCampaignAttributionFn; getAdCampaignPerformanceReport: GetAdCampaignPerformanceReportFn; createAdCampaignReportShare: CreateAdCampaignReportShareFn; deployAppFrontend: DeployAppFrontendFn; listAppFrontendDeployments: ListFrontendDeploymentsFn; activateAppFrontend: ActivateFrontendFn; createInfluencerProfile: CreateInfluencerProfileFn; createBooking: CreateBookingFn; listInfluencers: ListInfluencersFn; createPressRelease: CreatePressReleaseFn; listPressReleases: ListPressReleasesFn; submitPressRelease: SubmitPressReleaseFn; exportAppBackup: ExportAppBackupFn; getAppDeployStatus: GetAppDeployStatusFn; deleteApp: DeleteAppFn; updateApp: UpdateAppFn; updateMonetization: UpdateMonetizationFn; getAppEarnings: GetAppEarningsFn; withdrawAppEarnings: WithdrawAppEarningsFn; regenerateAppApiKey: RegenerateAppApiKeyFn; checkAppDomain: CheckAppDomainFn; buyAppDomain: BuyAppDomainFn; listAppDomains: ListAppDomainsFn; } function defaultState(): SdkState { return { listApps: () => Promise.resolve({ success: true, apps: [] }), getApp: () => Promise.resolve({ success: true, app: undefined as unknown as AppDto }), createApp: () => Promise.resolve({ success: true, app: undefined as unknown as AppDto, apiKey: "eliza_app_secret", }), deployApp: () => Promise.resolve({ success: true, deploymentId: "dep_1", status: "BUILDING", startedAt: "2026-06-29T00:00:00.000Z", }), createAdSlot: () => Promise.resolve({ success: true, slot: { id: "slot_1", app_id: "app_1", name: "Slot", format: "banner", status: "active", floor_cpm: "10.0000", total_impressions: 0, total_clicks: 0, total_revenue: "0.000000", }, adTagToken: "v1.9999999999.deadbeef", }), listAdSlots: () => Promise.resolve({ success: true, slots: [] }), updateAdCampaignDayparting: (_campaignId, input) => Promise.resolve({ success: true, campaignId: "campaign_1", status: "draft", dayparting: input.dayparting, updatedAt: "2026-07-02T00:00:00.000Z", }), duplicateAdCampaign: (_campaignId, input) => Promise.resolve({ success: true, campaign: { id: "campaign_copy", name: input?.name ?? "Campaign Copy", platform: "meta", objective: "traffic", status: "draft", budgetType: "daily", budgetAmount: "100.00", budgetCurrency: "USD", creditsAllocated: "0.00", externalCampaignId: null, dayparting: null, sourceCampaignId: "campaign_1", createdAt: "2026-07-02T00:00:00.000Z", }, creativesCopied: 1, }), getAdCampaignAttribution: (campaignId) => Promise.resolve({ success: true, campaignId, appId: "app_1", token: "payloadpart.signaturepart123456789", pixelEndpoint: "https://cloud.test/api/v1/advertising/conversions/track?token=payloadpart.signaturepart123456789", webhookEndpoint: "https://cloud.test/api/v1/advertising/conversions/track", install: { pixelHtml: '', webhook: { url: "https://cloud.test/api/v1/advertising/conversions/track", method: "POST", body: { token: "payloadpart.signaturepart123456789", eventType: "purchase", dedupeKey: "ORDER_OR_EVENT_ID", }, }, }, }), getAdCampaignPerformanceReport: (campaignId) => Promise.resolve({ success: true, report: { generatedAt: "2026-07-02T00:00:00.000Z", campaign: { id: campaignId, name: "Campaign", platform: "meta", objective: "traffic", status: "active", externalCampaignId: "ext_1", appId: null, budgetType: "daily", budgetAmount: 100, budgetCurrency: "USD", creditsAllocated: 110, creditsSpent: 11, startDate: null, endDate: null, createdAt: "2026-07-01T00:00:00.000Z", updatedAt: "2026-07-02T00:00:00.000Z", }, dateRange: null, summary: { spend: 10, impressions: 1000, clicks: 50, conversions: 5, ctr: 5, cpc: 0.2, cpm: 10, conversionRate: 10, costPerConversion: 2, budgetUtilization: 10, conversionValue: 0, }, provider: { platform: "meta", accountId: "acct_1", externalAccountId: "external_acct", externalCampaignId: "ext_1", }, }, }), createAdCampaignReportShare: (campaignId, input) => Promise.resolve({ success: true, share: { id: "share_1", campaignId, token: "token_1", publicPath: "/api/v1/advertising/reports/token_1", publicUrl: "https://elizacloud.ai/api/v1/advertising/reports/token_1", expiresAt: input?.expiresAt ?? "2026-07-09T00:00:00.000Z", }, }), deployAppFrontend: () => Promise.resolve({ success: true, deployment: { id: "fe_dep_1", app_id: "app_1", version: 1, status: "active", r2_prefix: "app-frontends/o/app_1/fe_dep_1/", content_hash: "a".repeat(64), file_count: 1, total_bytes: 100, error: null, created_at: "2026-06-29T00:00:00.000Z", activated_at: "2026-06-29T00:00:00.000Z", }, }), listAppFrontendDeployments: () => Promise.resolve({ success: true, active_deployment_id: null, deployments: [], }), activateAppFrontend: (_a, id) => Promise.resolve({ success: true, deployment: { id, app_id: "app_1", version: 1, status: "active", r2_prefix: "p", content_hash: null, file_count: 0, total_bytes: 0, error: null, created_at: "2020-01-01", activated_at: "2020-01-01", }, }), createInfluencerProfile: () => Promise.resolve({ success: true, profile: { id: "inf_1", display_name: "Creator", niche: null, bio: null, platforms: [], status: "active", }, }), listInfluencers: () => Promise.resolve({ success: true, profiles: [] }), createBooking: () => Promise.resolve({ success: true, booking: { id: "bk_1", advertiser_org_id: "org", influencer_profile_id: "inf_1", amount: "100.00", status: "offered", brief: "b", }, }), createPressRelease: (input) => Promise.resolve({ success: true, release: { id: "pr_1", organization_id: "org", created_by_user_id: "user", title: input.title, summary: input.summary ?? null, body: input.body, boilerplate: input.boilerplate ?? null, status: "draft", target_audience: input.targetAudience ?? {}, target_regions: input.targetRegions ?? [], assets: input.assets ?? [], embargo_at: input.embargoAt ?? null, submitted_at: null, distributed_at: null, failed_reason: null, idempotency_key: input.idempotencyKey ?? null, metadata: input.metadata ?? {}, created_at: "2026-07-03T00:00:00.000Z", updated_at: "2026-07-03T00:00:00.000Z", } satisfies PressReleaseDto, }), listPressReleases: () => Promise.resolve({ success: true, releases: [] }), submitPressRelease: (releaseId) => Promise.resolve({ success: true, release: { id: releaseId, organization_id: "org", created_by_user_id: "user", title: "Launch", summary: null, body: "Body", boilerplate: null, status: "submitted", target_audience: {}, target_regions: [], assets: [], embargo_at: null, submitted_at: "2026-07-03T00:00:00.000Z", distributed_at: null, failed_reason: null, idempotency_key: null, metadata: {}, created_at: "2026-07-03T00:00:00.000Z", updated_at: "2026-07-03T00:00:00.000Z", } satisfies PressReleaseDto, }), exportAppBackup: () => Promise.resolve({ success: true, backup: { version: 1, exportedAt: "2020-01-01T00:00:00Z", app: { name: "App", description: null, app_url: "https://a", allowed_origins: [], logo_url: null, website_url: null, contact_email: null, linked_character_ids: [], }, monetization: { enabled: false, inference_markup_percentage: 0, purchase_share_percentage: 0, }, } as AppBackupSnapshot, }), getAppDeployStatus: () => Promise.resolve({ success: true, deploymentId: "dep_1", status: "READY", vercelUrl: null, error: null, startedAt: null, }), deleteApp: () => Promise.resolve({ success: true, message: "deleted" }), updateApp: () => Promise.resolve({ success: true, app: undefined as unknown as AppDto }), updateMonetization: () => Promise.resolve({ success: true, monetization: null }), getAppEarnings: () => Promise.resolve({ success: true }), withdrawAppEarnings: () => Promise.resolve({ success: true, message: "withdrawn", newBalance: 0 }), regenerateAppApiKey: () => Promise.resolve({ success: true, apiKey: "eliza_app_rotated" }), checkAppDomain: (_id, input) => Promise.resolve({ success: true, domain: input.domain, available: true, currency: "USD", years: 1, price: { wholesaleUsdCents: 1029, marginUsdCents: 370, totalUsdCents: 1399, marginBps: 3600, }, renewal: { totalUsdCents: 1399 }, }), buyAppDomain: (_id, input) => Promise.resolve({ success: true, domain: input.domain, appDomainId: "ad_1", zoneId: "zone_1", status: "pending", verified: false, expiresAt: "2027-07-01T00:00:00.000Z", pendingZoneProvisioning: false, debited: { totalUsdCents: 1399, currency: "USD" }, }), listAppDomains: () => Promise.resolve({ success: true, domains: [] }), }; } const state: SdkState = defaultState(); const TEST_AGENT_ID = "agent-0000-0000-0000-000000000000" as UUID; export function setListApps(fn: ListAppsFn): void { state.listApps = fn; } export function setGetApp(fn: GetAppFn): void { state.getApp = fn; } export function setCreateApp(fn: CreateAppFn): void { state.createApp = fn; } export function setDeployApp(fn: DeployAppFn): void { state.deployApp = fn; } export function setCreateAdSlot(fn: CreateAdSlotFn): void { state.createAdSlot = fn; } export function setListAdSlots(fn: ListAdSlotsFn): void { state.listAdSlots = fn; } export function setUpdateAdCampaignDayparting( fn: UpdateAdCampaignDaypartingFn, ): void { state.updateAdCampaignDayparting = fn; } export function setDuplicateAdCampaign(fn: DuplicateAdCampaignFn): void { state.duplicateAdCampaign = fn; } export function setGetAdCampaignAttribution( fn: GetAdCampaignAttributionFn, ): void { state.getAdCampaignAttribution = fn; } export function setGetAdCampaignPerformanceReport( fn: GetAdCampaignPerformanceReportFn, ): void { state.getAdCampaignPerformanceReport = fn; } export function setCreateAdCampaignReportShare( fn: CreateAdCampaignReportShareFn, ): void { state.createAdCampaignReportShare = fn; } export function setDeployAppFrontend(fn: DeployAppFrontendFn): void { state.deployAppFrontend = fn; } export function setListAppFrontendDeployments( fn: ListFrontendDeploymentsFn, ): void { state.listAppFrontendDeployments = fn; } export function setActivateAppFrontend(fn: ActivateFrontendFn): void { state.activateAppFrontend = fn; } export function setCreateInfluencerProfile( fn: CreateInfluencerProfileFn, ): void { state.createInfluencerProfile = fn; } export function setListInfluencers(fn: ListInfluencersFn): void { state.listInfluencers = fn; } export function setCreateBooking(fn: CreateBookingFn): void { state.createBooking = fn; } export function setCreatePressRelease(fn: CreatePressReleaseFn): void { state.createPressRelease = fn; } export function setListPressReleases(fn: ListPressReleasesFn): void { state.listPressReleases = fn; } export function setSubmitPressRelease(fn: SubmitPressReleaseFn): void { state.submitPressRelease = fn; } export function setExportAppBackup(fn: ExportAppBackupFn): void { state.exportAppBackup = fn; } export function setGetAppDeployStatus(fn: GetAppDeployStatusFn): void { state.getAppDeployStatus = fn; } export function setDeleteApp(fn: DeleteAppFn): void { state.deleteApp = fn; } export function setUpdateApp(fn: UpdateAppFn): void { state.updateApp = fn; } export function setUpdateMonetization(fn: UpdateMonetizationFn): void { state.updateMonetization = fn; } export function setGetAppEarnings(fn: GetAppEarningsFn): void { state.getAppEarnings = fn; } export function setWithdrawAppEarnings(fn: WithdrawAppEarningsFn): void { state.withdrawAppEarnings = fn; } export function setRegenerateAppApiKey(fn: RegenerateAppApiKeyFn): void { state.regenerateAppApiKey = fn; } export function setCheckAppDomain(fn: CheckAppDomainFn): void { state.checkAppDomain = fn; } export function setBuyAppDomain(fn: BuyAppDomainFn): void { state.buyAppDomain = fn; } export function setListAppDomains(fn: ListAppDomainsFn): void { state.listAppDomains = fn; } /** Restore default (empty / no-op) behavior between tests. */ export function resetSdk(): void { Object.assign(state, defaultState()); } /** Stand-in for `ElizaCloudClient` — the methods the plugin calls. */ export class FakeElizaCloudClient { listApps(): Promise { return state.listApps(); } getApp(id: string, options?: SdkRequestOptions): Promise { return state.getApp(id, options); } createApp(input: CreateAppInput): Promise { return state.createApp(input); } deployApp(id: string, input?: DeployAppInput): Promise { return state.deployApp(id, input); } createAdSlot(input: CreateAdSlotInput): Promise { return state.createAdSlot(input); } listAdSlots(): Promise { return state.listAdSlots(); } updateAdCampaignDayparting( campaignId: string, input: UpdateCampaignDaypartingInput, ): Promise { return state.updateAdCampaignDayparting(campaignId, input); } duplicateAdCampaign( campaignId: string, input?: DuplicateAdCampaignInput, ): Promise { return state.duplicateAdCampaign(campaignId, input); } getAdCampaignAttribution( campaignId: string, ): Promise { return state.getAdCampaignAttribution(campaignId); } getAdCampaignPerformanceReport( campaignId: string, ): Promise { return state.getAdCampaignPerformanceReport(campaignId); } createAdCampaignReportShare( campaignId: string, input?: CreateCampaignReportShareInput, ): Promise { return state.createAdCampaignReportShare(campaignId, input); } deployAppFrontend( id: string, input: DeployAppFrontendInput, ): Promise { return state.deployAppFrontend(id, input); } listAppFrontendDeployments( appId: string, ): Promise { return state.listAppFrontendDeployments(appId); } activateAppFrontend( appId: string, deploymentId: string, ): Promise { return state.activateAppFrontend(appId, deploymentId); } createInfluencerProfile( input: CreateInfluencerProfileInput, ): Promise { return state.createInfluencerProfile(input); } listInfluencers(niche?: string): Promise { return state.listInfluencers(niche); } createBooking(input: CreateBookingInput): Promise { return state.createBooking(input); } createPressRelease( input: CreatePressReleaseInput, ): Promise { return state.createPressRelease(input); } listPressReleases(): Promise { return state.listPressReleases(); } submitPressRelease( releaseId: string, input?: SubmitPressReleaseInput, ): Promise { return state.submitPressRelease(releaseId, input); } exportAppBackup(appId: string): Promise { return state.exportAppBackup(appId); } getAppDeployStatus( id: string, options?: SdkRequestOptions, ): Promise { return state.getAppDeployStatus(id, options); } deleteApp(id: string): Promise { return state.deleteApp(id); } updateApp(id: string, patch: UpdateAppInput): Promise { return state.updateApp(id, patch); } updateMonetization( id: string, settings: UpdateAppMonetizationInput, ): Promise { return state.updateMonetization(id, settings); } getAppEarnings( id: string, options?: { days?: number }, ): Promise { return state.getAppEarnings(id, options); } withdrawAppEarnings( id: string, request: WithdrawAppEarningsRequest, ): Promise { return state.withdrawAppEarnings(id, request); } regenerateAppApiKey(id: string): Promise { return state.regenerateAppApiKey(id); } checkAppDomain( id: string, input: CheckAppDomainInput, ): Promise { return state.checkAppDomain(id, input); } buyAppDomain( id: string, input: BuyAppDomainInput, ): Promise { return state.buyAppDomain(id, input); } listAppDomains(id: string): Promise { return state.listAppDomains(id); } } /** Build a minimal runtime exposing just `getSetting`. */ export function makeRuntime( settings: Record = {}, ): IAgentRuntime { const tasks: Task[] = []; let taskCounter = 0; const runtime: CloudAppsTestRuntime = { agentId: TEST_AGENT_ID, getSetting: (key: string) => settings[key] ?? null, getTasks: (params) => Promise.resolve( tasks.filter((task) => { const agentMatches = params.agentIds.includes(task.agentId); const tagMatches = !params.tags || params.tags.every((tag) => task.tags?.includes(tag)); return agentMatches && tagMatches; }), ), createTask: (task: Task) => { const id = `task-0000-0000-0000-${String(++taskCounter).padStart(12, "0")}` as UUID; tasks.push({ ...task, id, agentId: task.agentId ?? TEST_AGENT_ID, }); return Promise.resolve(id); }, updateTask: (id: UUID, patch: Partial) => { const idx = tasks.findIndex((task) => task.id === id); if (idx >= 0) tasks[idx] = { ...tasks[idx], ...patch }; return Promise.resolve(); }, deleteTask: (id: UUID) => { const idx = tasks.findIndex((task) => task.id === id); if (idx >= 0) tasks.splice(idx, 1); return Promise.resolve(); }, }; return runtime as IAgentRuntime; } /** A runtime with a valid Cloud API key configured. */ export function keyedRuntime(): IAgentRuntime { return makeRuntime({ ELIZAOS_CLOUD_API_KEY: "eliza_test_key" }); } /** A runtime with no Cloud API key. */ export function unkeyedRuntime(): IAgentRuntime { return makeRuntime({}); } /** * A keyed runtime backed by a real in-memory `facts` store, so the facts-cache * code under test exercises its actual create/get/update logic (only the store * boundary is faked — the dedup + write path runs for real). */ export interface MemoryRuntime extends IAgentRuntime { __facts: Memory[]; } export function memoryRuntime( settings: Record = { ELIZAOS_CLOUD_API_KEY: "eliza_test_key", }, ): MemoryRuntime { const facts: Memory[] = []; let counter = 0; const tasks: Task[] = []; let taskCounter = 0; const runtime = { agentId: TEST_AGENT_ID, __facts: facts, getSetting: (key: string) => settings[key] as unknown, // Same in-memory task store as makeRuntime, so actions that combine the // two-phase confirm machine WITH facts writes run for real here too. getTasks: (params: { agentIds: string[]; tags?: string[] }) => Promise.resolve( tasks.filter((task) => { const agentMatches = params.agentIds.includes(task.agentId); const tagMatches = !params.tags || params.tags.every((tag) => task.tags?.includes(tag)); return agentMatches && tagMatches; }), ), createTask: (task: Task) => { const id = `task-0000-0000-0000-${String(++taskCounter).padStart(12, "0")}` as UUID; tasks.push({ ...task, id, agentId: task.agentId ?? TEST_AGENT_ID }); return Promise.resolve(id); }, updateTask: (id: UUID, patch: Partial) => { const idx = tasks.findIndex((task) => task.id === id); if (idx >= 0) tasks[idx] = { ...tasks[idx], ...patch }; return Promise.resolve(); }, deleteTask: (id: UUID) => { const idx = tasks.findIndex((task) => task.id === id); if (idx >= 0) tasks.splice(idx, 1); return Promise.resolve(); }, getMemories: (params: { tableName: string }) => Promise.resolve(params.tableName === "facts" ? [...facts] : []), createMemory: (memory: Memory, tableName: string) => { const id = `mem-${++counter}`; if (tableName === "facts") facts.push({ ...memory, id } as Memory); return Promise.resolve(id); }, updateMemory: (patch: Partial & { id: string }) => { const idx = facts.findIndex( (m) => (m as { id?: string }).id === patch.id, ); if (idx >= 0) facts[idx] = { ...facts[idx], ...patch } as Memory; return Promise.resolve(idx >= 0); }, deleteMemory: (id: string) => { const idx = facts.findIndex((m) => (m as { id?: string }).id === id); if (idx >= 0) facts.splice(idx, 1); return Promise.resolve(); }, } as unknown as MemoryRuntime; return runtime; } /** Build a message Memory with entity/room ids (for memory-writing actions). */ export function makeRoomMessage(text: string): Memory { return { id: "msg-0000-0000-0000-000000000000", entityId: "entity-0000-0000-0000-000000000000", roomId: "room-0000-0000-0000-000000000000", content: { text }, } as unknown as Memory; } /** Build a message Memory with the given text. */ export function makeMessage(text: string): Memory { return { content: { text }, } as unknown as Memory; } /** A callback that records the content it was called with. */ export function captureCallback(): { fn: (content: { text?: string; actions?: string[] }) => Promise; calls: Array<{ text?: string; actions?: string[] }>; } { const calls: Array<{ text?: string; actions?: string[] }> = []; const fn = mock((content: { text?: string; actions?: string[] }) => { calls.push(content); return Promise.resolve([] as Memory[]); }); return { fn, calls }; } /** Minimal AppDto factory — fills only the fields the read-core reads. */ export function makeApp(overrides: Partial = {}): AppDto { return { id: "00000000-0000-0000-0000-000000000001", name: "Test App", description: null, slug: "test-app", organization_id: "org-1", created_by_user_id: "user-1", app_url: "https://test-app.example.com", allowed_origins: [], api_key_id: null, affiliate_code: null, referral_bonus_credits: null, total_requests: 0, total_users: 0, total_credits_used: null, logo_url: null, website_url: null, contact_email: null, metadata: {}, deployment_status: "draft", production_url: null, last_deployed_at: null, github_repo: null, linked_character_ids: null, monetization_enabled: false, inference_markup_percentage: null, purchase_share_percentage: null, platform_offset_amount: null, custom_pricing_enabled: null, total_creator_earnings: null, total_platform_revenue: null, discord_automation: null, telegram_automation: null, twitter_automation: null, promotional_assets: null, email_notifications: null, response_notifications: null, is_active: true, is_approved: true, review_status: "draft", review_content_hash: null, reviewed_at: null, created_at: "2026-01-01T00:00:00.000Z", updated_at: "2026-01-01T00:00:00.000Z", last_used_at: null, ...overrides, }; }