ai-chatbot/scripts/test-agent-detail-replacement-recovery.ts

578 lines
16 KiB
TypeScript

import {
doesAgentProjectStateCoverReplacementResults,
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";
const STALE_DETAIL_RESULT_URL = "https://example.com/stale-detail-result.png";
const THIRD_DETAIL_RESULT_URL = "https://example.com/third-detail-result.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<T>(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`);
}
function simulateDatabaseRoundTrip(
state: AgentGenerationProjectPersistedState
) {
return JSON.parse(
JSON.stringify(state)
) as AgentGenerationProjectPersistedState;
}
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
);
expectEqual(
replacementStateOnlySave.workflowState.agentModeState?.detailReplacement.references.length,
1,
"replacement-state-only recovered reference count"
);
const collectionOnlyLegacyState = normalizeAgentGenerationProjectPersistedState({
workflowState: createWorkflowState({ detailReferences: 1 }),
workspaceState: {
input: "legacy collection-only replacement",
selectedModelId: "auto",
},
resultCollections: {
detail_replacement: [DETAIL_RESULT_URL_2],
},
});
expectEqual(
collectionOnlyLegacyState.replacementResultState?.detail_replacement?.["detail-card-1"]?.url,
DETAIL_RESULT_URL_2,
"collection-only recovered replacement record"
);
const recordOnlyLegacyState = normalizeAgentGenerationProjectPersistedState({
workflowState: createWorkflowState(),
workspaceState: {
input: "legacy record-only replacement",
selectedModelId: "auto",
},
replacementResultState: {
detail_replacement: {
"recovered-detail-card": {
url: DETAIL_RESULT_URL,
text: "completed",
status: "completed",
updatedAt: 4,
referenceImageUrl: DETAIL_REFERENCE_URL,
label: "Recovered detail",
},
},
},
});
expectEqual(
recordOnlyLegacyState.workflowState.agentModeState?.detailReplacement.references[0]?.id,
"recovered-detail-card",
"record-only recovered reference id"
);
expectEqual(
recordOnlyLegacyState.workflowState.agentModeState?.detailReplacement.references[0]?.imageUrl,
DETAIL_REFERENCE_URL,
"record-only recovered reference image"
);
const newerServerRecordState = mergeAgentProjectStateForSave({
existingState: normalizeAgentGenerationProjectPersistedState({
workflowState: createWorkflowState({ detailReferences: 1 }),
workspaceState: {
input: "",
selectedModelId: "auto",
},
replacementResultState: {
detail_replacement: {
"detail-card-1": {
url: DETAIL_RESULT_URL_2,
text: "newer server result",
status: "completed",
updatedAt: 20,
},
},
},
}),
incomingState: {
workflowState: createWorkflowState({ detailReferences: 1 }),
workspaceState: {
input: "",
selectedModelId: "auto",
activeBusinessModeKey: "detail",
},
replacementResultState: {
detail_replacement: {
"detail-card-1": {
url: STALE_DETAIL_RESULT_URL,
text: "stale browser result",
status: "completed",
updatedAt: 10,
},
},
},
},
});
expectEqual(
newerServerRecordState.replacementResultState?.detail_replacement?.["detail-card-1"]?.url,
DETAIL_RESULT_URL_2,
"newer server replacement record"
);
expect(
!doesAgentProjectStateCoverReplacementResults({
candidateState: normalizeAgentGenerationProjectPersistedState({
workflowState: createWorkflowState({ detailReferences: 1 }),
resultCollections: {
detail_replacement: [STALE_DETAIL_RESULT_URL],
},
}),
authoritativeState: newerServerRecordState,
}),
"stale local draft must not cover newer server replacement results"
);
expect(
doesAgentProjectStateCoverReplacementResults({
candidateState: normalizeAgentGenerationProjectPersistedState({
workflowState: createWorkflowState({ detailReferences: 1 }),
resultCollections: {
detail_replacement: [DETAIL_RESULT_URL_2, STALE_DETAIL_RESULT_URL],
},
}),
authoritativeState: newerServerRecordState,
}),
"newer local draft should cover all server replacement results"
);
const partialReferenceSaveState = mergeAgentProjectStateForSave({
existingState: normalizeAgentGenerationProjectPersistedState({
workflowState: createWorkflowState({ detailReferences: 3 }),
workspaceState: {
input: "",
selectedModelId: "auto",
},
replacementResultState: {
detail_replacement: {
"detail-card-1": {
url: DETAIL_RESULT_URL,
text: "completed 1",
status: "completed",
updatedAt: 11,
},
"detail-card-2": {
url: DETAIL_RESULT_URL_2,
text: "completed 2",
status: "completed",
updatedAt: 12,
},
"detail-card-3": {
url: THIRD_DETAIL_RESULT_URL,
text: "completed 3",
status: "completed",
updatedAt: 13,
},
},
},
}),
incomingState: {
workflowState: createWorkflowState({ detailReferences: 1 }),
workspaceState: {
input: "",
selectedModelId: "auto",
activeBusinessModeKey: "detail",
},
},
});
expectEqual(
partialReferenceSaveState.workflowState.agentModeState?.detailReplacement.references.length,
3,
"partial stale reference save keeps result cards"
);
const newerIncomingRecordState = mergeAgentProjectStateForSave({
existingState: newerServerRecordState,
incomingState: {
workflowState: createWorkflowState({ detailReferences: 1 }),
workspaceState: {
input: "",
selectedModelId: "auto",
activeBusinessModeKey: "detail",
},
replacementResultState: {
detail_replacement: {
"detail-card-1": {
url: THIRD_DETAIL_RESULT_URL,
text: "newest browser result",
status: "completed",
updatedAt: 30,
},
},
},
},
});
expectEqual(
newerIncomingRecordState.replacementResultState?.detail_replacement?.["detail-card-1"]?.url,
THIRD_DETAIL_RESULT_URL,
"newer incoming replacement record"
);
let repeatedAutosaveState = normalizeAgentGenerationProjectPersistedState({
workflowState: createWorkflowState({ detailReferences: 39 }),
workspaceState: {
input: "39-card detail replacement",
selectedModelId: "auto",
},
replacementResultState: {
detail_replacement: {
"detail-card-1": {
url: DETAIL_RESULT_URL,
text: "completed 1",
status: "completed",
updatedAt: 101,
},
"detail-card-2": {
url: DETAIL_RESULT_URL_2,
text: "completed 2",
status: "completed",
updatedAt: 102,
},
"detail-card-3": {
url: THIRD_DETAIL_RESULT_URL,
text: "completed 3",
status: "completed",
updatedAt: 103,
},
},
},
});
for (let index = 0; index < 40; index += 1) {
repeatedAutosaveState = simulateDatabaseRoundTrip(
mergeAgentProjectStateForSave({
existingState: simulateDatabaseRoundTrip(repeatedAutosaveState),
incomingState: {
workflowState: createWorkflowState(),
workspaceState: {
input: `stale autosave ${index + 1}`,
selectedModelId: "auto",
activeBusinessModeKey: "main_sku",
},
resultCollections: {},
replacementResultState: {},
},
})
);
}
expectEqual(
repeatedAutosaveState.workspaceState?.activeBusinessModeKey,
"detail",
"repeated autosave detail mode"
);
expectEqual(
repeatedAutosaveState.workflowState.agentModeState?.detailReplacement.references.length,
39,
"repeated autosave reference count"
);
expectIncludesUrl(
repeatedAutosaveState.resultCollections?.detail_replacement,
DETAIL_RESULT_URL
);
expectIncludesUrl(
repeatedAutosaveState.resultCollections?.detail_replacement,
DETAIL_RESULT_URL_2
);
expectIncludesUrl(
repeatedAutosaveState.resultCollections?.detail_replacement,
THIRD_DETAIL_RESULT_URL
);
expectEqual(
JSON.stringify(
normalizeAgentGenerationProjectPersistedState(repeatedAutosaveState)
),
JSON.stringify(repeatedAutosaveState),
"normalized recovery state is idempotent"
);
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",
"resultCollections rebuild replacementResultState",
"replacementResultState rebuilds missing reference cards",
"newer replacement records win concurrent saves",
"local draft must cover server replacement URLs",
"partial stale reference saves keep completed cards",
"newer incoming replacement records remain writable",
"39-card repeated stale autosaves preserve completed results",
"normalized recovery state remains idempotent",
"explicit deletion is respected",
"sku replacement recovery",
],
},
null,
2
)
);