* chore: start 3.2.4 * feat: track prompt usage (run_count, last_used_at) with server-side sorting - Migration adds last_used_at and run_count; existing prompts are estimated from linked images (count, newest image time, else created_at). The backfill only touches NULL rows, so it self-heals rows written after a downgrade or an interrupted upgrade. - Every node run records a use, including re-runs of an existing prompt; a tracking failure is logged and never blocks generation. - search_prompts/get_recent_prompts accept a whitelisted sort key (last_used, created, run_count, rating, text); default stays newest first. Refs #90 * feat: sort param on /recent and /search; microsecond last_used_at - /prompt_manager/recent and /search accept ?sort= (whitelisted in the DB layer; unknown values fall back to newest first). - last_used_at is written with microseconds from Python so a re-run in the same millisecond as another save still sorts first; ordering stays correct against millisecond values from the backfill. Refs #90 * feat: server-side sort in admin UI with Recently Used / Most Used and run badges - New web/js/prompt-list-sort.js owns the sort options, validation and request URLs; admin.js fills the dropdown from it and re-fetches from the server on change, so every sort spans all pages (the old client-side sort only reordered the current page). - Default sort is Recently Used; prompt cards show a run-count badge with the last-used time on hover. - The node's Recent button now lists recently used prompts. - Contract test keeps the UI sort keys and the node's key in sync with the server whitelist. Refs #90 * fix: link images and count runs by the positive prompt, not node execution With a negative-prompt PromptManager node in the workflow, every executed PromptManager node pushed onto the image-linking queue, so images could be linked to the negative prompt or to a previous run. ComfyUI also skips unchanged nodes, so re-runs of the same prompt were never counted. - utils/prompt_graph.py finds the PromptManager nodes feeding a sampler's positive input (KSampler, CFG/Basic guiders, ControlNet and combine pass-throughs, PromptManagerText into encoders), falling back to all PromptManager nodes when none can be traced. - Runs are counted by a ComfyUI on-prompt hook when a workflow is queued; prompts not yet saved are counted when the node first saves them. - Nodes receive the queued graph (hidden PROMPT/UNIQUE_ID). Only the run's positive prompt becomes the current prompt; only batch items with linked text are pushed onto the FIFO queue. - The image monitor links from the image's own metadata first, using the positive node, and falls back to the queue for batch items. Refs #90 * fix: address review — count cached runs of linked text, keep repeated pending runs - Text from known pure string nodes (core PrimitiveString/StringConcatenate, WAS Text Concatenate/Multiline, pysssss ShowText) is resolved at queue time, so cached re-runs of such prompts are counted and their images link from metadata. Unknown nodes are never guessed; the node compares its real text with the queue-time resolution and falls back to counting and queueing itself on any mismatch. - A new prompt queued several times before its first run keeps every pending run; the first save records all of them. - Registration flag is a threading.Event; consistent unittest imports; logging instead of print in the init fallback. * refactor: unquote PendingFirstUse annotation so Tuple usage is visible
67 lines
2.9 KiB
JavaScript
67 lines
2.9 KiB
JavaScript
// Run with: node --test tests/js/
|
|
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const sort = require("../../web/js/prompt-list-sort.js");
|
|
|
|
test("default sort is recently used and is a listed option", () => {
|
|
assert.equal(sort.DEFAULT_SORT, "last_used_desc");
|
|
assert.ok(sort.SORT_OPTIONS.some((o) => o.value === sort.DEFAULT_SORT));
|
|
});
|
|
|
|
test("options include Recently used and Most used, each with a label", () => {
|
|
const values = sort.SORT_OPTIONS.map((o) => o.value);
|
|
assert.ok(values.includes("last_used_desc"));
|
|
assert.ok(values.includes("run_count_desc"));
|
|
for (const option of sort.SORT_OPTIONS) assert.ok(option.label.trim(), option.value);
|
|
assert.equal(new Set(values).size, values.length, "values are unique");
|
|
});
|
|
|
|
test("options cannot be mutated by callers", () => {
|
|
assert.throws(() => sort.SORT_OPTIONS.push({ value: "x", label: "x" }));
|
|
// Non-strict code fails silently on frozen objects, so check the value survived
|
|
const first = sort.SORT_OPTIONS[0].value;
|
|
sort.SORT_OPTIONS[0].value = "hacked";
|
|
assert.equal(sort.SORT_OPTIONS[0].value, first);
|
|
assert.ok(sort.SORT_OPTIONS.every(Object.isFrozen));
|
|
});
|
|
|
|
test("normalizeSort keeps known values and falls back for anything else", () => {
|
|
assert.equal(sort.normalizeSort("run_count_desc"), "run_count_desc");
|
|
for (const bad of [undefined, null, "", "created_at; DROP TABLE prompts", 42, "LAST_USED_DESC"]) {
|
|
assert.equal(sort.normalizeSort(bad), sort.DEFAULT_SORT, String(bad));
|
|
}
|
|
});
|
|
|
|
test("buildRecentUrl computes offset from page and always sends a valid sort", () => {
|
|
assert.equal(
|
|
sort.buildRecentUrl({ page: 3, limit: 25, sort: "rating_desc" }),
|
|
"/prompt_manager/recent?limit=25&offset=50&page=3&sort=rating_desc",
|
|
);
|
|
assert.equal(
|
|
sort.buildRecentUrl({ sort: "bogus" }),
|
|
"/prompt_manager/recent?limit=50&offset=0&page=1&sort=last_used_desc",
|
|
);
|
|
});
|
|
|
|
test("withSort returns a copy with the sort set and leaves the input untouched", () => {
|
|
const params = new URLSearchParams({ text: "castle", sort: "text_asc" });
|
|
const result = sort.withSort(params, "run_count_desc");
|
|
assert.equal(result.get("sort"), "run_count_desc");
|
|
assert.equal(result.get("text"), "castle");
|
|
assert.equal(params.get("sort"), "text_asc");
|
|
assert.equal(sort.withSort(new URLSearchParams(), "nope").get("sort"), sort.DEFAULT_SORT);
|
|
});
|
|
|
|
test("formatRunCount labels zero, one and many runs", () => {
|
|
assert.equal(sort.formatRunCount(0), "Never run");
|
|
assert.equal(sort.formatRunCount(1), "1 run");
|
|
assert.equal(sort.formatRunCount(642), "642 runs");
|
|
assert.equal(sort.formatRunCount(1234), "1,234 runs");
|
|
});
|
|
|
|
test("formatRunCount hides the badge for missing or invalid counts", () => {
|
|
for (const bad of [undefined, null, -1, 1.5, "3", NaN]) {
|
|
assert.equal(sort.formatRunCount(bad), "", String(bad));
|
|
}
|
|
});
|