d3ro-voice/apps/desktop/tests/red/templates-field-ids-redteam-r3-7.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

90 lines
3.7 KiB
TypeScript

// 받아쓰기 템플릿 필드 id 회귀 테스트 (redteam r3-7)
// 버그: 편집기가 필드 개수로 id를 만들고(중간 삭제 후 재사용) 이름을 그대로 id로 써서
// 빈 id·중복 id 템플릿이 저장됐다. 세션에서 같은 id 필드의 받아쓴 값이 앞 값을 덮어써
// 출력({{id}})에서 앞 값이 조용히 사라졌다. 이제 서비스가 저장 전에 TemplateInvalidFormat 으로 거부한다.
import { describe, expect, it } from 'vitest'
import { D3ROError, ErrorCode } from '@d3ro/core/errors'
import type { TemplateField } from '@d3ro/core/types'
import { getDictationTemplateService } from '../../src/main/services/DictationTemplateService'
import { useRedHarness } from './harness'
useRedHarness()
function field(id: string): TemplateField {
return { id, name: id, label: id, promptText: '', required: true, maxDurationSec: 30 }
}
function expectInvalidFormat(run: () => unknown): void {
let caught: unknown
try {
run()
} catch (err) {
caught = err
}
expect(caught).toBeInstanceOf(D3ROError)
expect((caught as D3ROError).code).toBe(ErrorCode.TemplateInvalidFormat)
}
describe('받아쓰기 템플릿 필드 id 검증', () => {
it('중복 필드 id 템플릿 생성을 거부하고 저장하지 않는다', () => {
const svc = getDictationTemplateService()
const before = svc.getAll().length
// 편집기 시나리오: [field1, field2, field3] → field1 삭제 → 추가 → [field2, field3, field3]
expectInvalidFormat(() =>
svc.create({
name: 'dup',
description: '',
fields: [field('field2'), field('field3'), field('field3')],
outputFormat: '{{field2}} {{field3}}',
}),
)
expect(svc.getAll()).toHaveLength(before)
})
it('빈/공백 필드 id 템플릿 생성을 거부한다', () => {
const svc = getDictationTemplateService()
expectInvalidFormat(() =>
svc.create({ name: 'empty', description: '', fields: [field('a'), field('')], outputFormat: '{{a}}' }),
)
expectInvalidFormat(() =>
svc.create({ name: 'blank', description: '', fields: [field(' ')], outputFormat: '' }),
)
})
it('수정으로 중복 필드 id를 넣으면 거부하고 기존 필드를 유지한다', () => {
const svc = getDictationTemplateService()
const created = svc.create({
name: 'ok',
description: '',
fields: [field('a'), field('b')],
outputFormat: '{{a}} {{b}}',
})
expectInvalidFormat(() => svc.update({ id: created.id, fields: [field('a'), field('a')] }))
expect(svc.getById(created.id)?.fields.map((f) => f.id)).toEqual(['a', 'b'])
})
it('필드를 바꾸지 않는 수정과 유효한 필드 수정은 그대로 된다', () => {
const svc = getDictationTemplateService()
const created = svc.create({ name: 'ok', description: '', fields: [field('a')], outputFormat: '{{a}}' })
expect(svc.update({ id: created.id, name: 'renamed' }).name).toBe('renamed')
expect(svc.update({ id: created.id, fields: [field('a'), field('b')] }).fields).toHaveLength(2)
})
it('유효한 템플릿은 필드마다 받아쓴 값이 모두 출력에 남는다', () => {
const svc = getDictationTemplateService()
const created = svc.create({
name: 'flow',
description: '',
fields: [field('field2'), field('field3'), field('field4')],
outputFormat: '{{field2}}|{{field3}}|{{field4}}',
})
const outputs: string[] = []
svc.on('session-completed', (e: { outputText: string }) => outputs.push(e.outputText))
svc.startSession(created.id)
svc.consumeDictatedText('one')
svc.consumeDictatedText('two')
svc.consumeDictatedText('three')
expect(outputs).toEqual(['one|two|three'])
})
})