@things-factory/board-ai 10.1.5 → 10.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/client/components/default-catalog-entries.ts +1 -1
  2. package/client/components/default-slash-templates.ts +1 -1
  3. package/client/index.ts +39 -6
  4. package/dist-client/client/components/default-catalog-entries.d.ts +1 -1
  5. package/dist-client/client/components/default-catalog-entries.js.map +1 -1
  6. package/dist-client/client/components/default-slash-templates.d.ts +1 -1
  7. package/dist-client/client/components/default-slash-templates.js.map +1 -1
  8. package/dist-client/client/index.d.ts +3 -11
  9. package/dist-client/client/index.js +37 -6
  10. package/dist-client/client/index.js.map +1 -1
  11. package/dist-client/server/service/assistant.d.ts +22 -22
  12. package/dist-client/server/service/assistant.js +32 -70
  13. package/dist-client/server/service/assistant.js.map +1 -1
  14. package/dist-client/server/service/grounding.d.ts +2 -12
  15. package/dist-client/server/service/grounding.js +15 -23
  16. package/dist-client/server/service/grounding.js.map +1 -1
  17. package/dist-client/server/service/types.d.ts +2 -4
  18. package/dist-client/server/service/types.js.map +1 -1
  19. package/dist-client/server/service/validation/board-model-schema.d.ts +270 -270
  20. package/dist-client/tsconfig.tsbuildinfo +1 -1
  21. package/dist-server/service/assistant.d.ts +22 -22
  22. package/dist-server/service/assistant.js +25 -76
  23. package/dist-server/service/assistant.js.map +1 -1
  24. package/dist-server/service/board-ai-resolver.js +41 -30
  25. package/dist-server/service/board-ai-resolver.js.map +1 -1
  26. package/dist-server/service/board-session-resolver.d.ts +12 -0
  27. package/dist-server/service/board-session-resolver.js +203 -0
  28. package/dist-server/service/board-session-resolver.js.map +1 -0
  29. package/dist-server/service/board-tool-rights.d.ts +7 -0
  30. package/dist-server/service/board-tool-rights.js +62 -0
  31. package/dist-server/service/board-tool-rights.js.map +1 -0
  32. package/dist-server/service/grounding.d.ts +2 -12
  33. package/dist-server/service/grounding.js +16 -24
  34. package/dist-server/service/grounding.js.map +1 -1
  35. package/dist-server/service/index.d.ts +4 -10
  36. package/dist-server/service/index.js +16 -23
  37. package/dist-server/service/index.js.map +1 -1
  38. package/dist-server/service/patch-entry/board-patch-subscription.js +2 -2
  39. package/dist-server/service/patch-entry/board-patch-subscription.js.map +1 -1
  40. package/dist-server/service/patch-entry/patch-entry.d.ts +1 -1
  41. package/dist-server/service/patch-entry/patch-entry.js +3 -3
  42. package/dist-server/service/patch-entry/patch-entry.js.map +1 -1
  43. package/dist-server/service/types.d.ts +2 -4
  44. package/dist-server/service/types.js.map +1 -1
  45. package/dist-server/tsconfig.tsbuildinfo +1 -1
  46. package/package.json +7 -6
  47. package/server/service/apply-patch-drift.test.ts +3 -1
  48. package/server/service/assistant.ts +47 -73
  49. package/server/service/board-ai-resolver.ts +22 -12
  50. package/server/service/board-session-resolver.ts +183 -0
  51. package/server/service/board-tool-rights.test.ts +96 -0
  52. package/server/service/board-tool-rights.ts +73 -0
  53. package/server/service/dock-contract.test.ts +3 -0
  54. package/server/service/grounding.test.ts +3 -3
  55. package/server/service/grounding.ts +15 -34
  56. package/server/service/index.ts +16 -22
  57. package/server/service/patch-entry/board-patch-subscription.ts +1 -1
  58. package/server/service/patch-entry/patch-entry.ts +3 -2
  59. package/server/service/registry-tool-loop.test.ts +1 -0
  60. package/server/service/tool-exposure.test.ts +2 -0
  61. package/server/service/types.ts +3 -4
  62. package/translations/en.json +1 -62
  63. package/translations/ja.json +1 -61
  64. package/translations/ko.json +1 -62
  65. package/translations/ms.json +1 -61
  66. package/translations/zh.json +1 -61
  67. package/client/components/board-ai-chat.test.ts +0 -149
  68. package/client/components/board-ai-chat.ts +0 -3184
  69. package/client/components/chat-echo-dedup.test.ts +0 -230
  70. package/client/components/chat-echo-dedup.ts +0 -155
  71. package/client/components/chat-input-builder.ts +0 -69
  72. package/client/components/markdown.test.ts +0 -518
  73. package/client/components/markdown.ts +0 -508
  74. package/client/components/mention-popup-helpers.ts +0 -433
  75. package/client/components/mention-popup.test.ts +0 -588
  76. package/client/components/mention-popup.ts +0 -238
  77. package/client/utils/board-edit-patch.ts +0 -374
  78. package/dist-client/client/components/board-ai-chat.d.ts +0 -384
  79. package/dist-client/client/components/board-ai-chat.js +0 -3008
  80. package/dist-client/client/components/board-ai-chat.js.map +0 -1
  81. package/dist-client/client/components/board-ai-chat.test.d.ts +0 -1
  82. package/dist-client/client/components/board-ai-chat.test.js +0 -138
  83. package/dist-client/client/components/board-ai-chat.test.js.map +0 -1
  84. package/dist-client/client/components/chat-echo-dedup.d.ts +0 -54
  85. package/dist-client/client/components/chat-echo-dedup.js +0 -115
  86. package/dist-client/client/components/chat-echo-dedup.js.map +0 -1
  87. package/dist-client/client/components/chat-echo-dedup.test.d.ts +0 -1
  88. package/dist-client/client/components/chat-echo-dedup.test.js +0 -203
  89. package/dist-client/client/components/chat-echo-dedup.test.js.map +0 -1
  90. package/dist-client/client/components/chat-input-builder.d.ts +0 -55
  91. package/dist-client/client/components/chat-input-builder.js +0 -32
  92. package/dist-client/client/components/chat-input-builder.js.map +0 -1
  93. package/dist-client/client/components/markdown.d.ts +0 -60
  94. package/dist-client/client/components/markdown.js +0 -422
  95. package/dist-client/client/components/markdown.js.map +0 -1
  96. package/dist-client/client/components/markdown.test.d.ts +0 -1
  97. package/dist-client/client/components/markdown.test.js +0 -418
  98. package/dist-client/client/components/markdown.test.js.map +0 -1
  99. package/dist-client/client/components/mention-popup-helpers.d.ts +0 -165
  100. package/dist-client/client/components/mention-popup-helpers.js +0 -330
  101. package/dist-client/client/components/mention-popup-helpers.js.map +0 -1
  102. package/dist-client/client/components/mention-popup.d.ts +0 -35
  103. package/dist-client/client/components/mention-popup.js +0 -223
  104. package/dist-client/client/components/mention-popup.js.map +0 -1
  105. package/dist-client/client/components/mention-popup.test.d.ts +0 -1
  106. package/dist-client/client/components/mention-popup.test.js +0 -474
  107. package/dist-client/client/components/mention-popup.test.js.map +0 -1
  108. package/dist-client/client/utils/board-edit-patch.d.ts +0 -157
  109. package/dist-client/client/utils/board-edit-patch.js +0 -284
  110. package/dist-client/client/utils/board-edit-patch.js.map +0 -1
  111. package/dist-server/service/chat-message/chat-message-broadcast.d.ts +0 -13
  112. package/dist-server/service/chat-message/chat-message-broadcast.js +0 -13
  113. package/dist-server/service/chat-message/chat-message-broadcast.js.map +0 -1
  114. package/dist-server/service/chat-message/chat-message-publish.d.ts +0 -6
  115. package/dist-server/service/chat-message/chat-message-publish.js +0 -27
  116. package/dist-server/service/chat-message/chat-message-publish.js.map +0 -1
  117. package/dist-server/service/chat-message/chat-message-subscription.d.ts +0 -8
  118. package/dist-server/service/chat-message/chat-message-subscription.js +0 -53
  119. package/dist-server/service/chat-message/chat-message-subscription.js.map +0 -1
  120. package/dist-server/service/chat-message/chat-message.d.ts +0 -68
  121. package/dist-server/service/chat-message/chat-message.js +0 -172
  122. package/dist-server/service/chat-message/chat-message.js.map +0 -1
  123. package/dist-server/service/chat-message/fold-history.d.ts +0 -43
  124. package/dist-server/service/chat-message/fold-history.js +0 -33
  125. package/dist-server/service/chat-message/fold-history.js.map +0 -1
  126. package/dist-server/service/chat-message/history-summary.d.ts +0 -43
  127. package/dist-server/service/chat-message/history-summary.js +0 -77
  128. package/dist-server/service/chat-message/history-summary.js.map +0 -1
  129. package/dist-server/service/chat-message/index.d.ts +0 -5
  130. package/dist-server/service/chat-message/index.js +0 -12
  131. package/dist-server/service/chat-message/index.js.map +0 -1
  132. package/dist-server/service/chat-message/llm-history.d.ts +0 -46
  133. package/dist-server/service/chat-message/llm-history.js +0 -87
  134. package/dist-server/service/chat-message/llm-history.js.map +0 -1
  135. package/dist-server/service/chat-session/chat-session.d.ts +0 -62
  136. package/dist-server/service/chat-session/chat-session.js +0 -164
  137. package/dist-server/service/chat-session/chat-session.js.map +0 -1
  138. package/dist-server/service/chat-session/index.d.ts +0 -3
  139. package/dist-server/service/chat-session/index.js +0 -7
  140. package/dist-server/service/chat-session/index.js.map +0 -1
  141. package/dist-server/service/chat-session/session-activity-broadcast.d.ts +0 -30
  142. package/dist-server/service/chat-session/session-activity-broadcast.js +0 -34
  143. package/dist-server/service/chat-session/session-activity-broadcast.js.map +0 -1
  144. package/dist-server/service/chat-session/session-activity-publish.d.ts +0 -12
  145. package/dist-server/service/chat-session/session-activity-publish.js +0 -37
  146. package/dist-server/service/chat-session/session-activity-publish.js.map +0 -1
  147. package/dist-server/service/chat-session/session-activity-subscription.d.ts +0 -14
  148. package/dist-server/service/chat-session/session-activity-subscription.js +0 -87
  149. package/dist-server/service/chat-session/session-activity-subscription.js.map +0 -1
  150. package/dist-server/service/chat-session/session-anchor.d.ts +0 -12
  151. package/dist-server/service/chat-session/session-anchor.js +0 -20
  152. package/dist-server/service/chat-session/session-anchor.js.map +0 -1
  153. package/dist-server/service/chat-session/session-inbox.d.ts +0 -80
  154. package/dist-server/service/chat-session/session-inbox.js +0 -133
  155. package/dist-server/service/chat-session/session-inbox.js.map +0 -1
  156. package/dist-server/service/chat-session-participant/chat-session-participant.d.ts +0 -49
  157. package/dist-server/service/chat-session-participant/chat-session-participant.js +0 -141
  158. package/dist-server/service/chat-session-participant/chat-session-participant.js.map +0 -1
  159. package/dist-server/service/chat-session-participant/index.d.ts +0 -4
  160. package/dist-server/service/chat-session-participant/index.js +0 -10
  161. package/dist-server/service/chat-session-participant/index.js.map +0 -1
  162. package/dist-server/service/chat-session-participant/invite-notification.d.ts +0 -27
  163. package/dist-server/service/chat-session-participant/invite-notification.js +0 -30
  164. package/dist-server/service/chat-session-participant/invite-notification.js.map +0 -1
  165. package/dist-server/service/chat-session-participant/presence.d.ts +0 -4
  166. package/dist-server/service/chat-session-participant/presence.js +0 -22
  167. package/dist-server/service/chat-session-participant/presence.js.map +0 -1
  168. package/dist-server/service/chat-session-resolver.d.ts +0 -102
  169. package/dist-server/service/chat-session-resolver.js +0 -856
  170. package/dist-server/service/chat-session-resolver.js.map +0 -1
  171. package/dist-server/service/subscribe-auth.d.ts +0 -14
  172. package/dist-server/service/subscribe-auth.js +0 -33
  173. package/dist-server/service/subscribe-auth.js.map +0 -1
  174. package/server/service/chat-message/chat-message-broadcast.integration.test.ts +0 -73
  175. package/server/service/chat-message/chat-message-broadcast.test.ts +0 -40
  176. package/server/service/chat-message/chat-message-broadcast.ts +0 -27
  177. package/server/service/chat-message/chat-message-publish.ts +0 -22
  178. package/server/service/chat-message/chat-message-subscription.ts +0 -41
  179. package/server/service/chat-message/chat-message.ts +0 -195
  180. package/server/service/chat-message/fold-history.test.ts +0 -98
  181. package/server/service/chat-message/fold-history.ts +0 -77
  182. package/server/service/chat-message/history-summary.test.ts +0 -127
  183. package/server/service/chat-message/history-summary.ts +0 -100
  184. package/server/service/chat-message/index.ts +0 -12
  185. package/server/service/chat-message/llm-history.test.ts +0 -194
  186. package/server/service/chat-message/llm-history.ts +0 -163
  187. package/server/service/chat-session/chat-session.ts +0 -182
  188. package/server/service/chat-session/index.ts +0 -5
  189. package/server/service/chat-session/session-activity-broadcast.ts +0 -57
  190. package/server/service/chat-session/session-activity-publish.ts +0 -44
  191. package/server/service/chat-session/session-activity-subscription.ts +0 -67
  192. package/server/service/chat-session/session-anchor.ts +0 -35
  193. package/server/service/chat-session/session-inbox.test.ts +0 -185
  194. package/server/service/chat-session/session-inbox.ts +0 -161
  195. package/server/service/chat-session-participant/chat-session-participant.ts +0 -135
  196. package/server/service/chat-session-participant/index.ts +0 -6
  197. package/server/service/chat-session-participant/invite-notification.test.ts +0 -30
  198. package/server/service/chat-session-participant/invite-notification.ts +0 -48
  199. package/server/service/chat-session-participant/presence.test.ts +0 -39
  200. package/server/service/chat-session-participant/presence.ts +0 -21
  201. package/server/service/chat-session-resolver.ts +0 -779
  202. package/server/service/session-anchor.test.ts +0 -65
  203. package/server/service/subscribe-auth.test.ts +0 -57
  204. package/server/service/subscribe-auth.ts +0 -34
