The Library selection bar (D148) gets Add tags and select-all of the loaded
shelf, the legacy admin page's bulk Add Tags and select-all checkbox.
Add tags posts {prompt_ids, tags, expected_epoch} to
POST /prompt_manager/bulk/tags: in one BEGIN IMMEDIATE transaction the server
checks the storage epoch, normalizes the names as image tags are, gives each
Library parent only the names it lacks (casefold), finds or inserts tag rows
by exact name only when some prompt gets them, and skips prompts that would
pass 20 tags. Prompt tags only. Both bulk/tags forms refuse a non-object body
with 400.
Ctrl/Cmd+A in the grid and "Select all N loaded" in the bar select every
loaded prompt; the offer follows each loaded batch.
Closes #442
97 lines
6.4 KiB
JavaScript
97 lines
6.4 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
// #437 D148: the Prompt library's selection bar sets one category on the selected prompts, fenced by the storage epoch.
|
|
const source = import('../../web/js/workspace/library-selection.mjs');
|
|
|
|
test('the dialog is the shared simpleInput with the prompt title, and the route and cap are the legacy ones', async () => {
|
|
const {PROMPT_CATEGORY_DIALOG, CATEGORY_URL, MAX_CATEGORY_PROMPTS, TOO_MANY_PROMPTS} = await source;
|
|
assert.deepEqual({...PROMPT_CATEGORY_DIALOG}, {title:'Categorize selected prompts', label:'Category', action:'Apply'});
|
|
assert.equal(CATEGORY_URL, '/prompt_manager/bulk/category');
|
|
assert.equal(MAX_CATEGORY_PROMPTS, 1000);
|
|
assert.equal(TOO_MANY_PROMPTS, 'Select at most 1,000 prompts.');
|
|
});
|
|
|
|
test('a confirmed write toasts the shared outcome with "prompt", invalidates and reloads', async () => {
|
|
const {writeOutcome} = await source;
|
|
assert.deepEqual(writeOutcome({success:true, category:'Studio', updated:2, unchanged:1, not_found:0}, null),
|
|
{toast:'Category “Studio” set on 3 prompts.', committed:true, reload:true});
|
|
assert.deepEqual(writeOutcome({success:true, category:'Studio', updated:1, unchanged:0, not_found:2}, null),
|
|
{toast:'Category “Studio” set on 1 prompt. 2 could not be found.', committed:true, reload:true});
|
|
});
|
|
|
|
test('none found, a restored storage, an unconfirmed answer and a refused write each say what happened', async () => {
|
|
const {writeOutcome, NONE_FOUND, STORAGE_CHANGED, UNCONFIRMED, CATEGORY_FAILED} = await source;
|
|
assert.deepEqual(writeOutcome({success:true, category:'X', updated:0, unchanged:0, not_found:3}, null),
|
|
{toast:NONE_FOUND, committed:false, reload:true});
|
|
assert.deepEqual(writeOutcome(null, {status:409, code:'storage-changed'}), {toast:STORAGE_CHANGED, committed:false, reload:true});
|
|
for (const failure of [new TypeError('Failed to fetch'), {status:502}, {status:503}, {status:504}]) {
|
|
assert.deepEqual(writeOutcome(null, failure), {toast:UNCONFIRMED, committed:true, reload:true});
|
|
}
|
|
assert.deepEqual(writeOutcome(null, null), {toast:UNCONFIRMED, committed:true, reload:true});
|
|
for (const status of [400, 500]) {
|
|
assert.deepEqual(writeOutcome(null, {status}), {toast:CATEGORY_FAILED, committed:false, reload:false});
|
|
}
|
|
assert.equal(STORAGE_CHANGED, 'Prompt storage was restored since this page loaded. No category was changed.');
|
|
});
|
|
|
|
test('the field refuses control characters as validate_category does', async () => {
|
|
const {categoryProblem, CONTROL_CHARACTERS} = await source;
|
|
assert.equal(categoryProblem('Studio portraits'), null);
|
|
assert.equal(categoryProblem('tab\there'), CONTROL_CHARACTERS);
|
|
assert.equal(categoryProblem('nul\u0000'), CONTROL_CHARACTERS);
|
|
assert.equal(categoryProblem('unicode ✓ é'), null);
|
|
});
|
|
|
|
test('Add tags uses the Images dialog words for prompts, the tag routes and the editor limits (#442)', async () => {
|
|
const {PROMPT_TAGS_DIALOG, TAGS_URL, TAG_OPTIONS_URL, MAX_PROMPT_TAGS} = await source;
|
|
assert.deepEqual({...PROMPT_TAGS_DIALOG}, {title:'Tag selected prompts', label:'Tags, separated by commas', action:'Apply'});
|
|
assert.equal(TAGS_URL, '/prompt_manager/bulk/tags');
|
|
assert.equal(TAG_OPTIONS_URL, '/prompt_manager/tags');
|
|
assert.equal(MAX_PROMPT_TAGS, 20);
|
|
});
|
|
|
|
test('the Add tags toast says what was added, already present, skipped and missing', async () => {
|
|
const {tagsOutcome, writeOutcome, NONE_FOUND} = await source;
|
|
assert.equal(tagsOutcome({tags:['a', 'b'], updated:2, unchanged:0, over_limit:0, not_found:0}), 'Added “a, b” to 2 prompts.');
|
|
assert.equal(tagsOutcome({tags:['a'], updated:1, unchanged:2, over_limit:1, not_found:3}),
|
|
'Added “a” to 1 prompt. 2 prompts already had them. 1 prompt would pass 20 tags and was skipped. 3 could not be found.');
|
|
assert.equal(tagsOutcome({tags:['a'], updated:0, unchanged:2, over_limit:0, not_found:0}), '2 prompts already had “a”.');
|
|
// Every prompt skipped: found, so the skip sentence, never "none found"; nothing committed.
|
|
assert.deepEqual(writeOutcome({success:true, tags:['a'], updated:0, unchanged:0, over_limit:2, not_found:0}, null, 'tags'),
|
|
{toast:'2 prompts would pass 20 tags and were skipped.', committed:false, reload:true});
|
|
assert.deepEqual(writeOutcome({success:true, tags:['a'], updated:0, unchanged:0, over_limit:0, not_found:2}, null, 'tags'),
|
|
{toast:NONE_FOUND, committed:false, reload:true});
|
|
});
|
|
|
|
test('Add tags failures name tags, not the category', async () => {
|
|
const {writeOutcome, TAGS_STORAGE_CHANGED, TAGS_UNCONFIRMED, TAGS_FAILED} = await source;
|
|
assert.equal(writeOutcome(null, {status:409, code:'storage-changed'}, 'tags').toast, TAGS_STORAGE_CHANGED);
|
|
assert.equal(writeOutcome(null, {status:503}, 'tags').toast, TAGS_UNCONFIRMED);
|
|
assert.equal(writeOutcome(null, {status:400}, 'tags').toast, TAGS_FAILED);
|
|
assert.equal(TAGS_STORAGE_CHANGED, 'Prompt storage was restored since this page loaded. No tags were added.');
|
|
});
|
|
|
|
test('the Add tags field refuses what validate_tags refuses', async () => {
|
|
const {tagsProblem, TAG_PROBLEMS} = await source;
|
|
assert.equal(tagsProblem('red, blue'), null);
|
|
assert.equal(tagsProblem(' , ,'), TAG_PROBLEMS.none);
|
|
assert.equal(tagsProblem(Array.from({length:21}, (_, i) => `t${i}`).join(',')), TAG_PROBLEMS.many);
|
|
assert.equal(tagsProblem(Array.from({length:21}, () => 'same').join(',')), null);
|
|
assert.equal(tagsProblem('x'.repeat(51)), TAG_PROBLEMS.long);
|
|
assert.equal(tagsProblem('bad\ttag'), TAG_PROBLEMS.control);
|
|
});
|
|
|
|
test('Select all N loaded is offered while some loaded prompts are unchecked (#444 ruling)', async () => {
|
|
const {loadedOffer, offersLoaded} = await source;
|
|
assert.equal(loadedOffer(3), 'Select all 3 loaded');
|
|
assert.equal(loadedOffer(1203), 'Select all 1,203 loaded');
|
|
assert.equal(offersLoaded(new Set([1]), [1, 2, 3]), true);
|
|
assert.equal(offersLoaded(new Set([1, 2, 3]), [1, 2, 3]), false);
|
|
assert.equal(offersLoaded(new Set(), [1, 2]), false);
|
|
assert.equal(offersLoaded(new Set([9]), [9]), false);
|
|
// More loaded than any action accepts: never offered (Ctrl/Cmd+A still selects them).
|
|
assert.equal(offersLoaded(new Set([1]), Array.from({length:1001}, (_, i) => i + 1)), false);
|
|
assert.equal(offersLoaded(new Set([1]), Array.from({length:1000}, (_, i) => i + 1)), true);
|
|
});
|