feat(plugins): move game resources behind marketplace
This commit is contained in:
184
tests/unit/game-resource-plugin-adapter.test.ts
Normal file
184
tests/unit/game-resource-plugin-adapter.test.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { CodingPluginToolDefinition } from '../../shared/coding-plugins';
|
||||
import {
|
||||
GameResourcePluginAdapter,
|
||||
} from '../../electron/coding-plugins/adapters/game-resource';
|
||||
import type { GameResourceClient, GameResourceGeneration } from '../../electron/services/game-resource-client';
|
||||
import type { MarketplacePackageClientPort, PluginPackageStore } from '../../electron/coding-plugins/package-store';
|
||||
import type { TrustedCodingCapabilityContext } from '../../electron/coding-plugins/registry';
|
||||
|
||||
const EXECUTION_ID = '11111111-1111-4111-8111-111111111111';
|
||||
const RELEASE_ID = '22222222-2222-4222-8222-222222222222';
|
||||
const PROJECT_ID = '33333333-3333-4333-8333-333333333333';
|
||||
const roots: string[] = [];
|
||||
|
||||
const billing = {
|
||||
mode: 'platform_metered' as const,
|
||||
status: 'dispatched' as const,
|
||||
reserved_points: '2.00',
|
||||
usage_amount: 1,
|
||||
unit: 'generation',
|
||||
};
|
||||
|
||||
function generation(status: GameResourceGeneration['status'] = 'accepted'): GameResourceGeneration {
|
||||
return {
|
||||
executionId: EXECUTION_ID,
|
||||
releaseId: RELEASE_ID,
|
||||
projectId: PROJECT_ID,
|
||||
logicalOperationId: 'pi:run-a:resource-a',
|
||||
kind: 'pixel',
|
||||
templateName: 'character',
|
||||
status,
|
||||
outputCount: status === 'succeeded' ? 1 : 0,
|
||||
pollIntervalSeconds: 3,
|
||||
billing,
|
||||
};
|
||||
}
|
||||
|
||||
function tool(
|
||||
name: string,
|
||||
capabilityId = 'game-resource.generate',
|
||||
operation = name.replace('game_resource_', ''),
|
||||
): CodingPluginToolDefinition {
|
||||
return {
|
||||
name,
|
||||
label: name,
|
||||
description: name,
|
||||
capabilityId,
|
||||
operation,
|
||||
roles: ['parent'],
|
||||
mutation: name === 'game_resource_status' ? 'read' : 'write',
|
||||
projectWriteLease: name === 'game_resource_save_output',
|
||||
permissions: [`hosted.game-resource.${operation.replaceAll('_', '-')}`],
|
||||
inputSchema: { type: 'object' },
|
||||
};
|
||||
}
|
||||
|
||||
async function fixture() {
|
||||
const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-game-resource-'));
|
||||
roots.push(projectPath);
|
||||
const resolve = vi.fn(async () => ({
|
||||
resolveRequestId: 'pi:run-a:resource-a',
|
||||
resolveRequestDigest: 'a'.repeat(64),
|
||||
items: [{
|
||||
pluginId: 'makelore.game-resource', action: 'keep' as const,
|
||||
releaseId: RELEASE_ID, version: '1.0.0', sha256: 'a'.repeat(64), sizeBytes: 1,
|
||||
releaseAdmissionId: 'admission-a', expiresAt: '2100-01-01T00:00:00Z',
|
||||
channel: 'stable' as const, reason: null,
|
||||
}],
|
||||
catalogGeneration: 1,
|
||||
etag: '"plugins-1"',
|
||||
stale: false,
|
||||
}));
|
||||
const client = {
|
||||
templates: vi.fn(async () => ['character']),
|
||||
generate: vi.fn(async () => generation()),
|
||||
get: vi.fn(async () => generation('succeeded')),
|
||||
cancel: vi.fn(async () => generation('cancelled')),
|
||||
download: vi.fn(async () => ({
|
||||
bytes: new Uint8Array([1, 2, 3]), fileName: 'hero.png', contentType: 'image/png',
|
||||
})),
|
||||
};
|
||||
const adapter = new GameResourcePluginAdapter({
|
||||
client: client as unknown as GameResourceClient,
|
||||
marketplace: { resolve } as unknown as MarketplacePackageClientPort,
|
||||
packageStore: {
|
||||
getInstalled: vi.fn(async () => ({
|
||||
pluginId: 'makelore.game-resource', releaseId: RELEASE_ID, version: '1.0.0',
|
||||
sha256: 'a'.repeat(64), channel: 'stable',
|
||||
})),
|
||||
getInstalledRelease: vi.fn(async () => ({
|
||||
pluginId: 'makelore.game-resource', releaseId: RELEASE_ID, version: '1.0.0',
|
||||
sha256: 'a'.repeat(64), channel: 'stable',
|
||||
})),
|
||||
} as unknown as Pick<PluginPackageStore, 'getInstalled' | 'getInstalledRelease'>,
|
||||
makeloreVersion: '2.0.0',
|
||||
});
|
||||
const context: TrustedCodingCapabilityContext = {
|
||||
conversationId: 'conversation-a', runId: 'run-a', resourceId: 'resource-a',
|
||||
requestId: 'pi:run-a:resource-a', localProjectId: 'local-a',
|
||||
projectPath, durableProjectId: PROJECT_ID, workerRole: 'parent',
|
||||
effectiveSkillIds: ['game-resource'], pluginReleaseId: RELEASE_ID,
|
||||
};
|
||||
return { adapter, client, context, projectPath, resolve };
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(roots.splice(0).map(async (root) => await rm(root, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
describe('GameResourcePluginAdapter', () => {
|
||||
it('resolves a fresh admission and keeps project reference bytes behind the hosted boundary', async () => {
|
||||
const { adapter, client, context, projectPath, resolve } = await fixture();
|
||||
await writeFile(path.join(projectPath, 'reference.png'), new Uint8Array([4, 5, 6]));
|
||||
|
||||
const result = await adapter.invoke(context, tool('game_resource_generate'), {
|
||||
kind: 'pixel', templateName: 'character', requirement: 'Blue-armored hero',
|
||||
confirmed: true,
|
||||
referencePaths: ['reference.png'],
|
||||
});
|
||||
|
||||
expect(resolve).toHaveBeenCalledWith(expect.objectContaining({
|
||||
resolveRequestId: 'pi:run-a:resource-a', channel: 'stable',
|
||||
installed: [{ pluginId: 'makelore.game-resource', releaseId: RELEASE_ID, sha256: 'a'.repeat(64) }],
|
||||
}));
|
||||
expect(client.generate).toHaveBeenCalledWith(expect.objectContaining({
|
||||
releaseAdmissionId: 'admission-a', releaseId: RELEASE_ID,
|
||||
projectId: PROJECT_ID, logicalOperationId: 'pi:run-a:resource-a',
|
||||
referenceFiles: [{ name: 'reference.png', mimeType: 'image/png', dataBase64: 'BAUG' }],
|
||||
}));
|
||||
expect(result).toMatchObject({
|
||||
success: true, status: 202, billing,
|
||||
data: { executionId: EXECUTION_ID, status: 'accepted', outputCount: 0 },
|
||||
});
|
||||
expect(JSON.stringify(result)).not.toContain(projectPath);
|
||||
});
|
||||
|
||||
it('does not resolve admission or reserve usage before explicit generation confirmation', async () => {
|
||||
const { adapter, client, context, resolve } = await fixture();
|
||||
|
||||
await expect(adapter.invoke(context, tool('game_resource_generate'), {
|
||||
kind: 'pixel', templateName: 'character', requirement: 'Blue-armored hero',
|
||||
confirmed: false,
|
||||
})).resolves.toMatchObject({ success: false, code: 'confirmation_required' });
|
||||
|
||||
expect(resolve).not.toHaveBeenCalled();
|
||||
expect(client.generate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('requires explicit confirmation and never overwrites a project file', async () => {
|
||||
const { adapter, client, context, projectPath } = await fixture();
|
||||
const saveTool = tool('game_resource_save_output', 'game-resource.library', 'save_output');
|
||||
|
||||
await expect(adapter.invoke(context, saveTool, {
|
||||
executionId: EXECUTION_ID, relativePath: 'assets/hero.png', confirmed: false,
|
||||
})).resolves.toMatchObject({ success: false, code: 'confirmation_required' });
|
||||
expect(client.download).not.toHaveBeenCalled();
|
||||
|
||||
await expect(adapter.invoke(context, saveTool, {
|
||||
executionId: EXECUTION_ID, relativePath: 'assets/hero.png', confirmed: true,
|
||||
})).resolves.toMatchObject({
|
||||
success: true, data: { savedPath: 'assets/hero.png', bytes: 3 },
|
||||
});
|
||||
await expect(readFile(path.join(projectPath, 'assets/hero.png'))).resolves.toEqual(Buffer.from([1, 2, 3]));
|
||||
|
||||
await expect(adapter.invoke(context, saveTool, {
|
||||
executionId: EXECUTION_ID, relativePath: 'assets/hero.png', confirmed: true,
|
||||
})).resolves.toMatchObject({ success: false, code: 'game_resource_destination_exists' });
|
||||
await expect(readFile(path.join(projectPath, 'assets/hero.png'))).resolves.toEqual(Buffer.from([1, 2, 3]));
|
||||
});
|
||||
|
||||
it('lists server-owned templates through the same release admission', async () => {
|
||||
const { adapter, context, client } = await fixture();
|
||||
await expect(adapter.invoke(context, tool('game_resource_templates'), { kind: 'pixel' }))
|
||||
.resolves.toMatchObject({ success: true, data: { kind: 'pixel', templates: ['character'] } });
|
||||
expect(client.templates).toHaveBeenCalledWith({
|
||||
releaseId: RELEASE_ID, releaseAdmissionId: 'admission-a', kind: 'pixel',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user