@@ -1,418 +0,0 @@
1
- /**
2
- * renderMarkdown 단위 테스트.
3
- *
4
- * 핵심 회귀 방지 영역:
5
- * 1. 기본 markdown (bold, italic, list, code, link).
6
- * 2. 보안 — raw HTML, javascript: 링크 차단.
7
- * 3. CJK + 구두점 + ** 인접의 flanking 실패 케이스 (한국어 채팅 응답에서 빈번).
8
- * 4. 코드 펜스/인라인 코드 안의 ** 는 건드리지 않음.
9
- */
10
- import { renderMarkdown, sanitizeHtml, escapeHtml, extractCjkStrongs, extractMentions, stripMentionRefids, injectMentionRefids } from './markdown';
11
- describe('renderMarkdown — 기본 markdown', () => {
12
- test('빈 문자열은 빈 문자열 반환', () => {
13
- expect(renderMarkdown('')).toBe('');
14
- });
15
- test('일반 텍스트는 <p> 로 감쌈', () => {
16
- expect(renderMarkdown('hello')).toContain('<p>hello</p>');
17
- });
18
- test('** 로 굵게', () => {
19
- expect(renderMarkdown('**bold** text')).toContain('<strong>bold</strong>');
20
- });
21
- test('* 로 기울임', () => {
22
- expect(renderMarkdown('an *italic* word')).toContain('<em>italic</em>');
23
- });
24
- test('- 로 unordered list', () => {
25
- const out = renderMarkdown('- a\n- b\n- c');
26
- expect(out).toContain('<ul>');
27
- expect(out).toContain('<li>a</li>');
28
- expect(out).toContain('<li>b</li>');
29
- expect(out).toContain('<li>c</li>');
30
- });
31
- test('inline code', () => {
32
- expect(renderMarkdown('use `foo` here')).toContain('<code>foo</code>');
33
- });
34
- test('fenced code block', () => {
35
- const md = '```\nconst x = 1\n```';
36
- expect(renderMarkdown(md)).toContain('<pre>');
37
- expect(renderMarkdown(md)).toContain('<code>');
38
- });
39
- test('헤더', () => {
40
- expect(renderMarkdown('# Title')).toContain('<h1>Title</h1>');
41
- expect(renderMarkdown('## Sub')).toContain('<h2>Sub</h2>');
42
- });
43
- test('breaks: 단일 newline 은 <br>', () => {
44
- expect(renderMarkdown('line1\nline2')).toContain('<br>');
45
- });
46
- });
47
- describe('renderMarkdown — 보안', () => {
48
- test('raw <script> 차단', () => {
49
- const out = renderMarkdown("<script>alert('x')</script>");
50
- expect(out).not.toContain('<script>');
51
- expect(out).not.toContain('alert');
52
- });
53
- test('raw <iframe> 차단', () => {
54
- const out = renderMarkdown('<iframe src=x></iframe>');
55
- expect(out).not.toContain('<iframe');
56
- });
57
- test('inline HTML strong 차단 (renderer.html → 빈 문자열)', () => {
58
- const out = renderMarkdown('Hello <strong>world</strong>');
59
- // raw HTML 은 제거되고 텍스트만 남거나 비어야 함
60
- expect(out).not.toContain('<strong>world</strong>');
61
- });
62
- test('javascript: 링크 차단', () => {
63
- const out = renderMarkdown("[click](javascript:alert(1))");
64
- expect(out).not.toMatch(/href="javascript:/i);
65
- });
66
- test('data: 링크 차단', () => {
67
- const out = renderMarkdown('[x](data:text/html,<script>alert(1)</script>)');
68
- expect(out).not.toMatch(/href="data:/i);
69
- });
70
- test('정상 https 링크는 통과', () => {
71
- const out = renderMarkdown('[ok](https://example.com)');
72
- expect(out).toMatch(/href="https:\/\/example\.com"/);
73
- });
74
- });
75
- describe('sanitizeHtml — DOMPurify defense-in-depth', () => {
76
- // Node 환경 (jest) 에선 window 부재 → fallback 동작 검증.
77
- // 브라우저에서의 실제 정화는 통합 테스트 영역.
78
- test('window 부재 시 입력 그대로 통과 (fallback)', () => {
79
- // 직접 호출 — window undefined 환경에서 DOMPurify 안 도는 게 정상
80
- const html = '<p>safe</p><script>bad</script>';
81
- const out = sanitizeHtml(html);
82
- // 노드 환경에서 fallback 이라 그대로 통과 — 브라우저 환경에선 script 제거됨
83
- expect(typeof out).toBe('string');
84
- });
85
- test('window 있을 때 (jsdom mock) — script 차단 확인', () => {
86
- // jsdom 은 jest 의 testEnvironment 가 jsdom 일 때만 동작 — 본 setup 은 node.
87
- // 본 test 는 sanitizeHtml 의 *contract* 만 확인 (string in → string out).
88
- const out = sanitizeHtml('<p>hello</p>');
89
- expect(out).toContain('hello');
90
- });
91
- test('빈 문자열 안전 처리', () => {
92
- expect(sanitizeHtml('')).toBe('');
93
- });
94
- test('null/undefined fallback', () => {
95
- // 타입상 string 만 받지만 방어적으로 — 빈 문자열로 처리
96
- expect(sanitizeHtml('')).toBe('');
97
- });
98
- });
99
- describe('renderMarkdown — XSS 우회 시도 차단 (defense-in-depth)', () => {
100
- // 본 테스트들은 marked 의 1차 차단 + DOMPurify 의 2차 차단이 모두 작동하는지
101
- // 회귀 방지. node 환경에선 sanitizeHtml 이 fallback 이지만, marked 의 1차
102
- // renderer.html → '' 차단만으로도 대부분 케이스 차단 검증 가능.
103
- test('이미지 onerror 핸들러 시도 — img 태그 자체에 onerror attribute 없음', () => {
104
- const out = renderMarkdown('![alt](x" onerror="alert(1))');
105
- // 결과 텍스트에 escape 된 형태로 onerror= 라는 글자 자체는 남을 수 있지만,
106
- // *실행 가능한 attribute* 형태 (속성 이름으로) 는 없어야 한다.
107
- // 정밀 검사: <img 태그 안에 onerror=" 형태가 없는지
108
- expect(out).not.toMatch(/<img\b[^>]*\bonerror\s*=/i);
109
- // 또한 escape 안 된 따옴표 + onerror 조합 자체도 없어야
110
- expect(out).not.toMatch(/"\s+onerror=/i);
111
- });
112
- test('img src=javascript: 차단', () => {
113
- const out = renderMarkdown('![x](javascript:alert(1))');
114
- expect(out).not.toMatch(/src="javascript:/i);
115
- });
116
- test('SVG xss 패턴 차단', () => {
117
- const out = renderMarkdown('<svg onload=alert(1)>');
118
- expect(out).not.toContain('<svg');
119
- expect(out).not.toMatch(/onload/i);
120
- });
121
- test('style="javascript:..." 차단', () => {
122
- const out = renderMarkdown('[click](#){:style="javascript:alert(1)"}');
123
- expect(out).not.toMatch(/style="javascript:/i);
124
- });
125
- test('mention span 안의 사용자 token escape', () => {
126
- const out = renderMarkdown('#"><script>alert(1)</script>');
127
- expect(out).not.toMatch(/<script>/i);
128
- });
129
- test('event handler attribute 차단 (onmouseover 등)', () => {
130
- // raw HTML 이 marked.renderer.html 에서 빈 문자열로 → onmouseover 자체가 출력 안됨
131
- const out = renderMarkdown('<a href="#" onmouseover="alert(1)">x</a>');
132
- expect(out).not.toMatch(/onmouseover/i);
133
- });
134
- test('vbscript: 스킴 차단 (legacy IE 잔재)', () => {
135
- const out = renderMarkdown('[x](vbscript:msgbox(1))');
136
- expect(out).not.toMatch(/href="vbscript:/i);
137
- });
138
- });
139
- describe('renderMarkdown — CJK + 구두점 인접 (회귀 방지)', () => {
140
- // 이 그룹은 사용자 보고 사례. CommonMark flanking 규칙으로 인해 ** 가
141
- // 닫히지 않던 케이스들을 모두 회복해야 한다.
142
- const cases = [
143
- ["closing ** 이 punct + CJK 사이", "**'rect'**의", '<strong>&#039;rect&#039;</strong>의'],
144
- ["키 **'fillStyle'**의", "키 **'fillStyle'**의", '<strong>&#039;fillStyle&#039;</strong>의'],
145
- ["AGV 이름 + 조사", "**'AGV-1'**의 위치", '<strong>&#039;AGV-1&#039;</strong>의'],
146
- ["다중 인접 한글", "사용자가 **'AGV-1'**컴포넌트를", '<strong>&#039;AGV-1&#039;</strong>컴포넌트를'],
147
- ["bold 안 . 으로 끝", '**foo.**의', '<strong>foo.</strong>의'],
148
- ["bold 안 ) 으로 끝", '**foo)**의', '<strong>foo)</strong>의'],
149
- ["한 ** punct 시작 인접", "한**'foo'**", '한<strong>&#039;foo&#039;</strong>'],
150
- ["양쪽 모두 CJK + punct", "한**'foo'**글", '한<strong>&#039;foo&#039;</strong>글']
151
- ];
152
- for (const [name, input, expected] of cases) {
153
- test(name, () => {
154
- const out = renderMarkdown(input);
155
- expect(out).toContain(expected);
156
- });
157
- }
158
- test('정상 케이스는 그대로 marked 가 처리', () => {
159
- // 닫는 ** 가 공백 직전인 경우 — flanking 통과 (marked native; entity 표기는 &#39;)
160
- const a = renderMarkdown("키 **'fillStyle'** 의");
161
- expect(a).toMatch(/<strong>(&#0?39;|')fillStyle(&#0?39;|')<\/strong>/);
162
- const b = renderMarkdown("Type **'rect'** here");
163
- expect(b).toMatch(/<strong>(&#0?39;|')rect(&#0?39;|')<\/strong>/);
164
- });
165
- test('사용자 실제 케이스 — 산업용 다크 모드 대시보드', () => {
166
- const md = "**공장 설비 및 생산 현황을 한눈에 파악하기 위한 '산업용 다크 모드 대시보드'** 도 확인해보라.";
167
- const out = renderMarkdown(md);
168
- expect(out).toContain('<strong>');
169
- expect(out).toContain('대시보드');
170
- });
171
- test('punct 가 끝/시작에 없으면 변경 없음', () => {
172
- // marked 자체가 처리할 수 있는 케이스 — 우회 안 일어남
173
- const out = renderMarkdown('한**foo**글');
174
- expect(out).toContain('<strong>foo</strong>');
175
- });
176
- test('일본어 인접', () => {
177
- expect(renderMarkdown("これは**'値'**です")).toContain('<strong>&#039;値&#039;</strong>');
178
- });
179
- test('중국어 인접', () => {
180
- expect(renderMarkdown("这是**'值'**测试")).toContain('<strong>&#039;值&#039;</strong>');
181
- });
182
- test('인라인 코드 안의 ** 는 건드리지 않음', () => {
183
- // 코드 안의 ** 는 그대로 텍스트로 남아야 함
184
- const out = renderMarkdown('`**not bold**`');
185
- expect(out).toContain('<code>**not bold**</code>');
186
- });
187
- test('코드 펜스 안의 ** 는 건드리지 않음', () => {
188
- const md = '```\n**not bold**\n```';
189
- const out = renderMarkdown(md);
190
- expect(out).toContain('<code>');
191
- // 펜스 안의 ** 는 marked 가 텍스트로 그대로 둠
192
- expect(out).toMatch(/\*\*not bold\*\*/);
193
- });
194
- test('placeholder 토큰 (BAICJKSTRONG…) 이 출력에 노출되지 않음', () => {
195
- const out = renderMarkdown("키 **'fillStyle'**의");
196
- expect(out).not.toContain('BAICJKSTRONG');
197
- expect(out).not.toContain('ENDMARK');
198
- });
199
- test('치환된 strong 내용은 HTML escape 됨', () => {
200
- // CJK-fix 경로를 타는 케이스에 < > 가 섞여도 안전하게 escape 되어야 한다.
201
- // before='', after='의' (CJK), innerStart=', innerEnd=' (둘 다 punct) → fix 트리거.
202
- const out = renderMarkdown("**'<bad>'**의");
203
- expect(out).not.toContain('<bad>');
204
- expect(out).toContain('&lt;bad&gt;');
205
- expect(out).toContain('<strong>');
206
- });
207
- test('여러 strong 가 한 줄에 있어도 모두 처리', () => {
208
- const out = renderMarkdown("**'A'**과 **'B'**는 다르다");
209
- // 둘 다 strong 으로 렌더되어야 함
210
- const matches = out.match(/<strong>/g) ?? [];
211
- expect(matches.length).toBe(2);
212
- });
213
- });
214
- describe('extractCjkStrongs — 내부 동작', () => {
215
- test('필요 없는 케이스는 stored 비어 있음', () => {
216
- const r = extractCjkStrongs('regular **bold** text');
217
- expect(r.stored).toEqual([]);
218
- expect(r.processed).toBe('regular **bold** text');
219
- });
220
- test('필요한 케이스만 추출', () => {
221
- const r = extractCjkStrongs("키 **'fillStyle'**의");
222
- expect(r.stored).toEqual(["'fillStyle'"]);
223
- expect(r.processed).toContain('BAICJKSTRONG');
224
- });
225
- test('인라인 코드 안의 ** 는 추출 안 함', () => {
226
- const r = extractCjkStrongs("`**'foo'**의` 와 텍스트");
227
- expect(r.stored).toEqual([]);
228
- expect(r.processed).toContain("`**'foo'**의`");
229
- });
230
- test('코드 펜스 안의 ** 는 추출 안 함', () => {
231
- const r = extractCjkStrongs("```\n**'foo'**의\n```");
232
- expect(r.stored).toEqual([]);
233
- });
234
- test('짝이 안 맞는 ** 는 무시', () => {
235
- const r = extractCjkStrongs('한**foo and 글');
236
- expect(r.stored).toEqual([]);
237
- });
238
- });
239
- describe('extractMentions — `#token` placeholder 추출', () => {
240
- test('빈 문자열은 빈 결과', () => {
241
- const r = extractMentions('');
242
- expect(r.stored).toEqual([]);
243
- expect(r.processed).toBe('');
244
- });
245
- test('단일 mention — 영문', () => {
246
- const r = extractMentions('보드의 #motor1 위치');
247
- expect(r.stored).toEqual([{ trigger: '#', token: 'motor1' }]);
248
- expect(r.processed).toMatch(/보드의 BAIMENTION0ENDMENTION 위치/);
249
- });
250
- test('단일 mention — 한글', () => {
251
- const r = extractMentions('#경광등1 빨갛게');
252
- expect(r.stored).toEqual([{ trigger: '#', token: '경광등1' }]);
253
- });
254
- test('다중 mention — 순서대로 인덱스', () => {
255
- const r = extractMentions('#a 와 #b 비교');
256
- expect(r.stored).toEqual([
257
- { trigger: '#', token: 'a' },
258
- { trigger: '#', token: 'b' }
259
- ]);
260
- expect(r.processed).toMatch(/BAIMENTION0ENDMENTION 와 BAIMENTION1ENDMENTION/);
261
- });
262
- test('단어 중간 # 는 mention 아님', () => {
263
- const r = extractMentions('foo#bar 는 무시');
264
- expect(r.stored).toEqual([]);
265
- });
266
- test('# 직후 공백 (markdown ATX 헤딩) 은 매칭 안 함', () => {
267
- expect(extractMentions('# Title').stored).toEqual([]);
268
- expect(extractMentions('## Sub').stored).toEqual([]);
269
- expect(extractMentions('### 한글헤딩').stored).toEqual([]);
270
- });
271
- test('인라인 코드 안의 # 는 건드리지 않음', () => {
272
- const r = extractMentions('use `#define` here');
273
- expect(r.stored).toEqual([]);
274
- expect(r.processed).toBe('use `#define` here');
275
- });
276
- test('코드 펜스 안의 # 는 건드리지 않음', () => {
277
- const r = extractMentions('```\n#abc in code\n```\n그리고 #def 는 mention');
278
- expect(r.stored).toEqual([{ trigger: '#', token: 'def' }]);
279
- });
280
- test('구두점 직후 # — 매칭', () => {
281
- const r = extractMentions('(#abc) [#def]');
282
- expect(r.stored).toEqual([
283
- { trigger: '#', token: 'abc' },
284
- { trigger: '#', token: 'def' }
285
- ]);
286
- });
287
- test('@user 멘션도 추출 — trigger 보존', () => {
288
- const r = extractMentions('@경철 도와주세요');
289
- expect(r.stored).toEqual([{ trigger: '@', token: '경철' }]);
290
- });
291
- test('# 와 @ 혼합 — 순서대로', () => {
292
- const r = extractMentions('@경철 #motor1 같이 봐');
293
- expect(r.stored).toEqual([
294
- { trigger: '@', token: '경철' },
295
- { trigger: '#', token: 'motor1' }
296
- ]);
297
- });
298
- test('이메일 (user@host) — mention 아님', () => {
299
- const r = extractMentions('a@b.com 은 이메일');
300
- expect(r.stored).toEqual([]);
301
- });
302
- test('renderMarkdown 통합 — span 으로 복원', () => {
303
- const html = renderMarkdown('보드의 #motor1 위치');
304
- expect(html).toContain('<span class="mention">#motor1</span>');
305
- });
306
- test('renderMarkdown 통합 — 한글 mention', () => {
307
- const html = renderMarkdown('#경광등1 빨갛게');
308
- expect(html).toContain('<span class="mention">#경광등1</span>');
309
- });
310
- test('renderMarkdown 통합 — heading 과 mention 공존', () => {
311
- const html = renderMarkdown('# Title\n그리고 #foo 참조');
312
- expect(html).toContain('<h1>Title</h1>');
313
- expect(html).toContain('<span class="mention">#foo</span>');
314
- });
315
- test('renderMarkdown 통합 — 코드 안 # 는 평범한 code', () => {
316
- const html = renderMarkdown('use `#define`');
317
- expect(html).toContain('<code>#define</code>');
318
- expect(html).not.toContain('<span class="mention">');
319
- });
320
- test('renderMarkdown 통합 — XSS 안전 (token 의 < > escape)', () => {
321
- const html = renderMarkdown('#<script>');
322
- expect(html).not.toContain('<script>');
323
- // < > 이전까지만 토큰 — 매칭 안 될 수도 있고 매칭되더라도 escape
324
- if (html.includes('class="mention"')) {
325
- expect(html).toContain('&lt;');
326
- }
327
- });
328
- });
329
- describe('renderMarkdown — `#token{refid:N}` inline marker → data-refid', () => {
330
- test('marker 없으면 span 에 data-refid 없음', () => {
331
- const html = renderMarkdown('보드의 #motor1 위치');
332
- expect(html).toContain('<span class="mention">#motor1</span>');
333
- expect(html).not.toContain('data-refid');
334
- });
335
- test('marker 있으면 data-refid embed 되고 marker 자체는 출력 안 됨', () => {
336
- const html = renderMarkdown('#motor1{refid:35} 빨갛게');
337
- expect(html).toContain('<span class="mention" data-refid="35">#motor1</span>');
338
- expect(html).not.toContain('{refid:');
339
- });
340
- test('다중 mention — 각 token 별 refid 분리', () => {
341
- const html = renderMarkdown('#a{refid:10} 와 #b{refid:20}');
342
- expect(html).toContain('data-refid="10">#a</span>');
343
- expect(html).toContain('data-refid="20">#b</span>');
344
- });
345
- test('marker 일부만 있을 때 — marker 없는 mention 은 data-refid 없음', () => {
346
- const html = renderMarkdown('#a{refid:10} 와 #unknown');
347
- expect(html).toContain('data-refid="10">#a</span>');
348
- expect(html).toContain('<span class="mention">#unknown</span>');
349
- });
350
- test('한글 mention — data-refid 정상 embed', () => {
351
- const html = renderMarkdown('#경광등1{refid:35} 빨갛게');
352
- expect(html).toContain('data-refid="35">#경광등1</span>');
353
- });
354
- test('marker 안 본문 — escapeHtml 안전 (XSS)', () => {
355
- const html = renderMarkdown('#motor1{refid:35} <script>');
356
- expect(html).not.toContain('<script>');
357
- expect(html).toContain('data-refid="35"');
358
- });
359
- });
360
- describe('stripMentionRefids — marker 제거', () => {
361
- test('단일 marker 제거', () => {
362
- expect(stripMentionRefids('#motor1{refid:35} 빨갛게')).toBe('#motor1 빨갛게');
363
- });
364
- test('다중 marker 제거', () => {
365
- expect(stripMentionRefids('#a{refid:1} 와 #b{refid:2}')).toBe('#a 와 #b');
366
- });
367
- test('marker 없으면 원본 그대로', () => {
368
- expect(stripMentionRefids('#motor1 빨갛게')).toBe('#motor1 빨갛게');
369
- expect(stripMentionRefids('아무 마커 없는 텍스트')).toBe('아무 마커 없는 텍스트');
370
- });
371
- test('빈/널 안전', () => {
372
- expect(stripMentionRefids('')).toBe('');
373
- expect(stripMentionRefids(null)).toBe('');
374
- expect(stripMentionRefids(undefined)).toBe('');
375
- });
376
- });
377
- describe('injectMentionRefids — popup picks 를 본문에 marker 주입', () => {
378
- test('단일 token 주입', () => {
379
- expect(injectMentionRefids('#motor1 빨갛게', [{ token: 'motor1', refid: 35 }])).toBe('#motor1{refid:35} 빨갛게');
380
- });
381
- test('다중 token — 각각 주입', () => {
382
- expect(injectMentionRefids('#a 와 #b', [
383
- { token: 'a', refid: 10 },
384
- { token: 'b', refid: 20 }
385
- ])).toBe('#a{refid:10} 와 #b{refid:20}');
386
- });
387
- test('matches 가 없는 token 은 건드리지 않음', () => {
388
- expect(injectMentionRefids('#motor1 와 #other', [{ token: 'motor1', refid: 35 }])).toBe('#motor1{refid:35} 와 #other');
389
- });
390
- test('이미 marker 있으면 덮어쓰기 안 함 (idempotent)', () => {
391
- expect(injectMentionRefids('#motor1{refid:99} 빨갛게', [{ token: 'motor1', refid: 35 }])).toBe('#motor1{refid:99} 빨갛게');
392
- });
393
- test('단어 중간 # 는 매칭 안 함', () => {
394
- expect(injectMentionRefids('foo#motor1 끝', [{ token: 'motor1', refid: 35 }])).toBe('foo#motor1 끝');
395
- });
396
- test('빈 picks / 빈 텍스트 안전', () => {
397
- expect(injectMentionRefids('#a', [])).toBe('#a');
398
- expect(injectMentionRefids('', [{ token: 'a', refid: 1 }])).toBe('');
399
- expect(injectMentionRefids('#a', null)).toBe('#a');
400
- expect(injectMentionRefids('#a', undefined)).toBe('#a');
401
- });
402
- test('한글 token 주입', () => {
403
- expect(injectMentionRefids('#경광등1 빨갛게', [{ token: '경광등1', refid: 42 }])).toBe('#경광등1{refid:42} 빨갛게');
404
- });
405
- });
406
- describe('escapeHtml — HTML 이스케이프', () => {
407
- test('< > & " \' 모두 escape', () => {
408
- expect(escapeHtml('<script>')).toBe('&lt;script&gt;');
409
- expect(escapeHtml('a & b')).toBe('a &amp; b');
410
- expect(escapeHtml('"q"')).toBe('&quot;q&quot;');
411
- expect(escapeHtml("'a'")).toBe('&#039;a&#039;');
412
- });
413
- test('일반 문자는 변경 없음', () => {
414
- expect(escapeHtml('hello world')).toBe('hello world');
415
- expect(escapeHtml('한글 テスト 中文')).toBe('한글 テスト 中文');
416
- });
417
- });
418
- //# sourceMappingURL=markdown.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"markdown.test.js","sourceRoot":"","sources":["../../../client/components/markdown.test.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * renderMarkdown 단위 테스트.\n *\n * 핵심 회귀 방지 영역:\n * 1. 기본 markdown (bold, italic, list, code, link).\n * 2. 보안 — raw HTML, javascript: 링크 차단.\n * 3. CJK + 구두점 + ** 인접의 flanking 실패 케이스 (한국어 채팅 응답에서 빈번).\n * 4. 코드 펜스/인라인 코드 안의 ** 는 건드리지 않음.\n */\nimport {\n renderMarkdown,\n sanitizeHtml,\n escapeHtml,\n extractCjkStrongs,\n extractMentions,\n stripMentionRefids,\n injectMentionRefids\n} from './markdown'\n\ndescribe('renderMarkdown — 기본 markdown', () => {\n test('빈 문자열은 빈 문자열 반환', () => {\n expect(renderMarkdown('')).toBe('')\n })\n\n test('일반 텍스트는 <p> 로 감쌈', () => {\n expect(renderMarkdown('hello')).toContain('<p>hello</p>')\n })\n\n test('** 로 굵게', () => {\n expect(renderMarkdown('**bold** text')).toContain('<strong>bold</strong>')\n })\n\n test('* 로 기울임', () => {\n expect(renderMarkdown('an *italic* word')).toContain('<em>italic</em>')\n })\n\n test('- 로 unordered list', () => {\n const out = renderMarkdown('- a\\n- b\\n- c')\n expect(out).toContain('<ul>')\n expect(out).toContain('<li>a</li>')\n expect(out).toContain('<li>b</li>')\n expect(out).toContain('<li>c</li>')\n })\n\n test('inline code', () => {\n expect(renderMarkdown('use `foo` here')).toContain('<code>foo</code>')\n })\n\n test('fenced code block', () => {\n const md = '```\\nconst x = 1\\n```'\n expect(renderMarkdown(md)).toContain('<pre>')\n expect(renderMarkdown(md)).toContain('<code>')\n })\n\n test('헤더', () => {\n expect(renderMarkdown('# Title')).toContain('<h1>Title</h1>')\n expect(renderMarkdown('## Sub')).toContain('<h2>Sub</h2>')\n })\n\n test('breaks: 단일 newline 은 <br>', () => {\n expect(renderMarkdown('line1\\nline2')).toContain('<br>')\n })\n})\n\ndescribe('renderMarkdown — 보안', () => {\n test('raw <script> 차단', () => {\n const out = renderMarkdown(\"<script>alert('x')</script>\")\n expect(out).not.toContain('<script>')\n expect(out).not.toContain('alert')\n })\n\n test('raw <iframe> 차단', () => {\n const out = renderMarkdown('<iframe src=x></iframe>')\n expect(out).not.toContain('<iframe')\n })\n\n test('inline HTML strong 차단 (renderer.html → 빈 문자열)', () => {\n const out = renderMarkdown('Hello <strong>world</strong>')\n // raw HTML 은 제거되고 텍스트만 남거나 비어야 함\n expect(out).not.toContain('<strong>world</strong>')\n })\n\n test('javascript: 링크 차단', () => {\n const out = renderMarkdown(\"[click](javascript:alert(1))\")\n expect(out).not.toMatch(/href=\"javascript:/i)\n })\n\n test('data: 링크 차단', () => {\n const out = renderMarkdown('[x](data:text/html,<script>alert(1)</script>)')\n expect(out).not.toMatch(/href=\"data:/i)\n })\n\n test('정상 https 링크는 통과', () => {\n const out = renderMarkdown('[ok](https://example.com)')\n expect(out).toMatch(/href=\"https:\\/\\/example\\.com\"/)\n })\n})\n\ndescribe('sanitizeHtml — DOMPurify defense-in-depth', () => {\n // Node 환경 (jest) 에선 window 부재 → fallback 동작 검증.\n // 브라우저에서의 실제 정화는 통합 테스트 영역.\n\n test('window 부재 시 입력 그대로 통과 (fallback)', () => {\n // 직접 호출 — window undefined 환경에서 DOMPurify 안 도는 게 정상\n const html = '<p>safe</p><script>bad</script>'\n const out = sanitizeHtml(html)\n // 노드 환경에서 fallback 이라 그대로 통과 — 브라우저 환경에선 script 제거됨\n expect(typeof out).toBe('string')\n })\n\n test('window 있을 때 (jsdom mock) — script 차단 확인', () => {\n // jsdom 은 jest 의 testEnvironment 가 jsdom 일 때만 동작 — 본 setup 은 node.\n // 본 test 는 sanitizeHtml 의 *contract* 만 확인 (string in → string out).\n const out = sanitizeHtml('<p>hello</p>')\n expect(out).toContain('hello')\n })\n\n test('빈 문자열 안전 처리', () => {\n expect(sanitizeHtml('')).toBe('')\n })\n\n test('null/undefined fallback', () => {\n // 타입상 string 만 받지만 방어적으로 — 빈 문자열로 처리\n expect(sanitizeHtml('' as any)).toBe('')\n })\n})\n\ndescribe('renderMarkdown — XSS 우회 시도 차단 (defense-in-depth)', () => {\n // 본 테스트들은 marked 의 1차 차단 + DOMPurify 의 2차 차단이 모두 작동하는지\n // 회귀 방지. node 환경에선 sanitizeHtml 이 fallback 이지만, marked 의 1차\n // renderer.html → '' 차단만으로도 대부분 케이스 차단 검증 가능.\n\n test('이미지 onerror 핸들러 시도 — img 태그 자체에 onerror attribute 없음', () => {\n const out = renderMarkdown('![alt](x\" onerror=\"alert(1))')\n // 결과 텍스트에 escape 된 형태로 onerror= 라는 글자 자체는 남을 수 있지만,\n // *실행 가능한 attribute* 형태 (속성 이름으로) 는 없어야 한다.\n // 정밀 검사: <img 태그 안에 onerror=\" 형태가 없는지\n expect(out).not.toMatch(/<img\\b[^>]*\\bonerror\\s*=/i)\n // 또한 escape 안 된 따옴표 + onerror 조합 자체도 없어야\n expect(out).not.toMatch(/\"\\s+onerror=/i)\n })\n\n test('img src=javascript: 차단', () => {\n const out = renderMarkdown('![x](javascript:alert(1))')\n expect(out).not.toMatch(/src=\"javascript:/i)\n })\n\n test('SVG xss 패턴 차단', () => {\n const out = renderMarkdown('<svg onload=alert(1)>')\n expect(out).not.toContain('<svg')\n expect(out).not.toMatch(/onload/i)\n })\n\n test('style=\"javascript:...\" 차단', () => {\n const out = renderMarkdown('[click](#){:style=\"javascript:alert(1)\"}')\n expect(out).not.toMatch(/style=\"javascript:/i)\n })\n\n test('mention span 안의 사용자 token escape', () => {\n const out = renderMarkdown('#\"><script>alert(1)</script>')\n expect(out).not.toMatch(/<script>/i)\n })\n\n test('event handler attribute 차단 (onmouseover 등)', () => {\n // raw HTML 이 marked.renderer.html 에서 빈 문자열로 → onmouseover 자체가 출력 안됨\n const out = renderMarkdown('<a href=\"#\" onmouseover=\"alert(1)\">x</a>')\n expect(out).not.toMatch(/onmouseover/i)\n })\n\n test('vbscript: 스킴 차단 (legacy IE 잔재)', () => {\n const out = renderMarkdown('[x](vbscript:msgbox(1))')\n expect(out).not.toMatch(/href=\"vbscript:/i)\n })\n})\n\ndescribe('renderMarkdown — CJK + 구두점 인접 (회귀 방지)', () => {\n // 이 그룹은 사용자 보고 사례. CommonMark flanking 규칙으로 인해 ** 가\n // 닫히지 않던 케이스들을 모두 회복해야 한다.\n const cases: Array<[string, string, string]> = [\n [\"closing ** 이 punct + CJK 사이\", \"**'rect'**의\", '<strong>&#039;rect&#039;</strong>의'],\n [\"키 **'fillStyle'**의\", \"키 **'fillStyle'**의\", '<strong>&#039;fillStyle&#039;</strong>의'],\n [\"AGV 이름 + 조사\", \"**'AGV-1'**의 위치\", '<strong>&#039;AGV-1&#039;</strong>의'],\n [\"다중 인접 한글\", \"사용자가 **'AGV-1'**컴포넌트를\", '<strong>&#039;AGV-1&#039;</strong>컴포넌트를'],\n [\"bold 안 . 으로 끝\", '**foo.**의', '<strong>foo.</strong>의'],\n [\"bold 안 ) 으로 끝\", '**foo)**의', '<strong>foo)</strong>의'],\n [\"한 ** punct 시작 인접\", \"한**'foo'**\", '한<strong>&#039;foo&#039;</strong>'],\n [\"양쪽 모두 CJK + punct\", \"한**'foo'**글\", '한<strong>&#039;foo&#039;</strong>글']\n ]\n\n for (const [name, input, expected] of cases) {\n test(name, () => {\n const out = renderMarkdown(input)\n expect(out).toContain(expected)\n })\n }\n\n test('정상 케이스는 그대로 marked 가 처리', () => {\n // 닫는 ** 가 공백 직전인 경우 — flanking 통과 (marked native; entity 표기는 &#39;)\n const a = renderMarkdown(\"키 **'fillStyle'** 의\")\n expect(a).toMatch(/<strong>(&#0?39;|')fillStyle(&#0?39;|')<\\/strong>/)\n const b = renderMarkdown(\"Type **'rect'** here\")\n expect(b).toMatch(/<strong>(&#0?39;|')rect(&#0?39;|')<\\/strong>/)\n })\n\n test('사용자 실제 케이스 — 산업용 다크 모드 대시보드', () => {\n const md = \"**공장 설비 및 생산 현황을 한눈에 파악하기 위한 '산업용 다크 모드 대시보드'** 도 확인해보라.\"\n const out = renderMarkdown(md)\n expect(out).toContain('<strong>')\n expect(out).toContain('대시보드')\n })\n\n test('punct 가 끝/시작에 없으면 변경 없음', () => {\n // marked 자체가 처리할 수 있는 케이스 — 우회 안 일어남\n const out = renderMarkdown('한**foo**글')\n expect(out).toContain('<strong>foo</strong>')\n })\n\n test('일본어 인접', () => {\n expect(renderMarkdown(\"これは**'値'**です\")).toContain('<strong>&#039;値&#039;</strong>')\n })\n\n test('중국어 인접', () => {\n expect(renderMarkdown(\"这是**'值'**测试\")).toContain('<strong>&#039;值&#039;</strong>')\n })\n\n test('인라인 코드 안의 ** 는 건드리지 않음', () => {\n // 코드 안의 ** 는 그대로 텍스트로 남아야 함\n const out = renderMarkdown('`**not bold**`')\n expect(out).toContain('<code>**not bold**</code>')\n })\n\n test('코드 펜스 안의 ** 는 건드리지 않음', () => {\n const md = '```\\n**not bold**\\n```'\n const out = renderMarkdown(md)\n expect(out).toContain('<code>')\n // 펜스 안의 ** 는 marked 가 텍스트로 그대로 둠\n expect(out).toMatch(/\\*\\*not bold\\*\\*/)\n })\n\n test('placeholder 토큰 (BAICJKSTRONG…) 이 출력에 노출되지 않음', () => {\n const out = renderMarkdown(\"키 **'fillStyle'**의\")\n expect(out).not.toContain('BAICJKSTRONG')\n expect(out).not.toContain('ENDMARK')\n })\n\n test('치환된 strong 내용은 HTML escape 됨', () => {\n // CJK-fix 경로를 타는 케이스에 < > 가 섞여도 안전하게 escape 되어야 한다.\n // before='', after='의' (CJK), innerStart=', innerEnd=' (둘 다 punct) → fix 트리거.\n const out = renderMarkdown(\"**'<bad>'**의\")\n expect(out).not.toContain('<bad>')\n expect(out).toContain('&lt;bad&gt;')\n expect(out).toContain('<strong>')\n })\n\n test('여러 strong 가 한 줄에 있어도 모두 처리', () => {\n const out = renderMarkdown(\"**'A'**과 **'B'**는 다르다\")\n // 둘 다 strong 으로 렌더되어야 함\n const matches = out.match(/<strong>/g) ?? []\n expect(matches.length).toBe(2)\n })\n})\n\ndescribe('extractCjkStrongs — 내부 동작', () => {\n test('필요 없는 케이스는 stored 비어 있음', () => {\n const r = extractCjkStrongs('regular **bold** text')\n expect(r.stored).toEqual([])\n expect(r.processed).toBe('regular **bold** text')\n })\n\n test('필요한 케이스만 추출', () => {\n const r = extractCjkStrongs(\"키 **'fillStyle'**의\")\n expect(r.stored).toEqual([\"'fillStyle'\"])\n expect(r.processed).toContain('BAICJKSTRONG')\n })\n\n test('인라인 코드 안의 ** 는 추출 안 함', () => {\n const r = extractCjkStrongs(\"`**'foo'**의` 와 텍스트\")\n expect(r.stored).toEqual([])\n expect(r.processed).toContain(\"`**'foo'**의`\")\n })\n\n test('코드 펜스 안의 ** 는 추출 안 함', () => {\n const r = extractCjkStrongs(\"```\\n**'foo'**의\\n```\")\n expect(r.stored).toEqual([])\n })\n\n test('짝이 안 맞는 ** 는 무시', () => {\n const r = extractCjkStrongs('한**foo and 글')\n expect(r.stored).toEqual([])\n })\n})\n\ndescribe('extractMentions — `#token` placeholder 추출', () => {\n test('빈 문자열은 빈 결과', () => {\n const r = extractMentions('')\n expect(r.stored).toEqual([])\n expect(r.processed).toBe('')\n })\n\n test('단일 mention — 영문', () => {\n const r = extractMentions('보드의 #motor1 위치')\n expect(r.stored).toEqual([{ trigger: '#', token: 'motor1' }])\n expect(r.processed).toMatch(/보드의 BAIMENTION0ENDMENTION 위치/)\n })\n\n test('단일 mention — 한글', () => {\n const r = extractMentions('#경광등1 빨갛게')\n expect(r.stored).toEqual([{ trigger: '#', token: '경광등1' }])\n })\n\n test('다중 mention — 순서대로 인덱스', () => {\n const r = extractMentions('#a 와 #b 비교')\n expect(r.stored).toEqual([\n { trigger: '#', token: 'a' },\n { trigger: '#', token: 'b' }\n ])\n expect(r.processed).toMatch(/BAIMENTION0ENDMENTION 와 BAIMENTION1ENDMENTION/)\n })\n\n test('단어 중간 # 는 mention 아님', () => {\n const r = extractMentions('foo#bar 는 무시')\n expect(r.stored).toEqual([])\n })\n\n test('# 직후 공백 (markdown ATX 헤딩) 은 매칭 안 함', () => {\n expect(extractMentions('# Title').stored).toEqual([])\n expect(extractMentions('## Sub').stored).toEqual([])\n expect(extractMentions('### 한글헤딩').stored).toEqual([])\n })\n\n test('인라인 코드 안의 # 는 건드리지 않음', () => {\n const r = extractMentions('use `#define` here')\n expect(r.stored).toEqual([])\n expect(r.processed).toBe('use `#define` here')\n })\n\n test('코드 펜스 안의 # 는 건드리지 않음', () => {\n const r = extractMentions('```\\n#abc in code\\n```\\n그리고 #def 는 mention')\n expect(r.stored).toEqual([{ trigger: '#', token: 'def' }])\n })\n\n test('구두점 직후 # — 매칭', () => {\n const r = extractMentions('(#abc) [#def]')\n expect(r.stored).toEqual([\n { trigger: '#', token: 'abc' },\n { trigger: '#', token: 'def' }\n ])\n })\n\n test('@user 멘션도 추출 — trigger 보존', () => {\n const r = extractMentions('@경철 도와주세요')\n expect(r.stored).toEqual([{ trigger: '@', token: '경철' }])\n })\n\n test('# 와 @ 혼합 — 순서대로', () => {\n const r = extractMentions('@경철 #motor1 같이 봐')\n expect(r.stored).toEqual([\n { trigger: '@', token: '경철' },\n { trigger: '#', token: 'motor1' }\n ])\n })\n\n test('이메일 (user@host) — mention 아님', () => {\n const r = extractMentions('a@b.com 은 이메일')\n expect(r.stored).toEqual([])\n })\n\n test('renderMarkdown 통합 — span 으로 복원', () => {\n const html = renderMarkdown('보드의 #motor1 위치')\n expect(html).toContain('<span class=\"mention\">#motor1</span>')\n })\n\n test('renderMarkdown 통합 — 한글 mention', () => {\n const html = renderMarkdown('#경광등1 빨갛게')\n expect(html).toContain('<span class=\"mention\">#경광등1</span>')\n })\n\n test('renderMarkdown 통합 — heading 과 mention 공존', () => {\n const html = renderMarkdown('# Title\\n그리고 #foo 참조')\n expect(html).toContain('<h1>Title</h1>')\n expect(html).toContain('<span class=\"mention\">#foo</span>')\n })\n\n test('renderMarkdown 통합 — 코드 안 # 는 평범한 code', () => {\n const html = renderMarkdown('use `#define`')\n expect(html).toContain('<code>#define</code>')\n expect(html).not.toContain('<span class=\"mention\">')\n })\n\n test('renderMarkdown 통합 — XSS 안전 (token 의 < > escape)', () => {\n const html = renderMarkdown('#<script>')\n expect(html).not.toContain('<script>')\n // < > 이전까지만 토큰 — 매칭 안 될 수도 있고 매칭되더라도 escape\n if (html.includes('class=\"mention\"')) {\n expect(html).toContain('&lt;')\n }\n })\n})\n\ndescribe('renderMarkdown — `#token{refid:N}` inline marker → data-refid', () => {\n test('marker 없으면 span 에 data-refid 없음', () => {\n const html = renderMarkdown('보드의 #motor1 위치')\n expect(html).toContain('<span class=\"mention\">#motor1</span>')\n expect(html).not.toContain('data-refid')\n })\n\n test('marker 있으면 data-refid embed 되고 marker 자체는 출력 안 됨', () => {\n const html = renderMarkdown('#motor1{refid:35} 빨갛게')\n expect(html).toContain('<span class=\"mention\" data-refid=\"35\">#motor1</span>')\n expect(html).not.toContain('{refid:')\n })\n\n test('다중 mention — 각 token 별 refid 분리', () => {\n const html = renderMarkdown('#a{refid:10} 와 #b{refid:20}')\n expect(html).toContain('data-refid=\"10\">#a</span>')\n expect(html).toContain('data-refid=\"20\">#b</span>')\n })\n\n test('marker 일부만 있을 때 — marker 없는 mention 은 data-refid 없음', () => {\n const html = renderMarkdown('#a{refid:10} 와 #unknown')\n expect(html).toContain('data-refid=\"10\">#a</span>')\n expect(html).toContain('<span class=\"mention\">#unknown</span>')\n })\n\n test('한글 mention — data-refid 정상 embed', () => {\n const html = renderMarkdown('#경광등1{refid:35} 빨갛게')\n expect(html).toContain('data-refid=\"35\">#경광등1</span>')\n })\n\n test('marker 안 본문 — escapeHtml 안전 (XSS)', () => {\n const html = renderMarkdown('#motor1{refid:35} <script>')\n expect(html).not.toContain('<script>')\n expect(html).toContain('data-refid=\"35\"')\n })\n})\n\ndescribe('stripMentionRefids — marker 제거', () => {\n test('단일 marker 제거', () => {\n expect(stripMentionRefids('#motor1{refid:35} 빨갛게')).toBe('#motor1 빨갛게')\n })\n\n test('다중 marker 제거', () => {\n expect(stripMentionRefids('#a{refid:1} 와 #b{refid:2}')).toBe('#a 와 #b')\n })\n\n test('marker 없으면 원본 그대로', () => {\n expect(stripMentionRefids('#motor1 빨갛게')).toBe('#motor1 빨갛게')\n expect(stripMentionRefids('아무 마커 없는 텍스트')).toBe('아무 마커 없는 텍스트')\n })\n\n test('빈/널 안전', () => {\n expect(stripMentionRefids('')).toBe('')\n expect(stripMentionRefids(null)).toBe('')\n expect(stripMentionRefids(undefined)).toBe('')\n })\n})\n\ndescribe('injectMentionRefids — popup picks 를 본문에 marker 주입', () => {\n test('단일 token 주입', () => {\n expect(\n injectMentionRefids('#motor1 빨갛게', [{ token: 'motor1', refid: 35 }])\n ).toBe('#motor1{refid:35} 빨갛게')\n })\n\n test('다중 token — 각각 주입', () => {\n expect(\n injectMentionRefids('#a 와 #b', [\n { token: 'a', refid: 10 },\n { token: 'b', refid: 20 }\n ])\n ).toBe('#a{refid:10} 와 #b{refid:20}')\n })\n\n test('matches 가 없는 token 은 건드리지 않음', () => {\n expect(\n injectMentionRefids('#motor1 와 #other', [{ token: 'motor1', refid: 35 }])\n ).toBe('#motor1{refid:35} 와 #other')\n })\n\n test('이미 marker 있으면 덮어쓰기 안 함 (idempotent)', () => {\n expect(\n injectMentionRefids('#motor1{refid:99} 빨갛게', [{ token: 'motor1', refid: 35 }])\n ).toBe('#motor1{refid:99} 빨갛게')\n })\n\n test('단어 중간 # 는 매칭 안 함', () => {\n expect(\n injectMentionRefids('foo#motor1 끝', [{ token: 'motor1', refid: 35 }])\n ).toBe('foo#motor1 끝')\n })\n\n test('빈 picks / 빈 텍스트 안전', () => {\n expect(injectMentionRefids('#a', [])).toBe('#a')\n expect(injectMentionRefids('', [{ token: 'a', refid: 1 }])).toBe('')\n expect(injectMentionRefids('#a', null)).toBe('#a')\n expect(injectMentionRefids('#a', undefined)).toBe('#a')\n })\n\n test('한글 token 주입', () => {\n expect(\n injectMentionRefids('#경광등1 빨갛게', [{ token: '경광등1', refid: 42 }])\n ).toBe('#경광등1{refid:42} 빨갛게')\n })\n})\n\ndescribe('escapeHtml — HTML 이스케이프', () => {\n test('< > & \" \\' 모두 escape', () => {\n expect(escapeHtml('<script>')).toBe('&lt;script&gt;')\n expect(escapeHtml('a & b')).toBe('a &amp; b')\n expect(escapeHtml('\"q\"')).toBe('&quot;q&quot;')\n expect(escapeHtml(\"'a'\")).toBe('&#039;a&#039;')\n })\n\n test('일반 문자는 변경 없음', () => {\n expect(escapeHtml('hello world')).toBe('hello world')\n expect(escapeHtml('한글 テスト 中文')).toBe('한글 テスト 中文')\n })\n})\n"]}