100 lines
6.8 KiB
TypeScript
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' } });
|
|
});
|
|
});
|