feat: integrate automatic game resource delivery

This commit is contained in:
2026-09-06 09:50:49 +08:00
parent f721966f3c
commit 4df4bc9624
20 changed files with 1667 additions and 191 deletions

View File

@@ -8,6 +8,11 @@ import type { CodingPluginToolDefinition } from '../../shared/coding-plugins';
import {
GameResourcePluginAdapter,
} from '../../electron/coding-plugins/adapters/game-resource';
import { PiProjectWriteLeaseCoordinator } from '../../electron/coding-runtime/pi/write-lease';
import {
GameResourceDeliveryCoordinator,
GameResourceDeliveryReceiptStore,
} from '../../electron/services/game-resource-delivery';
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';
@@ -52,8 +57,8 @@ function tool(
capabilityId,
operation,
roles: ['parent'],
mutation: name === 'game_resource_status' ? 'read' : 'write',
projectWriteLease: name === 'game_resource_save_output',
mutation: operation === 'templates' ? 'read' : 'write',
projectWriteLease: false,
permissions: [`hosted.game-resource.${operation.replaceAll('_', '-')}`],
inputSchema: { type: 'object' },
};
@@ -61,7 +66,8 @@ function tool(
async function fixture() {
const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-game-resource-'));
roots.push(projectPath);
const userDataDir = await mkdtemp(path.join(tmpdir(), 'makelore-game-resource-receipts-'));
roots.push(projectPath, userDataDir);
const resolve = vi.fn(async () => ({
resolveRequestId: 'pi:run-a:resource-a',
resolveRequestDigest: 'a'.repeat(64),
@@ -84,8 +90,15 @@ async function fixture() {
bytes: new Uint8Array([1, 2, 3]), fileName: 'hero.png', contentType: 'image/png',
})),
};
const delivery = new GameResourceDeliveryCoordinator({
client: client as unknown as GameResourceClient,
receipts: new GameResourceDeliveryReceiptStore(path.join(userDataDir, 'receipts.json')),
leases: new PiProjectWriteLeaseCoordinator(),
sleep: async () => undefined,
});
const adapter = new GameResourcePluginAdapter({
client: client as unknown as GameResourceClient,
delivery,
marketplace: { resolve } as unknown as MarketplacePackageClientPort,
packageStore: {
getInstalled: vi.fn(async () => ({
@@ -113,15 +126,16 @@ afterEach(async () => {
});
describe('GameResourcePluginAdapter', () => {
it('resolves a fresh admission and keeps project reference bytes behind the hosted boundary', async () => {
it('resolves admission, hides polling, and automatically saves generated output in the project', async () => {
const { adapter, client, context, projectPath, resolve } = await fixture();
await writeFile(path.join(projectPath, 'reference.png'), new Uint8Array([4, 5, 6]));
const onProgress = vi.fn();
const result = await adapter.invoke(context, tool('game_resource_generate'), {
kind: 'pixel', templateName: 'character', requirement: 'Blue-armored hero',
confirmed: true,
referencePaths: ['reference.png'],
});
}, onProgress);
expect(resolve).toHaveBeenCalledWith(expect.objectContaining({
resolveRequestId: 'pi:run-a:resource-a', channel: 'stable',
@@ -133,9 +147,26 @@ describe('GameResourcePluginAdapter', () => {
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 },
success: true, status: 200, billing,
data: {
executionId: EXECUTION_ID,
providerStatus: 'succeeded',
deliveryStatus: 'saved',
files: [{
path: `assets/generated/game-resource/${EXECUTION_ID}/output-1.png`,
bytes: 3,
}],
},
});
expect(client.get).toHaveBeenCalledTimes(1);
expect(client.download).toHaveBeenCalledWith(EXECUTION_ID, 0);
await expect(readFile(path.join(
projectPath,
'assets', 'generated', 'game-resource', EXECUTION_ID, 'output-1.png',
))).resolves.toEqual(Buffer.from([1, 2, 3]));
expect(onProgress.mock.calls.map(([progress]) => progress.data.phase)).toEqual([
'submitted', 'generating', 'saving', 'saved',
]);
expect(JSON.stringify(result)).not.toContain(projectPath);
});
@@ -151,28 +182,6 @@ describe('GameResourcePluginAdapter', () => {
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' }))