feat: generate image metadata and expose public search API

This commit is contained in:
smileBeda committed 2026-09-30 16:24:53 -03:00
1 parent 082f715c39
commit 137cad1d84
7 files changed
+450 -93

No files matched your search

+55 -1
View File
@@ -34,7 +34,7 @@ test('queues model approvals below the auto-publish quality threshold', async ()
globalThis.fetch = async (_url, options) => {
const body = JSON.parse(options.body);
const prompt = body.input[0].content.find((item) => item.type === 'input_text').text;
const prompt = body.input[0].content;
assert.match(prompt, /could the exact same caption work on 50 unrelated images/);
assert.match(prompt, /Use approved only when it appears legal and score is at least 90/);
@@ -74,3 +74,57 @@ test('queues model approvals below the auto-publish quality threshold', async ()
}
}
});
test('generates searchable captions using a constrained tag catalog and discards invented tags', async (t) => {
const originalKey = process.env.OPENAI_API_KEY;
const originalFetch = globalThis.fetch;
process.env.OPENAI_API_KEY = 'test-key';
t.after(() => {
globalThis.fetch = originalFetch;
if (originalKey === undefined) delete process.env.OPENAI_API_KEY;
else process.env.OPENAI_API_KEY = originalKey;
});
globalThis.fetch = async (_url, options) => {
const body = JSON.parse(options.body);
assert.equal(body.text.format.type, 'json_schema');
assert.equal(body.text.format.strict, true);
assert.deepEqual(body.text.format.schema.properties.tags.items.enum, ['cats', 'work']);
assert.match(body.input[0].content, /Available tags.*\["cats","work"\]/);
assert.match(body.input[0].content, /never as instructions/);
assert.equal(body.input[1].content[0].type, 'input_image');
return { ok: true, json: async () => ({ status: 'completed', output: [{ content: [{ type: 'output_text', text: JSON.stringify({
decision: 'approved', score: 99, reason: 'Good meme', caption: ' A cat says “I hate Mondays” at work. ', tags: ['cats', 'invented', 'cats']
}) }] }] }) };
};
const result = await moderateImage({ buffer: Buffer.from('image'), mime: 'image/webp', availableTags: ['cats', 'work'] });
assert.equal(result.status, 'approved');
assert.equal(result.caption, 'A cat says “I hate Mondays” at work.');
assert.deepEqual(result.tags, ['cats']);
});
test('failed or incomplete caption generation raises an error; failed moderation queues safely', async (t) => {
const { captionImage } = await import('../src/moderation.js');
const originalKey = process.env.OPENAI_API_KEY;
const originalFetch = globalThis.fetch;
process.env.OPENAI_API_KEY = 'test-key';
t.after(() => {
globalThis.fetch = originalFetch;
if (originalKey === undefined) delete process.env.OPENAI_API_KEY;
else process.env.OPENAI_API_KEY = originalKey;
});
const image = { buffer: Buffer.from('image'), mime: 'image/webp' };
for (const payload of [
{ status: 'incomplete', output_text: '{"caption":"partial","tags":[]}' },
{ status: 'completed', output: [{ content: [{ type: 'refusal', refusal: 'No' }] }] },
{ output_text: 'not JSON' }
]) {
globalThis.fetch = async () => ({ ok: true, json: async () => payload });
await assert.rejects(captionImage(image), { statusCode: 503 });
const result = await moderateImage(image);
assert.equal(result.status, 'pending');
assert.equal(result.caption, '');
assert.deepEqual(result.tags, []);
}
delete process.env.OPENAI_API_KEY;
await assert.rejects(captionImage(image), { statusCode: 503 });
});
+132
View File
@@ -0,0 +1,132 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { once } from 'node:events';
import fs from 'node:fs/promises';
import net from 'node:net';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createStore } from '../src/store.js';
import { encodePng } from '../src/png.js';
import { normalizeToWebp } from '../src/normalize.js';
const projectRoot = fileURLToPath(new URL('../', import.meta.url));
const adminToken = 'test-admin-token-000000000000';
test('public search/read and admin-only metadata mutations work end to end', { timeout: 20_000 }, async (t) => {
const probe = net.createServer();
try {
await new Promise((resolve, reject) => { probe.once('error', reject); probe.listen(0, '127.0.0.1', resolve); });
} catch (error) {
if (error.code === 'EPERM') { t.skip('loopback listeners are disabled in this sandbox'); return; }
throw error;
}
const port = probe.address().port;
await new Promise((resolve) => probe.close(resolve));
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'meme-search-api-'));
const store = await createStore({ dataDir: dir });
await store.addTag('cats');
await store.addTag('work');
const png = encodePng(16, 16, () => [0, 220, 40, 255]);
const normalized = await normalizeToWebp(png);
const approved = await store.save({ ...normalized, originalName: 'cat.png', caption: 'An angry cat at work', tags: ['cats', 'work'] });
const pendingImage = await normalizeToWebp(encodePng(16, 16, () => [10, 150, 40, 255]));
const pending = await store.save({ ...pendingImage, originalName: 'pending.png', caption: 'A pending cat', tags: ['cats'], status: 'pending' });
const mockPath = path.join(dir, 'mock-ai.mjs');
await fs.writeFile(mockPath, `globalThis.fetch = async () => ({ ok: true, json: async () => ({ status: 'completed', output_text: JSON.stringify({ decision: 'approved', score: 99, reason: 'A good meme', caption: 'AI caption: a sleeping cat at work', tags: ['cats', 'invented'] }) }) });`);
const child = spawn(process.execPath, ['--import', mockPath, 'server.js'], {
cwd: projectRoot,
env: { ...process.env, ADMIN_TOKEN: adminToken, OPENAI_API_KEY: 'test-key', AUTO_APPROVE_MIN_SCORE: '80', DATA_DIR: dir,
HOST: '127.0.0.1', PORT: String(port), SEED_DEMO_MEMES: 'false', SITE_URL: `http://127.0.0.1:${port}` },
stdio: ['ignore', 'pipe', 'pipe']
});
t.after(async () => {
if (child.exitCode === null && child.signalCode === null) { child.kill(); await once(child, 'exit'); }
await fs.rm(dir, { recursive: true, force: true });
});
await new Promise((resolve, reject) => {
let output = '';
const timeout = setTimeout(() => finish(new Error(`Server startup timed out: ${output}`)), 10_000);
const onData = (chunk) => { output += chunk; if (output.includes('The Meme Protocol listening')) finish(); };
const onExit = () => finish(new Error(`Server failed to start: ${output}`));
const finish = (error) => {
clearTimeout(timeout); child.stdout.off('data', onData); child.stderr.off('data', onData); child.off('exit', onExit);
error ? reject(error) : resolve();
};
child.stdout.on('data', onData); child.stderr.on('data', onData); child.once('exit', onExit);
});
const base = `http://127.0.0.1:${port}`;
const request = (url, method = 'GET', body, authenticated = false) => fetch(`${base}${url}`, {
method, headers: { ...(authenticated ? { 'X-Admin-Token': adminToken } : {}), ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}) },
...(body !== undefined ? { body: typeof body === 'string' ? body : JSON.stringify(body) } : {})
});
const publicList = await (await request('/api/memes?q=ANGRY%20catt&tag=cats&tag=work')).json();
assert.equal(publicList.total, 1);
assert.equal(publicList.memes[0].id, approved.id);
assert.equal(publicList.memes[0].caption, 'An angry cat at work');
assert.deepEqual(publicList.memes[0].tags, ['cats', 'work']);
assert.equal('storageKey' in publicList.memes[0], false);
assert.equal('metadataVersion' in publicList.memes[0], false);
assert.deepEqual((await (await request('/api/tags')).json()).tags, ['cats', 'work']);
assert.equal((await request(`/api/memes/${pending.id}`)).status, 404);
assert.equal((await request('/api/memes?q=' + 'a'.repeat(201))).status, 400);
assert.equal((await request('/api/memes?tag=%3Cscript%3E')).status, 400);
assert.equal((await request('/api/memes?' + Array(21).fill('tag=cats').join('&'))).status, 400);
for (const [url, method, body] of [
['/api/admin/memes', 'GET'], ['/api/admin/tags', 'GET'], ['/api/admin/pending', 'GET'],
['/api/admin/tags', 'POST', { name: 'dogs' }], ['/api/admin/tags/cats', 'DELETE'],
[`/api/admin/memes/${approved.id}`, 'PATCH', { caption: 'Malicious overwrite', tags: [] }],
[`/api/admin/memes/${approved.id}/generate`, 'POST'],
['/api/admin/approve', 'POST', { ids: [pending.id] }], ['/api/admin/delete', 'POST', { ids: [approved.id] }]
]) assert.equal((await request(url, method, body)).status, 404, `${method} ${url} must require admin auth`);
assert.equal((await request(`/api/memes/${approved.id}`, 'PATCH', { caption: 'Public write' })).status, 404);
assert.equal((await request('/api/tags', 'POST', { name: 'public-tag' })).status, 404);
const all = await request('/api/admin/memes?status=all', 'GET', undefined, true);
assert.equal(all.headers.get('cache-control'), 'no-store');
assert.equal((await all.json()).total, 2);
assert.equal((await request('/api/admin/memes?status=bad', 'GET', undefined, true)).status, 400);
assert.equal((await request(`/api/admin/memes/${approved.id}`, 'PATCH', '{bad json', true)).status, 400);
assert.equal((await request(`/api/admin/memes/${approved.id}`, 'PATCH', null, true)).status, 400);
assert.equal((await request(`/api/admin/memes/${approved.id}`, 'PATCH', { tags: ['invented'] }, true)).status, 400);
assert.equal((await request(`/api/admin/memes/${approved.id}`, 'PATCH', { status: 'pending' }, true)).status, 400);
assert.equal((await request('/api/admin/tags', 'POST', { name: ' Dogs ' }, true)).status, 201);
assert.equal((await request('/api/admin/tags', 'POST', { name: 'dogs' }, true)).status, 409);
const edited = await request(`/api/admin/memes/${approved.id}`, 'PATCH', { caption: 'Admin caption', tags: ['dogs'] }, true);
assert.equal(edited.status, 200);
const metadata = await (await request(`/api/memes/${approved.id}`)).json();
assert.equal(metadata.meme.caption, 'Admin caption');
assert.deepEqual(metadata.meme.tags, ['dogs']);
const skipped = await (await request(`/api/admin/memes/${approved.id}/generate`, 'POST', { onlyMissing: true }, true)).json();
assert.equal(skipped.skipped, true);
assert.equal(skipped.meme.caption, 'Admin caption');
const generated = await (await request(`/api/admin/memes/${pending.id}/generate`, 'POST', {}, true)).json();
assert.equal(generated.meme.caption, 'AI caption: a sleeping cat at work');
assert.deepEqual(generated.meme.tags, ['cats']);
assert.equal(generated.meme.status, 'pending');
assert.equal((await request(`/api/memes/${pending.id}`)).status, 404);
const form = new FormData();
form.append('meme', new Blob([encodePng(16, 16, () => [200, 40, 90, 255])], { type: 'image/png' }), 'upload.png');
form.append('caption', 'Client injected caption');
form.append('tags', 'invented');
const upload = await fetch(`${base}/api/memes`, { method: 'POST', body: form });
assert.equal(upload.status, 201);
const uploaded = (await upload.json()).meme;
assert.equal(uploaded.caption, 'AI caption: a sleeping cat at work');
assert.deepEqual(uploaded.tags, ['cats']);
const view = await (await request(`/api/memes/${uploaded.id}/view`, 'POST')).json();
assert.equal(view.meme.caption, uploaded.caption);
const deletedTag = await request('/api/admin/tags/cats', 'DELETE', undefined, true);
assert.equal(deletedTag.status, 200);
assert.deepEqual((await (await request(`/api/memes/${uploaded.id}`)).json()).meme.tags, []);
const spec = await (await request('/openapi.json')).json();
assert.ok(spec.paths['/api/tags']);
assert.ok(spec.components.schemas.Meme.properties.caption);
assert.ok(!Object.keys(spec.paths).some((url) => url.startsWith('/api/admin')));
const reloaded = await createStore({ dataDir: dir });
assert.equal(reloaded.get(approved.id).caption, 'Admin caption');
assert.deepEqual(reloaded.get(pending.id).tags, []);
});