Repository: tommie/v8go
Pull request #95: Add support for ObjectTemplate.MarkAsUndetectable
Author: login: stroiman
Created: 2025-04-24T11:35:59Z | Merged: 2025-05-21T07:33:16Z

Addresses #94 

Documentation is a brief copy of C++ documentation, but while testing, I noticed odd behavior.

Calling `MarkAsUndetectable()` on an a plain `ObjectTemplate` caused a seg fault. 

But following the C++ example, it used an instance template from a function template, and then using `functionTemplate.InstanceTemplate()`, and that did make the test pass.

But the C++ documentation doesn't indicate that this is only valid on instance templates, making me unsure if it should be documented.


Diff:
diff --git a/function_template.h b/function_template.h
index c1672829e..a315b1ba9 100644
--- a/function_template.h
+++ b/function_template.h
@@ -7,7 +7,11 @@
 
 namespace v8 {
 class Isolate;
-}
+template <class F>
+class FunctionCallbackInfo;
+}  // namespace v8
+
+void FunctionTemplateCallback(const v8::FunctionCallbackInfo<v8::Value>& info);
 
 typedef v8::Isolate v8Isolate;
 
diff --git a/inspector.cc b/inspector.cc
index d2ecec0fc..48c792b5c 100644
--- a/inspector.cc
+++ b/inspector.cc
@@ -8,9 +8,9 @@ using namespace v8;
 using namespace v8_inspector;
 
 /**
- * InspectorClient is an implementation of v8_inspector::V8InspectorClient that is
- * designed to be able to call back to Go code to a specific instance identified
- * by a cgo handle.
+ * InspectorClient is an implementation of v8_inspector::V8InspectorClient that
+ * is designed to be able to call back to Go code to a specific instance
+ * identified by a cgo handle.
  *
  * See also: https://pkg.go.dev/runtime/cgo#Handle
  */
diff --git a/object_template.cc b/object_template.cc
index 71255da36..6666c865a 100644
--- a/object_template.cc
+++ b/object_template.cc
@@ -4,6 +4,7 @@
 #include "deps/include/v8-isolate.h"
 #include "deps/include/v8-locker.h"
 #include "deps/include/v8-template.h"
+#include "function_template.h"
 #include "template-macros.h"
 
 using namespace v8;
@@ -50,6 +51,13 @@ void ObjectTemplateSetInternalFieldCount(TemplatePtr ptr, int field_count) {
   obj_tmpl->SetInternalFieldCount(field_count);
 }
 
