@@ -9,7 +9,7 @@ use crate::glossary::GlossaryCandidate;
99use crate :: note_filter:: { FilterStats , NoteFilterResult } ;
1010use crate :: outline:: OutlineEntry ;
1111use crate :: purify_config:: PurifyConfig ;
12- use crate :: structure_note:: { render_note_structure, NoteStructureConfig } ;
12+ use crate :: structure_note:: { render_note_structure, word_boundary_contains , NoteStructureConfig } ;
1313use crate :: types:: SessionSegment ;
1414
1515/// 构造会话段(净化后保留段形态)。
@@ -413,3 +413,52 @@ fn structure_config_roundtrips_json() {
413413 assert ! ( parsed. structure. chapter_headings) ;
414414 assert ! ( parsed. structure. glossary_block) ;
415415}
416+
417+ // TD-2026-08-20-B:词边界 + 大小写折叠(词边界匹配直接单测 + 经 first_occurrence_ms 集成)
418+
419+ #[ test]
420+ fn word_boundary_rejects_short_term_inside_longer_word ( ) {
421+ // Arrange/Act/Assert:短术语 "AI" 不得锚到 "AIR"/"said" 等子串
422+ assert ! ( !word_boundary_contains( "AIR 飞行指南" , "ai" ) ) ;
423+ assert ! ( !word_boundary_contains( "saidthings" , "said" ) ) ; // 折叠后仍在词内 → 拒绝
424+ assert ! ( word_boundary_contains( "AI 时代来了" , "ai" ) ) ;
425+ assert ! ( word_boundary_contains( "he said it" , "said" ) ) ;
426+ }
427+
428+ #[ test]
429+ fn word_boundary_case_insensitive ( ) {
430+ // Act/Assert:大小写折叠——"OCR" 命中 "ocr 引擎"
431+ assert ! ( word_boundary_contains( "OCR 引擎已加载" , "ocr" ) ) ;
432+ assert ! ( word_boundary_contains( "OCR 引擎已加载" , "OCR" ) ) ;
433+ assert ! ( !word_boundary_contains( "SOCRATES 哲学" , "ocr" ) ) ; // 折叠后仍在词内 → 拒绝
434+ }
435+
436+ #[ test]
437+ fn word_boundary_chinese_substring_unaffected ( ) {
438+ // Act/Assert:汉字前后不设边界(中文无词边界概念)——"项目" 命中 "项目管理"
439+ assert ! ( word_boundary_contains( "项目管理很关键" , "项目" ) ) ;
440+ assert ! ( word_boundary_contains( "敏捷开发" , "开发" ) ) ;
441+ }
442+
443+ #[ test]
444+ fn word_boundary_empty_term_and_edge_positions ( ) {
445+ // Act/Assert:空术语防御性命中;串首/串尾边界成立
446+ assert ! ( word_boundary_contains( "任意文本" , "" ) ) ;
447+ assert ! ( word_boundary_contains( "AI起步" , "ai" ) ) ;
448+ assert ! ( word_boundary_contains( "起步AI" , "ai" ) ) ;
449+ }
450+
451+ #[ test]
452+ fn first_occurrence_anchor_respects_word_boundary ( ) {
453+ // Arrange:段文本含 "AI 提效"(应命中)与 "AIR 质量"(不应命中)
454+ let kept = vec ! [
455+ seg( 1 , 1_000 , 2_000 , "本章讲 AIR 质量模型" ) ,
456+ seg( 2 , 3_000 , 4_000 , "AI 提效的三个方法" ) ,
457+ ] ;
458+
459+ // Act:锚点应为第二段
460+ let anchor = super :: first_occurrence_ms ( & kept, "AI" ) ;
461+
462+ // Assert
463+ assert_eq ! ( anchor, Some ( 3_000 ) ) ;
464+ }
0 commit comments