feat: expand ISAC content modeling
Validate and deploy production / validate (push) Canceled after 0s
Validate and deploy production / deploy (push) Canceled after 0s
Validate and deploy staging / validate (push) Successful in 28s
Validate and deploy staging / deploy (push) Successful in 1m3s

This commit is contained in:
jb
2026-08-03 01:23:20 -04:00
parent d9924a95b9
commit 57068b61cd
90 changed files with 9635 additions and 1270 deletions
+167 -2
View File
@@ -1,17 +1,24 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import sharp from 'sharp';
const { rmMock } = vi.hoisted(() => ({ rmMock: vi.fn() }));
const { mkdirMock, rmMock, writeFileMock } = vi.hoisted(() => ({
mkdirMock: vi.fn(),
rmMock: vi.fn(),
writeFileMock: vi.fn(),
}));
vi.mock('node:fs/promises', async (importOriginal) => ({
...(await importOriginal<typeof import('node:fs/promises')>()),
mkdir: mkdirMock,
rm: rmMock,
writeFile: writeFileMock,
}));
import { GAME_ICON_TYPES } from './game-data-icons';
import {
GAME_DATA_RENDER_TYPES,
ManagedImageDeletionError,
applyGameDataRender,
deleteManagedImage,
processManagedImage,
} from './managed-images';
@@ -26,6 +33,16 @@ class FakeDatabase {
prepare(sql: string) {
return Promise.resolve({
get: async (...args: unknown[]) => {
if (
sql.startsWith('SELECT * FROM managed_images') &&
sql.includes('source_entity_type')
) {
return [...this.images.values()].find(
(row) =>
row.source_entity_type === args[0] &&
row.source_entity_id === args[1],
);
}
if (sql.startsWith('SELECT * FROM managed_images'))
return this.images.get(String(args[0]));
if (sql.startsWith('SELECT * FROM game_data_icons')) {
@@ -42,7 +59,64 @@ class FakeDatabase {
}
if (sql.startsWith('DELETE FROM managed_images')) {
const row = this.images.get(String(args[0]));
if (row?.storage_key === args[1]) this.images.delete(String(args[0]));
if (!sql.includes('storage_key') || row?.storage_key === args[1])
this.images.delete(String(args[0]));
return;
}
if (sql.startsWith('UPDATE managed_images SET')) {
const row = this.images.get(String(args[7]));
if (!row) throw new Error('Missing managed image for update');
this.images.set(String(args[7]), {
...row,
category: args[0],
tags_json: args[1],
is_icon: 0,
force_white: 0,
original_filename: args[2],
byte_size: args[3],
width: args[4],
height: args[5],
updated_at: args[6],
});
return;
}
if (sql.startsWith('INSERT INTO managed_images')) {
const [
id,
name,
category,
tagsJson,
isIcon,
forceWhite,
storageKey,
originalFilename,
byteSize,
width,
height,
sourceType,
sourceEntityId,
createdBy,
createdAt,
updatedAt,
] = args;
this.images.set(String(id), {
id,
name,
category,
tags_json: tagsJson,
is_icon: isIcon,
force_white: forceWhite,
storage_key: storageKey,
original_filename: originalFilename,
byte_size: byteSize,
width,
height,
source_entity_type: sourceType,
source_entity_id: sourceEntityId,
created_by: createdBy,
created_at: createdAt,
updated_at: updatedAt,
});
return;
}
throw new Error(`Unexpected run: ${sql}`);
@@ -210,3 +284,94 @@ describe('processManagedImage', () => {
expect(metadata.height).toBe(1000);
});
});
describe('applyGameDataRender', () => {
beforeEach(() => {
mkdirMock.mockReset().mockResolvedValue(undefined);
rmMock.mockReset().mockResolvedValue(undefined);
writeFileMock.mockReset().mockResolvedValue(undefined);
});
it.each(GAME_DATA_RENDER_TYPES)(
'persists and returns authoritative %s render metadata',
async (type) => {
const database = new FakeDatabase();
const result = await applyGameDataRender(
database as never,
type,
sourceId,
'Test Entity',
{
file: new File(['source'], 'source.png', { type: 'image/png' }),
remove: false,
processed: {
output: Buffer.from('processed-render'),
width: 1000,
height: 1000,
},
},
'operator-1',
);
expect(result).toMatchObject({
original_filename: 'source.png',
byte_size: Buffer.byteLength('processed-render'),
width: 1000,
height: 1000,
source_entity_type: `${type}_render`,
source_entity_id: sourceId,
created_by: 'operator-1',
created_at: expect.any(String),
updated_at: expect.any(String),
});
expect(writeFileMock).toHaveBeenCalledOnce();
},
);
it('returns refreshed replacement metadata without rewriting creator provenance', async () => {
const database = new FakeDatabase();
database.images.set(imageId, {
id: imageId,
name: 'Ability render',
category: 'Specializations',
tags_json: '[]',
is_icon: 0,
force_white: 0,
storage_key: `library-${imageId}.webp`,
original_filename: 'old.png',
byte_size: 3,
width: 100,
height: 100,
source_entity_type: 'specialization_ability_render',
source_entity_id: sourceId,
created_by: 'original-operator',
created_at: '2026-01-01T00:00:00.000Z',
updated_at: '2026-01-01T00:00:00.000Z',
});
const result = await applyGameDataRender(
database as never,
'specialization_ability',
sourceId,
'Ability',
{
file: new File(['new'], 'new.png', { type: 'image/png' }),
remove: false,
processed: {
output: Buffer.from('replacement'),
width: 1000,
height: 1000,
},
},
'replacement-operator',
);
expect(result).toMatchObject({
id: imageId,
original_filename: 'new.png',
byte_size: Buffer.byteLength('replacement'),
created_by: 'original-operator',
created_at: '2026-01-01T00:00:00.000Z',
});
});
});