diff --git a/cmd/gh-aw-wasm/compile_recovery.go b/cmd/gh-aw-wasm/compile_recovery.go new file mode 100644 index 00000000000..c6a13430ef9 --- /dev/null +++ b/cmd/gh-aw-wasm/compile_recovery.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "runtime/debug" +) + +func compileWorkflowPanicError(r any) error { + return fmt.Errorf("compileWorkflow panic: %v\n%s", r, debug.Stack()) +} + +func runCompileWithRecovery(doCompile func() (any, error), resolve func(any), reject func(error)) { + defer func() { + if r := recover(); r != nil { + reject(compileWorkflowPanicError(r)) + } + }() + + result, err := doCompile() + if err != nil { + reject(err) + return + } + resolve(result) +} diff --git a/cmd/gh-aw-wasm/compile_recovery_test.go b/cmd/gh-aw-wasm/compile_recovery_test.go new file mode 100644 index 00000000000..6ee97f4e689 --- /dev/null +++ b/cmd/gh-aw-wasm/compile_recovery_test.go @@ -0,0 +1,71 @@ +package main + +import ( + "errors" + "strings" + "testing" +) + +func TestRunCompileWithRecoverySuccess(t *testing.T) { + var resolved any + var rejected error + + runCompileWithRecovery( + func() (any, error) { return "ok", nil }, + func(result any) { resolved = result }, + func(err error) { rejected = err }, + ) + + if rejected != nil { + t.Fatalf("expected no rejection, got %v", rejected) + } + if resolved != "ok" { + t.Fatalf("expected resolve value %q, got %v", "ok", resolved) + } +} + +func TestRunCompileWithRecoveryError(t *testing.T) { + want := errors.New("compile failed") + var resolved any + var rejected error + + runCompileWithRecovery( + func() (any, error) { return nil, want }, + func(result any) { resolved = result }, + func(err error) { rejected = err }, + ) + + if resolved != nil { + t.Fatalf("expected no resolve value, got %v", resolved) + } + if !errors.Is(rejected, want) { + t.Fatalf("expected rejection %v, got %v", want, rejected) + } +} + +func TestRunCompileWithRecoveryPanic(t *testing.T) { + var resolved any + var rejected error + + runCompileWithRecovery( + func() (any, error) { + panic("boom") + }, + func(result any) { resolved = result }, + func(err error) { rejected = err }, + ) + + if resolved != nil { + t.Fatalf("expected no resolve value, got %v", resolved) + } + if rejected == nil { + t.Fatal("expected rejection from panic, got nil") + } + message := rejected.Error() + if !strings.Contains(message, "compileWorkflow panic: boom") { + t.Fatalf("expected panic prefix in rejection, got %q", message) + } + if !strings.Contains(message, "TestRunCompileWithRecoveryPanic") { + t.Fatalf("expected stack trace in rejection, got %q", message) + } +} diff --git a/cmd/gh-aw-wasm/main.go b/cmd/gh-aw-wasm/main.go index 8adf27c6c2c..29859e02fb5 100644 --- a/cmd/gh-aw-wasm/main.go +++ b/cmd/gh-aw-wasm/main.go @@ -47,13 +47,17 @@ func compileWorkflow(this js.Value, args []js.Value) any { go func() { defer handler.Release() - - result, err := doCompile(markdown, files, filename) - if err != nil { - reject.Invoke(js.Global().Get("Error").New(err.Error())) - return - } - resolve.Invoke(result) + runCompileWithRecovery( + func() (any, error) { + return doCompile(markdown, files, filename) + }, + func(result any) { + resolve.Invoke(result) + }, + func(err error) { + reject.Invoke(js.Global().Get("Error").New(err.Error())) + }, + ) }() return nil