import { test } from 'node:test'
import assert from 'node:assert/strict'
import { MAX_TEMPLATE_DEPTH, MAX_TEMPLATE_SOURCE_LENGTH, renderTemplate } from '../template'
// 设计稿 §10.1 的「边界/性能」组:单文件上限是 1 MiB(§6.2),引擎不能在这个量级上炸或卡死。
// 后面几组钉住审查 [11-1] 要求的安全阀:嵌套太深 / 体积太大一律降级成结构化 error,不抛异常。
test('1 MiB 量级的模板能正常渲染', () => {
const filler = 'x'.repeat(1024)
const source = `${filler}\n`.repeat(1024) + '{{ title }}\n'
assert.ok(source.length > 1024 * 1024, '构造的模板要超过 1 MiB')
const result = renderTemplate(source, { title: '超长模板' })
assert.equal(result.error, null)
assert.equal(result.html.endsWith('超长模板\n'), true)
assert.deepEqual(result.warnings, [])
})
test('1000 项循环:结果正确,同一处缺失路径只留一条警告', () => {
const rows = Array.from({ length: 1000 }, (_, i) => ({ n: i }))
const result = renderTemplate('{{#each rows}}{{ this.n }};{{/each}}', { rows })
assert.equal(result.error, null)
assert.equal(result.html.startsWith('0;1;2;'), true)
assert.equal(result.html.endsWith('999;'), true)
const missing = renderTemplate('{{#each rows}}{{ this.absent }}{{/each}}', { rows })
assert.equal(missing.html, '')
assert.equal(missing.warnings.length, 1)
assert.equal(missing.warnings[0].count, 1000)
})
test('深嵌套 #each/#if(30 层)不炸,作用域逐层正确', () => {
const depth = 30
let source = ''
const root: Record = { name: 'root' }
let cursor = root
for (let i = 0; i < depth; i += 1) {
source += `{{#each level${i}}}`
const inner: Record = { name: `n${i}` }
cursor[`level${i}`] = [inner]
cursor = inner
}
source += '{{ this.name }}'
for (let i = 0; i < depth; i += 1) source += '{{/each}}'
const result = renderTemplate(source, root)
assert.equal(result.error, null)
assert.equal(result.html, `n${depth - 1}`)
})
test('10 层嵌套 #if 渲染正确,没有警告(人工模板的正常量级)', () => {
const depth = 10
const source = '{{#if ok}}'.repeat(depth) + '{{ title }}' + '{{/if}}'.repeat(depth)
const result = renderTemplate(source, { ok: true, title: '十层' })
assert.equal(result.error, null)
assert.equal(result.html, '十层')
assert.deepEqual(result.warnings, [])
})
test(`正好 ${MAX_TEMPLATE_DEPTH} 层照常渲染,多一层降级成 too-deep`, () => {
const openTag = '{{#if ok}}'
const wrap = (depth: number): string =>
openTag.repeat(depth) + 'x' + '{{/if}}'.repeat(depth)
const atLimit = renderTemplate(wrap(MAX_TEMPLATE_DEPTH), { ok: true })
assert.equal(atLimit.error, null)
assert.equal(atLimit.html, 'x')
const overLimit = renderTemplate(wrap(MAX_TEMPLATE_DEPTH + 1), { ok: true })
assert.equal(overLimit.error?.code, 'too-deep')
// 位置指向超出的那个 {{#if}}(全在一行)
assert.equal(overLimit.error?.line, 1)
assert.equal(overLimit.error?.offset, MAX_TEMPLATE_DEPTH * openTag.length)
assert.equal(overLimit.error?.column, overLimit.error!.offset + 1)
assert.ok(overLimit.error?.message.includes(String(MAX_TEMPLATE_DEPTH)))
assert.equal(overLimit.html, '')
assert.deepEqual(overLimit.warnings, [])
})
test('审查 [11-1] 的复现输入(3000 层 {{#if}},约 48 KB)不抛异常,降级为 too-deep', () => {
const depth = 3000
const source = '{{#if a}}'.repeat(depth) + 'x' + '{{/if}}'.repeat(depth)
assert.doesNotThrow(() => renderTemplate(source, { a: true }))
const result = renderTemplate(source, { a: true })
assert.equal(result.error?.code, 'too-deep')
assert.equal(result.html, '')
assert.deepEqual(result.warnings, [])
})
test('超深 {{#each}} 同样降级(两种块共用同一个层数计数器)', () => {
const depth = MAX_TEMPLATE_DEPTH + 1
const source = '{{#each rows}}'.repeat(depth) + 'x' + '{{/each}}'.repeat(depth)
assert.doesNotThrow(() => renderTemplate(source, { rows: [] }))
assert.equal(renderTemplate(source, { rows: [] }).error?.code, 'too-deep')
})
test(`体积超过 ${MAX_TEMPLATE_SOURCE_LENGTH}:不解析、不抛异常,降级为 too-large`, () => {
const source = `{{ a }}${'x'.repeat(MAX_TEMPLATE_SOURCE_LENGTH)}`
assert.doesNotThrow(() => renderTemplate(source, { a: 1 }))
const result = renderTemplate(source, { a: 1 })
assert.equal(result.error?.code, 'too-large')
assert.equal(result.error?.line, 1)
assert.equal(result.error?.column, 1)
assert.equal(result.error?.offset, 0)
assert.equal(result.html, '')
assert.deepEqual(result.warnings, [])
})
test('体积正好等于上限:照常解析(上限是「超过才拒」)', () => {
const source = '{{ a }}'.padEnd(MAX_TEMPLATE_SOURCE_LENGTH, 'x')
assert.equal(source.length, MAX_TEMPLATE_SOURCE_LENGTH)
const result = renderTemplate(source, { a: 1 })
assert.equal(result.error, null)
assert.equal(result.html.startsWith('1'), true)
})