mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-28 02:19:16 +00:00
First real builtin under Phase 04 infrastructure. Registers two DB rows
(user_prompt_submit + pre_tool_use) on startup, runs the embedded JS with
source-tier gate + dispatcher dotted-path allowlist.
- internal/hooks/builtin/pii-redactor.js (ES5.1, goja-compatible):
handle(event) returns updatedInput only when email or E.164-ish phone
actually matched. Redacts rawInput + toolInput.{command,query,content}.
- builtins.yaml: pii-redactor entry (version 1, priority 900, timeout 2s,
on_timeout=allow — missing redaction < blocking conversation).
- Drop _placeholder.js; pii-redactor.js is now the first real //go:embed match.
- Export builtin.Source(name) so handler tests load embedded JS without
going through Load()/cache state.
- Tests:
* loader_test: registry contains pii-redactor after Load().
* script_pii_redactor_test (handler-level, real goja runtime): email
masked in rawInput; +E164 phone masked; toolInput.command redacted,
toolInput.path left alone (not in mutable_fields); no PII → no
mutation; redact(redact(s)) == redact(s).
* dispatcher_test: 3 dotted-path walker cases (toolInput.command only
lets that key through; rawInput-only doesn't let forged toolInput
override original event keys; toolInput wildcard merges all keys).
Bench: ~242µs/op on 1 KiB Apple M4 Max, well under <1ms target.
124 lines
3.5 KiB
Go
124 lines
3.5 KiB
Go
package builtin
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// TestLoad_ShippedRegistry verifies the baked-in builtins parse clean at
|
|
// build time. Phase 05 ships pii-redactor (2 events), so we assert the
|
|
// registry contains at least that spec and that its embedded source file
|
|
// loaded successfully.
|
|
func TestLoad_ShippedRegistry(t *testing.T) {
|
|
if err := Load(); err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
specs := RegisteredSpecs()
|
|
var pii *Spec
|
|
for i := range specs {
|
|
if specs[i].ID == "pii-redactor" {
|
|
pii = &specs[i]
|
|
break
|
|
}
|
|
}
|
|
if pii == nil {
|
|
t.Fatalf("pii-redactor not in registry; have %v", specIDs(specs))
|
|
}
|
|
if pii.SourceFile != "pii-redactor.js" {
|
|
t.Errorf("pii-redactor source_file=%q, want pii-redactor.js", pii.SourceFile)
|
|
}
|
|
if len(pii.Events) != 2 {
|
|
t.Errorf("pii-redactor events=%v, want 2", pii.Events)
|
|
}
|
|
if src, ok := source("pii-redactor.js"); !ok || len(src) == 0 {
|
|
t.Error("pii-redactor.js bytes not cached after Load")
|
|
}
|
|
}
|
|
|
|
func specIDs(ss []Spec) []string {
|
|
out := make([]string, 0, len(ss))
|
|
for _, s := range ss {
|
|
out = append(out, s.ID)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// TestBuiltinNamespace_Stable guards against a silent namespace change that
|
|
// would orphan every already-seeded row at the next boot.
|
|
func TestBuiltinNamespace_Stable(t *testing.T) {
|
|
want := uuid.MustParse("082ab084-a25f-52b4-a4a4-eb8a816bd9a8")
|
|
if BuiltinNamespace != want {
|
|
t.Fatalf("namespace drifted: have %s, want %s", BuiltinNamespace, want)
|
|
}
|
|
}
|
|
|
|
// TestBuiltinID_Deterministic locks in the per-id hash so that a renamed id
|
|
// gets caught in review — downgrade-safety depends on stable IDs.
|
|
func TestBuiltinID_Deterministic(t *testing.T) {
|
|
a := BuiltinID("pii-redactor")
|
|
b := BuiltinID("pii-redactor")
|
|
if a != b {
|
|
t.Fatalf("BuiltinID non-deterministic: %s vs %s", a, b)
|
|
}
|
|
c := BuiltinID("sql-guard")
|
|
if a == c {
|
|
t.Fatal("distinct ids produced equal UUID — namespace collision")
|
|
}
|
|
}
|
|
|
|
// TestBuiltinEventID_Distinct ensures each event of a multi-event spec gets
|
|
// its own row id so per-event enabled toggles don't clobber siblings.
|
|
func TestBuiltinEventID_Distinct(t *testing.T) {
|
|
a := BuiltinEventID("pii-redactor", "user_prompt_submit")
|
|
b := BuiltinEventID("pii-redactor", "pre_tool_use")
|
|
if a == b {
|
|
t.Fatal("event UUIDs collided for different events")
|
|
}
|
|
if a == BuiltinID("pii-redactor") {
|
|
t.Fatal("per-event UUID must differ from bare spec id")
|
|
}
|
|
}
|
|
|
|
// TestAllowlistFor_Unknown returns nil for ids that aren't registered.
|
|
// Dispatcher treats nil as strip → defense-in-depth against scripts with
|
|
// forged source='builtin' on a random UUID.
|
|
func TestAllowlistFor_Unknown(t *testing.T) {
|
|
if err := Load(); err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if got := AllowlistFor(uuid.New()); got != nil {
|
|
t.Errorf("unknown id → allowlist=%v, want nil", got)
|
|
}
|
|
}
|
|
|
|
func TestIsBuiltin_Unknown(t *testing.T) {
|
|
if err := Load(); err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if IsBuiltin(uuid.New()) {
|
|
t.Error("random UUID reported as builtin")
|
|
}
|
|
}
|
|
|
|
// TestMetadataVersion covers the JSONB float64 decode path plus int fast path.
|
|
func TestMetadataVersion(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
meta map[string]any
|
|
want int
|
|
}{
|
|
{"nil", nil, 0},
|
|
{"missing", map[string]any{}, 0},
|
|
{"int", map[string]any{"version": 3}, 3},
|
|
{"int64", map[string]any{"version": int64(4)}, 4},
|
|
{"float64", map[string]any{"version": float64(5)}, 5},
|
|
{"string-ignored", map[string]any{"version": "oops"}, 0},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := metadataVersion(tc.meta); got != tc.want {
|
|
t.Errorf("%s: got %d want %d", tc.name, got, tc.want)
|
|
}
|
|
}
|
|
}
|