578 lines
16 KiB
TypeScript
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
|
|
)
|
|
);
|