Files
makelore/tests/unit/game-audio-plugin.test.ts

100 lines
6.8 KiB
TypeScript

// @vitest-environment node
import path from 'node:path';
import { describe, expect, it, vi } from 'vitest';
import { loadBundledCodingPluginDefinitionsSync } from '../../electron/coding-plugins/manifest';
import { isProjectWideCodingPluginId } from '../../shared/coding-plugins';
import { GameAudioPluginAdapter } from '../../electron/coding-plugins/adapters/game-audio';
import { CodingCapabilityRegistryImpl } from '../../electron/coding-plugins/registry';
import type { PluginPolicyClientState } from '../../electron/services/plugin-policy-client';
import type { AudioDeliveryResult } from '../../electron/services/game-audio-delivery';
const context = { conversationId: 'conversation', runId: 'run', resourceId: 'tool',
requestId: 'trusted-id', localProjectId: 'local', projectPath: '/project',
durableProjectId: 'durable', workerRole: 'parent' as const, effectiveSkillIds: ['game-audio'],
pluginReleaseId: '00000000-0000-4000-8000-000000000505' };
function definition() {
return loadBundledCodingPluginDefinitionsSync(path.resolve('resources/coding-plugins'))
.find((item) => item.id === 'makelore.game-audio')!;
}
describe('official Game Audio plugin', () => {
it('passes normalized receipts through the registry with progress and saved files', async () => {
const plugin = definition();
const delivered: AudioDeliveryResult = {
executionId: '00000000-0000-4000-8000-000000000601', logicalOperationId: 'request',
providerStatus: 'succeeded', deliveryStatus: 'saved', phase: 'saved', progressPercent: 100,
outputCount: 1, files: [{ index: 0, path: 'assets/generated/game-audio/00000000-0000-4000-8000-000000000601/music-1.mp3', bytes: 10, mimeType: 'audio/mpeg' }],
billing: { mode: 'platform_metered', status: 'settled', reserved_points: '1.00', actual_points: '1.00', usage_amount: 1, unit: 'generation' },
};
const adapter = new GameAudioPluginAdapter({
delivery: { generateAndMaterialize: async (_context, _command, progress) => {
progress?.({ ...delivered, providerStatus: 'running', deliveryStatus: 'waiting', phase: 'generating', files: [],
billing: { mode: 'platform_metered', status: 'dispatched', reserved_points: '1.00', usage_amount: 1, unit: 'generation' } });
return delivered;
} },
client: { cancel: vi.fn() },
admission: { resolve: vi.fn(async () => ({ releaseId: context.pluginReleaseId, releaseAdmissionId: 'admission' })) },
getPricingVersion: () => 'price', getAccountKey: () => 'owner',
});
const policy: PluginPolicyClientState = { status: 'current', revision: 1, lastVerifiedAt: 1,
catalog: { schema_version: 1, catalog_version: '1', pricing_version: 'price',
plugins: [{ plugin_id: plugin.id, supported_contract_versions: [1], status: 'active',
capabilities: plugin.operations.map((op) => ({ capability_id: op.capabilityId, operations: [{
operation: op.operation, billing: { mode: 'platform_metered', entitlement_scope: 'plugin_usage', notice: 'Metered',
unit_name: 'generation', unit_size: 1, rate_points: '1.00', minimum_charge_points: '0.00', rounding_mode: 'ceil' },
}] })) }] } };
const registry = new CodingCapabilityRegistryImpl({ definitions: [plugin], adapters: [adapter],
getEnabledPluginIds: async () => [plugin.id], policyClient: { getState: () => policy, refresh: vi.fn() } });
const onUpdate = vi.fn();
const output = await registry.invoke({ toolName: 'game_music_generate', workerRole: 'parent',
effectiveSkillIds: ['game-audio'], context: { conversationId: 'c', runId: 'r', resourceId: 't',
projectId: 'p', projectPath: '/project', skillIds: ['game-audio'] },
value: { prompt: 'forest', confirmed: true }, onUpdate });
expect(output.details).toMatchObject({ success: true, data: { files: delivered.files } });
expect(onUpdate).toHaveBeenCalledWith(expect.objectContaining({
details: expect.objectContaining({ success: true, billing: { ...delivered.billing, status: 'dispatched', actual_points: undefined } }),
}));
});
it('ships one project-wide Skill and exactly four statically priced parent tools', () => {
const plugin = definition();
expect(plugin).toBeDefined();
expect(isProjectWideCodingPluginId(plugin.id)).toBe(true);
expect(plugin.tools.map((tool) => [tool.name, tool.capabilityId, tool.operation])).toEqual([
['game_music_generate', 'game-audio.music', 'generate'],
['game_music_preview', 'game-audio.music', 'preview'],
['game_sound_generate', 'game-audio.sound', 'generate'],
['game_audio_cancel', 'game-audio.control', 'cancel'],
]);
expect(plugin.tools.every((tool) => tool.roles.length === 1 && tool.roles[0] === 'parent' && !tool.projectWriteLease)).toBe(true);
});
it('requires literal confirmation; Main freezes identity, mode and trusted price before auto-delivery', async () => {
const generateAndMaterialize = vi.fn(async (): Promise<AudioDeliveryResult> => ({
executionId: null, logicalOperationId: 'trusted-id', providerStatus: 'running',
deliveryStatus: 'waiting', files: [], outputCount: 0, phase: 'generating', progressPercent: 40,
billing: { mode: 'platform_metered', status: 'receipt_unavailable' },
}));
const resolve = vi.fn(async () => ({ releaseId: context.pluginReleaseId, releaseAdmissionId: 'admission' }));
const adapter = new GameAudioPluginAdapter({
delivery: { generateAndMaterialize },
client: { cancel: vi.fn() }, admission: { resolve },
getPricingVersion: () => 'trusted-price', getAccountKey: () => 'owner',
});
const tool = definition().tools[1]!;
expect(await adapter.invoke(context, tool, { prompt: 'ambient forest', confirmed: 1 }))
.toMatchObject({ success: false, code: 'confirmation_required', billing: { status: 'not_started' } });
expect(generateAndMaterialize).not.toHaveBeenCalled();
expect(await adapter.invoke(context, tool, { prompt: 'ambient forest', confirmed: true }))
.toMatchObject({ success: true, status: 202 });
expect(generateAndMaterialize).toHaveBeenCalledWith(expect.objectContaining({
accountKey: 'owner', logicalOperationId: 'trusted-id', projectPath: '/project',
}), expect.objectContaining({
kind: 'music', mode: 'demo', pricing_version_id: 'trusted-price', project_id: 'durable',
release_id: context.pluginReleaseId, release_admission_id: 'admission', logical_operation_id: 'trusted-id',
}), expect.any(Function));
expect(await adapter.invoke(context, tool, { prompt: 'x', confirmed: true, mode: 'full' }))
.toMatchObject({ success: false, code: 'plugin_input_invalid' });
generateAndMaterialize.mockRejectedValueOnce(new Error('receipt disk failed after POST'));
expect(await adapter.invoke(context, tool, { prompt: 'forest', confirmed: true }))
.toMatchObject({ success: false, billing: { status: 'receipt_unavailable' } });
});
});