+void ObjectTemplateMarkAsUndetectable(TemplatePtr ptr) {
+  LOCAL_TEMPLATE(ptr);
+
+  Local<ObjectTemplate> obj_tmpl = tmpl.As<ObjectTemplate>();
+  obj_tmpl->MarkAsUndetectable();
+}
+
 int ObjectTemplateInternalFieldCount(TemplatePtr ptr) {
   LOCAL_TEMPLATE(ptr);
 
@@ -77,3 +85,12 @@ void ObjectTemplateSetAccessorProperty(TemplatePtr ptr,
   return obj_tmpl->SetAccessorProperty(key_val, get_tmpl, set_tmpl,
                                        (PropertyAttribute)attributes);
 }
+
+void ObjectTemplateSetCallAsFunctionHandler(TemplatePtr ptr, int callback_ref) {
+  LOCAL_TEMPLATE(ptr);
+
+  Local<Integer> cbData = Integer::New(iso, callback_ref);
+
+  Local<ObjectTemplate> obj_tmpl = tmpl.As<ObjectTemplate>();
+  obj_tmpl->SetCallAsFunctionHandler(FunctionTemplateCallback, cbData);
+}
diff --git a/object_template.go b/object_template.go
index b13e1d26d..6fe5e9283 100644
--- a/object_template.go
+++ b/object_template.go
@@ -105,3 +105,27 @@ func (o *ObjectTemplate) InternalFieldCount() uint32 {
 func (o *ObjectTemplate) apply(opts *contextOptions) {
 	opts.gTmpl = o
 }
+
+// MarkAsUndetectable marks object instances of the template as undetectable. Undetectable
+// objects behave like undefined, but you can access properties defined on undetectable
+// objects.
+//
+// Note: Undetectable objects MUST have a CallAsFunctionHandler, see
+// [ObjectTemplate.SetCallAsFunctionHandler]
+func (o *ObjectTemplate) MarkAsUndetectable() {
+	C.ObjectTemplateMarkAsUndetectable(o.ptr)
+}
+
+// SetCallAsFunctionHandler sets the callback to be used when calling instances created
+// from this template. If no callback is set, instances behave like normal JavaScript
+// objects that cannot be called as a function.
+func (o *ObjectTemplate) SetCallAsFunctionHandler(callback FunctionCallbackWithError) {
+	if callback == nil {
+		panic("nil callback argument not supported")
+	}
+	cbref := o.iso.registerCallback(callback)
+	C.ObjectTemplateSetCallAsFunctionHandler(
+		o.ptr,
+		C.int(cbref),
+	)
+}
diff --git a/object_template.h b/object_template.h
index cae03a382..6b2803d18 100644
--- a/object_template.h
+++ b/object_template.h
@@ -32,6 +32,9 @@ extern void ObjectTemplateSetAccessorProperty(m_template* ptr,
                                               m_template* get,
                                               m_template* set,
                                               int attributes);
+extern void ObjectTemplateMarkAsUndetectable(m_template* ptr);
+extern void ObjectTemplateSetCallAsFunctionHandler(m_template* ptr,
+                                                   int callback_ref);
 
 #ifdef __cplusplus
 }
diff --git a/object_template_test.go b/object_template_test.go
index f6f6a8094..77fab6e73 100644
--- a/object_template_test.go
+++ b/object_template_test.go
@@ -27,10 +27,9 @@ func TestObjectTemplate(t *testing.T) {
 
 	val, _ := v8.NewValue(iso, "bar")
 	objVal := v8.NewObjectTemplate(iso)
-	bigbigint, _ := new(
-		big.Int,
-	).SetString("36893488147419099136", 10)
+
 	// larger than a single word size (64bit)
+	bigbigint, _ := new(big.Int).SetString("36893488147419099136", 10)
 	bigbignegint, _ := new(big.Int).SetString("-36893488147419099136", 10)
 
 	tests := [...]struct {
@@ -273,3 +272,123 @@ func ExampleObjectTemplate_SetAccessorProperty() {
 	// Output:
 	// Property value: Value
 }
+
+func TestObjectTemplateSetCallAsFunctionHandler(t *testing.T) {
+	t.Parallel()
+	iso := v8.NewIsolate()
+	defer iso.Dispose()
+	tmpl := v8.NewObjectTemplate(iso)
+	ctx := v8.NewContext(iso)
+	defer ctx.Close()
+
+	tmpl.SetCallAsFunctionHandler(func(info *v8.FunctionCallbackInfo) (*v8.Value, error) {
+		return v8.NewValue(iso, "42")
+	})
+	instance, err := tmpl.NewInstance(ctx)
+	if err != nil {
+		t.Fatalf("Error creating instance: %v", err)
+	}
+	ctx.Global().Set("obj", instance)
+
+	res, err := ctx.RunScript(`obj()`, "")
+	resStr := res.String()
+	if resStr != "42" {
+		t.Errorf(`unexpected result. Expected "42", got: %s`, resStr)
+	}
+}
+
+func TestObjectTemplateMarkAsUndetectable(t *testing.T) {
+	t.Parallel()
+
+	iso := v8.NewIsolate()
+	defer iso.Dispose()
+	obj := v8.NewObjectTemplate(iso)
+	obj.SetCallAsFunctionHandler(func(info *v8.FunctionCallbackInfo) (*v8.Value, error) {
+		return info.This().Value, nil
+	})
+	obj.MarkAsUndetectable()
+	ctx := v8.NewContext(iso)
+	defer ctx.Close()
+	v, err := v8.NewValue(iso, "42")
+	if err != nil {
+		t.Fatalf("Error creating value: %v", err)
+	}
+	obj.Set("val", v)
+	instance, err := obj.NewInstance(ctx)
+	if err != nil {
+		t.Fatalf("Error calling NewInstance: %v", err)
+	}
+	ctx.Global().Set("obj", instance)
+	res, err := ctx.RunScript("typeof obj", "")
+	if err != nil {
+		t.Fatalf("Error run 'typeof obj': %v", err)
+	}
+	str := res.String()
+	if str != "undefined" {
+		t.Errorf(`Expected 'typeof obj' to return "undefined", got: %#v`, str)
+	}
+	res, err = ctx.RunScript("obj.val", "")
+	if err != nil {
+		t.Fatalf("Error evaluating 'obj.val': %v", err)
+	}
+	str = res.String()
+	if str != "42" {
+		t.Errorf(`Expected 'typeof obj' to return "42", got: %s`, str)
+	}
+}
+
+func TestObjectTemplateMarkAsUndetectableOnInstanceTemplate(t *testing.T) {
+	t.Parallel()
+
+	iso := v8.NewIsolate()
+	defer iso.Dispose()
+	ctx := v8.NewContext(iso)
+	defer ctx.Close()
+
+	desc := v8.NewFunctionTemplate(iso, func(info *v8.FunctionCallbackInfo) *v8.Value {
+		return nil
+	})
+	desc.InstanceTemplate().MarkAsUndetectable()
+	desc.InstanceTemplate().
+		SetCallAsFunctionHandler(func(info *v8.FunctionCallbackInfo) (*v8.Value, error) {
+			return info.This().Value, nil
+		})
+	instance, err := desc.InstanceTemplate().NewInstance(ctx)
+	if err != nil {
+		t.Fatalf("Error creating instance: %v", err)
+	}
+	ctx.Global().Set("undetectable", instance)
+
+	res, err := ctx.RunScript("undetectable.toString()", "")
+	if err != nil {
+		t.Errorf("Error calling toString(): %v", err)
+	} else {
+		if res.String() != "[object Object]" {
+			t.Errorf(
+				`Error running "undetectable.toString()". Expected "[object Object]", got: %s`,
+				res.String(),
+			)
+		}
+	}
+
+	res, err = ctx.RunScript("typeof undetectable", "")
+	if err != nil {
+		t.Errorf("Error calling typeof undetectable: %v", err)
+	} else {
+		if res.String() != "undefined" {
+			t.Errorf(
+				`Error running "typeof undetectable". Expected "undefined", got: %s`,
+				res.String(),
+			)
+		}
+	}
+
+	res, err = ctx.RunScript("Boolean(undetectable)", "")
+	if err != nil {
+		t.Errorf("Error calling typeof undetectable: %v", err)
+	} else {
+		if res.Boolean() {
+			t.Errorf("Expected undetectable object to be falsy")
+		}
+	}
+}
diff --git a/v8go.cc b/v8go.cc
index db6cc120b..86aea6582 100644
--- a/v8go.cc
+++ b/v8go.cc
@@ -11,6 +11,7 @@
 #include "utils.h"
 
 #include "context-macros.h"
+#include "function_template.h"
 #include "isolate-macros.h"
 #include "template-macros.h"
 #include "template.h"
@@ -18,8 +19,6 @@
 
 using namespace v8;
 
-void FunctionTemplateCallback(const FunctionCallbackInfo<Value>& info);
-
 const int ScriptCompilerNoCompileOptions = ScriptCompiler::kNoCompileOptions;
 const int ScriptCompilerConsumeCodeCache = ScriptCompiler::kConsumeCodeCache;
 const int ScriptCompilerEagerCompile = ScriptCompiler::kEagerCompile;


Reviews:
user: login: tommie
body: Re. segfault. I don't quite understand. Your test now is using a plain `ObjectTemplate`, right?
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-04-24T15:00:31Z
user: login: stroiman
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-27T14:54:15Z
user: login: tommie
body: Looks generally very good. Thanks!
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-30T18:05:37Z
user: login: stroiman
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-09T09:45:53Z
user: login: stroiman
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-09T09:46:04Z
user: login: stroiman
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-09T09:55:53Z
user: login: tommie
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T12:36:07Z
user: login: stroiman
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-09T12:40:20Z
user: login: stroiman
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-09T12:43:23Z
user: login: stroiman
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-09T12:43:36Z
user: login: tommie
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T12:46:17Z
user: login: stroiman
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-09T12:46:20Z
user: login: stroiman
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-09T12:48:17Z

Review comments:
diff_hunk: @@ -26,7 +26,10 @@ func TestObjectTemplate(t *testing.T) {
 
 	val, _ := v8.NewValue(iso, "bar")
 	objVal := v8.NewObjectTemplate(iso)
-	bigbigint, _ := new(big.Int).SetString("36893488147419099136", 10) // larger than a single word size (64bit)
+	bigbigint, _ := new(
+		big.Int,
+	).SetString("36893488147419099136", 10)
+	// larger than a single word size (64bit)
path: object_template_test.go
user: login: tommie
body: Comment needs to be moved up.
created_at: 2025-04-24T14:53:46Z
author_association: OWNER
diff_hunk: @@ -173,3 +176,71 @@ func TestObjectTemplate_garbageCollection(t *testing.T) {
 
 	runtime.GC()
 }
+
+func TestObjectTemplateMarkAsUndetectable(t *testing.T) {
+	t.Parallel()
+
+	iso := v8.NewIsolate()
+	defer iso.Dispose()
+	obj := v8.NewObjectTemplate(iso)
+	obj.MarkAsUndetectable()
+	ctx := v8.NewContext(iso)
+	defer ctx.Close()
+	ft := v8.NewFunctionTemplate(
+		iso,
+		func(info *v8.FunctionCallbackInfo) *v8.Value { return nil },
+	)
+	v, err := v8.NewValue(iso, "42")
+	if err != nil {
+		t.Fatalf("Error creating value: %v", err)
+	}
+	ft.InstanceTemplate().Set("val", v)
+	instance, err := ft.InstanceTemplate().NewInstance(ctx)
+	if err != nil {
+		t.Fatalf("Error getting new instance: %v", err)
+	}
+	ctx.Global().Set("obj", instance)
+	res, err := ctx.RunScript("'undefined'", "")
+	if err != nil {
+		t.Fatalf("Error run 'typeof obj': %v", err)
+	}
+	str := res.String()
+	if str != "undefined" {
+		t.Errorf(`Expected 'typeof obj' to return "undefined", got: %s`, str)
+	}
+	res, err = ctx.RunScript("obj.val", "")
+	if err != nil {
+		t.Fatalf("Error evaluating 'obj.val': %v", err)
+	}
+	str = res.String()
+	if str != "42" {
+		t.Errorf(`Expected 'typeof obj' to return "undefined", got: %s`, str)
+	}
+
+	/*
path: object_template_test.go
user: login: tommie
body: Remove dead code.
created_at: 2025-04-24T14:56:47Z
author_association: OWNER
path: inspector.cc
user: login: stroiman
body: I have no idea why clang-format suddenly thinks it should break words differently ???
created_at: 2025-04-27T14:54:15Z
author_association: NONE
path: inspector.cc
user: login: tommie
body: :shrug: Well, the tools run our lives now.
created_at: 2025-04-30T17:53:30Z
author_association: OWNER
diff_hunk: @@ -173,3 +175,127 @@ func TestObjectTemplate_garbageCollection(t *testing.T) {
 
 	runtime.GC()
 }
+
+func TestObjectTemplateSetCallAsFunctionHandler(t *testing.T) {
+	t.Parallel()
+	iso := v8.NewIsolate()
+	defer iso.Dispose()
+	tmpl := v8.NewObjectTemplate(iso)
+	if _, err := tmpl.NewInstance(nil); err == nil {
path: object_template_test.go
user: login: tommie
body: Does this do anything useful in this test?
created_at: 2025-04-30T17:56:54Z
author_association: OWNER
diff_hunk: @@ -173,3 +175,127 @@ func TestObjectTemplate_garbageCollection(t *testing.T) {
 
 	runtime.GC()
 }
+
+func TestObjectTemplateSetCallAsFunctionHandler(t *testing.T) {
+	t.Parallel()
+	iso := v8.NewIsolate()
+	defer iso.Dispose()
+	tmpl := v8.NewObjectTemplate(iso)
+	if _, err := tmpl.NewInstance(nil); err == nil {
+		t.Error("expected error but got <nil>")
+	}
+
+	ctx := v8.NewContext(iso)
+	defer ctx.Close()
+
+	tmpl.SetCallAsFunctionHandler(func(info *v8.FunctionCallbackInfo) (*v8.Value, error) {
+		return v8.NewValue(iso, "42")
+	})
+	instance, err := tmpl.NewInstance(ctx)
+	if err != nil {
+		t.Fatalf("Error creating instance: %v", err)
+	}
+	ctx.Global().Set("obj", instance)
+
+	res, err := ctx.RunScript(`obj()`, "")
+	resStr := res.String()
+	if resStr != "42" {
+		t.Errorf(`unexpected result. Expected "42", got: %s`, resStr)
+	}
+}
+
+func TestObjectTemplateMarkAsUndetectable(t *testing.T) {
+	t.Parallel()
+
+	iso := v8.NewIsolate()
+	defer iso.Dispose()
+	obj := v8.NewObjectTemplate(iso)
+	obj.MarkAsUndetectable()
path: object_template_test.go
user: login: tommie
body: Since SetCallAsFunctionHandler can be called without MarkAsUndetectable, but not vice versa, I think it would make sense to always run them in that order. To ensure objects are always in a valid state.
created_at: 2025-04-30T18:03:01Z
author_association: OWNER
diff_hunk: @@ -173,3 +175,127 @@ func TestObjectTemplate_garbageCollection(t *testing.T) {
 
 	runtime.GC()
 }
+
+func TestObjectTemplateSetCallAsFunctionHandler(t *testing.T) {
+	t.Parallel()
+	iso := v8.NewIsolate()
+	defer iso.Dispose()
+	tmpl := v8.NewObjectTemplate(iso)
+	if _, err := tmpl.NewInstance(nil); err == nil {
+		t.Error("expected error but got <nil>")
+	}
+
+	ctx := v8.NewContext(iso)
+	defer ctx.Close()
+
+	tmpl.SetCallAsFunctionHandler(func(info *v8.FunctionCallbackInfo) (*v8.Value, error) {
+		return v8.NewValue(iso, "42")
+	})
+	instance, err := tmpl.NewInstance(ctx)
+	if err != nil {
+		t.Fatalf("Error creating instance: %v", err)
+	}
+	ctx.Global().Set("obj", instance)
+
+	res, err := ctx.RunScript(`obj()`, "")
+	resStr := res.String()
+	if resStr != "42" {
+		t.Errorf(`unexpected result. Expected "42", got: %s`, resStr)
+	}
+}
+
+func TestObjectTemplateMarkAsUndetectable(t *testing.T) {
+	t.Parallel()
+
+	iso := v8.NewIsolate()
+	defer iso.Dispose()
+	obj := v8.NewObjectTemplate(iso)
+	obj.MarkAsUndetectable()
+	obj.SetCallAsFunctionHandler(func(info *v8.FunctionCallbackInfo) (*v8.Value, error) {
+		return info.This().Value, nil
+	})
+	ctx := v8.NewContext(iso)
+	defer ctx.Close()
+	v, err := v8.NewValue(iso, "42")
+	if err != nil {
+		t.Fatalf("Error creating value: %v", err)
+	}
+	obj.Set("val", v)
+	instance, err := obj.NewInstance(ctx)
+	if err != nil {
+		t.Fatalf("Error calling NewInstance: %v", err)
+	}
+	ctx.Global().Set("obj", instance)
+	res, err := ctx.RunScript("typeof obj", "")
+	if err != nil {
+		t.Fatalf("Error run 'typeof obj': %v", err)
+	}
+	str := res.String()
+	if str != "undefined" {
+		t.Errorf(`Expected 'typeof obj' to return "undefined", got: %#v`, str)
+	}
+	res, err = ctx.RunScript("obj.val", "")
+	if err != nil {
+		t.Fatalf("Error evaluating 'obj.val': %v", err)
+	}
+	str = res.String()
+	if str != "42" {
+		t.Errorf(`Expected 'typeof obj' to return "42", got: %s`, str)
+	}
+}
+
+func TestObjectTemplateMarkAsUndetectableOnInstanceTemplate(t *testing.T) {
+	t.Parallel()
+
+	iso := v8.NewIsolate()
+	defer iso.Dispose()
+	ctx := v8.NewContext(iso)
+	defer ctx.Close()
+
+	desc := v8.NewFunctionTemplate(iso, func(info *v8.FunctionCallbackInfo) *v8.Value {
+		return nil
+	})
+	desc.InstanceTemplate().MarkAsUndetectable()
+	desc.InstanceTemplate().
+		SetCallAsFunctionHandler(func(info *v8.FunctionCallbackInfo) (*v8.Value, error) {
+			return info.This().Value, nil
+		})
+	instance, err := desc.InstanceTemplate().NewInstance(ctx)
+	if err != nil {
+		t.Fatalf("Error creating instance: %v", err)
+	}
+	ctx.Global().Set("undetectable", instance)
+
+	res, err := ctx.RunScript("undetectable.toString()", "")
+	if err != nil {
+		t.Errorf("Error calling toString(): %v", err)
+	} else {
+		if res.String() != "[object Object]" {
+			t.Errorf(
+				`Error running "undetectable.toString()". Expected "[object Object]", got: %s`,
+				res.String(),
+			)
+		}
+	}
+
+	res, err = ctx.RunScript("typeof undetectable", "")
+	if err != nil {
+		t.Errorf("Error calling typeof undetectable: %v", err)
+	} else {
+		if res.String() != "undefined" {
+			t.Errorf(
+				`Error running "typeof undetectable". Expected "undefined", got: %s`,
+				res.String(),
+			)
+		}
+	}
+
+	res, err = ctx.RunScript("if (undetectable) { true; } else { false; }", "")
path: object_template_test.go
user: login: tommie
body: Why not just `Boolean(undetectable)` ?
created_at: 2025-04-30T18:04:30Z
author_association: OWNER
diff_hunk: @@ -173,3 +175,127 @@ func TestObjectTemplate_garbageCollection(t *testing.T) {
 
 	runtime.GC()
 }
+
+func TestObjectTemplateSetCallAsFunctionHandler(t *testing.T) {
+	t.Parallel()
+	iso := v8.NewIsolate()
+	defer iso.Dispose()
+	tmpl := v8.NewObjectTemplate(iso)
+	if _, err := tmpl.NewInstance(nil); err == nil {
path: object_template_test.go
user: login: stroiman
body: Nope, remnant of a previous attempt I failed to clean up.
created_at: 2025-05-09T09:45:53Z
author_association: NONE
diff_hunk: @@ -173,3 +175,127 @@ func TestObjectTemplate_garbageCollection(t *testing.T) {
 
 	runtime.GC()
 }
+
+func TestObjectTemplateSetCallAsFunctionHandler(t *testing.T) {
+	t.Parallel()
+	iso := v8.NewIsolate()
+	defer iso.Dispose()
+	tmpl := v8.NewObjectTemplate(iso)
+	if _, err := tmpl.NewInstance(nil); err == nil {
+		t.Error("expected error but got <nil>")
+	}
+
+	ctx := v8.NewContext(iso)
+	defer ctx.Close()
+
+	tmpl.SetCallAsFunctionHandler(func(info *v8.FunctionCallbackInfo) (*v8.Value, error) {
+		return v8.NewValue(iso, "42")
+	})
+	instance, err := tmpl.NewInstance(ctx)
+	if err != nil {
+		t.Fatalf("Error creating instance: %v", err)
+	}
+	ctx.Global().Set("obj", instance)
+
+	res, err := ctx.RunScript(`obj()`, "")
+	resStr := res.String()
+	if resStr != "42" {
+		t.Errorf(`unexpected result. Expected "42", got: %s`, resStr)
+	}
+}
+
+func TestObjectTemplateMarkAsUndetectable(t *testing.T) {
+	t.Parallel()
+
+	iso := v8.NewIsolate()
+	defer iso.Dispose()
+	obj := v8.NewObjectTemplate(iso)
+	obj.MarkAsUndetectable()
path: object_template_test.go
user: login: stroiman
body: Good point, fixed.
created_at: 2025-05-09T09:46:04Z
author_association: NONE
diff_hunk: @@ -173,3 +175,127 @@ func TestObjectTemplate_garbageCollection(t *testing.T) {
 
 	runtime.GC()
 }
+
+func TestObjectTemplateSetCallAsFunctionHandler(t *testing.T) {
+	t.Parallel()
+	iso := v8.NewIsolate()
+	defer iso.Dispose()
+	tmpl := v8.NewObjectTemplate(iso)
+	if _, err := tmpl.NewInstance(nil); err == nil {
+		t.Error("expected error but got <nil>")
+	}
+
+	ctx := v8.NewContext(iso)
+	defer ctx.Close()
+
+	tmpl.SetCallAsFunctionHandler(func(info *v8.FunctionCallbackInfo) (*v8.Value, error) {
+		return v8.NewValue(iso, "42")
+	})
+	instance, err := tmpl.NewInstance(ctx)
+	if err != nil {
+		t.Fatalf("Error creating instance: %v", err)
+	}
+	ctx.Global().Set("obj", instance)
+
+	res, err := ctx.RunScript(`obj()`, "")
+	resStr := res.String()
+	if resStr != "42" {
+		t.Errorf(`unexpected result. Expected "42", got: %s`, resStr)
+	}
+}
+
+func TestObjectTemplateMarkAsUndetectable(t *testing.T) {
+	t.Parallel()
+
+	iso := v8.NewIsolate()
+	defer iso.Dispose()
+	obj := v8.NewObjectTemplate(iso)
+	obj.MarkAsUndetectable()
+	obj.SetCallAsFunctionHandler(func(info *v8.FunctionCallbackInfo) (*v8.Value, error) {
+		return info.This().Value, nil
+	})
+	ctx := v8.NewContext(iso)
+	defer ctx.Close()
+	v, err := v8.NewValue(iso, "42")
+	if err != nil {
+		t.Fatalf("Error creating value: %v", err)
+	}
+	obj.Set("val", v)
+	instance, err := obj.NewInstance(ctx)
+	if err != nil {
+		t.Fatalf("Error calling NewInstance: %v", err)
+	}
+	ctx.Global().Set("obj", instance)
+	res, err := ctx.RunScript("typeof obj", "")
+	if err != nil {
+		t.Fatalf("Error run 'typeof obj': %v", err)
+	}
+	str := res.String()
+	if str != "undefined" {
+		t.Errorf(`Expected 'typeof obj' to return "undefined", got: %#v`, str)
+	}
+	res, err = ctx.RunScript("obj.val", "")
+	if err != nil {
+		t.Fatalf("Error evaluating 'obj.val': %v", err)
+	}
+	str = res.String()
+	if str != "42" {
+		t.Errorf(`Expected 'typeof obj' to return "42", got: %s`, str)
+	}
+}
+
+func TestObjectTemplateMarkAsUndetectableOnInstanceTemplate(t *testing.T) {
+	t.Parallel()
+
+	iso := v8.NewIsolate()
+	defer iso.Dispose()
+	ctx := v8.NewContext(iso)
+	defer ctx.Close()
+
+	desc := v8.NewFunctionTemplate(iso, func(info *v8.FunctionCallbackInfo) *v8.Value {
+		return nil
+	})
+	desc.InstanceTemplate().MarkAsUndetectable()
+	desc.InstanceTemplate().
+		SetCallAsFunctionHandler(func(info *v8.FunctionCallbackInfo) (*v8.Value, error) {
+			return info.This().Value, nil
+		})
+	instance, err := desc.InstanceTemplate().NewInstance(ctx)
+	if err != nil {
+		t.Fatalf("Error creating instance: %v", err)
+	}
+	ctx.Global().Set("undetectable", instance)
+
+	res, err := ctx.RunScript("undetectable.toString()", "")
+	if err != nil {
+		t.Errorf("Error calling toString(): %v", err)
+	} else {
+		if res.String() != "[object Object]" {
+			t.Errorf(
+				`Error running "undetectable.toString()". Expected "[object Object]", got: %s`,
+				res.String(),
+			)
+		}
+	}
+
+	res, err = ctx.RunScript("typeof undetectable", "")
+	if err != nil {
+		t.Errorf("Error calling typeof undetectable: %v", err)
+	} else {
+		if res.String() != "undefined" {
+			t.Errorf(
+				`Error running "typeof undetectable". Expected "undefined", got: %s`,
+				res.String(),
+			)
+		}
+	}
+
+	res, err = ctx.RunScript("if (undetectable) { true; } else { false; }", "")
path: object_template_test.go
user: login: stroiman
body: Why indeed 🤣

In fact, it could have been `RunScript("!!undetectable")` (what I'd normally write in JS myself) or simply `RunScript("undetectable")` because the Go line `res.Boolean()` converts to a boolean value just as JS `Boolean()` does.

But I kept your suggestion, as it requires the least amount of JS knowledge to understand.
created_at: 2025-05-09T09:55:53Z
author_association: NONE
diff_hunk: @@ -173,3 +175,127 @@ func TestObjectTemplate_garbageCollection(t *testing.T) {
 
 	runtime.GC()
 }
+
+func TestObjectTemplateSetCallAsFunctionHandler(t *testing.T) {
+	t.Parallel()
+	iso := v8.NewIsolate()
+	defer iso.Dispose()
+	tmpl := v8.NewObjectTemplate(iso)
+	if _, err := tmpl.NewInstance(nil); err == nil {
+		t.Error("expected error but got <nil>")
+	}
+
+	ctx := v8.NewContext(iso)
+	defer ctx.Close()
+
+	tmpl.SetCallAsFunctionHandler(func(info *v8.FunctionCallbackInfo) (*v8.Value, error) {
+		return v8.NewValue(iso, "42")
+	})
+	instance, err := tmpl.NewInstance(ctx)
+	if err != nil {
+		t.Fatalf("Error creating instance: %v", err)
+	}
+	ctx.Global().Set("obj", instance)
+
+	res, err := ctx.RunScript(`obj()`, "")
+	resStr := res.String()
+	if resStr != "42" {
+		t.Errorf(`unexpected result. Expected "42", got: %s`, resStr)
+	}
+}
+
+func TestObjectTemplateMarkAsUndetectable(t *testing.T) {
+	t.Parallel()
+
+	iso := v8.NewIsolate()
+	defer iso.Dispose()
+	obj := v8.NewObjectTemplate(iso)
+	obj.MarkAsUndetectable()
+	obj.SetCallAsFunctionHandler(func(info *v8.FunctionCallbackInfo) (*v8.Value, error) {
+		return info.This().Value, nil
+	})
+	ctx := v8.NewContext(iso)
+	defer ctx.Close()
+	v, err := v8.NewValue(iso, "42")
+	if err != nil {
+		t.Fatalf("Error creating value: %v", err)
+	}
+	obj.Set("val", v)
+	instance, err := obj.NewInstance(ctx)
+	if err != nil {
+		t.Fatalf("Error calling NewInstance: %v", err)
+	}
+	ctx.Global().Set("obj", instance)
+	res, err := ctx.RunScript("typeof obj", "")
+	if err != nil {
+		t.Fatalf("Error run 'typeof obj': %v", err)
+	}
+	str := res.String()
+	if str != "undefined" {
+		t.Errorf(`Expected 'typeof obj' to return "undefined", got: %#v`, str)
+	}
+	res, err = ctx.RunScript("obj.val", "")
+	if err != nil {
+		t.Fatalf("Error evaluating 'obj.val': %v", err)
+	}
+	str = res.String()
+	if str != "42" {
+		t.Errorf(`Expected 'typeof obj' to return "42", got: %s`, str)
+	}
+}
+
+func TestObjectTemplateMarkAsUndetectableOnInstanceTemplate(t *testing.T) {
+	t.Parallel()
+
+	iso := v8.NewIsolate()
+	defer iso.Dispose()
+	ctx := v8.NewContext(iso)
+	defer ctx.Close()
+
+	desc := v8.NewFunctionTemplate(iso, func(info *v8.FunctionCallbackInfo) *v8.Value {
+		return nil
+	})
+	desc.InstanceTemplate().MarkAsUndetectable()
+	desc.InstanceTemplate().
+		SetCallAsFunctionHandler(func(info *v8.FunctionCallbackInfo) (*v8.Value, error) {
+			return info.This().Value, nil
+		})
+	instance, err := desc.InstanceTemplate().NewInstance(ctx)
+	if err != nil {
+		t.Fatalf("Error creating instance: %v", err)
+	}
+	ctx.Global().Set("undetectable", instance)
+
+	res, err := ctx.RunScript("undetectable.toString()", "")
+	if err != nil {
+		t.Errorf("Error calling toString(): %v", err)
+	} else {
+		if res.String() != "[object Object]" {
+			t.Errorf(
+				`Error running "undetectable.toString()". Expected "[object Object]", got: %s`,
+				res.String(),
+			)
+		}
+	}
+
+	res, err = ctx.RunScript("typeof undetectable", "")
+	if err != nil {
+		t.Errorf("Error calling typeof undetectable: %v", err)
+	} else {
+		if res.String() != "undefined" {
+			t.Errorf(
+				`Error running "typeof undetectable". Expected "undefined", got: %s`,
+				res.String(),
+			)
+		}
+	}
+
+	res, err = ctx.RunScript("if (undetectable) { true; } else { false; }", "")
path: object_template_test.go
user: login: tommie
body: Unrelated: Sad thing about `Boolean()`. TypeScript can't seem to infer that `array.filter(Boolean)` means the result is `NonNullable`, but it does understand `array.filter(x => !!x)`. (Might just be tsserver.) So weird.
created_at: 2025-05-09T12:36:06Z
author_association: OWNER
diff_hunk: @@ -173,3 +175,127 @@ func TestObjectTemplate_garbageCollection(t *testing.T) {
 
 	runtime.GC()
 }
+
+func TestObjectTemplateSetCallAsFunctionHandler(t *testing.T) {
+	t.Parallel()
+	iso := v8.NewIsolate()
+	defer iso.Dispose()
+	tmpl := v8.NewObjectTemplate(iso)
+	if _, err := tmpl.NewInstance(nil); err == nil {
+		t.Error("expected error but got <nil>")
+	}
+
+	ctx := v8.NewContext(iso)
+	defer ctx.Close()
+
+	tmpl.SetCallAsFunctionHandler(func(info *v8.FunctionCallbackInfo) (*v8.Value, error) {
+		return v8.NewValue(iso, "42")
+	})
+	instance, err := tmpl.NewInstance(ctx)
+	if err != nil {
+		t.Fatalf("Error creating instance: %v", err)
+	}
+	ctx.Global().Set("obj", instance)
+
+	res, err := ctx.RunScript(`obj()`, "")
+	resStr := res.String()
+	if resStr != "42" {
+		t.Errorf(`unexpected result. Expected "42", got: %s`, resStr)
+	}
+}
+
+func TestObjectTemplateMarkAsUndetectable(t *testing.T) {
+	t.Parallel()
+
+	iso := v8.NewIsolate()
+	defer iso.Dispose()
+	obj := v8.NewObjectTemplate(iso)
+	obj.MarkAsUndetectable()
+	obj.SetCallAsFunctionHandler(func(info *v8.FunctionCallbackInfo) (*v8.Value, error) {
+		return info.This().Value, nil
+	})
+	ctx := v8.NewContext(iso)
+	defer ctx.Close()
+	v, err := v8.NewValue(iso, "42")
+	if err != nil {
+		t.Fatalf("Error creating value: %v", err)
+	}
+	obj.Set("val", v)
+	instance, err := obj.NewInstance(ctx)
+	if err != nil {
+		t.Fatalf("Error calling NewInstance: %v", err)
+	}
+	ctx.Global().Set("obj", instance)
+	res, err := ctx.RunScript("typeof obj", "")
+	if err != nil {
+		t.Fatalf("Error run 'typeof obj': %v", err)
+	}
+	str := res.String()
+	if str != "undefined" {
+		t.Errorf(`Expected 'typeof obj' to return "undefined", got: %#v`, str)
+	}
+	res, err = ctx.RunScript("obj.val", "")
+	if err != nil {
+		t.Fatalf("Error evaluating 'obj.val': %v", err)
+	}
+	str = res.String()
+	if str != "42" {
+		t.Errorf(`Expected 'typeof obj' to return "42", got: %s`, str)
+	}
+}
+
+func TestObjectTemplateMarkAsUndetectableOnInstanceTemplate(t *testing.T) {
+	t.Parallel()
+
+	iso := v8.NewIsolate()
+	defer iso.Dispose()
+	ctx := v8.NewContext(iso)
+	defer ctx.Close()
+
+	desc := v8.NewFunctionTemplate(iso, func(info *v8.FunctionCallbackInfo) *v8.Value {
+		return nil
+	})
+	desc.InstanceTemplate().MarkAsUndetectable()
+	desc.InstanceTemplate().
+		SetCallAsFunctionHandler(func(info *v8.FunctionCallbackInfo) (*v8.Value, error) {
+			return info.This().Value, nil
+		})
+	instance, err := desc.InstanceTemplate().NewInstance(ctx)
+	if err != nil {
+		t.Fatalf("Error creating instance: %v", err)
+	}
+	ctx.Global().Set("undetectable", instance)
+
+	res, err := ctx.RunScript("undetectable.toString()", "")
+	if err != nil {
+		t.Errorf("Error calling toString(): %v", err)
+	} else {
+		if res.String() != "[object Object]" {
+			t.Errorf(
+				`Error running "undetectable.toString()". Expected "[object Object]", got: %s`,
+				res.String(),
+			)
+		}
+	}
+
+	res, err = ctx.RunScript("typeof undetectable", "")
+	if err != nil {
+		t.Errorf("Error calling typeof undetectable: %v", err)
+	} else {
+		if res.String() != "undefined" {
+			t.Errorf(
+				`Error running "typeof undetectable". Expected "undefined", got: %s`,
+				res.String(),
+			)
+		}
+	}
+
+	res, err = ctx.RunScript("if (undetectable) { true; } else { false; }", "")
path: object_template_test.go
user: login: stroiman
body: I remember sometime in the not too distant past, a 5.x release that improved on type inference, type narrowing, and array functions - exactly which scenario, I can't remember.
created_at: 2025-05-09T12:40:20Z
author_association: NONE
diff_hunk: @@ -76,22 +76,22 @@ func (o *ObjectTemplate) apply(opts *contextOptions) {
 	opts.gTmpl = o
 }
 
-// Mark object instances of the template as undetectable. Undetectable objects
-// behave like undefined, but you can access properties defined on undetectable
+// MarkAsUndetectable marks object instances of the template as undetectable. Undetectable
+// objects behave like `undefined`, but you can access properties defined on undetectable
 // objects.
 //
-// Note: Undetectable objects MUST have a CallAsFunctionHandler, see
+// Note: Undetectable objects MUST have a `CallAsFunctionHandler`, see
path: object_template.go
user: login: stroiman
body: That is not idiomatic godoc, to use backticks for functions in doc comments. Could be a link though, [ObjectTemplate.CallAsFunctionHandler] I believe is the way.


created_at: 2025-05-09T12:43:23Z
author_association: NONE
diff_hunk: @@ -76,22 +76,22 @@ func (o *ObjectTemplate) apply(opts *contextOptions) {
 	opts.gTmpl = o
 }
 
-// Mark object instances of the template as undetectable. Undetectable objects
-// behave like undefined, but you can access properties defined on undetectable
+// MarkAsUndetectable marks object instances of the template as undetectable. Undetectable
path: object_template.go
user: login: stroiman
body: Thanks, good catch.
created_at: 2025-05-09T12:43:36Z
author_association: NONE
diff_hunk: @@ -76,22 +76,22 @@ func (o *ObjectTemplate) apply(opts *contextOptions) {
 	opts.gTmpl = o
 }
 
-// Mark object instances of the template as undetectable. Undetectable objects
-// behave like undefined, but you can access properties defined on undetectable
+// MarkAsUndetectable marks object instances of the template as undetectable. Undetectable
+// objects behave like `undefined`, but you can access properties defined on undetectable
 // objects.
 //
-// Note: Undetectable objects MUST have a CallAsFunctionHandler, see
+// Note: Undetectable objects MUST have a `CallAsFunctionHandler`, see
path: object_template.go
user: login: tommie
body: Ah, fair enough.

Note that it's not a reference to the `Set` function, but the concept of a handler.
created_at: 2025-05-09T12:46:16Z
author_association: OWNER
diff_hunk: @@ -76,22 +76,22 @@ func (o *ObjectTemplate) apply(opts *contextOptions) {
 	opts.gTmpl = o
 }
 
-// Mark object instances of the template as undetectable. Undetectable objects
-// behave like undefined, but you can access properties defined on undetectable
+// MarkAsUndetectable marks object instances of the template as undetectable. Undetectable
+// objects behave like `undefined`, but you can access properties defined on undetectable
 // objects.
 //
-// Note: Undetectable objects MUST have a CallAsFunctionHandler, see
+// Note: Undetectable objects MUST have a `CallAsFunctionHandler`, see
path: object_template.go
user: login: stroiman
body: https://tip.golang.org/doc/comment#func


created_at: 2025-05-09T12:46:20Z
author_association: NONE
diff_hunk: @@ -76,22 +76,22 @@ func (o *ObjectTemplate) apply(opts *contextOptions) {
 	opts.gTmpl = o
 }
 
-// Mark object instances of the template as undetectable. Undetectable objects
-// behave like undefined, but you can access properties defined on undetectable
+// MarkAsUndetectable marks object instances of the template as undetectable. Undetectable
+// objects behave like `undefined`, but you can access properties defined on undetectable
 // objects.
 //
-// Note: Undetectable objects MUST have a CallAsFunctionHandler, see
+// Note: Undetectable objects MUST have a `CallAsFunctionHandler`, see
path: object_template.go
user: login: stroiman
body: Ah, true 
created_at: 2025-05-09T12:48:17Z
author_association: NONE

PR comments:
user: login: stroiman
created_at: 2025-04-24T15:03:30Z
body: > Re. segfault. I don't quite understand. Your test now is using a plain `ObjectTemplate`, right?

Hmmm, I don't understand either, looking at the code, it's not what I though it was ... Let me just check up on that again.
author_association: NONE
user: login: stroiman
created_at: 2025-04-24T15:20:45Z
body: Arg, I don't know what the h... I did, but this is way off, and doesn't work at all.

The tests are passing because they are completely wrong. 

```
	res, err := ctx.RunScript("'undefined'", "") WTF!!! evaluate the string "undefined"
    // ...
	str := res.String()
	if str != "undefined" {
	    // Yeah, not a surprice
		t.Errorf(`Expected 'typeof obj' to return "undefined", got: %s`, str)
	}
```

🙈

I hope I sleep better tonight.

It's not a seg-fault - wrong word, but what I assume is a C++ exception bubbling. It could appear that an unreachable object needs to have a call handler, i.e., the object is callable like a function object.

The C++ example had the following

```
desc->InstanceTemplate()->SetCallAsFunctionHandler(ReturnThis);
```

`SetCallAsFunctionHandler` is not exposed in v8go either. If it's just a matter of setting a FunctionTemplate, then it should be simple enough to implement. But if it involves new ways of dealing with callbacks, then some more though need to go into this.

```
#
# Fatal error in , line 0
# Check failed: !IsUndefined(obj->GetInstanceCallHandler(), isolate).
#
#
#
#FailureMessage Object: 0x175e4e938
==== C stack trace ===============================

    0   v8go.test                           0x0000000103b783b0 v8::base::debug::StackTrace::StackTrace() + 24
 
...

SIGTRAP: trace trap
PC=0x10392e564 m=8 sigcode=0
signal arrived during cgo execution
```
author_association: NONE
user: login: stroiman
created_at: 2025-04-24T15:24:36Z
body: And the code doc comment I mentioned, was unsaved in my editor at the time of commit ...
author_association: NONE
user: login: tommie
created_at: 2025-04-24T15:57:30Z
body: > `!IsUndefined(obj->GetInstanceCallHandler(), isolate).`

It's a check from here, which I assume is when the object is instantiated, making it more obvious it's a one-off hack:

```c++
  // Mark as undetectable if needed.
  if (obj->undetectable()) {
    // We only allow callable undetectable receivers here, since this whole
    // undetectable business is only to support document.all, which is both
    // undetectable and callable. If we ever see the need to have an object
    // that is undetectable but not callable, we need to update the types.h
    // to allow encoding this.
    CHECK(!IsUndefined(obj->GetInstanceCallHandler(), isolate));

    if (Protectors::IsNoUndetectableObjectsIntact(isolate)) {
      Protectors::InvalidateNoUndetectableObjects(isolate);
    }
    map->set_is_undetectable(true);
  }
```

https://source.chromium.org/chromium/chromium/src/+/main:v8/src/api/api-natives.cc;l=654;drc=a5ce8d1de21670482f940066e40084f6138d1be7

So yeah, needs a call handler. I think that can use the existing `FunctionCallback` glue.

```c++
  /**
   * Sets the callback to be used when calling instances created from
   * this template as a function.  If no callback is set, instances
   * behave like normal JavaScript objects that cannot be called as a
   * function.
   */
  void SetCallAsFunctionHandler(FunctionCallback callback,
                                Local<Value> data = Local<Value>());
```

https://source.chromium.org/chromium/chromium/src/+/main:v8/include/v8-template.h;l=983;drc=24a8b2c6105aed6b4ab527f213e6f49e53ff5a51
author_association: OWNER
user: login: go-solve-api
created_at: 2025-04-25T14:42:25Z
body: I have no understanding of C code at all. This is the code implemented in someone else's nodeJS that I found online
```c++
#include <node.h>

namespace faker {

using v8::FunctionCallbackInfo;/src
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::Value;
using v8::Context;
using v8::FunctionTemplate;

static void ReturnThis(const FunctionCallbackInfo<Value>& args) {
  args.GetReturnValue().Set(args.This());
}

void DocumentAll(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();

  Local<FunctionTemplate> desc = FunctionTemplate::New(isolate);
  desc->InstanceTemplate()->MarkAsUndetectable();  // undetectable
  desc->InstanceTemplate()->SetCallAsFunctionHandler(ReturnThis);  // callable

  Local<Object> obj = desc->GetFunction(context).ToLocalChecked()
                            ->NewInstance(context).ToLocalChecked();
  // printf("Create <document.all> Object From faker.node\n");
  args.GetReturnValue().Set(obj);
}


void Initialize(Local<Object> exports) {
  NODE_SET_METHOD(exports, "DocumentAll", DocumentAll);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

}
```


author_association: NONE
user: login: stroiman
created_at: 2025-04-27T10:28:43Z
body: Ok, think I got it now :)

Added support for MarkAsFunctionHandler. I'll just add some code docs for this too.
author_association: NONE
user: login: stroiman
created_at: 2025-04-27T10:30:56Z
body: Oh, I see I didn't have the C formatter installed in my editor either ...
author_association: NONE
user: login: stroiman
created_at: 2025-04-27T10:38:42Z
body: The missing C-file formatting made me look where it's "specified", which is `cgo.go`, where it says

```
//go:generate clang-format -i --verbose -style=Chromium v8go.h v8go.cc
```

So it doesn't check formatting of all the other `.h` and `.cc` files. Changing to `*.h *.cc` doesn't work, I assume because it's not running in a shell, which is responsible for evaluating wildcards.

I think it would make sense to move this to a makefile perhaps? Or another solution that supports wildcards.
author_association: NONE
user: login: go-solve-api
created_at: 2025-04-29T07:30:01Z
body: ....
author_association: NONE
user: login: stroiman
created_at: 2025-04-29T08:15:50Z
body: @tommie did you notice the changes? Should work now ...

I didn't re-request the review, just did now.
author_association: NONE
user: login: stroiman
created_at: 2025-05-09T09:56:43Z
body: I think this should be good now.
author_association: NONE
user: login: stroiman
created_at: 2025-05-09T10:29:02Z
body: And now it's even pushed to the right branch ...
author_association: NONE
user: login: tommie
created_at: 2025-05-09T12:46:50Z
body: Good to go. Needs rebase (and potentially unscrewing the backticks :)
author_association: OWNER
user: login: stroiman
created_at: 2025-05-13T15:32:13Z
body: > Good to go. Needs rebase (and potentially unscrewing the backticks :)

Should I do that, I was unsure as you brought up the backticks.
author_association: NONE
user: login: tommie
created_at: 2025-05-13T19:34:30Z
body: > > Good to go. Needs rebase (and potentially unscrewing the backticks :)
> 
> Should I do that, I was unsure as you brought up the backticks.

I'm happy to keep the backticks, but it's not a strong opinion.
author_association: OWNER
user: login: tommie
created_at: 2025-05-21T07:33:07Z
body: Gotta do something about that flaky TestCPUProfileNode.
author_association: OWNER