Files
goclaw/tests/integration/hooks_command_autodisable_test.go
viettranx c0acfa1bd9 feat(hooks): phase-08 integration tests (A-F buckets)
27 test cases across 7 files exercising real PG + SQLite backends:
- A: tenant isolation (resolver scope, cross-tenant guard)
- B: sandbox escape + per-tenant fairness (goja hardening e2e)
- C: capability tier + FireResult propagation (source-tier gate)
- D: builtin seed reconciliation + idempotency + v21 rebuild
- E: command auto-disable migration (Standard vs Lite)
- F: WS method-equivalent (Validate, Create, TestRunner)
All green with -race on PG; D6 green on sqliteonly.
2026-04-16 14:17:48 +07:00

147 lines
4.6 KiB
Go

//go:build integration
package integration
import (
"context"
"testing"
"github.com/google/uuid"
"github.com/nextlevelbuilder/goclaw/internal/edition"
"github.com/nextlevelbuilder/goclaw/internal/hooks"
"github.com/nextlevelbuilder/goclaw/internal/store/pg"
)
// Phase 08 — E bucket: DisableLegacyCommandHooks against live PG.
//
// Unit coverage in internal/hooks/migration_command_autodisable_test.go uses
// a fake store + tests the branching. Here we replay the same scenarios
// against the real PG store + live ResolveForEvent path so any drift between
// fake-store stubs and the actual SQL guards is caught.
// makeCommandHook seeds a command-handler hook for the given tenant. Returns
// the row's id; auto-cleanup via t.Cleanup.
func makeCommandHook(t *testing.T, hs hooks.HookStore, tenantID uuid.UUID, source string, enabled bool) uuid.UUID {
t.Helper()
cfg := hooks.HookConfig{
TenantID: tenantID,
Scope: hooks.ScopeTenant,
Event: hooks.EventPreToolUse,
HandlerType: hooks.HandlerCommand,
Config: map[string]any{"command": "echo ok"},
TimeoutMS: 2000,
OnTimeout: hooks.DecisionAllow,
Enabled: enabled,
Version: 1,
Source: source,
Metadata: map[string]any{},
}
id, err := hs.Create(crossTenantOwnerCtx(), cfg)
if err != nil {
t.Fatalf("Create command hook: %v", err)
}
t.Cleanup(func() { purgeAgentHookByID(t, testDB(t), id) })
return id
}
func makeScriptHook(t *testing.T, hs hooks.HookStore, tenantID uuid.UUID, source string, enabled bool) uuid.UUID {
t.Helper()
cfg := hookCfgScript(tenantID, hooks.EventPreToolUse, source, allowAllScript)
cfg.Enabled = enabled
if cfg.Source == hooks.SourceBuiltin {
cfg.Scope = hooks.ScopeGlobal
cfg.TenantID = hooks.SentinelTenantID
cfg.ID = uuid.New()
}
id, err := hs.Create(crossTenantOwnerCtx(), cfg)
if err != nil {
t.Fatalf("Create script hook: %v", err)
}
t.Cleanup(func() { purgeAgentHookByID(t, testDB(t), id) })
return id
}
// test-E1: Standard edition with 3 enabled command hooks across 2 tenants →
// all 3 disabled. Idempotent rerun returns 0.
func TestHooksE1_StandardDisablesAllCommand(t *testing.T) {
db := testDB(t)
tenantA, _ := seedTenantAgent(t, db)
tenantB := seedExtraTenant(t, db)
hs := pg.NewPGHookStore(db)
id1 := makeCommandHook(t, hs, tenantA, "ui", true)
id2 := makeCommandHook(t, hs, tenantA, "api", true)
id3 := makeCommandHook(t, hs, tenantB, "ui", true)
n, err := hooks.DisableLegacyCommandHooks(context.Background(), hs, edition.Standard)
if err != nil {
t.Fatalf("first run: %v", err)
}
if n < 3 {
t.Errorf("disabled count=%d, want >= 3", n)
}
for _, id := range []uuid.UUID{id1, id2, id3} {
row, _ := hs.GetByID(crossTenantOwnerCtx(), id)
if row == nil || row.Enabled {
t.Errorf("hook %s still enabled after migration", id)
}
}
// Idempotency: second run finds none enabled (we just disabled them all).
n2, err := hooks.DisableLegacyCommandHooks(context.Background(), hs, edition.Standard)
if err != nil {
t.Fatalf("second run: %v", err)
}
if n2 != 0 {
t.Errorf("idempotent re-run disabled=%d, want 0", n2)
}
}
// test-E2: Lite edition is a no-op even when command hooks are present.
func TestHooksE2_LiteIsNoOp(t *testing.T) {
db := testDB(t)
tenantA, _ := seedTenantAgent(t, db)
hs := pg.NewPGHookStore(db)
id := makeCommandHook(t, hs, tenantA, "ui", true)
n, err := hooks.DisableLegacyCommandHooks(context.Background(), hs, edition.Lite)
if err != nil {
t.Fatalf("Lite run: %v", err)
}
if n != 0 {
t.Errorf("Lite disabled=%d, want 0", n)
}
row, _ := hs.GetByID(crossTenantOwnerCtx(), id)
if row == nil || !row.Enabled {
t.Fatalf("Lite touched the row: enabled=%v", row.Enabled)
}
}
// test-E3: command + builtin script mix → only the command row gets disabled;
// builtin script row left alone (defensive carve-out).
func TestHooksE3_BuiltinScriptUntouched(t *testing.T) {
db := testDB(t)
tenantA, _ := seedTenantAgent(t, db)
hs := pg.NewPGHookStore(db)
cmdID := makeCommandHook(t, hs, tenantA, "ui", true)
scriptBuiltinID := makeScriptHook(t, hs, tenantA, hooks.SourceBuiltin, true)
if _, err := hooks.DisableLegacyCommandHooks(context.Background(), hs, edition.Standard); err != nil {
t.Fatalf("migration: %v", err)
}
// Command row off.
cmdRow, _ := hs.GetByID(crossTenantOwnerCtx(), cmdID)
if cmdRow == nil || cmdRow.Enabled {
t.Errorf("command row %s still enabled", cmdID)
}
// Builtin script row untouched.
scriptRow, _ := hs.GetByID(crossTenantOwnerCtx(), scriptBuiltinID)
if scriptRow == nil || !scriptRow.Enabled {
t.Errorf("builtin script row %s incorrectly disabled (defensive carve-out failed)", scriptBuiltinID)
}
}