use super::*;
use std::fs;
use crate::parse::TsGrammar;
fn ts_purpose(src: &str, grammar: TsGrammar) -> Option<String> {
extract_ts_purpose(src, grammar)
}
const ARTICLE_CARD_TSX: &str = include_str!("../../../fixtures/ts-grammar/ArticleCard.tsx");
const USE_WIDGET_DATA_TS: &str = include_str!("../../../fixtures/ts-grammar/useWidgetData.ts");
const ORDER_SERVICE_TS: &str = include_str!("../../../fixtures/ts-grammar/OrderService.ts");
const WIDGETS_SLICE_TS: &str = include_str!("../../../fixtures/ts-grammar/widgetsSlice.ts");
const STRING_UTILS_TS: &str = include_str!("../../../fixtures/ts-grammar/stringUtils.ts");
const ARTICLE_TYPES_TS: &str = include_str!("../../../fixtures/ts-grammar/articleTypes.ts");
const FORMAT_CURRENCY_TS: &str = include_str!("../../../fixtures/ts-grammar/formatCurrency.ts");
const NOTIFICATION_DIGEST_TS: &str =
include_str!("../../../fixtures/ts-grammar/notificationDigest.ts");
const PATH_HELPERS_JS: &str = include_str!("../../../fixtures/ts-grammar/pathHelpers.js");
const REEXPORT_BARREL_TS: &str = include_str!("../../../fixtures/ts-grammar/reexportBarrel.ts");
const WIDGET_PANEL_WITH_NOTE_TS: &str =
include_str!("../../../fixtures/ts-grammar/widgetPanelWithNote.ts");
#[test]
fn ts_grammar_fixture_article_card_tsx() {
assert_eq!(
ts_purpose(ARTICLE_CARD_TSX, TsGrammar::Tsx).as_deref(),
Some("function ArticleCard | interface ArticleCardProps")
);
}
#[test]
fn ts_grammar_fixture_use_widget_data_ts() {
assert_eq!(
ts_purpose(USE_WIDGET_DATA_TS, TsGrammar::Typescript).as_deref(),
Some("function useWidgetData")
);
}
#[test]
fn ts_grammar_fixture_order_service_ts() {
assert_eq!(
ts_purpose(ORDER_SERVICE_TS, TsGrammar::Typescript).as_deref(),
Some("class OrderService : IOrderService; getOrder, cancelOrder | interface IOrderService")
);
}
#[test]
fn ts_grammar_fixture_widgets_slice_ts() {
assert_eq!(
ts_purpose(WIDGETS_SLICE_TS, TsGrammar::Typescript).as_deref(),
Some("const widgetsSlice | const setLoading | const setItems | default (anonymous)")
);
}
#[test]
fn ts_grammar_fixture_string_utils_ts() {
assert_eq!(
ts_purpose(STRING_UTILS_TS, TsGrammar::Typescript).as_deref(),
Some("function truncate | function slugify | const DEFAULT_MAX_LENGTH")
);
}
#[test]
fn ts_grammar_fixture_article_types_ts() {
assert_eq!(
ts_purpose(ARTICLE_TYPES_TS, TsGrammar::Typescript).as_deref(),
Some("interface ArticleItem | interface ArticleAuthor | type ArticleItemStatus | type ArticlePage")
);
}
#[test]
fn ts_grammar_fixture_format_currency_ts() {
assert_eq!(
ts_purpose(FORMAT_CURRENCY_TS, TsGrammar::Typescript).as_deref(),
Some("default (anonymous)")
);
}
#[test]
fn ts_grammar_fixture_notification_digest_ts() {
assert_eq!(
ts_purpose(NOTIFICATION_DIGEST_TS, TsGrammar::Typescript).as_deref(),
Some("class NotificationDigestBuilder; addEvent, build | function buildDailyDigest")
);
}
#[test]
fn ts_grammar_fixture_path_helpers_js() {
assert_eq!(
ts_purpose(PATH_HELPERS_JS, TsGrammar::Javascript).as_deref(),
Some("function joinSafe | function isAbsolute")
);
}
#[test]
fn ts_grammar_fixture_reexport_barrel_ts() {
assert_eq!(
ts_purpose(REEXPORT_BARREL_TS, TsGrammar::Typescript).as_deref(),
Some("reexports WidgetCard, WidgetList, Dialog, *, WidgetTypes")
);
}
#[test]
fn reexport_bucket_mixes_with_a_real_export_and_lands_last() {
let src = "export function real() {}\nexport { A, B } from './m';\n";
assert_eq!(
ts_purpose(src, TsGrammar::Typescript).as_deref(),
Some("function real | reexports A, B")
);
}
#[test]
fn local_export_clause_without_from_is_still_skipped() {
let src = "const A = 1;\nexport { A };\n";
assert_eq!(ts_purpose(src, TsGrammar::Typescript), None);
}
#[test]
fn bare_star_reexports_dedupe_to_one_token_regardless_of_statement_count() {
let src = "export * from './a';\nexport * from './b';\n";
assert_eq!(
ts_purpose(src, TsGrammar::Typescript).as_deref(),
Some("reexports *")
);
}
#[test]
fn ts_grammar_fixture_widget_panel_with_note_pure_extractor_stays_ast_only() {
assert_eq!(
ts_purpose(WIDGET_PANEL_WITH_NOTE_TS, TsGrammar::Typescript).as_deref(),
Some("function WidgetPanel | interface WidgetPanelProps")
);
}
fn scratch_root(label: &str) -> std::path::PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let dir = std::env::temp_dir().join(format!(
"scout-extract-hybrid-test-{label}-{}-{nanos}",
std::process::id()
));
fs::create_dir_all(&dir).expect("create scratch dir");
dir
}
#[test]
fn extract_ts_purpose_with_heuristic_prefixes_a_leading_comment() {
let root = scratch_root("comment");
fs::write(root.join("widget.ts"), WIDGET_PANEL_WITH_NOTE_TS).unwrap();
let purpose = extract_ts_purpose_with_heuristic(
&root,
"widget.ts",
WIDGET_PANEL_WITH_NOTE_TS,
TsGrammar::Typescript,
);
assert_eq!(
purpose.as_deref(),
Some("Renders the widget summary panel and its retry action — function WidgetPanel | interface WidgetPanelProps")
);
fs::remove_dir_all(&root).ok();
}
#[test]
fn extract_ts_purpose_with_heuristic_leaves_a_code_first_line_unprefixed() {
let root = scratch_root("code-first");
let src = "export function plain() { return 1; }\n";
fs::write(root.join("plain.ts"), src).unwrap();
let purpose = extract_ts_purpose_with_heuristic(&root, "plain.ts", src, TsGrammar::Typescript);
assert_eq!(purpose.as_deref(), Some("function plain"));
fs::remove_dir_all(&root).ok();
}
#[test]
fn extract_ts_purpose_with_heuristic_returns_none_for_comment_plus_zero_export() {
let root = scratch_root("comment-zero-export");
let src = "// just notes, nothing exported\nfunction internalOnly() { return 1; }\n";
fs::write(root.join("notesOnly.ts"), src).unwrap();
let purpose =
extract_ts_purpose_with_heuristic(&root, "notesOnly.ts", src, TsGrammar::Typescript);
assert!(purpose.is_none());
fs::remove_dir_all(&root).ok();
}
#[test]
fn broken_syntax_ts_degrades_to_none_not_a_panic() {
let purpose = ts_purpose(
"export class {{{ not valid typescript at all @@@",
TsGrammar::Typescript,
);
assert!(purpose.is_none());
}
#[test]
fn zero_export_file_yields_none_heuristic_fallback() {
assert!(ts_purpose(
"function internalOnly() { return 1; }\nconst alsoInternal = 2;\n",
TsGrammar::Typescript
)
.is_none());
assert!(ts_purpose("// just a comment, no code\n", TsGrammar::Typescript).is_none());
}