chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:46:20 +08:00
commit eb21b65815
830 changed files with 113645 additions and 0 deletions
+227
View File
@@ -0,0 +1,227 @@
// xmodem.spec.js
// Unit tests for XmodemSession verifies that the session resets properly
// after a successful send so that normal serial-port I/O resumes.
process.env.NODE_ENV = 'development'
const { describe, it } = require('node:test')
const assert = require('node:assert/strict')
const fs = require('node:fs')
const os = require('node:os')
const path = require('node:path')
const { XmodemSession, XMODEM_STATE } = require('../../src/app/server/xmodem')
// XMODEM control bytes
const SOH = 0x01
const EOT = 0x04
const ACK = 0x06
const CRC = 0x43 // 'C'
/**
* Helper: create a minimal XmodemSession with mock terminal and websocket.
* The mock terminal records all writes; the mock websocket records all
* JSON messages sent to the client.
*/
function createMockSession () {
const written = []
const clientMessages = []
const term = {
writeRaw: (data) => written.push(Buffer.from(data)),
write: (data) => written.push(Buffer.from(data))
}
const ws = {
s: (msg) => clientMessages.push(msg)
}
const session = new XmodemSession(term, ws)
return { session, written, clientMessages, term, ws }
}
/**
* CRC-16/XMODEM calculation (mirrors the one in xmodem.js)
*/
function crc16Xmodem (data) {
let crc = 0
for (let i = 0; i < data.length; i++) {
crc = crc ^ (data[i] << 8)
for (let j = 0; j < 8; j++) {
if (crc & 0x8000) {
crc = (crc << 1) ^ 0x1021
} else {
crc = crc << 1
}
}
crc = crc & 0xFFFF
}
return crc
}
describe('XmodemSession send lifecycle', () => {
it('isActive returns false after send completes (session-end)', async () => {
const { session, clientMessages } = createMockSession()
// Start send
session.startSend()
assert.equal(session.isActive(), true)
// Set up a temp file to send (one 128-byte packet)
const tmpFile = path.join(os.tmpdir(), `xmodem-test-${Date.now()}.bin`)
const fileData = Buffer.alloc(128, 0xAA)
fs.writeFileSync(tmpFile, fileData)
session.setSendFiles([{
name: 'test.bin',
path: tmpFile,
size: 128
}])
// Simulate remote requesting CRC mode
session.handleData(Buffer.from([CRC]))
assert.equal(session.state, XMODEM_STATE.SENDING)
// Simulate remote ACKing the data packet
session.handleData(Buffer.from([ACK]))
// After ACK, sendNextPacket sees file is done → sendEot fires.
// sendEot sends EOT, fires session-end, and calls resetState.
// The state should now be IDLE.
assert.equal(session.state, XMODEM_STATE.IDLE)
assert.equal(session.isActive(), false)
// Verify session-end was sent to client
const sessionEndMsg = clientMessages.find(m => m.event === 'session-end')
assert.ok(sessionEndMsg, 'session-end message should be sent')
// Verify file-complete was sent to client
const fileCompleteMsg = clientMessages.find(m => m.event === 'file-complete')
assert.ok(fileCompleteMsg, 'file-complete message should be sent')
// Cleanup
try { fs.unlinkSync(tmpFile) } catch (e) {}
})
it('sendEot does not loop when called multiple times (stale ACK)', async () => {
const { session, written } = createMockSession()
session.startSend()
const tmpFile = path.join(os.tmpdir(), `xmodem-test-loop-${Date.now()}.bin`)
fs.writeFileSync(tmpFile, Buffer.alloc(128, 0xBB))
session.setSendFiles([{
name: 'test.bin',
path: tmpFile,
size: 128
}])
// Remote requests CRC mode
session.handleData(Buffer.from([CRC]))
// Remote ACKs the data packet → triggers sendEot internally
session.handleData(Buffer.from([ACK]))
assert.equal(session.state, XMODEM_STATE.IDLE)
// Now simulate a stale ACK arriving after the session is already IDLE.
// Before the fix this would cause sendEot → sendNextPacket → sendEot loop.
const eotCountBefore = written.filter(b => b.length === 1 && b[0] === EOT).length
session.handleData(Buffer.from([ACK]))
const eotCountAfter = written.filter(b => b.length === 1 && b[0] === EOT).length
// No additional EOT should have been sent
assert.equal(eotCountAfter, eotCountBefore, 'no extra EOT on stale ACK')
// State should still be IDLE
assert.equal(session.state, XMODEM_STATE.IDLE)
try { fs.unlinkSync(tmpFile) } catch (e) {}
})
it('resetState clears timeouts', async () => {
const { session } = createMockSession()
session.startSend()
// startSend sets a receive timeout
assert.ok(session.receiveTimeout, 'receiveTimeout should be set after startSend')
session.resetState()
// After reset, timeouts should be nulled and state should be IDLE
assert.equal(session.receiveTimeout, null)
assert.equal(session.sendTimeout, null)
assert.equal(session.state, XMODEM_STATE.IDLE)
})
it('handleData returns false when session is IDLE (data passes through)', async () => {
const { session } = createMockSession()
// Session starts as IDLE
assert.equal(session.state, XMODEM_STATE.IDLE)
// Regular terminal data should NOT be consumed by xmodem
const consumed = session.handleData(Buffer.from('hello'))
assert.equal(consumed, false)
})
it('handleData returns false for non-protocol data when session is IDLE after send', async () => {
const { session } = createMockSession()
// Full send cycle: start → send files → remote ACKs → EOT → reset
session.startSend()
const fs = require('fs')
const tmpFile = path.join(os.tmpdir(), `xmodem-test-idle-${Date.now()}.bin`)
fs.writeFileSync(tmpFile, Buffer.alloc(128, 0xDD))
session.setSendFiles([{ name: 'test.bin', path: tmpFile, size: 128 }])
session.handleData(Buffer.from([CRC])) // remote requests CRC
session.handleData(Buffer.from([ACK])) // remote ACKs packet → sendEot → resetState
// Session should be IDLE
assert.equal(session.state, XMODEM_STATE.IDLE)
// Normal terminal text should NOT be consumed (falls through to ws.send)
assert.equal(session.handleData(Buffer.from('hello\n')), false)
assert.equal(session.handleData(Buffer.from([0x0D])), false) // Enter key
try { fs.unlinkSync(tmpFile) } catch (e) {}
})
it('receive path resets state after EOT completion', async () => {
const { session, written, clientMessages } = createMockSession()
// Set up save path
session.setSavePath(os.tmpdir())
// Build a valid XMODEM-128 CRC packet: SOH + block 1 + ~block1 + 128 data + CRC16
const blockNum = 1
const data = Buffer.alloc(128, 0xCC)
const crc = crc16Xmodem(data)
const packet = Buffer.alloc(3 + 128 + 2)
packet[0] = SOH
packet[1] = blockNum
packet[2] = blockNum ^ 0xFF
data.copy(packet, 3)
packet[3 + 128] = (crc >> 8) & 0xFF
packet[3 + 128 + 1] = crc & 0xFF
// Send the data packet
session.handleData(packet)
assert.equal(session.state, XMODEM_STATE.RECEIVING)
// Send EOT (end of transmission)
session.handleData(Buffer.from([EOT]))
// After EOT, receive path should have reset state to IDLE
assert.equal(session.state, XMODEM_STATE.IDLE)
assert.equal(session.isActive(), false)
// Verify ACK was sent for EOT
const ackWritten = written.find(b => b.length === 1 && b[0] === ACK)
assert.ok(ackWritten, 'ACK should be sent for EOT')
// Verify session-end was sent to client
const sessionEndMsg = clientMessages.find(m => m.event === 'session-end')
assert.ok(sessionEndMsg, 'session-end should be sent after receive complete')
})
})