81 lines
5.4 KiB
TypeScript
81 lines
5.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
import { dirname, resolve } from "node:path";
|
|
|
|
import { GeminiFlashAdapter, geminiFlashMockContract } from "../../apps/worker/src/ai-adapter-gemini-flash.js";
|
|
import { GeminiProAdapter, geminiProMockContract } from "../../apps/worker/src/ai-adapter-gemini-pro.js";
|
|
import { GptImageAdapter, gptImageMockContract } from "../../apps/worker/src/ai-adapter-gpt-image.js";
|
|
import { mockPngBytes, type AdapterTransport } from "../../apps/worker/src/model-adapter.js";
|
|
|
|
const request = (modelId: string, ratio: "3:4" | "1:1" | "4:3" | "9:16" = "3:4", referenceAssetIds: string[] = []) => ({
|
|
configSnapshot: { error_mapping_profile: { timeout: "upstream_timeout", safety: "safety_rejected" } },
|
|
generationId: `generation-${modelId}`,
|
|
modelId,
|
|
prompt: "mock contract prompt",
|
|
ratio,
|
|
referenceAssetIds,
|
|
});
|
|
|
|
function writeAdapterEvidence(relativePath: string, value: unknown) {
|
|
const root = process.env.DADA_EVIDENCE_DIR_ADAPTERS;
|
|
if (!root) return;
|
|
const path = resolve(root, relativePath);
|
|
mkdirSync(dirname(path), { recursive: true });
|
|
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
}
|
|
|
|
describe("TDD-WP3-ADP-001-per-model-contract", () => {
|
|
it("keeps three independent fixtures and validates their own profiles", async () => {
|
|
const adapters = [new GeminiFlashAdapter(), new GeminiProAdapter(), new GptImageAdapter()];
|
|
expect(new Set(adapters.map((adapter) => adapter.modelId)).size).toBe(3);
|
|
expect(new Set(adapters.map((adapter) => adapter.fixtureId)).size).toBe(3);
|
|
expect(adapters[0].validateContract(geminiFlashMockContract).status).toBe("verified");
|
|
expect(adapters[1].validateContract(geminiProMockContract).status).toBe("verified");
|
|
expect(adapters[2].validateContract(gptImageMockContract).status).toBe("verified");
|
|
expect(adapters[0].validateContract(geminiProMockContract).status).toBe("blocked");
|
|
for (const adapter of adapters) {
|
|
for (const ratio of ["3:4", "1:1", "4:3", "9:16"] as const) {
|
|
const result = await adapter.start(request(adapter.modelId, ratio, ["reference-fixture"]));
|
|
expect(result).toMatchObject({ status: "completed", outputs: [{ pixelHeight: expect.any(Number), pixelWidth: expect.any(Number) }] });
|
|
expect(result.status === "completed" ? result.outputs : []).toHaveLength(1);
|
|
}
|
|
}
|
|
const errorMapping = ["upstream_timeout", "upstream_failed", "safety_rejected", "gateway_balance_insufficient", "gateway_contract_invalid", "reference_invalid", "unknown_retryable", "unknown_non_retryable"];
|
|
for (const adapter of adapters) {
|
|
const executionModes = adapter === adapters[1] ? ["async", "poll"] : ["sync"];
|
|
const modelRoot = adapter.modelId.replaceAll(".", "_");
|
|
writeAdapterEvidence(`${modelRoot}/contract-matrix.json`, {
|
|
contract_version: adapter.contractVersion, execution_modes: executionModes, fixture_id: adapter.fixtureId,
|
|
model_id: adapter.modelId, pure_text: { outputs: 1, status: "passed" }, reference_image: { outputs: 1, status: "passed" },
|
|
ratios: ["3:4", "1:1", "4:3", "9:16"].map((ratio) => ({ outputs: 1, ratio, status: "passed" })),
|
|
error_mapping: errorMapping,
|
|
});
|
|
writeAdapterEvidence(`${modelRoot}/response.json`, { model_id: adapter.modelId, output_count: 1, response_shape: "single-image" });
|
|
writeAdapterEvidence(`${modelRoot}/external-calls.json`, { mode: "mock", real_calls: 0, service: "ai-gateway-service-id" });
|
|
writeAdapterEvidence(`${modelRoot}/redaction.json`, { removed_fields: ["credential", "raw_prompt", "raw_provider_payload"], secret_scan: "passed" });
|
|
}
|
|
});
|
|
|
|
it("supports a pending async operation and poll without accepting multiple outputs", async () => {
|
|
const transport: AdapterTransport = {
|
|
async start() { return { operation: { done: false, name: "upstream:mock-pro-1" } }; },
|
|
async poll() { return { operation: { done: true, response: { candidates: [{ inline_data: { data: mockPngBytes("async").toString("base64"), mime_type: "image/png" }, pixelHeight: 1440, pixelWidth: 1080 }] } } }; },
|
|
};
|
|
const adapter = new GeminiProAdapter({ transport });
|
|
await expect(adapter.start(request(adapter.modelId))).resolves.toEqual({ status: "pending", upstreamJobReference: "upstream:mock-pro-1" });
|
|
await expect(adapter.poll("upstream:mock-pro-1")).resolves.toMatchObject({ status: "completed", outputs: [{ mimeType: "image/png" }] });
|
|
expect(() => adapter.normalizeOutput({ candidates: [{}, {}] })).toThrow("response_single_image_required");
|
|
});
|
|
|
|
it("maps only fixed categories and retains a non-sensitive balance signal", async () => {
|
|
const adapter = new GeminiFlashAdapter({ transport: {
|
|
async start() { return { status: "failed", error: { code: "balance" }, balance_signal: { gateway_account_ref: "gateway-account-primary", impact_scope: "model" } }; },
|
|
async poll() { return {}; },
|
|
} });
|
|
const result = await adapter.start(request(adapter.modelId));
|
|
expect(result).toMatchObject({ status: "failed", category: "unknown_retryable" });
|
|
expect(adapter.classifyError({ code: "safety" }, { safety: "safety_rejected" })).toEqual({ category: "safety_rejected", sourceCategory: "safety" });
|
|
expect(adapter.checkBalanceSignal({ balance_signal: { gateway_account_ref: "gateway-account-primary", impact_scope: "account", raw_provider_text: "do-not-return" } })).toEqual({ gatewayAccountRef: "gateway-account-primary", impactScope: "account" });
|
|
});
|
|
});
|