From a78111c8d4680a60e4124f9cd7bc497ee744cd9f Mon Sep 17 00:00:00 2001 From: LyAhn Date: Wed, 1 Jul 2026 10:31:59 +0100 Subject: [PATCH] test(ui-lab): add extreme mock scenario Add an extreme UI Lab scenario with virtual-scale library, album, cluster, and tag counts while keeping the rendered media fixture set manageable. Return the full extreme tag set from the mock backend so tag manager layouts can be stress-tested with 10k-100k tag counts. --- src/dev/mockBackend.ts | 8 +++-- src/dev/mockFixtures.ts | 69 ++++++++++++++++++++++++++++++---------- src/dev/mockScenarios.ts | 3 +- 3 files changed, 61 insertions(+), 19 deletions(-) diff --git a/src/dev/mockBackend.ts b/src/dev/mockBackend.ts index 404620b..d55ac27 100644 --- a/src/dev/mockBackend.ts +++ b/src/dev/mockBackend.ts @@ -206,7 +206,7 @@ function updateImages(ids: number[], updates: { favorite?: boolean | null; ratin function refreshAlbumCounts() { for (const album of db.albums) { const ids = db.albumImageIds[album.id] ?? []; - album.image_count = ids.length; + if (db.scenario !== "extreme") album.image_count = ids.length; album.cover_image_id = ids[0] ?? null; album.cover_thumbnail_path = db.images.find((image) => image.id === ids[0])?.thumbnail_path ?? null; album.updated_at = new Date().toISOString(); @@ -317,7 +317,11 @@ export async function handleMockCommand(cmd: string, payload?: unknown): Promise case "get_visual_clusters": return db.scenario === "empty" ? [] : db.visualClusters; case "get_explore_tags": - return db.scenario === "empty" ? [] : db.exploreTags.slice(0, Number(p.limit ?? 180)); + return db.scenario === "empty" + ? [] + : db.scenario === "extreme" + ? db.exploreTags + : db.exploreTags.slice(0, Number(p.limit ?? 180)); case "get_related_tags": { const relatedCounts = new Map(); for (const imageTags of Object.values(db.tagsByImageId)) { diff --git a/src/dev/mockFixtures.ts b/src/dev/mockFixtures.ts index 846d895..f966c76 100644 --- a/src/dev/mockFixtures.ts +++ b/src/dev/mockFixtures.ts @@ -29,6 +29,17 @@ export interface MockDb { } const folderNames = ["Camera Roll", "Client Selects", "Video Archive"]; +const extremeFolderNames = [ + "Camera Roll", + "Client Selects", + "Video Archive", + "Commercial Library", + "Archive Vault", + "Reference Pulls", + "Delivery Selects", + "Personal Work", +]; +const extremeFolderCounts = [98_420, 76_880, 52_340, 44_900, 31_760, 24_180, 16_540, 12_920]; const mediaNames = [ "alpine-lake", "city-after-rain", @@ -119,6 +130,16 @@ const hugeTags = [ }), ...Array.from({ length: 320 }, (_, index) => `fixture concept ${String(index + 1).padStart(3, "0")}`), ]; +const extremeTags = [ + ...tags, + ...Array.from({ length: hugeTagThemes.length * hugeTagSubjects.length }, (_, index) => { + const theme = hugeTagThemes[index % hugeTagThemes.length]; + const subject = hugeTagSubjects[Math.floor(index / hugeTagThemes.length) % hugeTagSubjects.length]; + return `${theme} ${subject}`; + }), + ...Array.from({ length: 780 }, (_, index) => `extreme concept ${String(index + 1).padStart(4, "0")}`), + ...Array.from({ length: 240 }, (_, index) => `client taxonomy ${String(index + 1).padStart(3, "0")}`), +]; const aiRatings: AiRating[] = ["general", "sensitive", "questionable"]; function daysAgo(days: number): string { @@ -177,7 +198,8 @@ function makeImage(index: number, folderId: number, scenario: MockScenario): Ima function makeFolders(scenario: MockScenario): Folder[] { if (scenario === "empty") return []; - return folderNames.map((name, index) => ({ + const names = scenario === "extreme" ? extremeFolderNames : folderNames; + return names.map((name, index) => ({ id: index + 1, path: `C:\\Users\\phokus\\Pictures\\${name.replace(/ /g, "")}`, name, @@ -190,21 +212,25 @@ function makeFolders(scenario: MockScenario): Folder[] { function makeImages(scenario: MockScenario, folders: Folder[]): ImageRecord[] { if (scenario === "empty") return []; - const count = scenario === "huge" ? 720 : scenario === "errors" ? 54 : 72; + const count = scenario === "extreme" ? 1_440 : scenario === "huge" ? 720 : scenario === "errors" ? 54 : 72; const images = Array.from({ length: count }, (_, index) => makeImage(index, folders[index % folders.length].id, scenario)); for (const folder of folders) { - folder.image_count = images.filter((image) => image.folder_id === folder.id).length; + folder.image_count = + scenario === "extreme" + ? extremeFolderCounts[folder.id - 1] ?? images.filter((image) => image.folder_id === folder.id).length + : images.filter((image) => image.folder_id === folder.id).length; } return images; } function tagVocabularyForScenario(scenario: MockScenario): string[] { + if (scenario === "extreme") return extremeTags; return scenario === "huge" ? hugeTags : tags; } function makeTags(images: ImageRecord[], scenario: MockScenario): Record { const vocabulary = tagVocabularyForScenario(scenario); - const tagCount = scenario === "huge" ? 9 : 3; + const tagCount = scenario === "extreme" ? 14 : scenario === "huge" ? 9 : 3; return Object.fromEntries( images.map((image, index) => [ image.id, @@ -227,10 +253,13 @@ function makeAlbums(images: ImageRecord[], scenario: MockScenario): { albums: Al const motion = images.filter((image) => image.media_kind === "video").map((image) => image.id); const favorites = images.filter((image) => image.favorite).slice(0, 40).map((image) => image.id); const albumImageIds = { 1: selects, 2: motion, 3: favorites }; + const portfolioCount = scenario === "extreme" ? 18_450 : selects.length; + const motionCount = scenario === "extreme" ? 7_820 : motion.length; + const favoritesCount = scenario === "extreme" ? 42_610 : favorites.length; const albums: Album[] = [ - { id: 1, name: "Portfolio Shortlist", cover_image_id: selects[0] ?? null, cover_thumbnail_path: images.find((image) => image.id === selects[0])?.thumbnail_path ?? null, image_count: selects.length, sort_order: 1, created_at: daysAgo(40), updated_at: daysAgo(2) }, - { id: 2, name: "Motion Tests", cover_image_id: motion[0] ?? null, cover_thumbnail_path: images.find((image) => image.id === motion[0])?.thumbnail_path ?? null, image_count: motion.length, sort_order: 2, created_at: daysAgo(38), updated_at: daysAgo(3) }, - { id: 3, name: "Favorites", cover_image_id: favorites[0] ?? null, cover_thumbnail_path: images.find((image) => image.id === favorites[0])?.thumbnail_path ?? null, image_count: favorites.length, sort_order: 3, created_at: daysAgo(20), updated_at: daysAgo(1) }, + { id: 1, name: "Portfolio Shortlist", cover_image_id: selects[0] ?? null, cover_thumbnail_path: images.find((image) => image.id === selects[0])?.thumbnail_path ?? null, image_count: portfolioCount, sort_order: 1, created_at: daysAgo(40), updated_at: daysAgo(2) }, + { id: 2, name: "Motion Tests", cover_image_id: motion[0] ?? null, cover_thumbnail_path: images.find((image) => image.id === motion[0])?.thumbnail_path ?? null, image_count: motionCount, sort_order: 2, created_at: daysAgo(38), updated_at: daysAgo(3) }, + { id: 3, name: "Favorites", cover_image_id: favorites[0] ?? null, cover_thumbnail_path: images.find((image) => image.id === favorites[0])?.thumbnail_path ?? null, image_count: favoritesCount, sort_order: 3, created_at: daysAgo(20), updated_at: daysAgo(1) }, ]; return { albums, albumImageIds }; } @@ -238,15 +267,15 @@ function makeAlbums(images: ImageRecord[], scenario: MockScenario): { albums: Al function makeProgress(folders: Folder[], scenario: MockScenario): FolderJobProgress[] { return folders.map((folder, index) => ({ folder_id: folder.id, - thumbnail_pending: scenario === "busy" ? 18 - index * 3 : 0, - metadata_pending: scenario === "busy" ? 12 - index * 2 : 0, - embedding_pending: scenario === "busy" ? 36 - index * 4 : scenario === "errors" ? 4 : 0, + thumbnail_pending: scenario === "extreme" ? 180 - index * 9 : scenario === "busy" ? 18 - index * 3 : 0, + metadata_pending: scenario === "extreme" ? 140 - index * 7 : scenario === "busy" ? 12 - index * 2 : 0, + embedding_pending: scenario === "extreme" ? 260 - index * 11 : scenario === "busy" ? 36 - index * 4 : scenario === "errors" ? 4 : 0, embedding_ready: Math.max(0, folder.image_count - (scenario === "busy" ? 20 : 2)), embedding_failed: scenario === "errors" ? 3 + index : 0, - caption_pending: scenario === "busy" ? 24 - index * 2 : 0, + caption_pending: scenario === "extreme" ? 210 - index * 10 : scenario === "busy" ? 24 - index * 2 : 0, caption_ready: Math.max(0, Math.floor(folder.image_count * 0.5)), caption_failed: scenario === "errors" ? 2 : 0, - tagging_pending: scenario === "busy" ? 30 - index * 5 : 0, + tagging_pending: scenario === "extreme" ? 320 - index * 13 : scenario === "busy" ? 30 - index * 5 : 0, tagging_ready: Math.max(0, Math.floor(folder.image_count * 0.65)), tagging_failed: scenario === "errors" ? 4 : 0, })); @@ -258,8 +287,8 @@ function makeVisualClusters(images: ImageRecord[], scenario: MockScenario): Visu // "huge" scenario so Explore shows a realistic field of clusters rather than a // fixed handful, and size the counts off a large virtual library so the badges // read like a big collection. - const clusterCount = scenario === "huge" ? 30 : Math.min(30, Math.max(5, Math.round(images.length / 20))); - const virtualTotal = scenario === "huge" ? 12_000 : images.length; + const clusterCount = scenario === "huge" || scenario === "extreme" ? 30 : Math.min(30, Math.max(5, Math.round(images.length / 20))); + const virtualTotal = scenario === "extreme" ? 250_000 : scenario === "huge" ? 12_000 : images.length; // Long-tailed sizes (a few large clusters, many smaller), like real k-means output. const weights = Array.from({ length: clusterCount }, (_, i) => 1 / (i + 1.5)); const weightSum = weights.reduce((sum, weight) => sum + weight, 0); @@ -279,11 +308,19 @@ function makeExploreTags(images: ImageRecord[], scenario: MockScenario): Explore const vocabulary = tagVocabularyForScenario(scenario); return vocabulary.map((tag, index) => { const representative = images[index % Math.max(images.length, 1)]; - const divisor = scenario === "huge" ? 2 + Math.pow(index + 1, 0.58) : index + 3; + const divisor = + scenario === "extreme" + ? 1 + Math.pow(index + 1, 0.42) + : scenario === "huge" + ? 2 + Math.pow(index + 1, 0.58) + : index + 3; const sourceCycle = index % 3; return { tag, - count: Math.max(1, Math.round(images.length / divisor)), + count: + scenario === "extreme" + ? Math.max(10_000, Math.min(100_000, Math.round(200_000 / divisor))) + : Math.max(1, Math.round(images.length / divisor)), representative_image_id: representative?.id ?? index + 1, thumbnail_path: representative?.thumbnail_path ?? null, has_ai_source: sourceCycle !== 1, diff --git a/src/dev/mockScenarios.ts b/src/dev/mockScenarios.ts index b78a016..db7d36f 100644 --- a/src/dev/mockScenarios.ts +++ b/src/dev/mockScenarios.ts @@ -1,4 +1,4 @@ -export type MockScenario = "rich" | "empty" | "busy" | "duplicates" | "album" | "errors" | "huge"; +export type MockScenario = "rich" | "empty" | "busy" | "duplicates" | "album" | "errors" | "huge" | "extreme"; const SCENARIOS = new Set([ "rich", @@ -8,6 +8,7 @@ const SCENARIOS = new Set([ "album", "errors", "huge", + "extreme", ]); export function getMockScenario(): MockScenario {