fix: complete October 22 workflow feedback and request failure recovery

This commit is contained in:
2026-10-22 23:06:02 +08:00
parent 22baa5b5c0
commit 3ffa706b89
7 changed files with 123 additions and 381 deletions
+18 -34
View File
@@ -54,16 +54,8 @@ test('successful net debit excludes the original estimate and counts only succes
assert.equal(presentation.feeStatus(fee('DUTY', null, 100)), '待核定')
})
test('request recovery preserves content and identity while malformed recovery cannot create a replacement', () => {
const original = presentation.buildProcurementCreate(draft(), 'Company', 'fixed-key')
assert.deepEqual(normalized(submission.restoreProcurementSubmission(JSON.stringify(original))), normalized(original))
assert.throws(() => submission.restoreProcurementSubmission('{}'))
assert.throws(() => submission.restoreProcurementSubmission(JSON.stringify({ ...original, currency: 'EUR' })))
assert.equal(submission.definitelyRejected({ code: 'TIMEOUT' }), false)
assert.equal(submission.definitelyRejected({ status: 500 }), false)
assert.equal(submission.definitelyRejected({ status: 409 }), false)
assert.equal(submission.definitelyRejected({ status: 400 }), true)
assert.equal(submission.procurementRequestKey({ getRandomValues: target => { target.set([1, 2, 3, 4]); return target } }), 'proc-00000001000000020000000300000004')
test('new requests get unique keys without restoring stored submissions', () => {
assert.notEqual(submission.procurementRequestKey(crypto), submission.procurementRequestKey(crypto))
})
class ApiError extends Error { constructor(message, code, status) { super(message); this.code = code; this.status = status } }
@@ -109,18 +101,16 @@ test('detail has one in-flight request and an older response cannot overwrite an
} finally { harness.dispose() }
})
test('timeout retries the exact original body after reload and does not accept changed form data', async () => {
const storage = new Map(), calls = []
const first = viewHarness({ createPage: true, storage, api: { createProcurement: async payload => { calls.push(normalized(payload)); throw new ApiError('timeout', 'TIMEOUT') } } })
test('timeout releases the form and another click submits the edited current values', async () => {
const calls = []
const harness = viewHarness({ createPage: true, api: { createProcurement: async payload => { calls.push(normalized(payload)); throw new ApiError('timeout', 'TIMEOUT') } } })
try {
Object.assign(first.state.form, draft()); await first.state.submit(); assert.equal(calls.length, 1); assert.ok(first.state.pending.value)
first.state.form.productName = 'changed'; await first.state.submit(); assert.deepEqual(calls[1], calls[0])
} finally { first.dispose() }
const recovered = viewHarness({ createPage: true, storage, api: { createProcurement: async payload => { calls.push(normalized(payload)); return { order: { orderNo: 'PO-SAME' } } } } })
try {
assert.equal(recovered.state.form.productName, 'Test product'); await recovered.state.submit(); assert.deepEqual(calls[2], calls[0])
assert.equal(storage.size, 0); assert.equal(recovered.pushes[0].params.orderNo, 'PO-SAME')
} finally { recovered.dispose() }
Object.assign(harness.state.form, draft()); await harness.state.submit()
assert.equal(harness.state.submitting.value, false); assert.equal(harness.storage.size, 0)
harness.state.form.productName = 'changed'; await harness.state.submit()
assert.equal(calls[1].productName, 'changed'); assert.notEqual(calls[0].idempotencyKey, calls[1].idempotencyKey)
assert.equal(harness.state.submitting.value, false)
} finally { harness.dispose() }
})
test('submission double-click sends once and definitive validation rejection permits correction', async () => {
@@ -129,7 +119,7 @@ test('submission double-click sends once and definitive validation rejection per
try {
Object.assign(harness.state.form, draft()); const first = harness.state.submit(); await harness.state.submit(); assert.equal(calls, 1)
reject(new ApiError('invalid', 'A0422')); await first
assert.equal(harness.state.pending.value, null); assert.equal(harness.storage.size, 0)
assert.equal(harness.state.submitting.value, false); assert.equal(harness.storage.size, 0)
} finally { harness.dispose() }
})
@@ -149,24 +139,18 @@ test('pagination batches one request and switching account clears old rows befor
})
test('replay rejection cannot discard an earlier unknown creation outcome', async () => {
const calls = []; let attempt = 0
const harness = viewHarness({ createPage: true, api: { createProcurement: async payload => {
calls.push(normalized(payload)); attempt++
if (attempt === 1) throw new ApiError('timeout', 'TIMEOUT')
throw new ApiError('permission revoked', 'A0403')
} } })
test('consecutive timeout and permission failures never persist or freeze a submission', async () => {
let attempt = 0
const harness = viewHarness({ createPage: true, api: { createProcurement: async () => { throw new ApiError('failed', ++attempt === 1 ? 'TIMEOUT' : 'A0403') } } })
try {
Object.assign(harness.state.form, draft()); await harness.state.submit()
const saved = [...harness.storage.values()][0]; await harness.state.submit()
assert.equal([...harness.storage.values()][0], saved); assert.ok(harness.state.pending.value)
assert.deepEqual(calls[1], calls[0])
Object.assign(harness.state.form, draft()); await harness.state.submit(); await harness.state.submit()
assert.equal(attempt, 2); assert.equal(harness.state.submitting.value, false); assert.equal(harness.storage.size, 0)
harness.state.form.productName = 'editable'; assert.equal(harness.state.form.productName, 'editable')
} finally { harness.dispose() }
})
test('purchase category is optional while its length remains bounded', () => {
const payload = presentation.buildProcurementCreate({ ...draft(), purchaseCategory: ' ' }, 'Company', 'key')
assert.equal(payload.purchaseCategory, '')
assert.equal(submission.restoreProcurementSubmission(JSON.stringify(payload)).purchaseCategory, '')
assert.throws(() => presentation.buildProcurementCreate({ ...draft(), purchaseCategory: 'x'.repeat(129) }, 'Company', 'key'), /品类/)
})