d3ro-voice/packages/core/__tests__/meeting-markdown-inline-redteam-r3-27.test.ts
Yun Chan ba9ef9741e fix: red-team round 3 hardening across desktop, mobile, core and server
Batch of red-team r3 fixes that were in the working tree before the
2026-09-28 design overhaul, committed as one unit with their tests.

- desktop main: STT timeouts and sidecar, voice recording store, sync
  (credentials, audio, knowledge reindex, push gates), runtime
  provisioner, update policy, AltGr keybindings, voice-command policy,
  dictionary file codec/limits, meeting transcript condensing and a
  local recording ledger so interrupted-session recovery only closes
  meetings this device recorded (a phone's live meeting is left alone).
- mobile: login CSRF via implicit token callbacks rejected, account
  deletion/retention, durable queue retention, knowledge realtime
  without unfiltered DELETE, meeting re-record failure paths, cloud STT
  client, preferences store/resync.
- core: text chunking splits long unbroken transcripts to fit, template
  field policy, dictionary limits, meeting markdown inline handling.
- server: payple webhook policy and cancellation order scope, meeting
  document generation quota, team RPC null-role guard, unified LLM
  quota in-flight accounting, knowledge chunk vector index, meeting
  re-record failure paths (migrations 20260929*).
- ci: portable/runtime feed gates, update-policy schema, Forgejo file
  delete and alias planning.

Four older tests are updated to the new contracts rather than the old
behavior: token-pair auth callbacks are rejected, knowledge realtime no
longer subscribes to DELETE, long transcript lines are split, and
meeting recovery requires the local recording ledger for empty rows.
2026-09-28 20:45:52 +09:00

95 lines
3.6 KiB
TypeScript

// packages/core/__tests__/meeting-markdown-inline-redteam-r3-27.test.ts
// 회귀: DOCX 내보내기가 인라인 강조(**x**)를 별표째 찍어 PDF 와 결과가 달랐다.
// 이제 두 경로가 parseInlineRuns 하나로 굵기를 해석해야 한다.
import { describe, expect, it } from 'vitest'
import JSZip from 'jszip'
import { markdownToDocx } from '../src/utils/markdown-to-docx'
import { markdownToSimpleHtml, parseInlineRuns } from '../src/utils/meeting-markdown'
interface RunSpan {
text: string
bold: boolean
}
/** document.xml 에서 본문 run(텍스트·굵기)만 뽑는다. */
async function docxRuns(markdown: string): Promise<RunSpan[]> {
const buffer = await markdownToDocx(markdown, 'T')
const zip = await JSZip.loadAsync(buffer)
const file = zip.file('word/document.xml')
if (!file) throw new Error('document.xml missing')
const xml = await file.async('string')
const runs: RunSpan[] = []
for (const match of xml.matchAll(/<w:r>([\s\S]*?)<\/w:r>/g)) {
const body = match[1]
const text = [...body.matchAll(/<w:t[^>]*>([\s\S]*?)<\/w:t>/g)].map((m) => m[1]).join('')
const bold = /<w:b\/>|<w:b w:val="(?:true|1|on)"\/>/.test(body)
runs.push({ text, bold })
}
// 첫 run 은 문서 제목('T')
return runs.slice(1)
}
function boldTexts(runs: readonly RunSpan[]): string[] {
return runs.filter((r) => r.bold).map((r) => r.text)
}
function htmlBoldTexts(html: string): string[] {
return [...html.matchAll(/<strong>([\s\S]*?)<\/strong>/g)].map((m) => m[1])
}
describe('parseInlineRuns', () => {
it('강조 표기를 벗기고 굵은 조각으로 나눈다', () => {
expect(parseInlineRuns('**결정**: 3월 출시')).toEqual([
{ text: '결정', bold: true },
{ text: ': 3월 출시', bold: false },
])
expect(parseInlineRuns('a **b** c **d**')).toEqual([
{ text: 'a ', bold: false },
{ text: 'b', bold: true },
{ text: ' c ', bold: false },
{ text: 'd', bold: true },
])
})
it('짝이 없는 별표·빈 문자열은 그대로 둔다', () => {
expect(parseInlineRuns('2 ** 3')).toEqual([{ text: '2 ** 3', bold: false }])
expect(parseInlineRuns('****')).toEqual([{ text: '****', bold: false }])
expect(parseInlineRuns('')).toEqual([])
})
it('HTML 렌더링은 기존 출력과 같다 (이스케이프 후 <strong>)', () => {
expect(markdownToSimpleHtml('- **A** <b> & "c"')).toBe(
'<ul>\n<li><strong>A</strong> &lt;b&gt; &amp; &quot;c&quot;</li>\n</ul>',
)
})
})
describe('DOCX 인라인 강조 (PDF 와 동일)', () => {
it("'- **A** b' 는 DOCX 와 HTML 에서 같은 굵은 조각을 만든다", async () => {
const md = '- **A** b'
const runs = await docxRuns(md)
expect(runs.map((r) => r.text).join('')).toBe('A b')
expect(boldTexts(runs)).toEqual(htmlBoldTexts(markdownToSimpleHtml(md)))
expect(boldTexts(runs)).toEqual(['A'])
})
it('제목·체크박스·문단·표 셀에 별표가 남지 않는다', async () => {
const md = [
'## **결정** 사항',
'- [x] **담당**: 윤',
'**요약** 줄',
'',
'| **담당** | 기한 |',
'|---|---|',
'| **윤** | 3월 |',
].join('\n')
const runs = await docxRuns(md)
const allText = runs.map((r) => r.text).join('')
expect(allText).not.toContain('**')
expect(allText).toContain('☑ 담당: 윤')
// 표 머리글은 전체가 굵다 — 기한도 굵음
expect(boldTexts(runs)).toEqual(['결정', '담당', '요약', '담당', '기한', '윤'])
expect(htmlBoldTexts(markdownToSimpleHtml(md))).toEqual(['결정', '담당', '요약', '담당', '윤'])
})
})