import test from 'node:test' import assert from 'node:assert/strict' import { createRequire } from 'node:module' import { ART_CODE, buildOwnerPatches, applyOwnerPatches, digest } from './backfill-art-workflow-planner-owner.mjs' const { normalizeArtPlannerOwner, selectArtPlannerOwner } = createRequire(import.meta.url)('../lib/art-workflow-owner.cjs') const source = { processInstanceId: 'w', name: '策划案负责人', value: '测试甲', extValue: JSON.stringify([{ emplId: 'ding-1', name: '测试甲', avatar: 'private-avatar' }]) } const snapshot = { processInstanceId: 'w', businessId: 'code', formFields: { brand: '测试', planningTaskStats: 'unchanged' } } test('共享提取器保留稳定ID、去掉无关联系人资料、支持姓名和明确空值', () => { assert.equal(normalizeArtPlannerOwner(source.value, source.extValue), '[{"emplId":"ding-1","name":"测试甲"}]') assert.equal(normalizeArtPlannerOwner(' 测试甲 ', 'invalid'), '测试甲') assert.equal(normalizeArtPlannerOwner('', null), null) assert.equal(selectArtPlannerOwner([{ ...source, sourceRowId: 1 }, { ...source, sourceRowId: 2, value: '', extValue: '' }]), null) assert.equal(selectArtPlannerOwner([{ ...source, sourceRowId: '9007199254740993' }, { ...source, sourceRowId: '9007199254740992', value: '旧人', extValue: '' }]), normalizeArtPlannerOwner(source.value, source.extValue)) }) test('只计划缺失字段;已有空值不覆盖;上游缺失流程阻断,字段不存在保存明确空值', () => { assert.equal(buildOwnerPatches([{ ...snapshot, formFields: { plannerOwner: null } }], []).length, 0) assert.throws(() => buildOwnerPatches([snapshot], []), /SOURCE_WORKFLOW_NOT_FOUND/) assert.equal(buildOwnerPatches([snapshot], [{ processInstanceId: 'w', name: null, value: null }])[0].owner, null) const [patch] = buildOwnerPatches([snapshot], [source]) assert.deepEqual(patch.before, snapshot.formFields) assert.equal(patch.beforeHash, digest({ planningTaskStats: 'unchanged', brand: '测试' })) }) function database(initial, owner = 'lease', expires = new Date(Date.now() + 60000)) { let state = structuredClone(initial), writes = 0 const tx = { $queryRawUnsafe: async () => [{ lease_owner: owner, lease_expires_at: expires }], dingWorkflowSnapshot: { findUnique: async () => ({ formFields: state, processCode: ART_CODE }) }, $executeRawUnsafe: async (_sql, next, _id, code, previous) => { assert.equal(code, ART_CODE); assert.deepEqual(JSON.parse(previous), state) state = JSON.parse(next); writes++; return 1 }, } return { $transaction: async work => { const before = structuredClone(state); try { return await work(tx) } catch (e) { state = before; throw e } }, state: () => state, writes: () => writes } } test('apply仅补字段、幂等、精确restore,保留原字段', async () => { const patches = buildOwnerPatches([snapshot], [source]), db = database(snapshot.formFields) assert.deepEqual(await applyOwnerPatches(db, patches, 'lease'), { changed: 1, skipped: 0 }) assert.deepEqual(db.state(), { ...snapshot.formFields, plannerOwner: patches[0].owner }) assert.deepEqual(await applyOwnerPatches(db, patches, 'lease'), { changed: 0, skipped: 1 }) assert.deepEqual(await applyOwnerPatches(db, patches, 'lease', true), { changed: 1, skipped: 0 }) assert.deepEqual(db.state(), snapshot.formFields) assert.deepEqual(await applyOwnerPatches(db, patches, 'lease', true), { changed: 0, skipped: 1 }) }) test('并发更改要求重新审计;其他同步已补齐字段时不覆盖;租约失效不写入', async () => { const patches = buildOwnerPatches([snapshot], [source]) const changed = database({ ...snapshot.formFields, brand: 'changed' }) await assert.rejects(applyOwnerPatches(changed, patches, 'lease'), /SNAPSHOT_CHANGED_REAUDIT/) assert.equal(changed.writes(), 0) const synced = database({ ...snapshot.formFields, plannerOwner: 'authoritative-new-owner' }) assert.deepEqual(await applyOwnerPatches(synced, patches, 'lease'), { changed: 0, skipped: 1 }) for (const db of [database(snapshot.formFields, 'other'), database(snapshot.formFields, 'lease', new Date(0))]) { await assert.rejects(applyOwnerPatches(db, patches, 'lease'), /SYNC_LEASE_LOST/) assert.equal(db.writes(), 0) } }) test('恢复时不覆盖已发生的后续同步或其他字段变更', async () => { const patches = buildOwnerPatches([snapshot], [source]) const db = database({ ...snapshot.formFields, plannerOwner: patches[0].owner, brand: 'later' }) await assert.rejects(applyOwnerPatches(db, patches, 'lease', true), /RESTORE_CONFLICT_REVIEW_MANUALLY/) assert.equal(db.writes(), 0) })