fix(adapter-wp): 内存上界注释纠正;错误前事件排空确定化;post_id 严格解析;嵌套畸形 fail-fast;类型收紧与测试钉
审查修复六项(wxr-stream): - 内存上界表述纠正:saxes 按节点内部缓冲整个 text/CDATA 节点,对抗性 单字段文件峰值 ≤ maxWxrBytes 的 UTF-16 表示(约 2×),并非与文件 大小解耦;maxFieldLength/maxCdataLength 封顶的是本模块缓冲与产出 字段长度 - parser.write/close 抛错前先排空待 yield 队列再重抛:错误前已完成 事件的交付不依赖 chunk 划分(测试钉死单 chunk 与 7 字节切块一致) - wp:post_id / wp:term_id 严格解析:整串十进制 + Number.isSafeInteger, 否则 null(表驱动测试:''/'0x10'/'12abc'/超安全域 → null) - 嵌套 item/wp:term 立即 WxrParseError(fail-fast,防外层截断产出) - Target.field 经 Exclude 收窄为纯文本字段,消除 as string 断言 - 测试钉死:提前 break 销毁上游流、text+CDATA 混合累计、限额错误 message 含行列位置 142 测试全绿(新增 12)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,17 @@ async function captureRejection(p: Promise<unknown>): Promise<unknown> {
|
||||
throw new Error('期望抛出但未抛出')
|
||||
}
|
||||
|
||||
/** 迭代到错误或完结:同时保留已交付事件与错误(审查 Fix 2 的交付确定性断言用) */
|
||||
async function collectUntilError(gen: AsyncGenerator<WxrEvent>): Promise<{ events: WxrEvent[]; error: unknown }> {
|
||||
const events: WxrEvent[] = []
|
||||
try {
|
||||
for await (const ev of gen) events.push(ev)
|
||||
return { events, error: null }
|
||||
} catch (e) {
|
||||
return { events, error: e }
|
||||
}
|
||||
}
|
||||
|
||||
function itemsOf(events: WxrEvent[]): WxrItem[] {
|
||||
return events
|
||||
.filter((ev): ev is Extract<WxrEvent, { kind: 'item' }> => ev.kind === 'item')
|
||||
@@ -212,6 +223,21 @@ describe('parseWxrStream:限额防线(WxrLimitError,kind 区分)', () =>
|
||||
)) as WxrLimitError
|
||||
expect(err).toBeInstanceOf(WxrLimitError)
|
||||
expect(err.kind).toBe('cdata')
|
||||
// 限额错误 message 附 saxes 行列位置(钉死,审查 Fix 6c)
|
||||
expect(err.message).toMatch(/line \d+, column \d+/)
|
||||
})
|
||||
|
||||
it('超限抛错前已完成事件确定性交付:单 chunk 与 7 字节切块产出一致(审查 Fix 2)', async () => {
|
||||
const single = await collectUntilError(
|
||||
parseWxrStream(fromString(SMALL_XML), { limits: { maxItems: 2 } }),
|
||||
)
|
||||
const chunked = await collectUntilError(
|
||||
parseWxrStream(fromStringChunked(SMALL_XML), { limits: { maxItems: 2 } }),
|
||||
)
|
||||
expect(single.error).toBeInstanceOf(WxrLimitError)
|
||||
expect(chunked.error).toBeInstanceOf(WxrLimitError)
|
||||
expect(signature(single.events)).toStrictEqual(['item:1:post', 'item:2:post'])
|
||||
expect(signature(chunked.events)).toStrictEqual(['item:1:post', 'item:2:post'])
|
||||
})
|
||||
|
||||
it('maxFieldLength=10:单字段累计超限抛 kind fieldLength(防单个 title 灌爆内存)', async () => {
|
||||
@@ -362,3 +388,74 @@ describe('parseWxrStream:畸形 XML → WxrParseError(带 saxes 行列位置
|
||||
expect(err.message).toContain('unclosed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseWxrStream:wp:post_id / wp:term_id 严格整数解析(审查 Fix 3)', () => {
|
||||
it.each([
|
||||
['', null],
|
||||
['0x10', null],
|
||||
['12abc', null],
|
||||
['-7', -7],
|
||||
['99999999999999999999', null],
|
||||
['101', 101],
|
||||
])('post_id 输入 %j → %j', async (raw, expected) => {
|
||||
const xml =
|
||||
'<?xml version="1.0"?><rss version="2.0" xmlns:wp="http://wordpress.org/export/1.2/"><channel>' +
|
||||
`<item><title>t</title><wp:post_id>${raw}</wp:post_id><wp:post_type>post</wp:post_type></item>` +
|
||||
'</channel></rss>'
|
||||
const events = await collect(parseWxrStream(fromString(xml)))
|
||||
expect(itemsOf(events)[0]?.postId).toBe(expected)
|
||||
})
|
||||
|
||||
it('term_id 同一规则:非纯数字 → null', async () => {
|
||||
const xml =
|
||||
'<?xml version="1.0"?><rss version="2.0" xmlns:wp="http://wordpress.org/export/1.2/"><channel>' +
|
||||
'<wp:term><wp:term_id>0x10</wp:term_id><wp:term_taxonomy>category</wp:term_taxonomy></wp:term>' +
|
||||
'</channel></rss>'
|
||||
const events = await collect(parseWxrStream(fromString(xml)))
|
||||
expect(events[0]?.kind === 'term' && events[0].term.termId).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseWxrStream:嵌套畸形 fail-fast(审查 Fix 4)', () => {
|
||||
it('item 内嵌 item:立即 WxrParseError,外层不被截断产出', async () => {
|
||||
const xml =
|
||||
'<?xml version="1.0"?><rss version="2.0" xmlns:wp="http://wordpress.org/export/1.2/"><channel>' +
|
||||
'<item><title>outer</title><wp:post_id>1</wp:post_id><wp:post_type>post</wp:post_type>' +
|
||||
'<item><wp:post_id>2</wp:post_id><wp:post_type>post</wp:post_type></item>' +
|
||||
'</item></channel></rss>'
|
||||
const { events, error } = await collectUntilError(parseWxrStream(fromString(xml)))
|
||||
expect(error).toBeInstanceOf(WxrParseError)
|
||||
expect((error as WxrParseError).message).toContain('嵌套')
|
||||
expect(events).toStrictEqual([]) // 嵌套发生在 item 中途——错误前无已完成事件
|
||||
})
|
||||
|
||||
it('wp:term 内嵌 wp:term:同样 fail-fast', async () => {
|
||||
const xml =
|
||||
'<?xml version="1.0"?><rss version="2.0" xmlns:wp="http://wordpress.org/export/1.2/"><channel>' +
|
||||
'<wp:term><wp:term_id>1</wp:term_id><wp:term_taxonomy>category</wp:term_taxonomy>' +
|
||||
'<wp:term><wp:term_id>2</wp:term_id></wp:term>' +
|
||||
'</wp:term></channel></rss>'
|
||||
const err = (await captureRejection(collect(parseWxrStream(fromString(xml))))) as WxrParseError
|
||||
expect(err).toBeInstanceOf(WxrParseError)
|
||||
expect(err.message).toContain('嵌套')
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseWxrStream:流契约钉死(审查 Fix 6)', () => {
|
||||
it('消费方提前 break:上游流被销毁(finally 兜底契约)', async () => {
|
||||
const stream = fromString(SMALL_XML)
|
||||
for await (const _ev of parseWxrStream(stream)) {
|
||||
break // 收到首个事件即退出
|
||||
}
|
||||
expect(stream.destroyed).toBe(true)
|
||||
})
|
||||
|
||||
it('text 与 CDATA 混合内容累计为同一字段值', async () => {
|
||||
const xml =
|
||||
'<?xml version="1.0"?><rss version="2.0" xmlns:wp="http://wordpress.org/export/1.2/"><channel>' +
|
||||
'<item><title>pre<![CDATA[mid]]>post</title><wp:post_id>5</wp:post_id><wp:post_type>post</wp:post_type></item>' +
|
||||
'</channel></rss>'
|
||||
const events = await collect(parseWxrStream(fromString(xml)))
|
||||
expect(itemsOf(events)[0]?.title).toBe('premidpost')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user