Skip to main content

exspec_lang_typescript/
lib.rs

1pub mod observe;
2pub mod tsconfig;
3
4use std::sync::OnceLock;
5
6use exspec_core::extractor::{FileAnalysis, LanguageExtractor, TestAnalysis, TestFunction};
7use exspec_core::query_utils::{
8    collect_mock_class_names, count_captures, count_captures_within_context,
9    count_duplicate_literals, extract_suppression_from_previous_line, has_any_match,
10};
11use streaming_iterator::StreamingIterator;
12use tree_sitter::{Node, Parser, Query, QueryCursor};
13
14const TEST_FUNCTION_QUERY: &str = include_str!("../queries/test_function.scm");
15const ASSERTION_QUERY: &str = include_str!("../queries/assertion.scm");
16const MOCK_USAGE_QUERY: &str = include_str!("../queries/mock_usage.scm");
17const MOCK_ASSIGNMENT_QUERY: &str = include_str!("../queries/mock_assignment.scm");
18const PARAMETERIZED_QUERY: &str = include_str!("../queries/parameterized.scm");
19const IMPORT_PBT_QUERY: &str = include_str!("../queries/import_pbt.scm");
20const IMPORT_CONTRACT_QUERY: &str = include_str!("../queries/import_contract.scm");
21const HOW_NOT_WHAT_QUERY: &str = include_str!("../queries/how_not_what.scm");
22const PRIVATE_IN_ASSERTION_QUERY: &str = include_str!("../queries/private_in_assertion.scm");
23const ERROR_TEST_QUERY: &str = include_str!("../queries/error_test.scm");
24const RELATIONAL_ASSERTION_QUERY: &str = include_str!("../queries/relational_assertion.scm");
25const WAIT_AND_SEE_QUERY: &str = include_str!("../queries/wait_and_see.scm");
26
27fn ts_language() -> tree_sitter::Language {
28    tree_sitter_typescript::LANGUAGE_TSX.into()
29}
30
31fn cached_query<'a>(lock: &'a OnceLock<Query>, source: &str) -> &'a Query {
32    lock.get_or_init(|| Query::new(&ts_language(), source).expect("invalid query"))
33}
34
35static TEST_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
36static ASSERTION_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
37static MOCK_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
38static MOCK_ASSIGN_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
39static PARAMETERIZED_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
40static IMPORT_PBT_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
41static IMPORT_CONTRACT_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
42static HOW_NOT_WHAT_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
43static PRIVATE_IN_ASSERTION_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
44static ERROR_TEST_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
45static RELATIONAL_ASSERTION_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
46static WAIT_AND_SEE_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
47
48pub struct TypeScriptExtractor;
49
50impl TypeScriptExtractor {
51    pub fn new() -> Self {
52        Self
53    }
54
55    pub fn parser() -> Parser {
56        let mut parser = Parser::new();
57        let language = tree_sitter_typescript::LANGUAGE_TSX;
58        parser
59            .set_language(&language.into())
60            .expect("failed to load TypeScript grammar");
61        parser
62    }
63}
64
65impl Default for TypeScriptExtractor {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71/// Count fixture variables from enclosing describe() scopes.
72/// Walk up the AST from the test function, find describe callback bodies,
73/// and count `lexical_declaration` / `variable_declaration` direct children.
74/// Accumulates across all enclosing describes (handles nesting).
75fn count_enclosing_describe_fixtures(root: Node, test_start_byte: usize, source: &[u8]) -> usize {
76    let Some(start_node) = root.descendant_for_byte_range(test_start_byte, test_start_byte) else {
77        return 0;
78    };
79
80    let mut count = 0;
81    let mut current = start_node.parent();
82    while let Some(node) = current {
83        // Look for statement_block that is a describe callback body
84        if node.kind() == "statement_block" && is_describe_callback_body(node, source) {
85            // Count direct children that are variable declarations
86            let child_count = node.named_child_count();
87            for i in 0..child_count {
88                if let Some(child) = node.named_child(i) {
89                    let kind = child.kind();
90                    if kind == "lexical_declaration" || kind == "variable_declaration" {
91                        // Count the number of variable declarators in this declaration
92                        // e.g., `let a, b;` has 2 declarators
93                        let declarator_count = (0..child.named_child_count())
94                            .filter_map(|j| child.named_child(j))
95                            .filter(|c| c.kind() == "variable_declarator")
96                            .count();
97                        count += declarator_count;
98                    }
99                }
100            }
101        }
102        current = node.parent();
103    }
104
105    count
106}
107
108/// Check if a statement_block is the body of a describe() callback.
109/// Pattern: statement_block → arrow_function/function_expression → arguments → call_expression(describe)
110fn is_describe_callback_body(block: Node, source: &[u8]) -> bool {
111    let parent = match block.parent() {
112        Some(p) => p,
113        None => return false,
114    };
115    let kind = parent.kind();
116    if kind != "arrow_function" && kind != "function_expression" {
117        return false;
118    }
119    let args = match parent.parent() {
120        Some(p) if p.kind() == "arguments" => p,
121        _ => return false,
122    };
123    let call = match args.parent() {
124        Some(p) if p.kind() == "call_expression" => p,
125        _ => return false,
126    };
127    // Check if the function being called is "describe"
128    if let Some(func_node) = call.child_by_field_name("function") {
129        if let Ok(name) = func_node.utf8_text(source) {
130            return name == "describe" || name.starts_with("describe.");
131        }
132    }
133    false
134}
135
136fn extract_mock_class_name(var_name: &str) -> String {
137    // camelCase: strip "mock" prefix and lowercase first char
138    if let Some(stripped) = var_name.strip_prefix("mock") {
139        if !stripped.is_empty() && stripped.starts_with(|c: char| c.is_uppercase()) {
140            return stripped.to_string();
141        }
142    }
143    var_name.to_string()
144}
145
146struct TestMatch {
147    name: String,
148    fn_start_byte: usize,
149    fn_end_byte: usize,
150    fn_start_row: usize,
151    fn_end_row: usize,
152}
153
154fn extract_functions_from_tree(source: &str, file_path: &str, root: Node) -> Vec<TestFunction> {
155    let test_query = cached_query(&TEST_QUERY_CACHE, TEST_FUNCTION_QUERY);
156    let assertion_query = cached_query(&ASSERTION_QUERY_CACHE, ASSERTION_QUERY);
157    let mock_query = cached_query(&MOCK_QUERY_CACHE, MOCK_USAGE_QUERY);
158    let mock_assign_query = cached_query(&MOCK_ASSIGN_QUERY_CACHE, MOCK_ASSIGNMENT_QUERY);
159    let how_not_what_query = cached_query(&HOW_NOT_WHAT_QUERY_CACHE, HOW_NOT_WHAT_QUERY);
160    let private_query = cached_query(
161        &PRIVATE_IN_ASSERTION_QUERY_CACHE,
162        PRIVATE_IN_ASSERTION_QUERY,
163    );
164    let wait_query = cached_query(&WAIT_AND_SEE_QUERY_CACHE, WAIT_AND_SEE_QUERY);
165
166    let name_idx = test_query
167        .capture_index_for_name("name")
168        .expect("no @name capture");
169    let function_idx = test_query
170        .capture_index_for_name("function")
171        .expect("no @function capture");
172
173    let source_bytes = source.as_bytes();
174
175    let mut test_matches = Vec::new();
176    {
177        let mut cursor = QueryCursor::new();
178        let mut matches = cursor.matches(test_query, root, source_bytes);
179        while let Some(m) = matches.next() {
180            let name_capture = match m.captures.iter().find(|c| c.index == name_idx) {
181                Some(c) => c,
182                None => continue,
183            };
184            let name = match name_capture.node.utf8_text(source_bytes) {
185                Ok(s) => s.to_string(),
186                Err(_) => continue,
187            };
188
189            let fn_capture = match m.captures.iter().find(|c| c.index == function_idx) {
190                Some(c) => c,
191                None => continue,
192            };
193
194            test_matches.push(TestMatch {
195                name,
196                fn_start_byte: fn_capture.node.start_byte(),
197                fn_end_byte: fn_capture.node.end_byte(),
198                fn_start_row: fn_capture.node.start_position().row,
199                fn_end_row: fn_capture.node.end_position().row,
200            });
201        }
202    }
203
204    let mut functions = Vec::new();
205    for tm in &test_matches {
206        let fn_node = match root.descendant_for_byte_range(tm.fn_start_byte, tm.fn_end_byte) {
207            Some(n) => n,
208            None => continue,
209        };
210
211        let line = tm.fn_start_row + 1;
212        let end_line = tm.fn_end_row + 1;
213        let line_count = end_line - line + 1;
214
215        let assertion_count = count_captures(assertion_query, "assertion", fn_node, source_bytes);
216        let mock_count = count_captures(mock_query, "mock", fn_node, source_bytes);
217        let mock_classes = collect_mock_class_names(
218            mock_assign_query,
219            fn_node,
220            source_bytes,
221            extract_mock_class_name,
222        );
223
224        let how_not_what_count =
225            count_captures(how_not_what_query, "how_pattern", fn_node, source_bytes);
226
227        let private_in_assertion_count = count_captures_within_context(
228            assertion_query,
229            "assertion",
230            private_query,
231            "private_access",
232            fn_node,
233            source_bytes,
234        );
235
236        let fixture_count = count_enclosing_describe_fixtures(root, tm.fn_start_byte, source_bytes);
237
238        // T108: wait-and-see detection
239        let has_wait = has_any_match(wait_query, "wait", fn_node, source_bytes);
240
241        // T106: duplicate literal count
242        let duplicate_literal_count = count_duplicate_literals(
243            assertion_query,
244            fn_node,
245            source_bytes,
246            &["number", "string"],
247        );
248
249        let suppressed_rules = extract_suppression_from_previous_line(source, tm.fn_start_row);
250
251        functions.push(TestFunction {
252            name: tm.name.clone(),
253            file: file_path.to_string(),
254            line,
255            end_line,
256            analysis: TestAnalysis {
257                assertion_count,
258                mock_count,
259                mock_classes,
260                line_count,
261                how_not_what_count: how_not_what_count + private_in_assertion_count,
262                fixture_count,
263                has_wait,
264                has_skip_call: false,
265                assertion_message_count: assertion_count, // T107 skipped for TypeScript: expect() has no msg arg
266                duplicate_literal_count,
267                suppressed_rules,
268            },
269        });
270    }
271
272    functions
273}
274
275impl LanguageExtractor for TypeScriptExtractor {
276    fn extract_test_functions(&self, source: &str, file_path: &str) -> Vec<TestFunction> {
277        let mut parser = Self::parser();
278        let tree = match parser.parse(source, None) {
279            Some(t) => t,
280            None => return Vec::new(),
281        };
282        extract_functions_from_tree(source, file_path, tree.root_node())
283    }
284
285    fn extract_file_analysis(&self, source: &str, file_path: &str) -> FileAnalysis {
286        let mut parser = Self::parser();
287        let tree = match parser.parse(source, None) {
288            Some(t) => t,
289            None => {
290                return FileAnalysis {
291                    file: file_path.to_string(),
292                    functions: Vec::new(),
293                    has_pbt_import: false,
294                    has_contract_import: false,
295                    has_error_test: false,
296                    has_relational_assertion: false,
297                    parameterized_count: 0,
298                };
299            }
300        };
301
302        let root = tree.root_node();
303        let source_bytes = source.as_bytes();
304
305        let functions = extract_functions_from_tree(source, file_path, root);
306
307        let param_query = cached_query(&PARAMETERIZED_QUERY_CACHE, PARAMETERIZED_QUERY);
308        let parameterized_count = count_captures(param_query, "parameterized", root, source_bytes);
309
310        let pbt_query = cached_query(&IMPORT_PBT_QUERY_CACHE, IMPORT_PBT_QUERY);
311        let has_pbt_import = has_any_match(pbt_query, "pbt_import", root, source_bytes);
312
313        let contract_query = cached_query(&IMPORT_CONTRACT_QUERY_CACHE, IMPORT_CONTRACT_QUERY);
314        let has_contract_import =
315            has_any_match(contract_query, "contract_import", root, source_bytes);
316
317        let error_test_query = cached_query(&ERROR_TEST_QUERY_CACHE, ERROR_TEST_QUERY);
318        let has_error_test = has_any_match(error_test_query, "error_test", root, source_bytes);
319
320        let relational_query = cached_query(
321            &RELATIONAL_ASSERTION_QUERY_CACHE,
322            RELATIONAL_ASSERTION_QUERY,
323        );
324        let has_relational_assertion =
325            has_any_match(relational_query, "relational", root, source_bytes);
326
327        FileAnalysis {
328            file: file_path.to_string(),
329            functions,
330            has_pbt_import,
331            has_contract_import,
332            has_error_test,
333            has_relational_assertion,
334            parameterized_count,
335        }
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342
343    fn fixture(name: &str) -> String {
344        let path = format!(
345            "{}/tests/fixtures/typescript/{}",
346            env!("CARGO_MANIFEST_DIR").replace("/crates/lang-typescript", ""),
347            name
348        );
349        std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("failed to read {path}: {e}"))
350    }
351
352    // --- Phase 1 preserved tests ---
353
354    #[test]
355    fn parse_typescript_source() {
356        let source = "const x: number = 42;\n";
357        let mut parser = TypeScriptExtractor::parser();
358        let tree = parser.parse(source, None).unwrap();
359        assert_eq!(tree.root_node().kind(), "program");
360    }
361
362    #[test]
363    fn typescript_extractor_implements_language_extractor() {
364        let extractor = TypeScriptExtractor::new();
365        let _: &dyn exspec_core::extractor::LanguageExtractor = &extractor;
366    }
367
368    // --- Cycle 1: Test function extraction ---
369
370    #[test]
371    fn extract_single_test_function() {
372        let source = fixture("t001_pass.test.ts");
373        let extractor = TypeScriptExtractor::new();
374        let funcs = extractor.extract_test_functions(&source, "t001_pass.test.ts");
375        assert_eq!(funcs.len(), 1);
376        assert_eq!(funcs[0].name, "create user");
377        assert_eq!(funcs[0].line, 1);
378    }
379
380    #[test]
381    fn extract_multiple_tests_excludes_helpers_and_describe() {
382        let source = fixture("multiple_tests.test.ts");
383        let extractor = TypeScriptExtractor::new();
384        let funcs = extractor.extract_test_functions(&source, "multiple_tests.test.ts");
385        assert_eq!(funcs.len(), 3);
386        let names: Vec<&str> = funcs.iter().map(|f| f.name.as_str()).collect();
387        assert_eq!(
388            names,
389            vec!["adds numbers", "subtracts numbers", "multiplies numbers"]
390        );
391    }
392
393    #[test]
394    fn line_count_calculation() {
395        let source = fixture("t001_pass.test.ts");
396        let extractor = TypeScriptExtractor::new();
397        let funcs = extractor.extract_test_functions(&source, "t001_pass.test.ts");
398        assert_eq!(
399            funcs[0].analysis.line_count,
400            funcs[0].end_line - funcs[0].line + 1
401        );
402    }
403
404    #[test]
405    fn violation_file_extracts_function() {
406        let source = fixture("t001_violation.test.ts");
407        let extractor = TypeScriptExtractor::new();
408        let funcs = extractor.extract_test_functions(&source, "t001_violation.test.ts");
409        assert_eq!(funcs.len(), 1);
410        assert_eq!(funcs[0].name, "create user");
411    }
412
413    // --- Cycle 2: Assertion detection ---
414
415    #[test]
416    fn assertion_count_zero_for_violation() {
417        let source = fixture("t001_violation.test.ts");
418        let extractor = TypeScriptExtractor::new();
419        let funcs = extractor.extract_test_functions(&source, "t001_violation.test.ts");
420        assert_eq!(funcs[0].analysis.assertion_count, 0);
421    }
422
423    #[test]
424    fn assertion_count_positive_for_pass() {
425        let source = fixture("t001_pass.test.ts");
426        let extractor = TypeScriptExtractor::new();
427        let funcs = extractor.extract_test_functions(&source, "t001_pass.test.ts");
428        assert!(funcs[0].analysis.assertion_count >= 1);
429    }
430
431    // --- TSX support (#53) ---
432
433    #[test]
434    fn tsx_file_detects_assertions() {
435        let source = fixture("t001_tsx_assertion.test.tsx");
436        let extractor = TypeScriptExtractor::new();
437        let funcs = extractor.extract_test_functions(&source, "t001_tsx_assertion.test.tsx");
438        assert_eq!(
439            funcs.len(),
440            2,
441            "should extract 2 test functions from TSX file"
442        );
443        for f in &funcs {
444            assert!(
445                f.analysis.assertion_count >= 1,
446                "test '{}' should have assertions detected in TSX file, got {}",
447                f.name,
448                f.analysis.assertion_count
449            );
450        }
451    }
452
453    // --- Cycle 2: Mock detection ---
454
455    #[test]
456    fn mock_count_for_violation() {
457        let source = fixture("t002_violation.test.ts");
458        let extractor = TypeScriptExtractor::new();
459        let funcs = extractor.extract_test_functions(&source, "t002_violation.test.ts");
460        assert_eq!(funcs.len(), 1);
461        assert_eq!(funcs[0].analysis.mock_count, 6);
462    }
463
464    #[test]
465    fn mock_count_for_pass() {
466        let source = fixture("t002_pass.test.ts");
467        let extractor = TypeScriptExtractor::new();
468        let funcs = extractor.extract_test_functions(&source, "t002_pass.test.ts");
469        assert_eq!(funcs.len(), 1);
470        assert_eq!(funcs[0].analysis.mock_count, 1);
471        assert_eq!(funcs[0].analysis.mock_classes, vec!["Db"]);
472    }
473
474    #[test]
475    fn mock_class_name_extraction() {
476        assert_eq!(extract_mock_class_name("mockDb"), "Db");
477        assert_eq!(
478            extract_mock_class_name("mockPaymentService"),
479            "PaymentService"
480        );
481        assert_eq!(extract_mock_class_name("myMock"), "myMock");
482    }
483
484    // --- Inline suppression ---
485
486    #[test]
487    fn suppressed_test_has_suppressed_rules() {
488        let source = fixture("suppressed.test.ts");
489        let extractor = TypeScriptExtractor::new();
490        let funcs = extractor.extract_test_functions(&source, "suppressed.test.ts");
491        assert_eq!(funcs.len(), 1);
492        assert_eq!(funcs[0].analysis.mock_count, 6);
493        assert!(funcs[0]
494            .analysis
495            .suppressed_rules
496            .iter()
497            .any(|r| r.0 == "T002"));
498    }
499
500    #[test]
501    fn non_suppressed_test_has_empty_suppressed_rules() {
502        let source = fixture("t002_violation.test.ts");
503        let extractor = TypeScriptExtractor::new();
504        let funcs = extractor.extract_test_functions(&source, "t002_violation.test.ts");
505        assert!(funcs[0].analysis.suppressed_rules.is_empty());
506    }
507
508    // --- Giant test ---
509
510    #[test]
511    fn giant_test_line_count() {
512        let source = fixture("t003_violation.test.ts");
513        let extractor = TypeScriptExtractor::new();
514        let funcs = extractor.extract_test_functions(&source, "t003_violation.test.ts");
515        assert_eq!(funcs.len(), 1);
516        assert!(funcs[0].analysis.line_count > 50);
517    }
518
519    #[test]
520    fn short_test_line_count() {
521        let source = fixture("t003_pass.test.ts");
522        let extractor = TypeScriptExtractor::new();
523        let funcs = extractor.extract_test_functions(&source, "t003_pass.test.ts");
524        assert_eq!(funcs.len(), 1);
525        assert!(funcs[0].analysis.line_count <= 50);
526    }
527
528    // --- File analysis: parameterized ---
529
530    #[test]
531    fn file_analysis_detects_parameterized() {
532        let source = fixture("t004_pass.test.ts");
533        let extractor = TypeScriptExtractor::new();
534        let fa = extractor.extract_file_analysis(&source, "t004_pass.test.ts");
535        assert!(
536            fa.parameterized_count >= 1,
537            "expected parameterized_count >= 1, got {}",
538            fa.parameterized_count
539        );
540    }
541
542    #[test]
543    fn file_analysis_no_parameterized() {
544        let source = fixture("t004_violation.test.ts");
545        let extractor = TypeScriptExtractor::new();
546        let fa = extractor.extract_file_analysis(&source, "t004_violation.test.ts");
547        assert_eq!(fa.parameterized_count, 0);
548    }
549
550    // --- File analysis: PBT import ---
551
552    #[test]
553    fn file_analysis_detects_pbt_import() {
554        let source = fixture("t005_pass.test.ts");
555        let extractor = TypeScriptExtractor::new();
556        let fa = extractor.extract_file_analysis(&source, "t005_pass.test.ts");
557        assert!(fa.has_pbt_import);
558    }
559
560    #[test]
561    fn file_analysis_no_pbt_import() {
562        let source = fixture("t005_violation.test.ts");
563        let extractor = TypeScriptExtractor::new();
564        let fa = extractor.extract_file_analysis(&source, "t005_violation.test.ts");
565        assert!(!fa.has_pbt_import);
566    }
567
568    // --- File analysis: contract import ---
569
570    #[test]
571    fn file_analysis_detects_contract_import() {
572        let source = fixture("t008_pass.test.ts");
573        let extractor = TypeScriptExtractor::new();
574        let fa = extractor.extract_file_analysis(&source, "t008_pass.test.ts");
575        assert!(fa.has_contract_import);
576    }
577
578    #[test]
579    fn file_analysis_no_contract_import() {
580        let source = fixture("t008_violation.test.ts");
581        let extractor = TypeScriptExtractor::new();
582        let fa = extractor.extract_file_analysis(&source, "t008_violation.test.ts");
583        assert!(!fa.has_contract_import);
584    }
585
586    // --- Suppression does not propagate from describe to inner tests ---
587
588    #[test]
589    fn suppression_on_describe_does_not_apply_to_inner_tests() {
590        let source = fixture("describe_suppression.test.ts");
591        let extractor = TypeScriptExtractor::new();
592        let funcs = extractor.extract_test_functions(&source, "describe_suppression.test.ts");
593        assert_eq!(funcs.len(), 2, "expected 2 test functions inside describe");
594        for f in &funcs {
595            assert!(
596                f.analysis.suppressed_rules.is_empty(),
597                "test '{}' should NOT have suppressed rules (suppression on describe does not propagate)",
598                f.name
599            );
600            assert_eq!(
601                f.analysis.assertion_count, 0,
602                "test '{}' should have 0 assertions (T001 violation expected)",
603                f.name
604            );
605        }
606    }
607
608    // --- File analysis preserves functions ---
609
610    #[test]
611    fn file_analysis_preserves_test_functions() {
612        let source = fixture("t001_pass.test.ts");
613        let extractor = TypeScriptExtractor::new();
614        let fa = extractor.extract_file_analysis(&source, "t001_pass.test.ts");
615        assert_eq!(fa.functions.len(), 1);
616        assert_eq!(fa.functions[0].name, "create user");
617    }
618
619    // --- T101: how-not-what ---
620
621    #[test]
622    fn how_not_what_count_for_violation() {
623        let source = fixture("t101_violation.test.ts");
624        let extractor = TypeScriptExtractor::new();
625        let funcs = extractor.extract_test_functions(&source, "t101_violation.test.ts");
626        assert_eq!(funcs.len(), 2);
627        assert!(
628            funcs[0].analysis.how_not_what_count > 0,
629            "expected how_not_what_count > 0 for first test, got {}",
630            funcs[0].analysis.how_not_what_count
631        );
632        assert!(
633            funcs[1].analysis.how_not_what_count > 0,
634            "expected how_not_what_count > 0 for second test, got {}",
635            funcs[1].analysis.how_not_what_count
636        );
637    }
638
639    #[test]
640    fn how_not_what_count_zero_for_pass() {
641        let source = fixture("t101_pass.test.ts");
642        let extractor = TypeScriptExtractor::new();
643        let funcs = extractor.extract_test_functions(&source, "t101_pass.test.ts");
644        assert_eq!(funcs.len(), 1);
645        assert_eq!(funcs[0].analysis.how_not_what_count, 0);
646    }
647
648    #[test]
649    fn how_not_what_coexists_with_assertions() {
650        let source = fixture("t101_violation.test.ts");
651        let extractor = TypeScriptExtractor::new();
652        let funcs = extractor.extract_test_functions(&source, "t101_violation.test.ts");
653        assert!(
654            funcs[0].analysis.assertion_count > 0,
655            "should also count as assertions"
656        );
657        assert!(
658            funcs[0].analysis.how_not_what_count > 0,
659            "should count as how-not-what"
660        );
661    }
662
663    // --- Query capture name verification (#14) ---
664
665    fn make_query(scm: &str) -> Query {
666        Query::new(&ts_language(), scm).unwrap()
667    }
668
669    #[test]
670    fn query_capture_names_test_function() {
671        let q = make_query(include_str!("../queries/test_function.scm"));
672        assert!(
673            q.capture_index_for_name("name").is_some(),
674            "test_function.scm must define @name capture"
675        );
676        assert!(
677            q.capture_index_for_name("function").is_some(),
678            "test_function.scm must define @function capture"
679        );
680    }
681
682    #[test]
683    fn query_capture_names_assertion() {
684        let q = make_query(include_str!("../queries/assertion.scm"));
685        assert!(
686            q.capture_index_for_name("assertion").is_some(),
687            "assertion.scm must define @assertion capture"
688        );
689    }
690
691    #[test]
692    fn query_capture_names_mock_usage() {
693        let q = make_query(include_str!("../queries/mock_usage.scm"));
694        assert!(
695            q.capture_index_for_name("mock").is_some(),
696            "mock_usage.scm must define @mock capture"
697        );
698    }
699
700    #[test]
701    fn query_capture_names_mock_assignment() {
702        let q = make_query(include_str!("../queries/mock_assignment.scm"));
703        assert!(
704            q.capture_index_for_name("var_name").is_some(),
705            "mock_assignment.scm must define @var_name (required by collect_mock_class_names .expect())"
706        );
707    }
708
709    #[test]
710    fn query_capture_names_parameterized() {
711        let q = make_query(include_str!("../queries/parameterized.scm"));
712        assert!(
713            q.capture_index_for_name("parameterized").is_some(),
714            "parameterized.scm must define @parameterized capture"
715        );
716    }
717
718    #[test]
719    fn query_capture_names_import_pbt() {
720        let q = make_query(include_str!("../queries/import_pbt.scm"));
721        assert!(
722            q.capture_index_for_name("pbt_import").is_some(),
723            "import_pbt.scm must define @pbt_import capture"
724        );
725    }
726
727    #[test]
728    fn query_capture_names_import_contract() {
729        let q = make_query(include_str!("../queries/import_contract.scm"));
730        assert!(
731            q.capture_index_for_name("contract_import").is_some(),
732            "import_contract.scm must define @contract_import capture"
733        );
734    }
735
736    #[test]
737    fn query_capture_names_how_not_what() {
738        let q = make_query(include_str!("../queries/how_not_what.scm"));
739        assert!(
740            q.capture_index_for_name("how_pattern").is_some(),
741            "how_not_what.scm must define @how_pattern capture"
742        );
743    }
744
745    // --- T102: fixture-sprawl ---
746
747    #[test]
748    fn fixture_count_for_violation() {
749        let source = fixture("t102_violation.test.ts");
750        let extractor = TypeScriptExtractor::new();
751        let funcs = extractor.extract_test_functions(&source, "t102_violation.test.ts");
752        assert_eq!(funcs.len(), 1);
753        assert_eq!(
754            funcs[0].analysis.fixture_count, 6,
755            "expected 6 describe-level let declarations"
756        );
757    }
758
759    #[test]
760    fn fixture_count_for_pass() {
761        let source = fixture("t102_pass.test.ts");
762        let extractor = TypeScriptExtractor::new();
763        let funcs = extractor.extract_test_functions(&source, "t102_pass.test.ts");
764        assert_eq!(funcs.len(), 1);
765        assert_eq!(
766            funcs[0].analysis.fixture_count, 2,
767            "expected 2 describe-level let declarations"
768        );
769    }
770
771    #[test]
772    fn fixture_count_nested_describe() {
773        let source = fixture("t102_nested.test.ts");
774        let extractor = TypeScriptExtractor::new();
775        let funcs = extractor.extract_test_functions(&source, "t102_nested.test.ts");
776        assert_eq!(funcs.len(), 2);
777        // Inner test sees outer (3) + inner (3) = 6
778        let inner = funcs
779            .iter()
780            .find(|f| f.name == "test in nested describe inherits all fixtures")
781            .unwrap();
782        assert_eq!(
783            inner.analysis.fixture_count, 6,
784            "inner test should see outer + inner fixtures"
785        );
786        // Outer test sees only outer (3)
787        let outer = funcs
788            .iter()
789            .find(|f| f.name == "test in outer describe only sees outer fixtures")
790            .unwrap();
791        assert_eq!(
792            outer.analysis.fixture_count, 3,
793            "outer test should see only outer fixtures"
794        );
795    }
796
797    #[test]
798    fn fixture_count_describe_each() {
799        let source = fixture("t102_describe_each.test.ts");
800        let extractor = TypeScriptExtractor::new();
801        let funcs = extractor.extract_test_functions(&source, "t102_describe_each.test.ts");
802        assert_eq!(funcs.len(), 1);
803        assert_eq!(
804            funcs[0].analysis.fixture_count, 2,
805            "describe.each should be recognized as describe scope"
806        );
807    }
808
809    #[test]
810    fn fixture_count_top_level_test_zero() {
811        // A test outside describe should have 0 fixtures
812        let source = "it('standalone test', () => { expect(1).toBe(1); });";
813        let extractor = TypeScriptExtractor::new();
814        let funcs = extractor.extract_test_functions(source, "top_level.test.ts");
815        assert_eq!(funcs.len(), 1);
816        assert_eq!(
817            funcs[0].analysis.fixture_count, 0,
818            "top-level test should have 0 fixtures"
819        );
820    }
821
822    // --- T101: private attribute access in assertions (#13) ---
823
824    #[test]
825    fn private_dot_notation_detected() {
826        let source = fixture("t101_private_violation.test.ts");
827        let extractor = TypeScriptExtractor::new();
828        let funcs = extractor.extract_test_functions(&source, "t101_private_violation.test.ts");
829        // "checks internal count via dot notation" has expect(service._count) and expect(service._processed)
830        let func = funcs
831            .iter()
832            .find(|f| f.name == "checks internal count via dot notation")
833            .unwrap();
834        assert!(
835            func.analysis.how_not_what_count >= 2,
836            "expected >= 2 private access in assertions (dot), got {}",
837            func.analysis.how_not_what_count
838        );
839    }
840
841    #[test]
842    fn private_bracket_notation_detected() {
843        let source = fixture("t101_private_violation.test.ts");
844        let extractor = TypeScriptExtractor::new();
845        let funcs = extractor.extract_test_functions(&source, "t101_private_violation.test.ts");
846        // "checks internal via bracket notation" has expect(service['_count']) and expect(service['_processed'])
847        let func = funcs
848            .iter()
849            .find(|f| f.name == "checks internal via bracket notation")
850            .unwrap();
851        assert!(
852            func.analysis.how_not_what_count >= 2,
853            "expected >= 2 private access in assertions (bracket), got {}",
854            func.analysis.how_not_what_count
855        );
856    }
857
858    #[test]
859    fn private_outside_expect_not_counted() {
860        let source = fixture("t101_private_violation.test.ts");
861        let extractor = TypeScriptExtractor::new();
862        let funcs = extractor.extract_test_functions(&source, "t101_private_violation.test.ts");
863        // "private outside expect not counted": service._internal is outside expect()
864        let func = funcs
865            .iter()
866            .find(|f| f.name == "private outside expect not counted")
867            .unwrap();
868        assert_eq!(
869            func.analysis.how_not_what_count, 0,
870            "private access outside expect should not count"
871        );
872    }
873
874    #[test]
875    fn private_adds_to_how_not_what() {
876        let source = fixture("t101_private_violation.test.ts");
877        let extractor = TypeScriptExtractor::new();
878        let funcs = extractor.extract_test_functions(&source, "t101_private_violation.test.ts");
879        // "mixed private and mock verification" has toHaveBeenCalledWith (mock) + expect(service._lastCreated) (private)
880        let func = funcs
881            .iter()
882            .find(|f| f.name == "mixed private and mock verification")
883            .unwrap();
884        assert!(
885            func.analysis.how_not_what_count >= 2,
886            "expected mock (1) + private (1) = >= 2, got {}",
887            func.analysis.how_not_what_count
888        );
889    }
890
891    #[test]
892    fn query_capture_names_private_in_assertion() {
893        let q = make_query(include_str!("../queries/private_in_assertion.scm"));
894        assert!(
895            q.capture_index_for_name("private_access").is_some(),
896            "private_in_assertion.scm must define @private_access capture"
897        );
898    }
899
900    // --- T103: missing-error-test ---
901
902    #[test]
903    fn error_test_to_throw() {
904        let source = fixture("t103_pass_toThrow.test.ts");
905        let extractor = TypeScriptExtractor::new();
906        let fa = extractor.extract_file_analysis(&source, "t103_pass_toThrow.test.ts");
907        assert!(fa.has_error_test, ".toThrow() should set has_error_test");
908    }
909
910    #[test]
911    fn error_test_to_throw_error() {
912        let source = fixture("t103_pass_toThrowError.test.ts");
913        let extractor = TypeScriptExtractor::new();
914        let fa = extractor.extract_file_analysis(&source, "t103_pass_toThrowError.test.ts");
915        assert!(
916            fa.has_error_test,
917            ".toThrowError() should set has_error_test"
918        );
919    }
920
921    #[test]
922    fn error_test_rejects() {
923        let source = fixture("t103_pass_rejects.test.ts");
924        let extractor = TypeScriptExtractor::new();
925        let fa = extractor.extract_file_analysis(&source, "t103_pass_rejects.test.ts");
926        assert!(fa.has_error_test, ".rejects should set has_error_test");
927    }
928
929    #[test]
930    fn error_test_false_positive_rejects_property() {
931        let source = fixture("t103_false_positive_rejects_property.test.ts");
932        let extractor = TypeScriptExtractor::new();
933        let fa = extractor
934            .extract_file_analysis(&source, "t103_false_positive_rejects_property.test.ts");
935        assert!(
936            !fa.has_error_test,
937            "service.rejects should NOT set has_error_test"
938        );
939    }
940
941    #[test]
942    fn error_test_no_patterns() {
943        let source = fixture("t103_violation.test.ts");
944        let extractor = TypeScriptExtractor::new();
945        let fa = extractor.extract_file_analysis(&source, "t103_violation.test.ts");
946        assert!(
947            !fa.has_error_test,
948            "no error patterns should set has_error_test=false"
949        );
950    }
951
952    #[test]
953    fn query_capture_names_error_test() {
954        let q = make_query(include_str!("../queries/error_test.scm"));
955        assert!(
956            q.capture_index_for_name("error_test").is_some(),
957            "error_test.scm must define @error_test capture"
958        );
959    }
960
961    // --- T105: deterministic-no-metamorphic ---
962
963    #[test]
964    fn relational_assertion_violation() {
965        let source = fixture("t105_violation.test.ts");
966        let extractor = TypeScriptExtractor::new();
967        let fa = extractor.extract_file_analysis(&source, "t105_violation.test.ts");
968        assert!(
969            !fa.has_relational_assertion,
970            "all toBe/toEqual file should not have relational"
971        );
972    }
973
974    #[test]
975    fn relational_assertion_pass_greater_than() {
976        let source = fixture("t105_pass_relational.test.ts");
977        let extractor = TypeScriptExtractor::new();
978        let fa = extractor.extract_file_analysis(&source, "t105_pass_relational.test.ts");
979        assert!(
980            fa.has_relational_assertion,
981            "toBeGreaterThan should set has_relational_assertion"
982        );
983    }
984
985    #[test]
986    fn relational_assertion_pass_truthy() {
987        let source = fixture("t105_pass_truthy.test.ts");
988        let extractor = TypeScriptExtractor::new();
989        let fa = extractor.extract_file_analysis(&source, "t105_pass_truthy.test.ts");
990        assert!(
991            fa.has_relational_assertion,
992            "toBeTruthy should set has_relational_assertion"
993        );
994    }
995
996    #[test]
997    fn query_capture_names_relational_assertion() {
998        let q = make_query(include_str!("../queries/relational_assertion.scm"));
999        assert!(
1000            q.capture_index_for_name("relational").is_some(),
1001            "relational_assertion.scm must define @relational capture"
1002        );
1003    }
1004
1005    // --- T108: wait-and-see ---
1006
1007    #[test]
1008    fn wait_and_see_violation_sleep() {
1009        let source = fixture("t108_violation_sleep.test.ts");
1010        let extractor = TypeScriptExtractor::new();
1011        let funcs = extractor.extract_test_functions(&source, "t108_violation_sleep.test.ts");
1012        assert!(!funcs.is_empty());
1013        for func in &funcs {
1014            assert!(
1015                func.analysis.has_wait,
1016                "test '{}' should have has_wait=true",
1017                func.name
1018            );
1019        }
1020    }
1021
1022    #[test]
1023    fn wait_and_see_pass_no_sleep() {
1024        let source = fixture("t108_pass_no_sleep.test.ts");
1025        let extractor = TypeScriptExtractor::new();
1026        let funcs = extractor.extract_test_functions(&source, "t108_pass_no_sleep.test.ts");
1027        assert_eq!(funcs.len(), 1);
1028        assert!(
1029            !funcs[0].analysis.has_wait,
1030            "test without sleep should have has_wait=false"
1031        );
1032    }
1033
1034    #[test]
1035    fn query_capture_names_wait_and_see() {
1036        let q = make_query(include_str!("../queries/wait_and_see.scm"));
1037        assert!(
1038            q.capture_index_for_name("wait").is_some(),
1039            "wait_and_see.scm must define @wait capture"
1040        );
1041    }
1042
1043    // --- T109: undescriptive-test-name ---
1044
1045    #[test]
1046    fn t109_violation_names_detected() {
1047        let source = fixture("t109_violation.test.ts");
1048        let extractor = TypeScriptExtractor::new();
1049        let funcs = extractor.extract_test_functions(&source, "t109_violation.test.ts");
1050        assert!(!funcs.is_empty());
1051        for func in &funcs {
1052            assert!(
1053                exspec_core::rules::is_undescriptive_test_name(&func.name),
1054                "test '{}' should be undescriptive",
1055                func.name
1056            );
1057        }
1058    }
1059
1060    #[test]
1061    fn t109_pass_descriptive_names() {
1062        let source = fixture("t109_pass.test.ts");
1063        let extractor = TypeScriptExtractor::new();
1064        let funcs = extractor.extract_test_functions(&source, "t109_pass.test.ts");
1065        assert!(!funcs.is_empty());
1066        for func in &funcs {
1067            assert!(
1068                !exspec_core::rules::is_undescriptive_test_name(&func.name),
1069                "test '{}' should be descriptive",
1070                func.name
1071            );
1072        }
1073    }
1074
1075    // TC-09: CJK fixture → all names should be descriptive (no T109 FP)
1076    #[test]
1077    fn t109_cjk_pass_descriptive_names() {
1078        let source = fixture("t109_cjk_pass.test.ts");
1079        let extractor = TypeScriptExtractor::new();
1080        let funcs = extractor.extract_test_functions(&source, "t109_cjk_pass.test.ts");
1081        assert!(!funcs.is_empty());
1082        for func in &funcs {
1083            assert!(
1084                !exspec_core::rules::is_undescriptive_test_name(&func.name),
1085                "CJK test '{}' should be descriptive",
1086                func.name
1087            );
1088        }
1089    }
1090
1091    // --- T106: duplicate-literal-assertion ---
1092
1093    #[test]
1094    fn t106_violation_duplicate_literal() {
1095        let source = fixture("t106_violation.test.ts");
1096        let extractor = TypeScriptExtractor::new();
1097        let funcs = extractor.extract_test_functions(&source, "t106_violation.test.ts");
1098        assert_eq!(funcs.len(), 1);
1099        assert!(
1100            funcs[0].analysis.duplicate_literal_count >= 3,
1101            "42 appears 3 times, should be >= 3: got {}",
1102            funcs[0].analysis.duplicate_literal_count
1103        );
1104    }
1105
1106    #[test]
1107    fn t106_pass_no_duplicates() {
1108        let source = fixture("t106_pass_no_duplicates.test.ts");
1109        let extractor = TypeScriptExtractor::new();
1110        let funcs = extractor.extract_test_functions(&source, "t106_pass_no_duplicates.test.ts");
1111        assert_eq!(funcs.len(), 1);
1112        assert!(
1113            funcs[0].analysis.duplicate_literal_count < 3,
1114            "each literal appears once: got {}",
1115            funcs[0].analysis.duplicate_literal_count
1116        );
1117    }
1118
1119    // --- T001 FP fix: rejects chain + expectTypeOf (#25) ---
1120
1121    #[test]
1122    fn t001_expect_to_throw_already_covered() {
1123        // TC-04: expect(fn).toThrow() -> already matched
1124        let source = "import { it, expect } from 'vitest';\nit('throws', () => { expect(() => fn()).toThrow(); });";
1125        let extractor = TypeScriptExtractor::new();
1126        let funcs = extractor.extract_test_functions(&source, "test_throw.test.ts");
1127        assert_eq!(funcs.len(), 1);
1128        assert!(
1129            funcs[0].analysis.assertion_count >= 1,
1130            "expect().toThrow() should already be covered, got {}",
1131            funcs[0].analysis.assertion_count
1132        );
1133    }
1134
1135    #[test]
1136    fn t001_rejects_to_throw_counts_as_assertion() {
1137        // TC-05: expect(promise).rejects.toThrow() -> T001 should NOT fire
1138        let source = fixture("t001_rejects_to_throw.test.ts");
1139        let extractor = TypeScriptExtractor::new();
1140        let funcs = extractor.extract_test_functions(&source, "t001_rejects_to_throw.test.ts");
1141        assert_eq!(funcs.len(), 1);
1142        assert!(
1143            funcs[0].analysis.assertion_count >= 1,
1144            "expect().rejects.toThrow() should count as assertion, got {}",
1145            funcs[0].analysis.assertion_count
1146        );
1147    }
1148
1149    #[test]
1150    fn t001_expect_type_of_counts_as_assertion() {
1151        // TC-06: expectTypeOf() -> T001 should NOT fire
1152        let source = fixture("t001_expect_type_of.test.ts");
1153        let extractor = TypeScriptExtractor::new();
1154        let funcs = extractor.extract_test_functions(&source, "t001_expect_type_of.test.ts");
1155        assert_eq!(funcs.len(), 1);
1156        assert!(
1157            funcs[0].analysis.assertion_count >= 1,
1158            "expectTypeOf() should count as assertion, got {}",
1159            funcs[0].analysis.assertion_count
1160        );
1161    }
1162
1163    // --- T001 FP fix: expect.soft/element/poll (#31) ---
1164
1165    #[test]
1166    fn t001_expect_soft_counts_as_assertion() {
1167        // TC-01: expect.soft(x).toBe(y) -> T001 should NOT fire
1168        let source = fixture("t001_expect_soft.test.ts");
1169        let extractor = TypeScriptExtractor::new();
1170        let funcs = extractor.extract_test_functions(&source, "t001_expect_soft.test.ts");
1171        assert_eq!(funcs.len(), 1);
1172        assert!(
1173            funcs[0].analysis.assertion_count >= 1,
1174            "expect.soft() should count as assertion, got {}",
1175            funcs[0].analysis.assertion_count
1176        );
1177    }
1178
1179    #[test]
1180    fn t001_expect_element_counts_as_assertion() {
1181        // TC-02: expect.element(locator).toHaveText(text) -> T001 should NOT fire
1182        let source = fixture("t001_expect_element.test.ts");
1183        let extractor = TypeScriptExtractor::new();
1184        let funcs = extractor.extract_test_functions(&source, "t001_expect_element.test.ts");
1185        assert_eq!(funcs.len(), 1);
1186        assert!(
1187            funcs[0].analysis.assertion_count >= 1,
1188            "expect.element() should count as assertion, got {}",
1189            funcs[0].analysis.assertion_count
1190        );
1191    }
1192
1193    #[test]
1194    fn t001_expect_poll_counts_as_assertion() {
1195        // TC-03: expect.poll(() => value).toBe(expected) -> T001 should NOT fire
1196        let source = fixture("t001_expect_poll.test.ts");
1197        let extractor = TypeScriptExtractor::new();
1198        let funcs = extractor.extract_test_functions(&source, "t001_expect_poll.test.ts");
1199        assert_eq!(funcs.len(), 1);
1200        assert!(
1201            funcs[0].analysis.assertion_count >= 1,
1202            "expect.poll() should count as assertion, got {}",
1203            funcs[0].analysis.assertion_count
1204        );
1205    }
1206
1207    // --- T001 FP fix: Chai property-style assertions (#32) ---
1208
1209    #[test]
1210    fn t001_chai_property_fixture_all_detected() {
1211        // All 6 test functions in chai property fixture should have assertions
1212        let source = fixture("t001_chai_property.test.ts");
1213        let extractor = TypeScriptExtractor::new();
1214        let funcs = extractor.extract_test_functions(&source, "t001_chai_property.test.ts");
1215        assert_eq!(funcs.len(), 6);
1216        for f in &funcs {
1217            assert!(
1218                f.analysis.assertion_count >= 1,
1219                "test '{}' should have assertion_count >= 1, got {}",
1220                f.name,
1221                f.analysis.assertion_count
1222            );
1223        }
1224    }
1225
1226    #[test]
1227    fn t001_chai_property_depth1_no_double_count() {
1228        // TC-02: expect(x).ok (depth 1) -> assertion_count == 1
1229        let source = r#"
1230import { expect } from 'chai';
1231describe('d', () => {
1232  it('t', () => {
1233    expect(x).ok;
1234  });
1235});
1236"#;
1237        let extractor = TypeScriptExtractor::new();
1238        let funcs = extractor.extract_test_functions(source, "test.ts");
1239        assert_eq!(funcs.len(), 1);
1240        assert_eq!(
1241            funcs[0].analysis.assertion_count, 1,
1242            "depth 1 property should count exactly 1, got {}",
1243            funcs[0].analysis.assertion_count
1244        );
1245    }
1246
1247    #[test]
1248    fn t001_chai_property_depth3_no_double_count() {
1249        // TC-03: expect(x).to.be.true (depth 3) -> assertion_count == 1
1250        let source = r#"
1251import { expect } from 'chai';
1252describe('d', () => {
1253  it('t', () => {
1254    expect(x).to.be.true;
1255  });
1256});
1257"#;
1258        let extractor = TypeScriptExtractor::new();
1259        let funcs = extractor.extract_test_functions(source, "test.ts");
1260        assert_eq!(funcs.len(), 1);
1261        assert_eq!(
1262            funcs[0].analysis.assertion_count, 1,
1263            "depth 3 property should count exactly 1, got {}",
1264            funcs[0].analysis.assertion_count
1265        );
1266    }
1267
1268    #[test]
1269    fn t001_chai_property_depth4_no_double_count() {
1270        // TC-04: expect(spy).to.have.been.calledOnce (depth 4) -> assertion_count == 1
1271        let source = r#"
1272import { expect } from 'chai';
1273describe('d', () => {
1274  it('t', () => {
1275    expect(spy).to.have.been.calledOnce;
1276  });
1277});
1278"#;
1279        let extractor = TypeScriptExtractor::new();
1280        let funcs = extractor.extract_test_functions(source, "test.ts");
1281        assert_eq!(funcs.len(), 1);
1282        assert_eq!(
1283            funcs[0].analysis.assertion_count, 1,
1284            "depth 4 property should count exactly 1, got {}",
1285            funcs[0].analysis.assertion_count
1286        );
1287    }
1288
1289    #[test]
1290    fn t001_chai_property_intermediate_not_counted() {
1291        // TC-05: expect(x).to; as bare expression_statement -> assertion_count == 0
1292        // .to is NOT in the terminal allowlist, so the allowlist filter rejects it.
1293        let source = r#"
1294import { expect } from 'chai';
1295describe('d', () => {
1296  it('t', () => {
1297    expect(x).to;
1298  });
1299});
1300"#;
1301        let extractor = TypeScriptExtractor::new();
1302        let funcs = extractor.extract_test_functions(source, "test.ts");
1303        assert_eq!(funcs.len(), 1);
1304        assert_eq!(
1305            funcs[0].analysis.assertion_count, 0,
1306            "intermediate property .to should NOT count as assertion, got {}",
1307            funcs[0].analysis.assertion_count
1308        );
1309    }
1310
1311    // --- T001 FP fix: expect modifier chains .not/.resolves (#37) ---
1312
1313    #[test]
1314    fn t001_not_modifier_all_detected() {
1315        // TC-01..03: expect(x).not.toBe/toEqual/toContain -> assertion_count == 1 each
1316        let source = fixture("t001_not_modifier.test.ts");
1317        let extractor = TypeScriptExtractor::new();
1318        let funcs = extractor.extract_test_functions(&source, "t001_not_modifier.test.ts");
1319        assert_eq!(funcs.len(), 3);
1320        for f in &funcs {
1321            assert_eq!(
1322                f.analysis.assertion_count, 1,
1323                "test '{}' with .not modifier should have assertion_count == 1, got {}",
1324                f.name, f.analysis.assertion_count
1325            );
1326        }
1327    }
1328
1329    #[test]
1330    fn t001_resolves_rejects_chain_all_detected() {
1331        // TC-04..06: resolves.toBe, resolves.not.toThrow, rejects.not.toThrow -> assertion_count == 1 each
1332        let source = fixture("t001_resolves_rejects_chain.test.ts");
1333        let extractor = TypeScriptExtractor::new();
1334        let funcs =
1335            extractor.extract_test_functions(&source, "t001_resolves_rejects_chain.test.ts");
1336        assert_eq!(funcs.len(), 3);
1337        for f in &funcs {
1338            assert_eq!(
1339                f.analysis.assertion_count, 1,
1340                "test '{}' with modifier chain should have assertion_count == 1, got {}",
1341                f.name, f.analysis.assertion_count
1342            );
1343        }
1344    }
1345
1346    // --- T001 FP fix: Chai method-call chain assertions (#40) ---
1347
1348    #[test]
1349    fn t001_chai_method_call_fixture_all_detected() {
1350        // TC-01~07 should have assertions, TC-08~09 should not, TC-10~18 should have assertions
1351        let source = fixture("t001_chai_method_call.test.ts");
1352        let extractor = TypeScriptExtractor::new();
1353        let funcs = extractor.extract_test_functions(&source, "t001_chai_method_call.test.ts");
1354        assert_eq!(funcs.len(), 18);
1355
1356        // TC-01: expect(x).to.equal(y) — depth 2, exactly 1 (no double-count)
1357        assert_eq!(
1358            funcs[0].analysis.assertion_count, 1,
1359            "TC-01 to.equal should count exactly 1, got {}",
1360            funcs[0].analysis.assertion_count
1361        );
1362
1363        // TC-02: expect(x).to.be.a('string') — depth 3
1364        assert!(
1365            funcs[1].analysis.assertion_count >= 1,
1366            "TC-02 to.be.a should have assertion_count >= 1, got {}",
1367            funcs[1].analysis.assertion_count
1368        );
1369
1370        // TC-03: expect(spy).to.have.callCount(3) — depth 3
1371        assert!(
1372            funcs[2].analysis.assertion_count >= 1,
1373            "TC-03 to.have.callCount should have assertion_count >= 1, got {}",
1374            funcs[2].analysis.assertion_count
1375        );
1376
1377        // TC-04: expect(spy).to.have.been.calledWith(arg) — depth 4
1378        assert!(
1379            funcs[3].analysis.assertion_count >= 1,
1380            "TC-04 to.have.been.calledWith should have assertion_count >= 1, got {}",
1381            funcs[3].analysis.assertion_count
1382        );
1383
1384        // TC-05: expect(spy).to.not.have.been.calledWith(arg) — depth 5
1385        assert!(
1386            funcs[4].analysis.assertion_count >= 1,
1387            "TC-05 to.not.have.been.calledWith should have assertion_count >= 1, got {}",
1388            funcs[4].analysis.assertion_count
1389        );
1390
1391        // TC-06: mixed property (.to.be.true) + method (.to.equal) — both counted
1392        assert!(
1393            funcs[5].analysis.assertion_count >= 2,
1394            "TC-06 mixed property+method should have assertion_count >= 2, got {}",
1395            funcs[5].analysis.assertion_count
1396        );
1397
1398        // TC-07: multiple method assertions
1399        assert!(
1400            funcs[6].analysis.assertion_count >= 2,
1401            "TC-07 multiple methods should have assertion_count >= 2, got {}",
1402            funcs[6].analysis.assertion_count
1403        );
1404
1405        // TC-08: no assertion (regression guard)
1406        assert_eq!(
1407            funcs[7].analysis.assertion_count, 0,
1408            "TC-08 no assertion should have assertion_count == 0, got {}",
1409            funcs[7].analysis.assertion_count
1410        );
1411
1412        // TC-09: expect(x).to.customHelper() — NOT in terminal vocabulary
1413        assert_eq!(
1414            funcs[8].analysis.assertion_count, 0,
1415            "TC-09 customHelper should have assertion_count == 0, got {}",
1416            funcs[8].analysis.assertion_count
1417        );
1418
1419        // TC-10: expect(x).not.to.equal(y) — depth 3, not at position 1
1420        assert!(
1421            funcs[9].analysis.assertion_count >= 1,
1422            "TC-10 not.to.equal should have assertion_count >= 1, got {}",
1423            funcs[9].analysis.assertion_count
1424        );
1425
1426        // TC-11 (regression): expect(x).to.equal(y) — exact count 1
1427        assert_eq!(
1428            funcs[10].analysis.assertion_count, 1,
1429            "TC-11 to.equal regression should count exactly 1, got {}",
1430            funcs[10].analysis.assertion_count
1431        );
1432
1433        // TC-12: expect(obj).to.have.deep.equal({a:1}) — deep intermediate
1434        assert!(
1435            funcs[11].analysis.assertion_count >= 1,
1436            "TC-12 deep intermediate should have assertion_count >= 1, got {}",
1437            funcs[11].analysis.assertion_count
1438        );
1439
1440        // TC-13: expect(obj).to.have.nested.property('a.b') — nested intermediate
1441        assert!(
1442            funcs[12].analysis.assertion_count >= 1,
1443            "TC-13 nested intermediate should have assertion_count >= 1, got {}",
1444            funcs[12].analysis.assertion_count
1445        );
1446
1447        // TC-14: expect(obj).to.have.own.property('x') — own intermediate
1448        assert!(
1449            funcs[13].analysis.assertion_count >= 1,
1450            "TC-14 own intermediate should have assertion_count >= 1, got {}",
1451            funcs[13].analysis.assertion_count
1452        );
1453
1454        // TC-15: expect(arr).to.have.ordered.members([1,2]) — ordered intermediate
1455        assert!(
1456            funcs[14].analysis.assertion_count >= 1,
1457            "TC-15 ordered intermediate should have assertion_count >= 1, got {}",
1458            funcs[14].analysis.assertion_count
1459        );
1460
1461        // TC-16: expect(obj).to.have.any.keys('x') — any intermediate
1462        assert!(
1463            funcs[15].analysis.assertion_count >= 1,
1464            "TC-16 any intermediate should have assertion_count >= 1, got {}",
1465            funcs[15].analysis.assertion_count
1466        );
1467
1468        // TC-17: expect(obj).to.have.all.keys('x','y') — all intermediate
1469        assert!(
1470            funcs[16].analysis.assertion_count >= 1,
1471            "TC-17 all intermediate should have assertion_count >= 1, got {}",
1472            funcs[16].analysis.assertion_count
1473        );
1474
1475        // TC-18: expect(obj).itself.to.respondTo('bar') — itself intermediate
1476        assert!(
1477            funcs[17].analysis.assertion_count >= 1,
1478            "TC-18 itself intermediate should have assertion_count >= 1, got {}",
1479            funcs[17].analysis.assertion_count
1480        );
1481    }
1482
1483    #[test]
1484    fn t001_chai_method_call_depth2_no_double_count() {
1485        // expect(x).to.equal(y) should count exactly 1 (not double-counted with depth-1)
1486        let source = r#"
1487import { expect } from 'chai';
1488describe('d', () => {
1489  it('t', () => {
1490    expect(x).to.equal(y);
1491  });
1492});
1493"#;
1494        let extractor = TypeScriptExtractor::new();
1495        let funcs = extractor.extract_test_functions(source, "test.ts");
1496        assert_eq!(funcs.len(), 1);
1497        assert_eq!(
1498            funcs[0].analysis.assertion_count, 1,
1499            "depth 2 method-call should count exactly 1, got {}",
1500            funcs[0].analysis.assertion_count
1501        );
1502    }
1503
1504    #[test]
1505    fn t001_chai_deep_intermediate_no_double_count() {
1506        // expect(obj).to.have.deep.equal({a:1}) should count exactly 1
1507        let source = r#"
1508import { expect } from 'chai';
1509describe('d', () => {
1510  it('t', () => {
1511    expect(obj).to.have.deep.equal({a: 1});
1512  });
1513});
1514"#;
1515        let extractor = TypeScriptExtractor::new();
1516        let funcs = extractor.extract_test_functions(source, "test.ts");
1517        assert_eq!(funcs.len(), 1);
1518        assert_eq!(
1519            funcs[0].analysis.assertion_count, 1,
1520            "deep intermediate should count exactly 1, got {}",
1521            funcs[0].analysis.assertion_count
1522        );
1523    }
1524
1525    #[test]
1526    fn t001_expect_soft_chain_fixture() {
1527        // B1-B10: expect.soft/element/poll modifier chain tests
1528        let source = fixture("t001_expect_soft_chain.test.ts");
1529        let extractor = TypeScriptExtractor::new();
1530        let funcs = extractor.extract_test_functions(&source, "t001_expect_soft_chain.test.ts");
1531        assert_eq!(funcs.len(), 10);
1532
1533        // B1 (regression): expect.soft(x).toBe(y) — depth-2 existing
1534        assert!(
1535            funcs[0].analysis.assertion_count >= 1,
1536            "B1 expect.soft depth-2 should have assertion_count >= 1, got {}",
1537            funcs[0].analysis.assertion_count
1538        );
1539
1540        // B2: expect.soft(x).not.toBe(y) — depth-3
1541        assert!(
1542            funcs[1].analysis.assertion_count >= 1,
1543            "B2 expect.soft.not depth-3 should have assertion_count >= 1, got {}",
1544            funcs[1].analysis.assertion_count
1545        );
1546
1547        // B3: expect.soft(x).resolves.toBe(y) — depth-3
1548        assert!(
1549            funcs[2].analysis.assertion_count >= 1,
1550            "B3 expect.soft.resolves depth-3 should have assertion_count >= 1, got {}",
1551            funcs[2].analysis.assertion_count
1552        );
1553
1554        // B4: expect.soft(x).rejects.toThrow() — depth-3
1555        assert!(
1556            funcs[3].analysis.assertion_count >= 1,
1557            "B4 expect.soft.rejects depth-3 should have assertion_count >= 1, got {}",
1558            funcs[3].analysis.assertion_count
1559        );
1560
1561        // B5: expect.soft(x).resolves.not.toBe(y) — depth-4
1562        assert!(
1563            funcs[4].analysis.assertion_count >= 1,
1564            "B5 expect.soft.resolves.not depth-4 should have assertion_count >= 1, got {}",
1565            funcs[4].analysis.assertion_count
1566        );
1567
1568        // B6: expect.soft(x).rejects.not.toThrow(TypeError) — depth-4
1569        assert!(
1570            funcs[5].analysis.assertion_count >= 1,
1571            "B6 expect.soft.rejects.not depth-4 should have assertion_count >= 1, got {}",
1572            funcs[5].analysis.assertion_count
1573        );
1574
1575        // B7 (negative): expect.soft(x).resolves.customHelper() — NOT a toX terminal
1576        assert_eq!(
1577            funcs[6].analysis.assertion_count, 0,
1578            "B7 customHelper should have assertion_count == 0, got {}",
1579            funcs[6].analysis.assertion_count
1580        );
1581
1582        // B8 (negative): no assertions
1583        assert_eq!(
1584            funcs[7].analysis.assertion_count, 0,
1585            "B8 no assertion should have assertion_count == 0, got {}",
1586            funcs[7].analysis.assertion_count
1587        );
1588
1589        // B9: expect.element(loc).not.toHaveText('x') — depth-3
1590        assert!(
1591            funcs[8].analysis.assertion_count >= 1,
1592            "B9 expect.element.not depth-3 should have assertion_count >= 1, got {}",
1593            funcs[8].analysis.assertion_count
1594        );
1595
1596        // B10: expect.poll(fn).not.toBe(0) — depth-3
1597        assert!(
1598            funcs[9].analysis.assertion_count >= 1,
1599            "B10 expect.poll.not depth-3 should have assertion_count >= 1, got {}",
1600            funcs[9].analysis.assertion_count
1601        );
1602    }
1603
1604    // --- T001 FP fix: supertest .expect() method-call oracle (#47) ---
1605
1606    #[test]
1607    fn t001_supertest_expect_method_call() {
1608        let source = fixture("t001_supertest.test.ts");
1609        let extractor = TypeScriptExtractor::new();
1610        let funcs = extractor.extract_test_functions(&source, "t001_supertest.test.ts");
1611        assert_eq!(funcs.len(), 6);
1612
1613        // TC-01: single .expect(200) on chain — assertion_count == 1
1614        assert_eq!(
1615            funcs[0].analysis.assertion_count, 1,
1616            "TC-01 single .expect(200) should have assertion_count == 1, got {}",
1617            funcs[0].analysis.assertion_count
1618        );
1619
1620        // TC-02: two .expect() on same chain — assertion_count == 2
1621        assert_eq!(
1622            funcs[1].analysis.assertion_count, 2,
1623            "TC-02 two .expect() should have assertion_count == 2, got {}",
1624            funcs[1].analysis.assertion_count
1625        );
1626
1627        // TC-03: chain with .set() and two .expect() — assertion_count == 2
1628        assert_eq!(
1629            funcs[2].analysis.assertion_count, 2,
1630            "TC-03 .set() + two .expect() should have assertion_count == 2, got {}",
1631            funcs[2].analysis.assertion_count
1632        );
1633
1634        // TC-04: no assertion (plain request) — assertion_count == 0
1635        assert_eq!(
1636            funcs[3].analysis.assertion_count, 0,
1637            "TC-04 no assertion should have assertion_count == 0, got {}",
1638            funcs[3].analysis.assertion_count
1639        );
1640
1641        // TC-05: standalone expect(x).toBe(y) — assertion_count == 1 (no double-count)
1642        assert_eq!(
1643            funcs[4].analysis.assertion_count, 1,
1644            "TC-05 standalone expect should have assertion_count == 1, got {}",
1645            funcs[4].analysis.assertion_count
1646        );
1647
1648        // TC-06: non-supertest chain: someBuilder().expect('foo') — assertion_count == 1
1649        assert_eq!(
1650            funcs[5].analysis.assertion_count, 1,
1651            "TC-06 non-supertest builder .expect() should have assertion_count == 1, got {}",
1652            funcs[5].analysis.assertion_count
1653        );
1654    }
1655
1656    // --- T001 FP fix: Chai/Sinon vocabulary expansion (#48) ---
1657
1658    #[test]
1659    fn t001_chai_vocab_expansion_fixture_all_detected() {
1660        let source = fixture("t001_chai_vocab_expansion.test.ts");
1661        let extractor = TypeScriptExtractor::new();
1662        let funcs = extractor.extract_test_functions(&source, "t001_chai_vocab_expansion.test.ts");
1663        assert_eq!(funcs.len(), 18);
1664
1665        // TC-01 through TC-16: all should have assertion_count >= 1
1666        for (i, f) in funcs.iter().enumerate().take(16) {
1667            assert!(
1668                f.analysis.assertion_count >= 1,
1669                "TC-{:02} '{}' should have assertion_count >= 1, got {}",
1670                i + 1,
1671                f.name,
1672                f.analysis.assertion_count
1673            );
1674        }
1675
1676        // TC-17: sinon.stub() — NOT an assertion
1677        assert_eq!(
1678            funcs[16].analysis.assertion_count, 0,
1679            "TC-17 sinon.stub should have assertion_count == 0, got {}",
1680            funcs[16].analysis.assertion_count
1681        );
1682
1683        // TC-18: no assertion
1684        assert_eq!(
1685            funcs[17].analysis.assertion_count, 0,
1686            "TC-18 no assertion should have assertion_count == 0, got {}",
1687            funcs[17].analysis.assertion_count
1688        );
1689    }
1690
1691    // --- T001 FP fix: Chai property in arrow body (#49) ---
1692
1693    #[test]
1694    fn t001_chai_property_arrow_fixture_all_detected() {
1695        let source = fixture("t001_chai_property_arrow.test.ts");
1696        let extractor = TypeScriptExtractor::new();
1697        let funcs = extractor.extract_test_functions(&source, "t001_chai_property_arrow.test.ts");
1698        assert_eq!(funcs.len(), 8);
1699
1700        // TC-01 through TC-07: all should have assertion_count >= 1
1701        for (i, f) in funcs.iter().enumerate().take(7) {
1702            assert!(
1703                f.analysis.assertion_count >= 1,
1704                "TC-{:02} '{}' should have assertion_count >= 1, got {}",
1705                i + 1,
1706                f.name,
1707                f.analysis.assertion_count
1708            );
1709        }
1710
1711        // TC-08: negative — no assertion
1712        assert_eq!(
1713            funcs[7].analysis.assertion_count, 0,
1714            "TC-08 no assertion should have assertion_count == 0, got {}",
1715            funcs[7].analysis.assertion_count
1716        );
1717    }
1718
1719    // --- T001 FP fix: Chai property in return statement (#52) ---
1720
1721    #[test]
1722    fn t001_chai_property_return_fixture_detected() {
1723        let source = fixture("t001_chai_property_return.test.ts");
1724        let extractor = TypeScriptExtractor::new();
1725        let funcs = extractor.extract_test_functions(&source, "t001_chai_property_return.test.ts");
1726        assert_eq!(funcs.len(), 6);
1727
1728        // TC-01 through TC-05: return-wrapped depths 1-5, each == 1 (no double-count)
1729        for (i, f) in funcs.iter().enumerate().take(5) {
1730            assert_eq!(
1731                f.analysis.assertion_count,
1732                1,
1733                "TC-{:02} '{}' should have assertion_count == 1, got {}",
1734                i + 1,
1735                f.name,
1736                f.analysis.assertion_count
1737            );
1738        }
1739
1740        // TC-06: negative — non-assertion return
1741        assert_eq!(
1742            funcs[5].analysis.assertion_count, 0,
1743            "TC-06 '{}' non-assertion return should have assertion_count == 0, got {}",
1744            funcs[5].name, funcs[5].analysis.assertion_count
1745        );
1746    }
1747
1748    #[test]
1749    fn t001_chai_property_existing_wrappers_regression() {
1750        let extractor = TypeScriptExtractor::new();
1751
1752        // expression_statement: "should detect to.be.true (depth 3)" at index 1
1753        let expr_source = fixture("t001_chai_property.test.ts");
1754        let expr_funcs =
1755            extractor.extract_test_functions(&expr_source, "t001_chai_property.test.ts");
1756        assert_eq!(expr_funcs[1].name, "should detect to.be.true (depth 3)");
1757        assert_eq!(
1758            expr_funcs[1].analysis.assertion_count, 1,
1759            "expression_statement wrapper regression: expected exactly 1 assertion, got {}",
1760            expr_funcs[1].analysis.assertion_count
1761        );
1762
1763        // arrow_function body: "should detect property in forEach arrow" at index 0
1764        let arrow_source = fixture("t001_chai_property_arrow.test.ts");
1765        let arrow_funcs =
1766            extractor.extract_test_functions(&arrow_source, "t001_chai_property_arrow.test.ts");
1767        assert_eq!(
1768            arrow_funcs[0].name,
1769            "should detect property in forEach arrow"
1770        );
1771        assert_eq!(
1772            arrow_funcs[0].analysis.assertion_count, 1,
1773            "arrow_function body regression: expected exactly 1 assertion, got {}",
1774            arrow_funcs[0].analysis.assertion_count
1775        );
1776    }
1777
1778    #[test]
1779    fn t107_skipped_for_typescript() {
1780        // TypeScript expect() has no message argument, so T107 should never fire.
1781        // assertion_message_count must equal assertion_count to prevent T107 from triggering.
1782        let source = fixture("t107_pass.test.ts");
1783        let extractor = TypeScriptExtractor::new();
1784        let funcs = extractor.extract_test_functions(&source, "t107_pass.test.ts");
1785        assert_eq!(funcs.len(), 1);
1786        let analysis = &funcs[0].analysis;
1787        assert!(
1788            analysis.assertion_count >= 2,
1789            "fixture should have 2+ assertions: got {}",
1790            analysis.assertion_count
1791        );
1792        assert_eq!(
1793            analysis.assertion_message_count, analysis.assertion_count,
1794            "TS assertion_message_count should equal assertion_count to skip T107"
1795        );
1796    }
1797
1798    // --- TC-14: TS test with myAssert() + config -> T001 does NOT fire ---
1799    #[test]
1800    fn t001_custom_helper_with_config_no_violation() {
1801        use exspec_core::query_utils::apply_custom_assertion_fallback;
1802        use exspec_core::rules::{evaluate_rules, Config};
1803
1804        let source = fixture("t001_custom_helper.test.ts");
1805        let extractor = TypeScriptExtractor::new();
1806        let mut analysis = extractor.extract_file_analysis(&source, "t001_custom_helper.test.ts");
1807        let patterns = vec!["myAssert(".to_string()];
1808        apply_custom_assertion_fallback(&mut analysis, &source, &patterns);
1809
1810        let config = Config::default();
1811        let diags = evaluate_rules(&analysis.functions, &config);
1812        let t001_diags: Vec<_> = diags.iter().filter(|d| d.rule.0 == "T001").collect();
1813        // "uses custom assertion" -> pass (custom pattern)
1814        // "uses standard expect" -> pass (standard detection)
1815        // "has no assertion" -> BLOCK
1816        assert_eq!(
1817            t001_diags.len(),
1818            1,
1819            "only 'has no assertion' test should trigger T001"
1820        );
1821    }
1822
1823    // --- T001 FP fix: expect.assertions / expect.unreachable / expectType (#39) ---
1824
1825    #[test]
1826    fn t001_expect_assertions_counts_as_assertion() {
1827        // TC-01: expect.assertions(1) -> T001 should NOT fire
1828        let source = fixture("t001_expect_assertions.test.ts");
1829        let extractor = TypeScriptExtractor::new();
1830        let funcs = extractor.extract_test_functions(&source, "t001_expect_assertions.test.ts");
1831        // 8 test functions in fixture
1832        assert_eq!(funcs.len(), 8);
1833
1834        // TC-01: expect.assertions(1) + expect(data).toBeDefined()
1835        assert!(
1836            funcs[0].analysis.assertion_count >= 1,
1837            "expect.assertions(N) should count as assertion, got {}",
1838            funcs[0].analysis.assertion_count
1839        );
1840
1841        // TC-02: expect.assertions(0) — single oracle, exact count catches double-counting
1842        assert_eq!(
1843            funcs[1].analysis.assertion_count, 1,
1844            "expect.assertions(0) should count as exactly 1 assertion, got {}",
1845            funcs[1].analysis.assertion_count
1846        );
1847
1848        // TC-03: expect.hasAssertions() + expect(data).toBeTruthy()
1849        assert!(
1850            funcs[2].analysis.assertion_count >= 1,
1851            "expect.hasAssertions() should count as assertion, got {}",
1852            funcs[2].analysis.assertion_count
1853        );
1854
1855        // TC-04: expect.unreachable() — single oracle, exact count catches double-counting
1856        assert_eq!(
1857            funcs[3].analysis.assertion_count, 1,
1858            "expect.unreachable() should count as exactly 1 assertion, got {}",
1859            funcs[3].analysis.assertion_count
1860        );
1861
1862        // TC-05: expectType<User>(user) — single oracle, exact count catches double-counting
1863        assert_eq!(
1864            funcs[4].analysis.assertion_count, 1,
1865            "expectType<T>(value) should count as exactly 1 assertion, got {}",
1866            funcs[4].analysis.assertion_count
1867        );
1868
1869        // TC-06: expect.assertions(2) + expect(a).toBe(1) + expect(b).toBe(2) -> 3+
1870        assert!(
1871            funcs[5].analysis.assertion_count >= 2,
1872            "mixed expect.assertions + expect().toBe() should count 2+, got {}",
1873            funcs[5].analysis.assertion_count
1874        );
1875
1876        // TC-07: expectType + expectTypeOf -> both counted
1877        assert!(
1878            funcs[6].analysis.assertion_count >= 2,
1879            "expectType + expectTypeOf should count 2+, got {}",
1880            funcs[6].analysis.assertion_count
1881        );
1882
1883        // TC-08: no assertions -> BLOCK (regression guard)
1884        assert_eq!(
1885            funcs[7].analysis.assertion_count, 0,
1886            "no-assertion test should have assertion_count == 0"
1887        );
1888    }
1889
1890    // --- T001 FP fix: Chai NestJS alias/property vocabulary expansion (#50) ---
1891
1892    #[test]
1893    fn t001_chai_nestjs_aliases_fixture() {
1894        let source = fixture("t001_chai_nestjs_aliases.test.ts");
1895        let extractor = TypeScriptExtractor::new();
1896        let funcs = extractor.extract_test_functions(&source, "t001_chai_nestjs_aliases.test.ts");
1897        assert_eq!(funcs.len(), 9);
1898
1899        // TC-01: instanceof method alias (depth-3)
1900        assert!(
1901            funcs[0].analysis.assertion_count >= 1,
1902            "TC-01 instanceof alias should have assertion_count >= 1, got {}",
1903            funcs[0].analysis.assertion_count
1904        );
1905
1906        // TC-02: throws method alias (depth-2)
1907        assert!(
1908            funcs[1].analysis.assertion_count >= 1,
1909            "TC-02 throws alias should have assertion_count >= 1, got {}",
1910            funcs[1].analysis.assertion_count
1911        );
1912
1913        // TC-03: contains method alias (depth-2)
1914        assert!(
1915            funcs[2].analysis.assertion_count >= 1,
1916            "TC-03 contains alias should have assertion_count >= 1, got {}",
1917            funcs[2].analysis.assertion_count
1918        );
1919
1920        // TC-04: equals method alias (depth-2)
1921        assert!(
1922            funcs[3].analysis.assertion_count >= 1,
1923            "TC-04 equals alias should have assertion_count >= 1, got {}",
1924            funcs[3].analysis.assertion_count
1925        );
1926
1927        // TC-05: ownProperty method (depth-3)
1928        assert!(
1929            funcs[4].analysis.assertion_count >= 1,
1930            "TC-05 ownProperty should have assertion_count >= 1, got {}",
1931            funcs[4].analysis.assertion_count
1932        );
1933
1934        // TC-06: length method alias (depth-3)
1935        assert!(
1936            funcs[5].analysis.assertion_count >= 1,
1937            "TC-06 length alias should have assertion_count >= 1, got {}",
1938            funcs[5].analysis.assertion_count
1939        );
1940
1941        // TC-07: throw property terminal (depth-3, no parens)
1942        assert!(
1943            funcs[6].analysis.assertion_count >= 1,
1944            "TC-07 throw property should have assertion_count >= 1, got {}",
1945            funcs[6].analysis.assertion_count
1946        );
1947
1948        // TC-08: and intermediate + instanceof alias (deep chain)
1949        assert!(
1950            funcs[7].analysis.assertion_count >= 1,
1951            "TC-08 and+instanceof deep chain should have assertion_count >= 1, got {}",
1952            funcs[7].analysis.assertion_count
1953        );
1954
1955        // TC-09: negative — truly assertion-free (TP control)
1956        assert_eq!(
1957            funcs[8].analysis.assertion_count, 0,
1958            "TC-09 no assertion should have assertion_count == 0, got {}",
1959            funcs[8].analysis.assertion_count
1960        );
1961    }
1962
1963    // --- T001 FP fix: Sinon mock .verify() method-call oracle (#51) ---
1964
1965    #[test]
1966    fn t001_sinon_verify_fixture_all_detected() {
1967        let source = fixture("t001_sinon_verify.test.ts");
1968        let extractor = TypeScriptExtractor::new();
1969        let funcs = extractor.extract_test_functions(&source, "t001_sinon_verify.test.ts");
1970        assert_eq!(funcs.len(), 7);
1971
1972        // TC-01 through TC-05: all should have assertion_count >= 1
1973        for (i, f) in funcs.iter().enumerate().take(5) {
1974            assert!(
1975                f.analysis.assertion_count >= 1,
1976                "TC-{:02} '{}' should have assertion_count >= 1, got {}",
1977                i + 1,
1978                f.name,
1979                f.analysis.assertion_count
1980            );
1981        }
1982
1983        // TC-05 specifically: verify + expect — should have assertion_count >= 2
1984        assert!(
1985            funcs[4].analysis.assertion_count >= 2,
1986            "TC-05 verify + expect should have assertion_count >= 2, got {}",
1987            funcs[4].analysis.assertion_count
1988        );
1989
1990        // TC-06: mock.restore() — NOT an assertion
1991        assert_eq!(
1992            funcs[5].analysis.assertion_count, 0,
1993            "TC-06 mock.restore should have assertion_count == 0, got {}",
1994            funcs[5].analysis.assertion_count
1995        );
1996
1997        // TC-07: no assertion
1998        assert_eq!(
1999            funcs[6].analysis.assertion_count, 0,
2000            "TC-07 no assertion should have assertion_count == 0, got {}",
2001            funcs[6].analysis.assertion_count
2002        );
2003    }
2004}