GuDingZiChan/print-agent/tests/print-recovery.test.mjs

158 lines
6.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import test from 'node:test'
import assert from 'node:assert/strict'
import fs from 'node:fs/promises'
import path from 'node:path'
import os from 'node:os'
import { createAgentRuntime } from '../src/services/agentRuntime.js'
import { createPrintJournal } from '../src/services/printJournal.js'
const config = { serverUrl: 'https://asset.example.com', terminalId: 1, deviceToken: 'test-only', selectedPrinterName: 'Test printer' }
const claimedJob = { id: 7, job_type: 'asset_label', claim_token: 'attempt-one' }
const pendingResult = (phase) => ({ jobId: 7, claimToken: 'attempt-one', phase, serverUrl: config.serverUrl, terminalId: config.terminalId })
const memoryJournal = (initial = null) => ({
entry: initial,
async load() { return this.entry },
async save(entry) { this.entry = structuredClone(entry) },
async clear() { this.entry = null }
})
async function fixture({ journal = memoryJournal(), dispatch = async () => ({ ok: true }), report, readState } = {}) {
let prints = 0
let claims = 0
const reports = []
const runtime = createAgentRuntime({
journal,
store: { async load() { return { ...config } } },
clientFactory: () => ({
async claimNextJob(payload) { claims += 1; assert.equal(payload.protocolVersion, 2); return { job: claimedJob } },
async reportJobStatus(id, payload) {
reports.push(payload)
assert.equal(payload.claimToken, 'attempt-one')
await report?.(id, payload)
},
getJobState: readState
}),
jobProcessor: { async process() { prints += 1; return dispatch() } },
getPrinters: async () => [],
setIntervalImpl: () => 1,
clearIntervalImpl: () => {}
})
await runtime.loadConfig()
return { runtime, journal, reports, prints: () => prints, claims: () => claims }
}
test('打印成功回写断网后只补结果,不能再出纸或报failed', async () => {
let completionCalls = 0
const f = await fixture({ report: async (_, payload) => {
if (payload.status === 'completed' && ++completionCalls === 1) throw new Error('network disconnected')
} })
await assert.rejects(f.runtime.pollOnce(), /network disconnected/)
assert.equal(f.journal.entry.phase, 'completed_pending_ack')
await f.runtime.pollOnce()
assert.equal(f.prints(), 1)
assert.equal(f.claims(), 1)
assert.deepEqual(f.reports.map(r => r.status), ['printing', 'completed', 'completed'])
assert.equal(f.journal.entry, null)
})
test('重启已完成任务只补completed;dispatching任务上报待核实', async () => {
for (const phase of ['completed_pending_ack', 'dispatching', 'claimed']) {
const f = await fixture({ journal: memoryJournal(pendingResult(phase)) })
await f.runtime.pollOnce()
assert.equal(f.prints(), 0)
assert.equal(f.claims(), 0)
assert.equal(f.reports[0].status, phase === 'completed_pending_ack' ? 'completed' : 'result_unknown')
}
})
test('部分出纸后执行异常进入待核实,不自动重派', async () => {
const f = await fixture({ dispatch: async () => { throw new Error('second label failed') } })
await f.runtime.pollOnce()
assert.equal(f.prints(), 1)
assert.deepEqual(f.reports.map(r => r.status), ['printing', 'result_unknown'])
})
test('恢复记录落盘失败时禁止出纸', async () => {
const journal = memoryJournal()
journal.save = async () => { throw new Error('disk full') }
const f = await fixture({ journal })
await assert.rejects(f.runtime.pollOnce(), /disk full/)
assert.equal(f.prints(), 0)
assert.equal(f.reports.length, 0)
})
test('领取或处理未结束时重复poll不再领取', async () => {
let release
const held = new Promise(resolve => { release = resolve })
const f = await fixture({ dispatch: async () => { await held; return { ok: true } } })
const first = f.runtime.pollOnce()
await new Promise(resolve => setImmediate(resolve))
await f.runtime.pollOnce()
assert.equal(f.prints(), 1)
assert.equal(f.claims(), 1)
release()
await first
})
test('409结果冲突必须先读服务器;同token未知结果保留恢复记录', async () => {
let reads = 0
const f = await fixture({
journal: memoryJournal(pendingResult('completed_pending_ack')),
report: async () => { throw Object.assign(new Error('conflict'), { status: 409 }) },
readState: async () => { reads += 1; return { id: 7, claim_token: 'attempt-one', status: 'result_unknown' } }
})
await assert.rejects(f.runtime.pollOnce(), /conflict/)
assert.equal(reads, 1)
assert.equal(f.journal.entry.phase, 'completed_pending_ack')
assert.equal(f.prints(), 0)
})
test('管理员已处理旧token时核实服务器后释放恢复记录', async () => {
const f = await fixture({
journal: memoryJournal(pendingResult('completed_pending_ack')),
report: async () => { throw Object.assign(new Error('conflict'), { status: 409 }) },
readState: async () => ({ id: 7, claim_token: null, status: 'queued' })
})
await f.runtime.pollOnce()
assert.equal(f.journal.entry, null)
assert.equal(f.claims(), 0)
assert.match(f.runtime.getState().lastError, /核对/)
})
test('409读回空对象、未知状态或缺token都不能清恢复记录', async () => {
for (const invalidState of [{}, { status: 'completed' }, { status: 'bogus', claim_token: null }]) {
const f = await fixture({
journal: memoryJournal(pendingResult('completed_pending_ack')),
report: async () => { throw Object.assign(new Error('conflict'), { status: 409 }) },
readState: async () => invalidState
})
await assert.rejects(f.runtime.pollOnce(), /状态响应不完整/)
assert.equal(f.journal.entry.phase, 'completed_pending_ack')
assert.equal(f.prints(), 0)
}
})
test('损坏或属于其他服务器的恢复记录阻止领取', async () => {
const journal = memoryJournal({ ...pendingResult('dispatching'), serverUrl: 'https://other.example.com' })
const f = await fixture({ journal })
await assert.rejects(f.runtime.pollOnce(), /其他服务器/)
assert.equal(f.claims(), 0)
})
test('真实journal跨实例重启保持阶段,损坏内容不当成无记录', async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'asset-print-journal-'))
try {
const journal = createPrintJournal(root)
await journal.save(pendingResult('dispatching'))
assert.deepEqual(await createPrintJournal(root).load(), pendingResult('dispatching'))
await journal.save(pendingResult('completed_pending_ack'))
assert.equal((await createPrintJournal(root).load()).phase, 'completed_pending_ack')
await fs.writeFile(path.join(root, 'pending-print-result.json'), '{bad')
await assert.rejects(journal.load())
await journal.clear()
assert.equal(await journal.load(), null)
} finally {
await fs.rm(root, { recursive: true, force: true })
}
})