Files
gmtool/tests/art/generate.test.js
T
slaguru666andClaude Opus 4.8 028dcb5e4e feat: online art Generate — configured endpoint, cached into the library
Add the art tray's online Generate: prompt → a GM-configured image-gen
endpoint → new pencil image cached into a persisted, searchable library
(offline thereafter). No endpoint or key is hardcoded — the GM pastes
their gen URL (⚙ field, persisted). Degrades gracefully: unconfigured or
offline → Generate disabled, library search fully works; network is never
load-bearing.

- src/art/generate.js: createArtGenerator (injectable fetch/navigator).
- src/art/library.js: pure addGenerated (dedupe + cap 24).
- art-tray: generate row, endpoint config, generated cache (gmd.art.*).

Tests: generate.test.js, library.test.js, art-tray-generate.test.js;
existing art-tray tests unchanged. 206 tests pass; build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 23:48:05 +01:00

60 lines
2.8 KiB
JavaScript

import { describe, it, expect } from 'vitest';
import { createArtGenerator, promptTags } from '../../src/art/generate.js';
const okFetch = (payload) => async (url, opts) => ({
ok: true, status: 200,
json: async () => payload,
_url: url, _body: opts.body,
});
describe('createArtGenerator', () => {
it('is not ready without an endpoint and throws on generate', async () => {
const g = createArtGenerator({ endpoint: null, fetchImpl: okFetch({}) });
expect(g.configured).toBe(false);
expect(g.ready()).toBe(false);
await expect(g.generate('rain')).rejects.toThrow(/no generate endpoint/);
});
it('is not ready and refuses when offline', async () => {
const g = createArtGenerator({ endpoint: 'https://gen', fetchImpl: okFetch({}), navigatorRef: { onLine: false } });
expect(g.ready()).toBe(false);
await expect(g.generate('rain')).rejects.toThrow(/offline/);
});
it('posts the prompt and normalises the reply into an asset', async () => {
const fetchImpl = okFetch({ image: 'data:image/png;base64,AAAA', label: 'Rainy pier', tags: ['rain', 'pier'] });
const g = createArtGenerator({ endpoint: 'https://gen', fetchImpl, navigatorRef: { onLine: true } });
expect(g.ready()).toBe(true);
const asset = await g.generate('rain over the pier');
expect(asset).toEqual({ src: 'data:image/png;base64,AAAA', label: 'Rainy pier', tags: ['rain', 'pier'] });
});
it('derives a label and tags from the prompt when the reply omits them', async () => {
const g = createArtGenerator({ endpoint: 'https://gen', fetchImpl: okFetch({ url: 'https://img/x.png' }), navigatorRef: { onLine: true } });
const asset = await g.generate('tired detective in the rain');
expect(asset.src).toBe('https://img/x.png');
expect(asset.label).toBe('tired detective in the rain');
expect(asset.tags).toContain('generated');
expect(asset.tags).toContain('detective');
});
it('surfaces a clear error on a non-ok response', async () => {
const g = createArtGenerator({ endpoint: 'https://gen', fetchImpl: async () => ({ ok: false, status: 503 }), navigatorRef: { onLine: true } });
await expect(g.generate('x')).rejects.toThrow(/generate failed \(503\)/);
});
it('errors when the reply has no image', async () => {
const g = createArtGenerator({ endpoint: 'https://gen', fetchImpl: okFetch({ label: 'nope' }), navigatorRef: { onLine: true } });
await expect(g.generate('x')).rejects.toThrow(/no image/);
});
it('requires a non-empty prompt', async () => {
const g = createArtGenerator({ endpoint: 'https://gen', fetchImpl: okFetch({ image: 'd' }), navigatorRef: { onLine: true } });
await expect(g.generate(' ')).rejects.toThrow(/prompt/);
});
it('promptTags dedupes and drops short words', () => {
expect(promptTags('a rain rain PIER of')).toEqual(['rain', 'pier', 'generated']);
});
});