From 3c6b7b777837764976cf019cde1f3a72a81092b0 Mon Sep 17 00:00:00 2001 From: 134 Date: Fri, 31 Jul 2026 19:43:13 +0800 Subject: [PATCH] test(agent): cover detail replacement recovery --- app/api/agent-projects/[id]/route.ts | 150 +-------- lib/agents/agent-project-save-merge.ts | 289 +++++++++++++++++ .../test-agent-detail-replacement-recovery.ts | 292 ++++++++++++++++++ 3 files changed, 586 insertions(+), 145 deletions(-) create mode 100644 lib/agents/agent-project-save-merge.ts create mode 100644 scripts/test-agent-detail-replacement-recovery.ts diff --git a/app/api/agent-projects/[id]/route.ts b/app/api/agent-projects/[id]/route.ts index 1a6768c3d..8ffa95a29 100644 --- a/app/api/agent-projects/[id]/route.ts +++ b/app/api/agent-projects/[id]/route.ts @@ -7,12 +7,10 @@ import { extractAgentProjectStateFromCanvasData, isAgentProjectRow, } from "@/lib/agents/agent-project-compat"; -import { normalizeAgentGenerationProjectPersistedState } from "@/lib/agents/generation-project-state"; -import type { - AgentGenerationProjectPersistedState, - AgentGenerationProjectReplacementResultState, - AgentGenerationProjectResultCollections, -} from "@/types/agent"; +import { + getAllResultUrlsFromAgentProjectState, + mergeAgentProjectStateForSave, +} from "@/lib/agents/agent-project-save-merge"; export const dynamic = "force-dynamic"; export const revalidate = 0; @@ -30,144 +28,6 @@ function mapAgentProjectRow(row: typeof canvasProject.$inferSelect) { }; } -const RESULT_COLLECTION_KEYS = [ - "scene_generation", - "detail_generation", - "detail_replacement", - "sku_replacement", -] as const; - -const REPLACEMENT_RESULT_KEYS = [ - "detail_replacement", - "sku_replacement", -] as const; - -function normalizeProjectResultUrlList(urls: string[] | null | undefined) { - return Array.from( - new Set( - (urls || []) - .map((url) => String(url || "").trim()) - .filter((url) => /^https?:\/\//i.test(url)) - ) - ); -} - -function mergeResultCollections( - existing: AgentGenerationProjectResultCollections | null | undefined, - incoming: AgentGenerationProjectResultCollections | null | undefined -): AgentGenerationProjectResultCollections { - return RESULT_COLLECTION_KEYS.reduce( - (next, key) => { - next[key] = normalizeProjectResultUrlList([ - ...normalizeProjectResultUrlList(existing?.[key]), - ...normalizeProjectResultUrlList(incoming?.[key]), - ]); - return next; - }, - {} - ); -} - -function getResultCollectionUrlSet( - collections: AgentGenerationProjectResultCollections | null | undefined -) { - return new Set( - RESULT_COLLECTION_KEYS.flatMap((key) => normalizeProjectResultUrlList(collections?.[key])) - ); -} - -function removeDeletedUrlsFromCollections( - collections: AgentGenerationProjectResultCollections, - deletedUrls: Set -): AgentGenerationProjectResultCollections { - return RESULT_COLLECTION_KEYS.reduce( - (next, key) => { - next[key] = normalizeProjectResultUrlList(collections[key]).filter( - (url) => !deletedUrls.has(url) - ); - return next; - }, - {} - ); -} - -function mergeReplacementResultState( - existing: AgentGenerationProjectReplacementResultState | null | undefined, - incoming: AgentGenerationProjectReplacementResultState | null | undefined, - deletedUrls: Set -): AgentGenerationProjectReplacementResultState { - return REPLACEMENT_RESULT_KEYS.reduce( - (next, key) => { - const existingModeState = existing?.[key] || {}; - const incomingModeState = incoming?.[key] || {}; - const mergedModeState = { ...existingModeState, ...incomingModeState }; - next[key] = Object.fromEntries( - Object.entries(mergedModeState).filter(([, record]) => { - const url = String(record?.url || "").trim(); - return !url || !deletedUrls.has(url); - }) - ); - return next; - }, - {} - ); -} - -function getReplacementResultUrls( - state: AgentGenerationProjectReplacementResultState | null | undefined -) { - return REPLACEMENT_RESULT_KEYS.flatMap((key) => - Object.values(state?.[key] || {}) - .map((record) => String(record?.url || "").trim()) - .filter((url) => /^https?:\/\//i.test(url)) - ); -} - -function getAllResultUrlsFromState(state: AgentGenerationProjectPersistedState) { - return normalizeProjectResultUrlList([ - ...normalizeProjectResultUrlList(state.resultImageUrls), - ...RESULT_COLLECTION_KEYS.flatMap((key) => - normalizeProjectResultUrlList(state.resultCollections?.[key]) - ), - ...getReplacementResultUrls(state.replacementResultState), - ]); -} - -function mergeAgentProjectStateForSave(params: { - existingState: AgentGenerationProjectPersistedState; - incomingState: unknown; -}) { - const existingState = normalizeAgentGenerationProjectPersistedState(params.existingState); - const incomingState = normalizeAgentGenerationProjectPersistedState(params.incomingState); - const deletedResultCollections = mergeResultCollections( - existingState.deletedResultCollections, - incomingState.deletedResultCollections - ); - const deletedUrls = getResultCollectionUrlSet(deletedResultCollections); - const replacementResultState = mergeReplacementResultState( - existingState.replacementResultState, - incomingState.replacementResultState, - deletedUrls - ); - const resultCollections = removeDeletedUrlsFromCollections( - mergeResultCollections(existingState.resultCollections, incomingState.resultCollections), - deletedUrls - ); - - return normalizeAgentGenerationProjectPersistedState({ - ...incomingState, - resultCollections, - deletedResultCollections, - replacementResultState, - resultImageUrls: normalizeProjectResultUrlList([ - ...getAllResultUrlsFromState(existingState), - ...getAllResultUrlsFromState(incomingState), - ...RESULT_COLLECTION_KEYS.flatMap((key) => resultCollections[key] || []), - ...getReplacementResultUrls(replacementResultState), - ]).filter((url) => !deletedUrls.has(url)), - }); -} - export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> } @@ -239,7 +99,7 @@ export async function PUT( incomingState: body.projectState, }) : existingProjectState; - const firstResultUrl = getAllResultUrlsFromState(nextProjectState)[0] || null; + const firstResultUrl = getAllResultUrlsFromAgentProjectState(nextProjectState)[0] || null; const requestedThumbnail = body.thumbnail !== undefined ? String(body.thumbnail || "").trim() || null : undefined; const thumbnail = diff --git a/lib/agents/agent-project-save-merge.ts b/lib/agents/agent-project-save-merge.ts new file mode 100644 index 000000000..4cfd3fa75 --- /dev/null +++ b/lib/agents/agent-project-save-merge.ts @@ -0,0 +1,289 @@ +import { normalizeAgentGenerationProjectPersistedState } from "@/lib/agents/generation-project-state"; +import type { + AgentGenerationProjectPersistedState, + AgentGenerationProjectReplacementResultState, + AgentGenerationProjectResultCollections, + AgentWorkflowBusinessModeKey, +} from "@/types/agent"; + +const RESULT_COLLECTION_KEYS = [ + "scene_generation", + "detail_generation", + "detail_replacement", + "sku_replacement", +] as const; + +const REPLACEMENT_RESULT_KEYS = [ + "detail_replacement", + "sku_replacement", +] as const; + +function createFallbackWorkspaceState() { + return { + input: "", + attachments: [], + selectedModelId: "auto", + activeBusinessModeKey: "main_sku" as const, + imageParams: { + aspectRatio: "auto", + imageSize: "2K", + imageCount: 1, + }, + }; +} + +function normalizeProjectResultUrlList(urls: string[] | null | undefined) { + return Array.from( + new Set( + (urls || []) + .map((url) => String(url || "").trim()) + .filter((url) => /^https?:\/\//i.test(url)) + ) + ); +} + +function mergeResultCollections( + existing: AgentGenerationProjectResultCollections | null | undefined, + incoming: AgentGenerationProjectResultCollections | null | undefined +): AgentGenerationProjectResultCollections { + return RESULT_COLLECTION_KEYS.reduce( + (next, key) => { + next[key] = normalizeProjectResultUrlList([ + ...normalizeProjectResultUrlList(existing?.[key]), + ...normalizeProjectResultUrlList(incoming?.[key]), + ]); + return next; + }, + {} + ); +} + +function getResultCollectionUrlSet( + collections: AgentGenerationProjectResultCollections | null | undefined +) { + return new Set( + RESULT_COLLECTION_KEYS.flatMap((key) => normalizeProjectResultUrlList(collections?.[key])) + ); +} + +function removeDeletedUrlsFromCollections( + collections: AgentGenerationProjectResultCollections, + deletedUrls: Set +): AgentGenerationProjectResultCollections { + return RESULT_COLLECTION_KEYS.reduce( + (next, key) => { + next[key] = normalizeProjectResultUrlList(collections[key]).filter( + (url) => !deletedUrls.has(url) + ); + return next; + }, + {} + ); +} + +function mergeReplacementResultState( + existing: AgentGenerationProjectReplacementResultState | null | undefined, + incoming: AgentGenerationProjectReplacementResultState | null | undefined, + deletedUrls: Set +): AgentGenerationProjectReplacementResultState { + return REPLACEMENT_RESULT_KEYS.reduce( + (next, key) => { + const existingModeState = existing?.[key] || {}; + const incomingModeState = incoming?.[key] || {}; + const mergedModeState = { ...existingModeState, ...incomingModeState }; + next[key] = Object.fromEntries( + Object.entries(mergedModeState).filter(([, record]) => { + const url = String(record?.url || "").trim(); + return !url || !deletedUrls.has(url); + }) + ); + return next; + }, + {} + ); +} + +function getReplacementResultUrls( + state: AgentGenerationProjectReplacementResultState | null | undefined +) { + return REPLACEMENT_RESULT_KEYS.flatMap((key) => + Object.values(state?.[key] || {}) + .map((record) => String(record?.url || "").trim()) + .filter((url) => /^https?:\/\//i.test(url)) + ); +} + +export function getAllResultUrlsFromAgentProjectState( + state: AgentGenerationProjectPersistedState +) { + return normalizeProjectResultUrlList([ + ...normalizeProjectResultUrlList(state.resultImageUrls), + ...RESULT_COLLECTION_KEYS.flatMap((key) => + normalizeProjectResultUrlList(state.resultCollections?.[key]) + ), + ...getReplacementResultUrls(state.replacementResultState), + ]); +} + +function hasDetailModeSignals(state: AgentGenerationProjectPersistedState) { + const detailReferenceCount = + state.workflowState.agentModeState?.detailReplacement?.references?.length || 0; + const skuReferenceCount = + state.workflowState.agentModeState?.skuReplacement?.references?.length || 0; + return ( + detailReferenceCount > 0 || + skuReferenceCount > 0 || + normalizeProjectResultUrlList(state.resultCollections?.detail_generation).length > 0 || + normalizeProjectResultUrlList(state.resultCollections?.detail_replacement).length > 0 || + normalizeProjectResultUrlList(state.resultCollections?.sku_replacement).length > 0 || + getReplacementResultUrls(state.replacementResultState).length > 0 + ); +} + +function hasDetailReplacementResultSignals(params: { + resultCollections: AgentGenerationProjectResultCollections; + replacementResultState: AgentGenerationProjectReplacementResultState; +}) { + return ( + normalizeProjectResultUrlList(params.resultCollections.detail_replacement).length > 0 || + getReplacementResultUrls({ + detail_replacement: params.replacementResultState.detail_replacement, + }).length > 0 + ); +} + +function hasSkuReplacementResultSignals(params: { + resultCollections: AgentGenerationProjectResultCollections; + replacementResultState: AgentGenerationProjectReplacementResultState; +}) { + return ( + normalizeProjectResultUrlList(params.resultCollections.sku_replacement).length > 0 || + getReplacementResultUrls({ + sku_replacement: params.replacementResultState.sku_replacement, + }).length > 0 + ); +} + +function mergeWorkflowStateForSave(params: { + existingState: AgentGenerationProjectPersistedState; + incomingState: AgentGenerationProjectPersistedState; + resultCollections: AgentGenerationProjectResultCollections; + replacementResultState: AgentGenerationProjectReplacementResultState; +}) { + const incomingAgentModeState = params.incomingState.workflowState.agentModeState; + const existingAgentModeState = params.existingState.workflowState.agentModeState; + const shouldKeepDetailReplacementState = + hasDetailReplacementResultSignals(params) && + (existingAgentModeState?.detailReplacement?.references?.length || 0) > 0 && + (incomingAgentModeState?.detailReplacement?.references?.length || 0) === 0; + const shouldKeepSkuReplacementState = + hasSkuReplacementResultSignals(params) && + (existingAgentModeState?.skuReplacement?.references?.length || 0) > 0 && + (incomingAgentModeState?.skuReplacement?.references?.length || 0) === 0; + + if (!shouldKeepDetailReplacementState && !shouldKeepSkuReplacementState) { + return params.incomingState.workflowState; + } + + return { + ...params.incomingState.workflowState, + agentModeState: { + ...incomingAgentModeState, + detailReplacement: shouldKeepDetailReplacementState + ? existingAgentModeState?.detailReplacement + : incomingAgentModeState?.detailReplacement, + skuReplacement: shouldKeepSkuReplacementState + ? existingAgentModeState?.skuReplacement + : incomingAgentModeState?.skuReplacement, + }, + }; +} + +function hasExplicitIncomingBusinessMode(value: unknown) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return false; + } + const raw = value as Record; + const workspaceState = + raw.workspaceState && typeof raw.workspaceState === "object" && !Array.isArray(raw.workspaceState) + ? (raw.workspaceState as Record) + : {}; + return Boolean(String(workspaceState.activeBusinessModeKey || "").trim()); +} + +function resolveMergedBusinessModeKey(params: { + existingState: AgentGenerationProjectPersistedState; + incomingState: AgentGenerationProjectPersistedState; + rawIncomingState: unknown; + mergedState: AgentGenerationProjectPersistedState; +}): AgentWorkflowBusinessModeKey { + if (hasDetailModeSignals(params.mergedState)) { + const incomingKey = params.incomingState.workspaceState?.activeBusinessModeKey; + const existingKey = params.existingState.workspaceState?.activeBusinessModeKey; + const incomingHasExplicitKey = hasExplicitIncomingBusinessMode(params.rawIncomingState); + + if (!incomingHasExplicitKey || existingKey === "detail" || incomingKey === "main_sku") { + return "detail"; + } + } + + return params.incomingState.workspaceState?.activeBusinessModeKey || + params.existingState.workspaceState?.activeBusinessModeKey || + "main_sku"; +} + +export function mergeAgentProjectStateForSave(params: { + existingState: AgentGenerationProjectPersistedState; + incomingState: unknown; +}): AgentGenerationProjectPersistedState { + const existingState = normalizeAgentGenerationProjectPersistedState(params.existingState); + const incomingState = normalizeAgentGenerationProjectPersistedState(params.incomingState); + const deletedResultCollections = mergeResultCollections( + existingState.deletedResultCollections, + incomingState.deletedResultCollections + ); + const deletedUrls = getResultCollectionUrlSet(deletedResultCollections); + const replacementResultState = mergeReplacementResultState( + existingState.replacementResultState, + incomingState.replacementResultState, + deletedUrls + ); + const resultCollections = removeDeletedUrlsFromCollections( + mergeResultCollections(existingState.resultCollections, incomingState.resultCollections), + deletedUrls + ); + const workflowState = mergeWorkflowStateForSave({ + existingState, + incomingState, + resultCollections, + replacementResultState, + }); + const mergedState = normalizeAgentGenerationProjectPersistedState({ + ...incomingState, + workflowState, + resultCollections, + deletedResultCollections, + replacementResultState, + resultImageUrls: normalizeProjectResultUrlList([ + ...getAllResultUrlsFromAgentProjectState(existingState), + ...getAllResultUrlsFromAgentProjectState(incomingState), + ...RESULT_COLLECTION_KEYS.flatMap((key) => resultCollections[key] || []), + ...getReplacementResultUrls(replacementResultState), + ]).filter((url) => !deletedUrls.has(url)), + }); + + const workspaceState = mergedState.workspaceState || createFallbackWorkspaceState(); + + return { + ...mergedState, + workspaceState: { + ...workspaceState, + activeBusinessModeKey: resolveMergedBusinessModeKey({ + existingState, + incomingState, + rawIncomingState: params.incomingState, + mergedState, + }), + }, + }; +} diff --git a/scripts/test-agent-detail-replacement-recovery.ts b/scripts/test-agent-detail-replacement-recovery.ts new file mode 100644 index 000000000..c7cb05328 --- /dev/null +++ b/scripts/test-agent-detail-replacement-recovery.ts @@ -0,0 +1,292 @@ +import { + getAllResultUrlsFromAgentProjectState, + mergeAgentProjectStateForSave, +} from "@/lib/agents/agent-project-save-merge"; +import { normalizeAgentGenerationProjectPersistedState } from "@/lib/agents/generation-project-state"; +import type { AgentGenerationProjectPersistedState } from "@/types/agent"; + +const DETAIL_RESULT_URL = "https://scqny.jiansun.vip/uploads/1785470582893-rmh0xr.png"; +const DETAIL_RESULT_URL_2 = "https://scqny.jiansun.vip/uploads/1785470577924-owj79k.png"; +const SKU_RESULT_URL = "https://scqny.jiansun.vip/uploads/1785470458027-c6yuar.png"; +const DETAIL_REFERENCE_URL = "https://example.com/detail-reference.png"; + +function createWorkflowState(params?: { + detailReferences?: number; + skuReferences?: number; +}) { + return { + inputs: { + primarySku: "", + secondarySku: "", + brand: "", + store: "", + thirdCategory: "", + fourthCategory: "", + fifthCategory: "", + priceBand: "", + }, + selections: {}, + currentStage: "mode", + agentModeState: { + detailReplacement: { + query: "766308902517", + references: Array.from({ length: params?.detailReferences || 0 }, (_, index) => ({ + id: `detail-card-${index + 1}`, + label: `Detail ${index + 1}`, + imageUrl: DETAIL_REFERENCE_URL, + previewUrl: DETAIL_REFERENCE_URL, + })), + }, + skuReplacement: { + query: "sku-query", + references: Array.from({ length: params?.skuReferences || 0 }, (_, index) => ({ + id: `sku-card-${index + 1}`, + label: `SKU ${index + 1}`, + imageUrl: DETAIL_REFERENCE_URL, + previewUrl: DETAIL_REFERENCE_URL, + })), + }, + }, + }; +} + +function createExistingDetailProjectState(): AgentGenerationProjectPersistedState { + return normalizeAgentGenerationProjectPersistedState({ + workflowState: createWorkflowState({ detailReferences: 1 }), + workspaceState: { + input: "legacy detail replacement", + selectedModelId: "auto", + }, + resultCollections: { + detail_replacement: [DETAIL_RESULT_URL], + }, + replacementResultState: { + detail_replacement: { + "detail-card-1": { + url: DETAIL_RESULT_URL, + text: "completed", + status: "completed", + updatedAt: 1, + compareSourceUrl: DETAIL_REFERENCE_URL, + referenceImageUrl: DETAIL_REFERENCE_URL, + referencePreviewUrl: DETAIL_REFERENCE_URL, + label: "Detail 1", + }, + }, + }, + }); +} + +function expect(condition: boolean, message: string) { + if (!condition) { + throw new Error(message); + } +} + +function expectEqual(actual: T, expected: T, label: string) { + if (!Object.is(actual, expected)) { + throw new Error(`${label}: expected ${String(expected)}, received ${String(actual)}`); + } +} + +function expectIncludesUrl(urls: string[] | undefined, url: string) { + expect(Boolean(urls?.includes(url)), `expected ${url} to be present`); +} + +function expectExcludesUrl(urls: string[] | undefined, url: string) { + expect(!urls?.includes(url), `expected ${url} to be absent`); +} + +const legacyDetailState = createExistingDetailProjectState(); +expectEqual( + legacyDetailState.workspaceState?.activeBusinessModeKey, + "detail", + "legacy detail mode" +); +expectIncludesUrl(legacyDetailState.resultCollections?.detail_replacement, DETAIL_RESULT_URL); +expectEqual( + legacyDetailState.workflowState.agentModeState?.detailReplacement.references.length, + 1, + "legacy detail reference count" +); + +const resultOnlyLegacyState = normalizeAgentGenerationProjectPersistedState({ + workflowState: createWorkflowState(), + workspaceState: { + input: "legacy result-only detail replacement", + selectedModelId: "auto", + }, + resultCollections: { + detail_replacement: [DETAIL_RESULT_URL_2], + }, +}); +expectEqual( + resultOnlyLegacyState.workspaceState?.activeBusinessModeKey, + "detail", + "result-only detail mode" +); +expectIncludesUrl( + resultOnlyLegacyState.resultCollections?.detail_replacement, + DETAIL_RESULT_URL_2 +); + +const emptyProjectState = normalizeAgentGenerationProjectPersistedState({ + workflowState: createWorkflowState(), + workspaceState: { + input: "", + selectedModelId: "auto", + }, +}); +expectEqual( + emptyProjectState.workspaceState?.activeBusinessModeKey, + "main_sku", + "empty project mode" +); + +const staleEmptySaveState = mergeAgentProjectStateForSave({ + existingState: legacyDetailState, + incomingState: { + workflowState: createWorkflowState(), + workspaceState: { + input: "", + selectedModelId: "auto", + activeBusinessModeKey: "main_sku", + imageParams: { + aspectRatio: "auto", + imageSize: "2K", + imageCount: 1, + }, + }, + resultImageUrls: [], + resultCollections: {}, + replacementResultState: {}, + }, +}); +expectEqual( + staleEmptySaveState.workspaceState?.activeBusinessModeKey, + "detail", + "stale empty save mode" +); +expectIncludesUrl( + staleEmptySaveState.resultCollections?.detail_replacement, + DETAIL_RESULT_URL +); +expectIncludesUrl(getAllResultUrlsFromAgentProjectState(staleEmptySaveState), DETAIL_RESULT_URL); +expectEqual( + staleEmptySaveState.workflowState.agentModeState?.detailReplacement.references.length, + 1, + "stale empty save reference count" +); +expectEqual( + staleEmptySaveState.replacementResultState?.detail_replacement?.["detail-card-1"]?.url, + DETAIL_RESULT_URL, + "stale empty save replacement record" +); + +const replacementStateOnlySave = mergeAgentProjectStateForSave({ + existingState: normalizeAgentGenerationProjectPersistedState({ + workflowState: createWorkflowState({ detailReferences: 1 }), + workspaceState: { + input: "", + selectedModelId: "auto", + }, + }), + incomingState: { + workflowState: createWorkflowState({ detailReferences: 1 }), + workspaceState: { + input: "", + selectedModelId: "auto", + }, + replacementResultState: { + detail_replacement: { + "detail-card-1": { + url: DETAIL_RESULT_URL_2, + text: "completed", + status: "completed", + updatedAt: 2, + }, + }, + }, + }, +}); +expectEqual( + replacementStateOnlySave.workspaceState?.activeBusinessModeKey, + "detail", + "replacement-state-only mode" +); +expectIncludesUrl( + replacementStateOnlySave.resultCollections?.detail_replacement, + DETAIL_RESULT_URL_2 +); +expectIncludesUrl( + getAllResultUrlsFromAgentProjectState(replacementStateOnlySave), + DETAIL_RESULT_URL_2 +); + +const deletedResultSave = mergeAgentProjectStateForSave({ + existingState: legacyDetailState, + incomingState: { + workflowState: createWorkflowState({ detailReferences: 1 }), + workspaceState: { + input: "", + selectedModelId: "auto", + activeBusinessModeKey: "detail", + }, + deletedResultCollections: { + detail_replacement: [DETAIL_RESULT_URL], + }, + resultCollections: {}, + replacementResultState: {}, + }, +}); +expectExcludesUrl(deletedResultSave.resultCollections?.detail_replacement, DETAIL_RESULT_URL); +expectExcludesUrl(getAllResultUrlsFromAgentProjectState(deletedResultSave), DETAIL_RESULT_URL); +expectEqual( + deletedResultSave.replacementResultState?.detail_replacement?.["detail-card-1"], + undefined, + "deleted replacement record" +); + +const skuReplacementState = normalizeAgentGenerationProjectPersistedState({ + workflowState: createWorkflowState({ skuReferences: 1 }), + workspaceState: { + input: "", + selectedModelId: "auto", + }, + replacementResultState: { + sku_replacement: { + "sku-card-1": { + url: SKU_RESULT_URL, + text: "completed", + status: "completed", + updatedAt: 3, + }, + }, + }, +}); +expectEqual( + skuReplacementState.workspaceState?.activeBusinessModeKey, + "detail", + "sku replacement mode" +); +expectIncludesUrl(skuReplacementState.resultCollections?.sku_replacement, SKU_RESULT_URL); + +console.log( + JSON.stringify( + { + ok: true, + covered: [ + "legacy detail mode inference", + "result-only detail mode inference", + "empty project stays main_sku", + "stale empty save keeps detail results", + "stale empty save keeps detail reference cards", + "replacementResultState repopulates resultCollections", + "explicit deletion is respected", + "sku replacement recovery", + ], + }, + null, + 2 + ) +);