228 lines
8.5 KiB
JavaScript
228 lines
8.5 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import test from 'node:test'
|
|
|
|
import {
|
|
calculateAutoQueueExtraExecutions,
|
|
completeExecutionGroupCount,
|
|
makeQueueReservationPayload,
|
|
markReservedGroupQueued,
|
|
normalizeImagesPerExecution,
|
|
processedItemIdsFromDelta,
|
|
queueReservationMembers,
|
|
selectExecutionGroup,
|
|
shouldReanchorGalleryResize
|
|
} from '../web/image_conveyor_math.mjs'
|
|
|
|
function queueItems(ids, status = 'pending') {
|
|
return ids.map((id) => ({
|
|
id,
|
|
annotated: `${id}.png [input]`,
|
|
status,
|
|
last_queued_at: 0,
|
|
last_processed_at: 0
|
|
}))
|
|
}
|
|
|
|
function reserveAndQueue(items, count, dontConsume = false, now = 100) {
|
|
const group = selectExecutionGroup(items, count, dontConsume)
|
|
const payload = group.length === normalizeImagesPerExecution(count)
|
|
? makeQueueReservationPayload(group)
|
|
: null
|
|
if (payload) markReservedGroupQueued(items, payload, dontConsume, now)
|
|
return { group, payload }
|
|
}
|
|
|
|
function applyProcessedIds(items, delta, now = 200) {
|
|
const ids = new Set(processedItemIdsFromDelta(delta))
|
|
let changed = 0
|
|
for (const item of items) {
|
|
if (!ids.has(item.id)) continue
|
|
if (delta.new_status === 'processed') item.status = 'processed'
|
|
item.last_processed_at = now
|
|
changed += 1
|
|
}
|
|
return changed
|
|
}
|
|
|
|
test('normalizes images-per-execution to the persisted 1-9 range', () => {
|
|
assert.equal(normalizeImagesPerExecution(0), 1)
|
|
assert.equal(normalizeImagesPerExecution(-4), 1)
|
|
assert.equal(normalizeImagesPerExecution('bad'), 1)
|
|
assert.equal(normalizeImagesPerExecution(10), 9)
|
|
for (let count = 1; count <= 9; count += 1) {
|
|
assert.equal(normalizeImagesPerExecution(count), count)
|
|
}
|
|
})
|
|
|
|
test('reserves the first complete pending group in Conveyor order', () => {
|
|
const items = queueItems(['A', 'B', 'C', 'D', 'E', 'F'])
|
|
assert.deepEqual(
|
|
selectExecutionGroup(items, 3).map((item) => item.id),
|
|
['A', 'B', 'C']
|
|
)
|
|
})
|
|
|
|
test('afterQueued marks the whole reserved group queued with one timestamp', () => {
|
|
const items = queueItems(['A', 'B', 'C', 'D'])
|
|
const { payload } = reserveAndQueue(items, 3, false, 12345)
|
|
assert.deepEqual(queueReservationMembers(payload).map((item) => item.id), ['A', 'B', 'C'])
|
|
assert.deepEqual(items.map((item) => item.status), ['queued', 'queued', 'queued', 'pending'])
|
|
assert.deepEqual(items.slice(0, 3).map((item) => item.last_queued_at), [12345, 12345, 12345])
|
|
})
|
|
|
|
test('next consuming reservation skips every already queued group member', () => {
|
|
const items = queueItems(['A', 'B', 'C', 'D', 'E', 'F'])
|
|
reserveAndQueue(items, 3, false, 1)
|
|
const second = selectExecutionGroup(items, 3, false)
|
|
assert.deepEqual(second.map((item) => item.id), ['D', 'E', 'F'])
|
|
})
|
|
|
|
test('three prompt reservations over nine items have zero overlap', () => {
|
|
const items = queueItems(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'])
|
|
const reservations = []
|
|
for (let prompt = 0; prompt < 3; prompt += 1) {
|
|
const { group } = reserveAndQueue(items, 3, false, prompt + 1)
|
|
reservations.push(group.map((item) => item.id))
|
|
}
|
|
assert.deepEqual(reservations, [
|
|
['A', 'B', 'C'],
|
|
['D', 'E', 'F'],
|
|
['G', 'H', 'I']
|
|
])
|
|
assert.equal(new Set(reservations.flat()).size, 9)
|
|
})
|
|
|
|
test("Don't consume leaves statuses unchanged and repeats the same group", () => {
|
|
const items = queueItems(['A', 'B', 'C', 'D'])
|
|
const first = reserveAndQueue(items, 3, true, 1)
|
|
const second = reserveAndQueue(items, 3, true, 2)
|
|
assert.deepEqual(first.group.map((item) => item.id), ['A', 'B', 'C'])
|
|
assert.deepEqual(second.group.map((item) => item.id), ['A', 'B', 'C'])
|
|
assert.deepEqual(items.map((item) => item.status), ['pending', 'pending', 'pending', 'pending'])
|
|
assert.deepEqual(items.map((item) => item.last_queued_at), [0, 0, 0, 0])
|
|
})
|
|
|
|
test("Don't consume uses processed fallback only when no pending or queued items exist", () => {
|
|
const items = queueItems(['A', 'B', 'C'], 'processed')
|
|
assert.deepEqual(
|
|
selectExecutionGroup(items, 2, true).map((item) => item.id),
|
|
['A', 'B']
|
|
)
|
|
|
|
items[0].status = 'pending'
|
|
assert.deepEqual(
|
|
selectExecutionGroup(items, 2, true).map((item) => item.id),
|
|
['A']
|
|
)
|
|
})
|
|
|
|
test('group payload preserves first-item compatibility fields and exact ordering', () => {
|
|
const items = queueItems(['A', 'B', 'C'])
|
|
const payload = makeQueueReservationPayload(items)
|
|
assert.equal(payload.id, 'A')
|
|
assert.equal(payload.annotated, 'A.png [input]')
|
|
assert.deepEqual(payload.items, [
|
|
{ id: 'A', annotated: 'A.png [input]' },
|
|
{ id: 'B', annotated: 'B.png [input]' },
|
|
{ id: 'C', annotated: 'C.png [input]' }
|
|
])
|
|
assert.deepEqual(queueReservationMembers(payload), payload.items)
|
|
})
|
|
|
|
test('single-item payload remains in legacy id/annotated shape', () => {
|
|
const [item] = queueItems(['A'])
|
|
const payload = makeQueueReservationPayload([item])
|
|
assert.deepEqual(payload, { id: 'A', annotated: 'A.png [input]' })
|
|
assert.deepEqual(queueReservationMembers(payload), [payload])
|
|
})
|
|
|
|
test('duplicate physical paths remain independent reservation members by queue id', () => {
|
|
const items = [
|
|
{ id: 'A1', annotated: 'same.png [input]', status: 'pending' },
|
|
{ id: 'A2', annotated: 'same.png [input]', status: 'pending' },
|
|
{ id: 'B', annotated: 'other.png [input]', status: 'pending' }
|
|
]
|
|
const payload = makeQueueReservationPayload(selectExecutionGroup(items, 3))
|
|
assert.deepEqual(queueReservationMembers(payload).map((item) => item.id), ['A1', 'A2', 'B'])
|
|
})
|
|
|
|
test('new grouped backend delta identifies every processed logical item', () => {
|
|
const delta = {
|
|
processed_item_id: 'A',
|
|
processed_items: [
|
|
{ id: 'A', annotated: 'A.png [input]' },
|
|
{ id: 'B', annotated: 'B.png [input]' },
|
|
{ id: 'C', annotated: 'C.png [input]' }
|
|
],
|
|
new_status: 'processed',
|
|
consumed: true
|
|
}
|
|
assert.deepEqual(processedItemIdsFromDelta(delta), ['A', 'B', 'C'])
|
|
const items = queueItems(['A', 'B', 'C', 'D'], 'queued')
|
|
assert.equal(applyProcessedIds(items, delta, 88), 3)
|
|
assert.deepEqual(items.map((item) => item.status), ['processed', 'processed', 'processed', 'queued'])
|
|
assert.deepEqual(items.map((item) => item.last_processed_at), [88, 88, 88, 0])
|
|
})
|
|
|
|
test('legacy singular backend delta remains supported', () => {
|
|
const delta = { processed_item_id: 'B', new_status: 'processed', consumed: true }
|
|
assert.deepEqual(processedItemIdsFromDelta(delta), ['B'])
|
|
const items = queueItems(['A', 'B'], 'queued')
|
|
applyProcessedIds(items, delta, 99)
|
|
assert.deepEqual(items.map((item) => item.status), ['queued', 'processed'])
|
|
})
|
|
|
|
test('consumed false backend delta produces zero frontend status mutations', () => {
|
|
const delta = {
|
|
processed_item_id: 'A',
|
|
processed_items: [{ id: 'A' }, { id: 'B' }],
|
|
new_status: 'processed',
|
|
consumed: false
|
|
}
|
|
const items = queueItems(['A', 'B'], 'pending')
|
|
assert.deepEqual(processedItemIdsFromDelta(delta), [])
|
|
assert.equal(applyProcessedIds(items, delta), 0)
|
|
assert.deepEqual(items.map((item) => item.status), ['pending', 'pending'])
|
|
})
|
|
|
|
test('auto queue counts only complete groups', () => {
|
|
assert.equal(completeExecutionGroupCount(7, 3), 2)
|
|
assert.equal(calculateAutoQueueExtraExecutions(7, 3, 1), 1)
|
|
})
|
|
|
|
test('auto queue count one preserves released arithmetic', () => {
|
|
assert.equal(completeExecutionGroupCount(7, 1), 7)
|
|
assert.equal(calculateAutoQueueExtraExecutions(7, 1, 1), 6)
|
|
})
|
|
|
|
test('auto queue respects an already requested batch count', () => {
|
|
assert.equal(calculateAutoQueueExtraExecutions(12, 3, 2), 2)
|
|
assert.equal(calculateAutoQueueExtraExecutions(6, 3, 2), 0)
|
|
assert.equal(calculateAutoQueueExtraExecutions(5, 3, 2), 0)
|
|
})
|
|
|
|
test('incomplete groups are never encoded as complete reservations', () => {
|
|
const items = queueItems(['A', 'B'])
|
|
const group = selectExecutionGroup(items, 3)
|
|
assert.deepEqual(group.map((item) => item.id), ['A', 'B'])
|
|
const payload = group.length === normalizeImagesPerExecution(3)
|
|
? makeQueueReservationPayload(group)
|
|
: null
|
|
assert.equal(payload, null)
|
|
})
|
|
|
|
test('clear queued semantics release every member because group state is per item', () => {
|
|
const items = queueItems(['A', 'B', 'C', 'D'])
|
|
reserveAndQueue(items, 3, false, 10)
|
|
for (const item of items) if (item.status === 'queued') item.status = 'pending'
|
|
assert.deepEqual(items.map((item) => item.status), ['pending', 'pending', 'pending', 'pending'])
|
|
})
|
|
|
|
test('scrollbar-only viewport width changes do not trigger resize reanchoring', () => {
|
|
assert.equal(shouldReanchorGalleryResize(620, 620, 603, 618), false)
|
|
})
|
|
|
|
test('real widget resize still triggers identity reanchoring', () => {
|
|
assert.equal(shouldReanchorGalleryResize(620, 760, 603, 743), true)
|
|
})
|