@@ -84,7 +84,7 @@ func TestError_WithContext(t *testing.T) {
8484 location := models.Location {Line : 1 , Column : 10 }
8585
8686 err := NewError (ErrCodeExpectedToken , "expected FROM, got FORM" , location )
87- err .WithContext (sql , 4 ) // Highlight "FORM" (4 characters)
87+ err = err .WithContext (sql , 4 ) // Highlight "FORM" (4 characters)
8888
8989 output := err .Error ()
9090
@@ -106,7 +106,7 @@ func TestError_WithContext(t *testing.T) {
106106
107107func TestError_WithHint (t * testing.T ) {
108108 err := NewError (ErrCodeUnexpectedToken , "unexpected token" , models.Location {Line : 1 , Column : 5 })
109- err .WithHint ("This is a helpful hint" )
109+ err = err .WithHint ("This is a helpful hint" )
110110
111111 if err .Hint != "This is a helpful hint" {
112112 t .Errorf ("WithHint() failed to set hint, got: %s" , err .Hint )
@@ -197,7 +197,7 @@ WHERE age > 18`,
197197 for _ , tt := range tests {
198198 t .Run (tt .name , func (t * testing.T ) {
199199 err := NewError (ErrCodeUnexpectedToken , "test" , tt .location )
200- err .WithContext (tt .sql , 1 )
200+ err = err .WithContext (tt .sql , 1 )
201201
202202 output := err .formatContext ()
203203
@@ -215,10 +215,127 @@ WHERE age > 18`,
215215func TestError_Unwrap (t * testing.T ) {
216216 causeErr := NewError (ErrCodeInvalidSyntax , "cause error" , models.Location {})
217217 err := NewError (ErrCodeUnexpectedToken , "wrapper error" , models.Location {})
218- err .WithCause (causeErr )
218+ err = err .WithCause (causeErr )
219219
220220 unwrapped := err .Unwrap ()
221221 if unwrapped != causeErr {
222222 t .Errorf ("Unwrap() = %v, want %v" , unwrapped , causeErr )
223223 }
224224}
225+
226+ // TestError_WithContext_Immutable verifies WithContext does not mutate the
227+ // receiver: the original *Error is unchanged, and the returned *Error carries
228+ // the new Context. Guards against H6 regressions (observer effects across
229+ // call sites that share a *Error pointer).
230+ func TestError_WithContext_Immutable (t * testing.T ) {
231+ orig := NewError (ErrCodeUnexpectedToken , "msg" , models.Location {Line : 1 , Column : 5 })
232+ if orig .Context != nil {
233+ t .Fatalf ("precondition: fresh error should have nil Context, got %+v" , orig .Context )
234+ }
235+
236+ withCtx := orig .WithContext ("SELECT * FORM users" , 4 )
237+
238+ if orig .Context != nil {
239+ t .Fatalf ("WithContext mutated the receiver: orig.Context = %+v" , orig .Context )
240+ }
241+ if withCtx .Context == nil {
242+ t .Fatalf ("WithContext returned error without Context set" )
243+ }
244+ if withCtx .Context .SQL != "SELECT * FORM users" {
245+ t .Errorf ("returned Context.SQL = %q, want %q" , withCtx .Context .SQL , "SELECT * FORM users" )
246+ }
247+ if withCtx .Context .HighlightLen != 4 {
248+ t .Errorf ("returned Context.HighlightLen = %d, want 4" , withCtx .Context .HighlightLen )
249+ }
250+ if orig == withCtx {
251+ t .Errorf ("WithContext returned the same pointer; expected a copy" )
252+ }
253+ }
254+
255+ // TestError_WithHint_Immutable verifies WithHint does not mutate the receiver.
256+ func TestError_WithHint_Immutable (t * testing.T ) {
257+ orig := NewError (ErrCodeUnexpectedToken , "msg" , models.Location {Line : 1 , Column : 5 })
258+ if orig .Hint != "" {
259+ t .Fatalf ("precondition: fresh error should have empty Hint, got %q" , orig .Hint )
260+ }
261+
262+ withHint := orig .WithHint ("new hint" )
263+
264+ if orig .Hint == "new hint" {
265+ t .Fatal ("WithHint mutated the receiver" )
266+ }
267+ if orig .Hint != "" {
268+ t .Fatalf ("receiver Hint unexpectedly changed: %q" , orig .Hint )
269+ }
270+ if withHint .Hint != "new hint" {
271+ t .Fatalf ("returned Error missing hint, got %q" , withHint .Hint )
272+ }
273+ if orig == withHint {
274+ t .Errorf ("WithHint returned the same pointer; expected a copy" )
275+ }
276+ }
277+
278+ // TestError_WithCause_Immutable verifies WithCause does not mutate the receiver.
279+ func TestError_WithCause_Immutable (t * testing.T ) {
280+ cause := NewError (ErrCodeInvalidSyntax , "root" , models.Location {})
281+ orig := NewError (ErrCodeUnexpectedToken , "wrapper" , models.Location {})
282+ if orig .Cause != nil {
283+ t .Fatalf ("precondition: fresh error should have nil Cause, got %v" , orig .Cause )
284+ }
285+
286+ withCause := orig .WithCause (cause )
287+
288+ if orig .Cause != nil {
289+ t .Fatalf ("WithCause mutated the receiver: orig.Cause = %v" , orig .Cause )
290+ }
291+ if withCause .Cause != cause {
292+ t .Fatalf ("returned Error missing cause; got %v, want %v" , withCause .Cause , cause )
293+ }
294+ if orig == withCause {
295+ t .Errorf ("WithCause returned the same pointer; expected a copy" )
296+ }
297+ }
298+
299+ // TestError_WithX_SharedReceiver_NoObserverEffects simulates the production
300+ // bug: two call sites holding the same *Error pointer. Before the fix, one
301+ // call site's WithHint would be visible to the other. After the fix, each
302+ // caller gets an independent copy.
303+ func TestError_WithX_SharedReceiver_NoObserverEffects (t * testing.T ) {
304+ shared := NewError (ErrCodeUnexpectedToken , "msg" , models.Location {Line : 1 , Column : 1 })
305+
306+ a := shared .WithHint ("hint from A" )
307+ b := shared .WithHint ("hint from B" )
308+
309+ if shared .Hint != "" {
310+ t .Fatalf ("shared receiver was mutated: shared.Hint = %q" , shared .Hint )
311+ }
312+ if a .Hint != "hint from A" {
313+ t .Errorf ("a.Hint = %q, want %q" , a .Hint , "hint from A" )
314+ }
315+ if b .Hint != "hint from B" {
316+ t .Errorf ("b.Hint = %q, want %q" , b .Hint , "hint from B" )
317+ }
318+ if a == b {
319+ t .Errorf ("both call sites got the same pointer; expected independent copies" )
320+ }
321+ }
322+
323+ // TestError_WithX_Chaining verifies the `err = err.WithA(...).WithB(...)`
324+ // fluent pattern still accumulates all fields on the final returned error.
325+ func TestError_WithX_Chaining (t * testing.T ) {
326+ cause := NewError (ErrCodeInvalidSyntax , "root" , models.Location {})
327+ err := NewError (ErrCodeUnexpectedToken , "msg" , models.Location {Line : 2 , Column : 3 }).
328+ WithContext ("SELECT 1" , 1 ).
329+ WithHint ("a hint" ).
330+ WithCause (cause )
331+
332+ if err .Context == nil || err .Context .SQL != "SELECT 1" {
333+ t .Errorf ("chained WithContext not applied: %+v" , err .Context )
334+ }
335+ if err .Hint != "a hint" {
336+ t .Errorf ("chained WithHint not applied: %q" , err .Hint )
337+ }
338+ if err .Cause != cause {
339+ t .Errorf ("chained WithCause not applied: %v" , err .Cause )
340+ }
341+ }
0 commit comments