/* * Copyright (C) 2024-present Puter Technologies Inc. * * This file is part of Puter. * * Puter is free software: you can redistribute it and/or modify it under the * terms of the GNU Affero General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see * [https://www.gnu.org/licenses/](https://www.gnu.org/licenses/). */ /** * Shared utilities for AI provider integration tests. * * Each test reads its credentials from `PUTER_TEST_AI_*` env vars (loaded by * the vitest config's `PUTER_` prefix) and skips itself when the var is missing * — tests run only on developer machines and in CI environments that supply the * right secrets. * * Filename intentionally omits `.test.` so vitest does not treat this helper as * a test file. */ import type { Actor } from '../core/actor.js'; import { SYSTEM_ACTOR, makeActor } from '../core/actor.js'; import { runWithContext } from '../core/context.js'; import type { MeteringService } from '../services/metering/MeteringService.js'; /** * Returns the env var value, or `undefined` if missing/empty. Used as the gate * for `describe.skipIf` blocks. */ export const optionalEnv = (name: string): string | undefined => { const v = process.env[name]; return v && v.length > 0 ? v : undefined; }; /** * Returns true when the env var is unset, signaling the test block should be * skipped. Pair with `describe.skipIf(skipUnlessEnv(...))`. */ export const skipUnlessEnv = (name: string): boolean => !optionalEnv(name); /** * Per-test timeout for provider integration tests. The default 5s vitest * timeout is way too short for real API calls — image generation in particular * routinely takes 15–30s. Pass this as the third argument to `it(...)`. */ export const INTEGRATION_TEST_TIMEOUT_MS = 90_000; /** * Returns a no-op MeteringService stub. Real metering would write to DynamoDB / * Redis, which integration tests for AI providers don't care about — we just * need the provider's metering calls to not throw and to short-circuit credit * checks. */ export const makeMeteringStub = (): MeteringService => ({ utilRecordUsageObject: () => Promise.resolve([] as never), incrementUsage: () => Promise.resolve({} as never), batchIncrementUsages: () => Promise.resolve([] as never), hasEnoughCredits: () => Promise.resolve(true), getRemainingUsage: () => Promise.resolve(Number.MAX_SAFE_INTEGER), getReportedCosts: () => [], }) as unknown as MeteringService; /** * Run `fn` inside a request-scoped context with `SYSTEM_ACTOR` set, which is * what providers expect (`Context.get('actor')`). The system actor bypasses * metering / quota gates by design. */ export const withTestActor = ( fn: () => T | Promise, actor: Actor = SYSTEM_ACTOR, ): Promise => Promise.resolve( runWithContext({ actor, requestId: 'integration-test' }, fn), ); /** * The four actor shapes provider tests exercise: a direct user session, the * user's own app, an app-issued access token (attributed through * `effectiveApp`), and the system actor. Shared so identifiers are tested * identically across providers instead of copied per suite. */ export const makeActorMatrix = (): Actor[] => { const user = { id: 42, uuid: 'u42', username: 'alice' }; const app = { uid: 'app-abc' }; return [ makeActor({ user }), makeActor({ user, app }), makeActor({ user, accessToken: { uid: 'tok-1', issuer: makeActor({ user, app }) }, }), SYSTEM_ACTOR, ]; }; /** Identifiers `makeActorMatrix()` should produce, in matrix order. */ export const ACTOR_MATRIX_IDENTIFIERS: (string | undefined)[] = [ 'puter-u42', 'puter-u42-app-abc', 'puter-u42-app-abc', undefined, ]; /** * Picks `fields` off the first argument of each recorded mock call, keyed by * field, so a suite can compare what a provider sent against * `expectedIdentifierFields(fields)` with one `toEqual`. */ export const sentIdentifierFields = ( calls: unknown[][], fields: string[], ): Record => Object.fromEntries( fields.map((field) => [ field, calls.map((call) => (call[0] as Record)[field]), ]), ); export const expectedIdentifierFields = ( fields: string[], ): Record => Object.fromEntries( fields.map((field) => [field, ACTOR_MATRIX_IDENTIFIERS]), );