Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 48s
Forgejo Docker Build / Root app tests (push) Successful in 51s
Forgejo Android APK / Build signed APK (push) Successful in 2m16s
Forgejo Docker Build / Build Docker image (push) Successful in 20s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
The image job key was the request-body hash plus the figure's position — slide index for a deck, reply index for a document. Two generations from the same form produce the same body hash, so figure 4 of the second generation collided with figure 4 of the first on the unique (owner, workflow, idempotency_key). The constraint handed back the existing job, and the new deck displayed the old deck's artwork. The decks are not even the same length, so the reused picture could land on a slide about something else entirely. Keyed on what is being drawn now: the body hash stays, so submitting the identical request twice still dedupes rather than billing twice, and a hash of the prompt (plus layout and shape) is what makes two different pictures two different jobs. Same fix in deckBuild and resourceImages. Also split fileLog out of logger. logger requires the database at module load, so importing it to record a diagnostic pulls in a connection pool — wrong on its own terms, and it hung the whole test suite when imageTool started logging its refusals: a unit test that never touches a database inherited an open pool handle and never exited. logger.file now delegates to fileLog, so there is still one implementation of where a line goes and how it is redacted. With that in place, every image-tool refusal is recorded durably. There are five of them, they want five different fixes, and until now none of them left any trace once the container was replaced. Verified against a mutation: restoring the index-based key fails two of the four collision tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
71 lines
2.9 KiB
JavaScript
71 lines
2.9 KiB
JavaScript
// Two generations from the same form used to collide: the image key was the
|
|
// request-body hash plus the figure's *position*, and the unique constraint on
|
|
// (owner, workflow, key) then handed back the first generation's job. The second
|
|
// deck showed the first deck's picture — and since the decks are not the same
|
|
// length, on a slide about something else.
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
const deckBuild = require('../src/utils/deckBuild');
|
|
const resourceImages = require('../src/utils/resourceImages');
|
|
|
|
const deckSchema = require('../src/utils/deckSchema');
|
|
|
|
// opts.images is the job queue, not the generatedImages module.
|
|
function recorder() {
|
|
const keys = [];
|
|
return {
|
|
keys,
|
|
images: { enqueue: async (owner, workflow, input, key) => { keys.push(key); return { jobId: 'job-' + keys.length, status: 'pending' }; } }
|
|
};
|
|
}
|
|
|
|
function deckOf(prompts) {
|
|
return deckSchema.normalise({
|
|
slides: prompts.map((p, i) => ({ type: 'figure', heading: 'Slide ' + i, bullets: ['a'], image_prompt: p }))
|
|
});
|
|
}
|
|
|
|
test('the same form twice, with different figures, makes different jobs', async () => {
|
|
const a = recorder();
|
|
await deckBuild.drawFigures(deckOf(['a neuron diagram', 'a rash photograph']),
|
|
{ owner: 1, body: { topic: 'x' }, subject: 'x', imageModel: 'm', images: a.images });
|
|
|
|
const b = recorder();
|
|
await deckBuild.drawFigures(deckOf(['a completely different airway diagram', 'a bilirubin chart']),
|
|
{ owner: 1, body: { topic: 'x' }, subject: 'x', imageModel: 'm', images: b.images });
|
|
|
|
assert.equal(a.keys.length, 2);
|
|
assert.equal(b.keys.length, 2);
|
|
for (const key of b.keys) {
|
|
assert.ok(!a.keys.includes(key),
|
|
'a second generation must not reuse the first generation\'s key: ' + key);
|
|
}
|
|
});
|
|
|
|
test('the identical figure requested again does dedupe, so a resubmit is not billed twice', async () => {
|
|
const a = recorder();
|
|
const opts = { owner: 1, body: { topic: 'x' }, subject: 'x', imageModel: 'm', images: a.images };
|
|
await deckBuild.drawFigures(deckOf(['a neuron diagram']), opts);
|
|
|
|
const b = recorder();
|
|
await deckBuild.drawFigures(deckOf(['a neuron diagram']),
|
|
{ ...opts, images: b.images });
|
|
|
|
assert.deepEqual(a.keys, b.keys, 'same request, same drawing, same key');
|
|
});
|
|
|
|
test('two figures in one deck are still two jobs', async () => {
|
|
const a = recorder();
|
|
await deckBuild.drawFigures(deckOf(['diagram one', 'diagram two']),
|
|
{ owner: 1, body: { topic: 'x' }, subject: 'x', imageModel: 'm', images: a.images });
|
|
|
|
assert.equal(a.keys.length, 2);
|
|
assert.notEqual(a.keys[0], a.keys[1]);
|
|
});
|
|
|
|
test('the key no longer contains the slide index, which is what collided', async () => {
|
|
const src = require('fs').readFileSync(require('path').join(__dirname, '..', 'src/utils/deckBuild.js'), 'utf8');
|
|
assert.doesNotMatch(src, /requestKey\(opts\.body\) \+ ':' \+ request\.index/);
|
|
assert.match(src, /Keyed on what is being drawn, not on where it sits/);
|
|
});
|