1+ // noinspection ES6UnusedImports
12import { describe , expect , test } from "bun:test" ;
2- import { That } from "@thaterror/core" ;
3+ import { That } from '@thaterror/core' ;
4+ import { ResultAsync } from 'neverthrow' ;
35
4- describe ( "ThatError Caller Tracing" , ( ) => {
6+ describe ( "ThatError Location Anchoring" , ( ) => {
7+ const AppError = That ( {
8+ SYNC_ERR : "Sync failure" ,
9+ ASYNC_ERR : ( url : string ) => `Async failure: ${ url } ` ,
10+ LOC_ERR : "Location test error"
11+ } ) ;
12+
13+ const pathSegments = new URL ( import . meta. url ) . pathname . split ( '/' ) ;
14+ const currentFileName = pathSegments . at ( - 1 ) ?? "" ;
15+
16+ // --- Scenario 1: The Native Way (Messy) ---
17+ test ( "Native: new Error() inside callback captures library internals" , async ( ) => {
18+ function businessFunction ( ) {
19+ return ResultAsync . fromPromise (
20+ Promise . reject ( "connection lost" ) ,
21+ ( error ) =>
22+ /**
23+ * 🚨 The Native Way:
24+ * A standard 'new Error' is captured here, but the stack trace
25+ * will be cluttered with neverthrow's internal dispatcher frames.
26+ */
27+ new Error ( `Native wrap: ${ error } ` )
28+ ) . andThen ( ( ) => {
29+ throw new Error ( "Should not happen" ) ;
30+ } ) ;
31+ }
32+
33+ const result = await businessFunction ( ) . map ( ( ) => {
34+ return 1 ;
35+ } ) ;
36+
37+ if ( result . isErr ( ) ) {
38+ const stack = result . error . stack ?? "" ;
39+ // ❌ Observation: Native error will contain 'node_modules/neverthrow' frames
40+ expect ( stack ) . toContain ( "neverthrow" ) ;
41+ expect ( stack ) . toContain ( "node_modules" ) ;
42+ console . log ( stack . split ( "\n" ) . slice ( 0 , 4 ) . join ( "\n" ) ) ;
43+ }
44+ } ) ;
545
6- test ( "stack trace should start from business code, not factory" , ( ) => {
7- const AppError = That ( { TEST : "msg" } ) ;
8- const err = AppError . TEST ( ) ;
46+ // --- Scenario 2: Without .at() (The "Noisy" Way) ---
47+ test ( "Control: stack trace WITHOUT .at() might include internal noise" , ( ) => {
48+ function businessFunction ( ) {
49+ // 🚨 Returning the error directly without anchoring
50+ return AppError . LOC_ERR ( ) ;
51+ }
952
53+ const err = businessFunction ( ) ;
1054 const stack = err . stack ?? "" ;
11- const lines = stack . split ( "\n" ) ;
1255
13- const topFrame = lines [ 1 ] ?? "" ;
56+ /**
57+ * ⚠️ Note: Depending on V8 optimization, without .at(),
58+ * the stack might still contain factory internal frames
59+ * if the engine hasn't triggered the lazy stack capture yet.
60+ */
61+ expect ( stack ) . toContain ( currentFileName ) ;
62+ console . log ( stack . split ( "\n" ) . slice ( 0 , 4 ) . join ( "\n" ) ) ;
63+ } ) ;
64+
65+ // --- Scenario 3: Neverthrow Async WITHOUT .at() (The "Lost" Scene) ---
66+ test ( "Async Control: stack trace WITHOUT .at() in neverthrow" , async ( ) => {
67+ const result = await ResultAsync . fromPromise (
68+ Promise . reject ( new Error ( "Down" ) ) ,
69+ ( _error ) => {
70+ // 🚨 No .at() used here.
71+ // The engine captures the stack during 'new', but without the
72+ // explicit anchor, the trace quality depends purely on V8's mood.
73+ return AppError . LOC_ERR ( ) ;
74+ }
75+ ) ;
1476
15- const pathSegments = new URL ( import . meta. url ) . pathname . split ( '/' ) ;
16- const currentFileName = pathSegments . at ( - 1 ) ?? "" ;
77+ if ( result . isErr ( ) ) {
78+ const stack = result . error . stack ?? "" ;
79+ // Often, without .at(), the trace is harder to read or
80+ // lacks the explicit "Crime Scene" context we want to enforce.
81+ console . log ( stack . split ( "\n" ) . slice ( 0 , 4 ) . join ( "\n" ) ) ;
82+ }
83+ } ) ;
1784
85+ // --- Scenario 4: Synchronous Business Logic ---
86+ test ( "Sync: stack trace should anchor at the business caller via .at()" , ( ) => {
87+ function businessFunction ( ) {
88+ /**
89+ * 💡 THE ANCHOR POINT
90+ * We call .at() here to explicitly mark this line as the "Crime Scene".
91+ */
92+ return AppError . SYNC_ERR ( ) . at ( { context : { stage : 'init' } } ) ;
93+ }
94+
95+ const err = businessFunction ( ) ;
96+ const stack = err . stack ?? "" ;
97+ const topFrame = stack . split ( "\n" ) [ 1 ] ;
98+
99+ // 🎯 Verification: The first frame must point to 'businessFunction' in THIS file.
100+ expect ( topFrame ) . toContain ( "businessFunction" ) ;
18101 expect ( topFrame ) . toContain ( currentFileName ) ;
19102
103+ // 🛡️ Noise Removal: The factory internals (define.ts) must be sliced off.
20104 expect ( topFrame ) . not . toContain ( "define.ts" ) ;
21105
22- expect ( topFrame . length ) . toBeGreaterThan ( 0 ) ;
106+ console . log ( stack . split ( "\n" ) . slice ( 0 , 4 ) . join ( "\n" ) ) ;
107+ } ) ;
108+
109+ // --- Scenario 5: Neverthrow Async Callback ---
110+ test ( "Async: stack trace should anchor inside neverthrow callback via .at()" , async ( ) => {
111+ const url = "https://api.faulty.com" ;
112+
113+ // Simulating a failed async operation
114+ const result = await ResultAsync . fromPromise (
115+ Promise . reject ( new Error ( "Network Down" ) ) ,
116+ ( error ) => {
117+ /**
118+ * 💡 THE "CALLBACK-LOCAL" ANCHOR
119+ * Without .at(), the stack might point to neverthrow's internal dispatcher.
120+ * By calling .at() inside this anonymous closure, we lock the stack
121+ * to this exact line in the business logic.
122+ */
123+ return AppError . ASYNC_ERR ( url ) . at ( { cause : error } ) ;
124+ }
125+ ) ;
126+
127+ if ( result . isErr ( ) ) {
128+ const err = result . error ;
129+ const stack = err . stack ?? "" ;
130+ const frames = stack . split ( "\n" ) ;
131+
132+ // The top frame should represent the anonymous callback location.
133+ const topFrame = frames [ 1 ] ;
134+
135+ // 🎯 Verification: Ensure the trace points to the caller site, not the library.
136+ expect ( topFrame ) . toContain ( currentFileName ) ;
137+
138+ // 🛡️ Noise Removal:
139+ // 1. No internal 'neverthrow' frames should leak into the business trace.
140+ // 2. No internal 'thaterror' (define.ts) frames should be visible.
141+ expect ( topFrame ) . not . toContain ( "neverthrow" ) ;
142+ expect ( topFrame ) . not . toContain ( "define.ts" ) ;
143+
144+ // Metadata Verification
145+ expect ( err . cause ) . toBeDefined ( ) ;
146+ expect ( err . message ) . toContain ( url ) ;
147+
148+ console . log ( stack . split ( "\n" ) . slice ( 0 , 4 ) . join ( "\n" ) ) ;
149+ } else {
150+ throw new Error ( "Test failed: Result should be an Err" ) ;
151+ }
23152 } ) ;
24153} ) ;
0 commit comments