Files
wehub-resource-sync 070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:00:47 +08:00

162 lines
6.3 KiB
TypeScript

/**
* Regression coverage for the lefarcen + codex P2 on PR #1309: when a
* user aliases a registered catalog id to a custom wire-name via
* `OD_MEDIA_MODEL_ALIASES` or media-config.json's `aliases` map, the
* dispatcher must still apply the model-FAMILY behaviour the catalog
* id implies (DALL-E response_format, dall-e-3 hd quality,
* gpt-4o-mini-tts instructions, etc.) and only swap the value that
* goes into the provider's `body.model` field.
*
* The test stubs fetch and asserts on the request body for an
* aliased dall-e-3 -> azure-custom-deployment call. Before the fix
* ctx.model was overwritten with the alias, so the
* `startsWith('dall-e-')` and `=== 'dall-e-3'` branches stopped
* firing and the body was missing both response_format and the hd
* quality flag — exactly the regression codex described.
*/
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { generateMedia } from '../../src/media/index.js';
const PNG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+X2uoAAAAASUVORK5CYII=';
describe('media alias preserves catalog-keyed capability branching (#1309 review)', () => {
let root: string;
let projectRoot: string;
let projectsRoot: string;
const realFetch = globalThis.fetch;
const originalEnvAliases = process.env.OD_MEDIA_MODEL_ALIASES;
const originalOpenAIKey = process.env.OPENAI_API_KEY;
const originalMediaConfigDir = process.env.OD_MEDIA_CONFIG_DIR;
const originalDataDir = process.env.OD_DATA_DIR;
beforeEach(async () => {
root = await mkdtemp(path.join(tmpdir(), 'od-media-alias-cap-'));
projectRoot = path.join(root, 'project-root');
projectsRoot = path.join(projectRoot, '.od', 'projects');
await mkdir(projectsRoot, { recursive: true });
delete process.env.OD_MEDIA_MODEL_ALIASES;
delete process.env.OD_MEDIA_CONFIG_DIR;
delete process.env.OD_DATA_DIR;
process.env.OPENAI_API_KEY = 'sk-test-key';
});
afterEach(async () => {
globalThis.fetch = realFetch;
vi.unstubAllGlobals();
if (originalEnvAliases == null) {
delete process.env.OD_MEDIA_MODEL_ALIASES;
} else {
process.env.OD_MEDIA_MODEL_ALIASES = originalEnvAliases;
}
if (originalOpenAIKey == null) {
delete process.env.OPENAI_API_KEY;
} else {
process.env.OPENAI_API_KEY = originalOpenAIKey;
}
if (originalMediaConfigDir == null) {
delete process.env.OD_MEDIA_CONFIG_DIR;
} else {
process.env.OD_MEDIA_CONFIG_DIR = originalMediaConfigDir;
}
if (originalDataDir == null) {
delete process.env.OD_DATA_DIR;
} else {
process.env.OD_DATA_DIR = originalDataDir;
}
await rm(root, { recursive: true, force: true });
});
async function writeStoredConfig(data: unknown) {
const file = path.join(projectRoot, '.od', 'media-config.json');
await mkdir(path.dirname(file), { recursive: true });
await writeFile(file, JSON.stringify(data), 'utf8');
}
it('alias dall-e-3 -> custom-deployment still sends dall-e-3 response_format + hd quality', async () => {
await writeStoredConfig({
providers: {},
aliases: { 'dall-e-3': 'azure-dalle3-deployment' },
});
let capturedBody: Record<string, unknown> | null = null;
const fetchMock = vi.fn(async (_input: unknown, init?: RequestInit) => {
capturedBody = JSON.parse(String(init?.body)) as Record<string, unknown>;
return new Response(
JSON.stringify({ data: [{ b64_json: PNG_BASE64 }] }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
});
vi.stubGlobal('fetch', fetchMock);
const result = await generateMedia({
projectRoot,
projectsRoot,
projectId: 'project-1',
surface: 'image',
model: 'dall-e-3',
prompt: 'A watercolor shiba inu under cherry blossoms',
aspect: '1:1',
output: 'aliased.png',
});
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(capturedBody).not.toBeNull();
// Wire name swap landed — the provider receives the alias.
expect(capturedBody!.model).toBe('azure-dalle3-deployment');
// Capability branches keyed on the catalog id continue to fire.
expect(capturedBody!.response_format).toBe('b64_json');
expect(capturedBody!.quality).toBe('hd');
// providerNote reflects what was actually sent, so a user
// inspecting the result sees the wire name.
expect(result.providerNote).toContain('azure-dalle3-deployment');
expect(result.providerNote).not.toContain('dall-e-3');
});
it('alias gpt-4o-mini-tts -> custom-deployment still attaches style instructions', async () => {
process.env.OD_MEDIA_MODEL_ALIASES = JSON.stringify({
'gpt-4o-mini-tts': 'custom-tts-deployment',
});
let capturedBody: Record<string, unknown> | null = null;
const fetchMock = vi.fn(async (_input: unknown, init?: RequestInit) => {
capturedBody = JSON.parse(String(init?.body)) as Record<string, unknown>;
// Speech endpoints return raw audio bytes, not JSON.
return new Response(Buffer.from([1, 2, 3, 4]), {
status: 200,
headers: { 'content-type': 'audio/mpeg' },
});
});
vi.stubGlobal('fetch', fetchMock);
const result = await generateMedia({
projectRoot,
projectsRoot,
projectId: 'project-1',
surface: 'audio',
audioKind: 'speech',
model: 'gpt-4o-mini-tts',
prompt: 'Hello there.',
// gpt-4o-mini-tts accepts free-form speaking style in `voice`
// when the value isn't a known OpenAI voice id. The dispatcher
// routes that string into `body.instructions` ONLY when the
// model branch fires.
voice: 'warm and slow',
output: 'aliased.mp3',
});
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(capturedBody).not.toBeNull();
expect(capturedBody!.model).toBe('custom-tts-deployment');
// Capability branch keyed on the catalog id continues to fire
// even though the wire-level model is the alias — the
// gpt-4o-mini-tts-specific instructions field is still attached.
expect(capturedBody!.instructions).toBe('warm and slow');
expect(result.providerNote).toContain('custom-tts-deployment');
});
});