import { describe, expect, it } from 'bun:test' import { generateMosaic, selectKeywords, type KeywordCandidate, } from './mosaic' const SOURCE = '我一直以为记忆会忠实地保存一切,后来才发现,每一次回想都在重新编造过去。' function retain( candidates: KeywordCandidate[], words: string[], ): KeywordCandidate[] { return candidates.map((candidate) => ({ ...candidate, selected: words.includes(candidate.text), })) } describe('selectKeywords', () => { it('优先提供中文实词候选并排除常见停用词', () => { const candidates = selectKeywords(SOURCE, 14, 1) const words = candidates.map((candidate) => candidate.text) expect(words).toContain('记忆') expect(words).toContain('保存') expect(words).toContain('回想') expect(words).toContain('编造') expect(words).not.toContain('一直') expect(words).not.toContain('后来') }) it('在有候选词时至少自动保留一个', () => { const candidates = selectKeywords('风穿过废弃车站', 5, 1) expect(candidates.some((candidate) => candidate.selected)).toBe(true) }) it('显影 0% 不保留词语,100% 保留全部候选词', () => { const hidden = selectKeywords(SOURCE, 0, 1) const revealed = selectKeywords(SOURCE, 100, 1) expect(hidden.some((candidate) => candidate.selected)).toBe(false) expect(revealed.every((candidate) => candidate.selected)).toBe(true) }) }) describe('generateMosaic', () => { it('保留指定词语并彻底替换原标点', () => { const candidates = retain(selectKeywords(SOURCE, 14, 1), [ '记忆', '保存', '回想', '编造', '过去', ]) const result = generateMosaic(SOURCE, candidates, { volume: 100, disorder: 58, seed: 42, }) expect(result.output).toContain('记忆') expect(result.output).toContain('保存') expect(result.output).toContain('回想') expect(result.output).toContain('编造') expect(result.output).toContain('过去') expect(result.output).not.toMatch(/[,。!?;:“”‘’()【】、]/u) }) it('将总可见长度维持在设定体量附近', () => { const candidates = selectKeywords(SOURCE, 14, 1) const result = generateMosaic(SOURCE, candidates, { volume: 100, disorder: 58, seed: 42, }) const ratio = result.outputLength / result.inputLength expect(ratio).toBeGreaterThanOrEqual(0.97) expect(ratio).toBeLessThanOrEqual(1.03) }) it('支持将体量压缩到 10% 或膨胀到 1000%', () => { const compressed = generateMosaic(SOURCE, [], { volume: 10, disorder: 0, seed: 42, }) const expanded = generateMosaic(SOURCE, [], { volume: 1000, disorder: 0, seed: 42, }) expect(compressed.outputLength).toBe(Math.round(compressed.inputLength * 0.1)) expect(expanded.outputLength).toBe(expanded.inputLength * 10) }) it('不生成可能跳出正常行框的 Unicode 组合符', () => { const outputs = Array.from({ length: 20 }, (_, seed) => generateMosaic(SOURCE, [], { volume: 1000, disorder: 100, seed, }).output, ).join('') expect(outputs).not.toMatch(/\p{Mark}/u) }) it('相同种子可复现,不同种子会重新侵蚀', () => { const candidates = selectKeywords(SOURCE, 14, 1) const first = generateMosaic(SOURCE, candidates, { volume: 100, disorder: 58, seed: 42, }) const repeated = generateMosaic(SOURCE, candidates, { volume: 100, disorder: 58, seed: 42, }) const rerolled = generateMosaic(SOURCE, candidates, { volume: 100, disorder: 58, seed: 43, }) expect(repeated.output).toBe(first.output) expect(rerolled.output).not.toBe(first.output) }) it('保留换行但替换每一行中的隐藏内容', () => { const source = '记忆,在这里。\n过去,在那里。' const candidates = retain(selectKeywords(source, 20, 1), ['记忆', '过去']) const result = generateMosaic(source, candidates, { volume: 100, disorder: 30, seed: 7, }) expect(result.output.split('\n')).toHaveLength(2) expect(result.output).not.toMatch(/[,。]/u) }) })