fix: align Grok hooks with official contracts
This commit is contained in:
1 parent
c60b46b994
commit
f71db95502
19 files changed
+2241
-641
No files matched your search
+235
-255
@@ -1,7 +1,6 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@@ -10,37 +9,50 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGrokRegistryAndFactory(t *testing.T) {
|
||||
func TestGrokRegistryFactoryAndRuntimeOwnership(t *testing.T) {
|
||||
entry := SupportedAgents[grokAgentKey]
|
||||
if entry.Name != "Grok Build" || entry.ConfigDir != "~/.grok/hooks" || entry.ConfigFile != grokConfigFile {
|
||||
if entry.Name != "Grok Build" || entry.ConfigDir != "~/.grok/hooks" ||
|
||||
entry.ConfigFile != grokConfigFile {
|
||||
t.Fatalf("Grok registry entry = %#v", entry)
|
||||
}
|
||||
if _, ok := NewPlugin(grokAgentKey).(*GrokPlugin); !ok {
|
||||
t.Fatalf("Grok plugin factory returned %T", NewPlugin(grokAgentKey))
|
||||
plugin, ok := NewPlugin(grokAgentKey).(*GrokPlugin)
|
||||
if !ok || !plugin.ManagesGuardRuntime() {
|
||||
t.Fatalf("Grok plugin = %#v", NewPlugin(grokAgentKey))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrokGeneratedArgumentParsers(t *testing.T) {
|
||||
posixCommand := quotePOSIXArgument("/tmp/node's runtime") + " " +
|
||||
quotePOSIXArgument("/tmp/home's agent/guard.js")
|
||||
executable, next, ok := readGrokPOSIXArgument(posixCommand, 0)
|
||||
if !ok || executable != "/tmp/node's runtime" || posixCommand[next] != ' ' {
|
||||
t.Fatalf("parse POSIX executable = %q, %d, %v", executable, next, ok)
|
||||
func TestGrokCommandRoundTripAndRejectsInjection(t *testing.T) {
|
||||
fixture := prepareGrokFixture(t, grokFixtureOptions{})
|
||||
nodePath, err := resolveNodeRuntime()
|
||||
if err != nil {
|
||||
t.Fatalf("resolve Node.js runtime: %v", err)
|
||||
}
|
||||
script, end, ok := readGrokPOSIXArgument(posixCommand, next+1)
|
||||
if !ok || script != "/tmp/home's agent/guard.js" || end != len(posixCommand) {
|
||||
t.Fatalf("parse POSIX script = %q, %d, %v", script, end, ok)
|
||||
command, err := buildGrokCommand(nodePath, fixture.guardPath)
|
||||
if err != nil {
|
||||
t.Fatalf("build Grok command: %v", err)
|
||||
}
|
||||
|
||||
windowsCommand := quoteGrokWindowsArgument(`C:\Program Files\node.exe`) + " " +
|
||||
quoteGrokWindowsArgument(`C:\home with spaces\.elydora\agent-1\guard.js`)
|
||||
executable, next, ok = readGrokWindowsArgument(windowsCommand, 0)
|
||||
if !ok || executable != `C:\Program Files\node.exe` || windowsCommand[next] != ' ' {
|
||||
t.Fatalf("parse Windows executable = %q, %d, %v", executable, next, ok)
|
||||
executable, script, ok := parseGrokCommand(command)
|
||||
if !ok || !sameGrokPath(executable, nodePath) ||
|
||||
!sameGrokPath(script, fixture.guardPath) {
|
||||
t.Fatalf("parsed Grok command = %q, %q, %v", executable, script, ok)
|
||||
}
|
||||
script, end, ok = readGrokWindowsArgument(windowsCommand, next+1)
|
||||
if !ok || script != `C:\home with spaces\.elydora\agent-1\guard.js` || end != len(windowsCommand) {
|
||||
t.Fatalf("parse Windows script = %q, %d, %v", script, end, ok)
|
||||
if strings.Contains(command, "%ELYDORA_HOOK_PATH%") {
|
||||
t.Fatalf("Grok command exposes an expandable path: %q", command)
|
||||
}
|
||||
if _, err := buildGrokCommand("node", fixture.guardPath); err == nil {
|
||||
t.Fatal("accepted a relative Grok runtime path")
|
||||
}
|
||||
if _, err := buildGrokCommand(nodePath, "guard.js"); err == nil {
|
||||
t.Fatal("accepted a relative Grok script path")
|
||||
}
|
||||
for _, invalid := range []string{
|
||||
command + " --inspect",
|
||||
command + "\nexit 0",
|
||||
"node " + fixture.guardPath,
|
||||
} {
|
||||
if _, _, ok := parseGrokCommand(invalid); ok {
|
||||
t.Fatalf("accepted non-canonical command %q", invalid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,56 +60,52 @@ func TestGrokInstallPreservesConfigAndIsIdempotent(t *testing.T) {
|
||||
existing := `{
|
||||
"schemaVersion": 1,
|
||||
"hooks": {
|
||||
"SessionStart": [{"matcher":"startup","hooks":[{"type":"http","url":"https://example.test/hook","timeout":5,"headers":{"x":"keep"}}],"label":"keep"}],
|
||||
"PreToolUse": [{"matcher":"Bash","hooks":[{"type":"command","command":"existing-command","timeout":5}]}]
|
||||
"SessionStart": [{"hooks":[{"type":"http","url":"https://example.test/hook","timeout":0}],"label":"keep"}],
|
||||
"PreToolUse": [{"matcher":"Bash|run_terminal_command","hooks":[{"type":"command","command":"existing-command","timeout":5,"env":{"A":"B"}}]}]
|
||||
}
|
||||
}`
|
||||
fixture := prepareGrokFixture(t, grokFixtureOptions{existingRaw: &existing})
|
||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||
t.Fatalf("install Grok hooks: %v", err)
|
||||
}
|
||||
first, err := os.ReadFile(fixture.configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read first Grok config: %v", err)
|
||||
}
|
||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||
t.Fatalf("repeat Grok install: %v", err)
|
||||
}
|
||||
second, err := os.ReadFile(fixture.configPath)
|
||||
if err != nil || string(second) != string(first) {
|
||||
t.Fatalf("repeat install changed config: %v", err)
|
||||
}
|
||||
settings := readGrokTestObject(t, fixture.configPath)
|
||||
if settings["schemaVersion"] != float64(1) {
|
||||
t.Fatalf("schema version changed: %#v", settings["schemaVersion"])
|
||||
}
|
||||
hooks := requireObject(t, settings["hooks"])
|
||||
if len(requireArray(t, hooks["SessionStart"])) != 1 ||
|
||||
len(requireArray(t, hooks["PreToolUse"])) != 2 ||
|
||||
len(requireArray(t, hooks["PostToolUse"])) != 1 {
|
||||
t.Fatalf("unexpected hooks after repeat install: %#v", hooks)
|
||||
}
|
||||
guard := grokTestManagedHandler(t, settings, "PreToolUse", grokGuardScript)
|
||||
audit := grokTestManagedHandler(t, settings, "PostToolUse", grokAuditScript)
|
||||
for _, handler := range []map[string]any{guard, audit} {
|
||||
wantKeys := map[string]struct{}{"type": {}, "command": {}, "timeout": {}}
|
||||
if len(handler) != len(wantKeys) {
|
||||
t.Fatalf("handler has extra fields: %#v", handler)
|
||||
}
|
||||
for key := range handler {
|
||||
if _, exists := wantKeys[key]; !exists {
|
||||
t.Fatalf("handler has unexpected field %q", key)
|
||||
}
|
||||
}
|
||||
if handler["type"] != "command" || handler["timeout"] != grokHookTimeout {
|
||||
t.Fatalf("unexpected handler: %#v", handler)
|
||||
}
|
||||
}
|
||||
runtimeConfig := readGrokTestObject(t, fixture.runtimeConfig)
|
||||
if runtimeConfig["agent_name"] != grokAgentKey {
|
||||
t.Fatalf("runtime agent name = %#v", runtimeConfig["agent_name"])
|
||||
len(requireArray(t, hooks["PreToolUse"])) != 2 {
|
||||
t.Fatalf("user hooks changed: %#v", hooks)
|
||||
}
|
||||
requireStrictGrokTriple(t, settings)
|
||||
for _, path := range []string{
|
||||
filepath.Join(fixture.homeDir, ".grok", "hooks", grokConfigFile),
|
||||
filepath.Join(fixture.homeDir, ".claude", "settings.json"),
|
||||
filepath.Join(fixture.homeDir, ".cursor", "hooks.json"),
|
||||
filepath.Join(fixture.homeDir, ".grok", "hooks", grokConfigFile),
|
||||
} {
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Fatalf("compatibility or default config was written at %s: %v", path, err)
|
||||
if _, err := os.Lstat(path); !os.IsNotExist(err) {
|
||||
t.Fatalf("compatibility config was written at %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
for _, path := range []string{
|
||||
fixture.guardPath, fixture.hookPath, fixture.runtimeConfig, fixture.privateKey,
|
||||
} {
|
||||
if info, err := os.Lstat(path); err != nil || !info.Mode().IsRegular() {
|
||||
t.Fatalf("managed runtime %s = %v, %v", path, info, err)
|
||||
}
|
||||
}
|
||||
assertNoGrokTransactionArtifacts(t, fixture.homeDir)
|
||||
}
|
||||
|
||||
func TestGrokEmptyHomeOverrideUsesDefault(t *testing.T) {
|
||||
@@ -106,208 +114,166 @@ func TestGrokEmptyHomeOverrideUsesDefault(t *testing.T) {
|
||||
t.Fatalf("install Grok hooks: %v", err)
|
||||
}
|
||||
status, err := fixture.plugin.Status()
|
||||
if err != nil || !status.Installed {
|
||||
if err != nil || !status.Installed || status.ConfigPath != fixture.configPath {
|
||||
t.Fatalf("default-home status = %#v, %v", status, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrokCommandsBlockAndForwardPayloadByteForByte(t *testing.T) {
|
||||
func TestGrokMigratesLegacyCommandsAndPreservesLookalikes(t *testing.T) {
|
||||
fixture := prepareGrokFixture(t, grokFixtureOptions{})
|
||||
legacyGuard := legacyGrokCommand(t, fixture.guardPath)
|
||||
legacyAudit := legacyGrokCommand(t, fixture.hookPath)
|
||||
lookalike := legacyGuard + " --inspect"
|
||||
existing := map[string]any{"hooks": map[string]any{
|
||||
"PreToolUse": []any{
|
||||
map[string]any{"hooks": []any{buildGrokHandler(legacyGuard)}},
|
||||
map[string]any{"hooks": []any{buildGrokHandler(lookalike)}},
|
||||
},
|
||||
"PostToolUse": []any{
|
||||
map[string]any{"hooks": []any{buildGrokHandler(legacyAudit)}},
|
||||
},
|
||||
}}
|
||||
if err := os.MkdirAll(filepath.Dir(fixture.configPath), 0700); err != nil {
|
||||
t.Fatalf("create Grok hooks directory: %v", err)
|
||||
}
|
||||
writeGrokTestObject(t, fixture.configPath, existing)
|
||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||
t.Fatalf("install Grok hooks: %v", err)
|
||||
}
|
||||
capturePath := filepath.Join(t.TempDir(), "captured-event.json")
|
||||
captureJSON, err := json.Marshal(capturePath)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal capture path: %v", err)
|
||||
}
|
||||
captureScript := "const fs = require('node:fs'); const chunks = []; " +
|
||||
"process.stdin.on('data', chunk => chunks.push(chunk)); " +
|
||||
"process.stdin.on('end', () => fs.writeFileSync(" + string(captureJSON) + ", Buffer.concat(chunks)));\n"
|
||||
if err := os.WriteFile(fixture.hookPath, []byte(captureScript), 0700); err != nil {
|
||||
t.Fatalf("write capture hook: %v", err)
|
||||
t.Fatalf("migrate Grok hooks: %v", err)
|
||||
}
|
||||
settings := readGrokTestObject(t, fixture.configPath)
|
||||
prePayload := `{"hookEventName":"PreToolUse","sessionId":"session-1","cwd":"workspace","workspaceRoot":"workspace","toolName":"Bash","toolInput":{"command":"echo test"}}`
|
||||
guard := grokTestManagedHandler(t, settings, "PreToolUse", grokGuardScript)
|
||||
exitCode, stderr := runGrokCommand(t, guard["command"].(string), fixture.homeDir, prePayload)
|
||||
if exitCode != 2 || !strings.Contains(stderr, "Agent is frozen by Elydora") {
|
||||
t.Fatalf("guard exit = %d, stderr = %q", exitCode, stderr)
|
||||
requireStrictGrokTriple(t, settings)
|
||||
raw, _ := os.ReadFile(fixture.configPath)
|
||||
if !strings.Contains(string(raw), "--inspect") {
|
||||
t.Fatalf("ownership lookalike was removed: %s", raw)
|
||||
}
|
||||
postPayload := `{"hookEventName":"PostToolUse","sessionId":"session-1","cwd":"workspace","workspaceRoot":"workspace","toolName":"Bash","toolInput":{"command":"echo test"},"toolResult":{"output":"test"}}`
|
||||
audit := grokTestManagedHandler(t, settings, "PostToolUse", grokAuditScript)
|
||||
exitCode, stderr = runGrokCommand(t, audit["command"].(string), fixture.homeDir, postPayload)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("audit exit = %d, stderr = %q", exitCode, stderr)
|
||||
}
|
||||
captured, err := os.ReadFile(capturePath)
|
||||
if err != nil || string(captured) != postPayload {
|
||||
t.Fatalf("captured payload = %q, %v", captured, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrokStatusRequiresPairAndBothRuntimes(t *testing.T) {
|
||||
fixture := prepareGrokFixture(t, grokFixtureOptions{})
|
||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||
t.Fatalf("install Grok hooks: %v", err)
|
||||
}
|
||||
status, err := fixture.plugin.Status()
|
||||
if err != nil || !status.Installed || !status.HookConfigured || !status.HookScriptExists {
|
||||
t.Fatalf("installed status = %#v, %v", status, err)
|
||||
}
|
||||
settings := readGrokTestObject(t, fixture.configPath)
|
||||
hooks := requireObject(t, settings["hooks"])
|
||||
delete(hooks, "PostToolUse")
|
||||
writeGrokTestObject(t, fixture.configPath, settings)
|
||||
status, err = fixture.plugin.Status()
|
||||
if err != nil || status.Installed || status.HookConfigured {
|
||||
t.Fatalf("incomplete status = %#v, %v", status, err)
|
||||
}
|
||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||
t.Fatalf("restore Grok hooks: %v", err)
|
||||
}
|
||||
if err := os.Remove(fixture.guardPath); err != nil {
|
||||
t.Fatalf("remove guard: %v", err)
|
||||
}
|
||||
status, err = fixture.plugin.Status()
|
||||
if err != nil || status.Installed || !status.HookConfigured || status.HookScriptExists {
|
||||
t.Fatalf("missing-runtime status = %#v, %v", status, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrokUninstallPreservesMixedHandlersAndExactAgent(t *testing.T) {
|
||||
existing := `{"owner":"user","hooks":{"Notification":[]}}`
|
||||
fixture := prepareGrokFixture(t, grokFixtureOptions{existingRaw: &existing})
|
||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||
t.Fatalf("install Grok hooks: %v", err)
|
||||
}
|
||||
settings := readGrokTestObject(t, fixture.configPath)
|
||||
hooks := requireObject(t, settings["hooks"])
|
||||
preGroups := requireArray(t, hooks["PreToolUse"])
|
||||
managedGroup := requireObject(t, preGroups[len(preGroups)-1])
|
||||
managedGroup["hooks"] = append(requireArray(t, managedGroup["hooks"]), map[string]any{
|
||||
"type": "command", "command": "user-command", "timeout": grokHookTimeout,
|
||||
})
|
||||
nodePath, err := resolveNodeRuntime()
|
||||
if err != nil {
|
||||
t.Fatalf("resolve Node.js: %v", err)
|
||||
}
|
||||
preGroups = append(preGroups,
|
||||
map[string]any{"hooks": []any{buildGrokHandler(buildGrokCommand(nodePath, filepath.Join(fixture.agentDir, "guard.js.backup")))}},
|
||||
map[string]any{"hooks": []any{buildGrokHandler(buildGrokCommand(nodePath, filepath.Join(filepath.Dir(fixture.agentDir), "agent-10", grokGuardScript)))}},
|
||||
groups := requireArray(t, hooks["PreToolUse"])
|
||||
managed := requireObject(t, groups[len(groups)-1])
|
||||
managed["hooks"] = append(
|
||||
requireArray(t, managed["hooks"]),
|
||||
map[string]any{"type": "command", "command": "user-command", "timeout": float64(10)},
|
||||
)
|
||||
hooks["PreToolUse"] = preGroups
|
||||
writeGrokTestObject(t, fixture.configPath, settings)
|
||||
uninstallID := grokTestAgentID
|
||||
if runtime.GOOS == "windows" {
|
||||
uninstallID = "AGENT-1"
|
||||
}
|
||||
if err := fixture.plugin.Uninstall(uninstallID); err != nil {
|
||||
if err := fixture.plugin.Uninstall(grokTestAgentID); err != nil {
|
||||
t.Fatalf("uninstall Grok hooks: %v", err)
|
||||
}
|
||||
remaining := readGrokTestObject(t, fixture.configPath)
|
||||
remainingHooks := requireObject(t, remaining["hooks"])
|
||||
if remaining["owner"] != "user" || len(requireArray(t, remainingHooks["PreToolUse"])) != 3 {
|
||||
if _, exists := remainingHooks["PostToolUse"]; exists {
|
||||
t.Fatalf("managed success hook remains: %#v", remainingHooks)
|
||||
}
|
||||
if _, exists := remainingHooks["PostToolUseFailure"]; exists {
|
||||
t.Fatalf("managed failure hook remains: %#v", remainingHooks)
|
||||
}
|
||||
commands := []string{}
|
||||
for _, groupValue := range requireArray(t, remainingHooks["PreToolUse"]) {
|
||||
for _, handlerValue := range requireArray(t, requireObject(t, groupValue)["hooks"]) {
|
||||
commands = append(commands, requireObject(t, handlerValue)["command"].(string))
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(commands, []string{lookalike, "user-command"}) {
|
||||
t.Fatalf("remaining commands = %#v", commands)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrokUninstallPreservesUserConfigAndRemovesOwnedConfig(t *testing.T) {
|
||||
userSource := `{"owner":"user","hooks":{"Notification":[]}}`
|
||||
fixture := prepareGrokFixture(t, grokFixtureOptions{existingRaw: &userSource})
|
||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||
t.Fatalf("install user Grok hooks: %v", err)
|
||||
}
|
||||
if err := fixture.plugin.Uninstall(grokTestAgentID); err != nil {
|
||||
t.Fatalf("uninstall user Grok hooks: %v", err)
|
||||
}
|
||||
remaining := readGrokTestObject(t, fixture.configPath)
|
||||
if remaining["owner"] != "user" || !reflect.DeepEqual(
|
||||
requireObject(t, remaining["hooks"])["Notification"],
|
||||
[]any{},
|
||||
) {
|
||||
t.Fatalf("user config changed: %#v", remaining)
|
||||
}
|
||||
if !reflect.DeepEqual(requireArray(t, remainingHooks["Notification"]), []any{}) {
|
||||
t.Fatalf("empty notification changed: %#v", remainingHooks["Notification"])
|
||||
}
|
||||
raw, _ := os.ReadFile(fixture.configPath)
|
||||
if !strings.Contains(string(raw), "guard.js.backup") || !strings.Contains(string(raw), "agent-10") {
|
||||
t.Fatalf("similar handlers were removed: %s", raw)
|
||||
}
|
||||
if _, exists := remainingHooks["PostToolUse"]; exists {
|
||||
t.Fatalf("managed PostToolUse remains: %#v", remainingHooks["PostToolUse"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrokInstallReplacesStaleHandlersForEveryAgent(t *testing.T) {
|
||||
fixture := prepareGrokFixture(t, grokFixtureOptions{})
|
||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||
t.Fatalf("install Grok hooks: %v", err)
|
||||
owned := prepareGrokFixture(t, grokFixtureOptions{})
|
||||
if err := owned.plugin.Install(owned.config); err != nil {
|
||||
t.Fatalf("install owned Grok hooks: %v", err)
|
||||
}
|
||||
settings := readGrokTestObject(t, fixture.configPath)
|
||||
hooks := requireObject(t, settings["hooks"])
|
||||
nodePath, err := resolveNodeRuntime()
|
||||
if err != nil {
|
||||
t.Fatalf("resolve Node.js: %v", err)
|
||||
if err := owned.plugin.Uninstall(grokTestAgentID); err != nil {
|
||||
t.Fatalf("uninstall owned Grok hooks: %v", err)
|
||||
}
|
||||
for _, contract := range []struct{ event, script string }{
|
||||
{"PreToolUse", grokGuardScript}, {"PostToolUse", grokAuditScript},
|
||||
} {
|
||||
hooks[contract.event] = append(requireArray(t, hooks[contract.event]), map[string]any{
|
||||
"hooks": []any{buildGrokHandler(buildGrokCommand(
|
||||
nodePath, filepath.Join(filepath.Dir(fixture.agentDir), "agent-old", contract.script),
|
||||
))},
|
||||
})
|
||||
}
|
||||
writeGrokTestObject(t, fixture.configPath, settings)
|
||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||
t.Fatalf("replace stale Grok hooks: %v", err)
|
||||
}
|
||||
raw, _ := os.ReadFile(fixture.configPath)
|
||||
current := readGrokTestObject(t, fixture.configPath)
|
||||
currentHooks := requireObject(t, current["hooks"])
|
||||
if strings.Contains(string(raw), "agent-old") ||
|
||||
len(requireArray(t, currentHooks["PreToolUse"])) != 1 ||
|
||||
len(requireArray(t, currentHooks["PostToolUse"])) != 1 {
|
||||
t.Fatalf("stale hooks remain: %s", raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrokUninstallPreservesUntouchedEmptyGroup(t *testing.T) {
|
||||
existing := `{"owner":"user"}`
|
||||
fixture := prepareGrokFixture(t, grokFixtureOptions{existingRaw: &existing})
|
||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||
t.Fatalf("install Grok hooks: %v", err)
|
||||
}
|
||||
settings := readGrokTestObject(t, fixture.configPath)
|
||||
hooks := requireObject(t, settings["hooks"])
|
||||
hooks["PreToolUse"] = []any{map[string]any{"hooks": []any{}}}
|
||||
writeGrokTestObject(t, fixture.configPath, settings)
|
||||
if err := fixture.plugin.Uninstall(grokTestAgentID); err != nil {
|
||||
t.Fatalf("uninstall Grok hook: %v", err)
|
||||
}
|
||||
remaining := requireObject(t, readGrokTestObject(t, fixture.configPath)["hooks"])
|
||||
preGroups := requireArray(t, remaining["PreToolUse"])
|
||||
if len(preGroups) != 1 || len(requireArray(t, requireObject(t, preGroups[0])["hooks"])) != 0 {
|
||||
t.Fatalf("untouched empty group changed: %#v", remaining)
|
||||
}
|
||||
if _, exists := remaining["PostToolUse"]; exists {
|
||||
t.Fatalf("managed event remains: %#v", remaining)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrokUninstallRemovesOwnedConfig(t *testing.T) {
|
||||
fixture := prepareGrokFixture(t, grokFixtureOptions{})
|
||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||
t.Fatalf("install Grok hooks: %v", err)
|
||||
}
|
||||
if err := fixture.plugin.Uninstall(grokTestAgentID); err != nil {
|
||||
t.Fatalf("uninstall Grok hooks: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(fixture.configPath); !os.IsNotExist(err) {
|
||||
if _, err := os.Lstat(owned.configPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("owned Grok config remains: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrokMalformedConfigAndShapesPreventRuntimeWrites(t *testing.T) {
|
||||
func TestGrokStatusRequiresExactTripleIdentityAndRuntimeFiles(t *testing.T) {
|
||||
fixture := prepareGrokFixture(t, grokFixtureOptions{})
|
||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||
t.Fatalf("install Grok hooks: %v", err)
|
||||
}
|
||||
assertInstalled := func(want bool) {
|
||||
t.Helper()
|
||||
status, err := fixture.plugin.Status()
|
||||
if err != nil || status.Installed != want {
|
||||
t.Fatalf("Grok status = %#v, %v; want installed %v", status, err, want)
|
||||
}
|
||||
}
|
||||
assertInstalled(true)
|
||||
settings := readGrokTestObject(t, fixture.configPath)
|
||||
hooks := requireObject(t, settings["hooks"])
|
||||
delete(hooks, "PostToolUseFailure")
|
||||
writeGrokTestObject(t, fixture.configPath, settings)
|
||||
assertInstalled(false)
|
||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||
t.Fatalf("repair Grok hooks: %v", err)
|
||||
}
|
||||
settings = readGrokTestObject(t, fixture.configPath)
|
||||
hooks = requireObject(t, settings["hooks"])
|
||||
hooks["PostToolUseFailure"] = append(
|
||||
requireArray(t, hooks["PostToolUseFailure"]),
|
||||
requireArray(t, hooks["PostToolUseFailure"])[0],
|
||||
)
|
||||
writeGrokTestObject(t, fixture.configPath, settings)
|
||||
assertInstalled(false)
|
||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||
t.Fatalf("repair duplicate Grok hooks: %v", err)
|
||||
}
|
||||
if err := os.Remove(fixture.guardPath); err != nil {
|
||||
t.Fatalf("remove guard runtime: %v", err)
|
||||
}
|
||||
status, err := fixture.plugin.Status()
|
||||
if err != nil || status.Installed || !status.HookConfigured ||
|
||||
status.HookScriptExists {
|
||||
t.Fatalf("missing guard status = %#v, %v", status, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrokRejectsInvalidHookShapesBeforeRuntimeWrites(t *testing.T) {
|
||||
for _, testCase := range []struct{ name, raw, want string }{
|
||||
{"malformed", "{ malformed", "parse Grok hooks config"},
|
||||
{"null-root", "null", "must contain a JSON object"},
|
||||
{"null-hooks", `{"hooks":null}`, `field "hooks" must be an object`},
|
||||
{"null-event", `{"hooks":{"PreToolUse":null}}`, `field "hooks.PreToolUse" must be an array`},
|
||||
{"null-group", `{"hooks":{"PreToolUse":[null]}}`, "must be an object"},
|
||||
{"null-matcher", `{"hooks":{"PreToolUse":[{"matcher":null,"hooks":[]}]}}`, "matcher must be a string"},
|
||||
{"null-handlers", `{"hooks":{"PreToolUse":[{"hooks":null}]}}`, "must contain a hooks array"},
|
||||
{"empty-command", `{"hooks":{"PreToolUse":[{"hooks":[{"type":"command","command":""}]}]}}`, "requires a non-empty command"},
|
||||
{"object-type", `{"hooks":{"PreToolUse":[{"hooks":[{"type":{},"command":"x"}]}]}}`, "unsupported type"},
|
||||
{"empty-url", `{"hooks":{"PreToolUse":[{"hooks":[{"type":"http","url":""}]}]}}`, "requires a non-empty url"},
|
||||
{"zero-timeout", `{"hooks":{"PreToolUse":[{"hooks":[{"type":"command","command":"x","timeout":0}]}]}}`, "positive finite number"},
|
||||
{"malformed", "{ malformed", "parse Grok user hooks"},
|
||||
{"comments", "{\"hooks\":{} // comment\n}", "invalid character"},
|
||||
{"trailing comma", `{"hooks":{},}`, "invalid character"},
|
||||
{"non-finite timeout", `{"hooks":{"PreToolUse":[{"hooks":[{"type":"command","command":"x","timeout":NaN}]}]}}`, "invalid character"},
|
||||
{"duplicate", `{"hooks":{},"hooks":{}}`, "duplicate"},
|
||||
{"hooks", `{"hooks":null}`, `field "hooks" must be an object`},
|
||||
{"event", `{"hooks":{"PreToolUse":null}}`, "must be an array"},
|
||||
{"group", `{"hooks":{"PreToolUse":[null]}}`, "must be an object"},
|
||||
{"matcher", `{"hooks":{"PreToolUse":[{"matcher":1,"hooks":[]}]}}`, "matcher must be a string"},
|
||||
{"lifecycle matcher", `{"hooks":{"SessionStart":[{"matcher":"x","hooks":[]}]}}`, "cannot declare a matcher"},
|
||||
{"handler", `{"hooks":{"PreToolUse":[{"hooks":[null]}]}}`, "must be an object"},
|
||||
{"type", `{"hooks":{"PreToolUse":[{"hooks":[{"type":"file"}]}]}}`, "unsupported type"},
|
||||
{"command", `{"hooks":{"PreToolUse":[{"hooks":[{"type":"command","command":""}]}]}}`, "non-empty command"},
|
||||
{"http", `{"hooks":{"PostToolUse":[{"hooks":[{"type":"http","url":""}]}]}}`, "non-empty url"},
|
||||
{"negative timeout", `{"hooks":{"PreToolUse":[{"hooks":[{"type":"command","command":"x","timeout":-1}]}]}}`, "non-negative integer"},
|
||||
{"fraction timeout", `{"hooks":{"PreToolUse":[{"hooks":[{"type":"command","command":"x","timeout":1.5}]}]}}`, "non-negative integer"},
|
||||
{"unsafe timeout", `{"hooks":{"PreToolUse":[{"hooks":[{"type":"command","command":"x","timeout":9007199254740992}]}]}}`, "non-negative integer"},
|
||||
{"environment", `{"hooks":{"PreToolUse":[{"hooks":[{"type":"command","command":"x","env":{"A":1}}]}]}}`, "env must map names to strings"},
|
||||
} {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
fixture := prepareGrokFixture(t, grokFixtureOptions{existingRaw: &testCase.raw})
|
||||
fixture := prepareGrokFixture(
|
||||
t,
|
||||
grokFixtureOptions{existingRaw: grokString(testCase.raw)},
|
||||
)
|
||||
err := fixture.plugin.Install(fixture.config)
|
||||
if err == nil || !strings.Contains(err.Error(), testCase.want) {
|
||||
t.Fatalf("install error = %v, want %q", err, testCase.want)
|
||||
@@ -316,53 +282,67 @@ func TestGrokMalformedConfigAndShapesPreventRuntimeWrites(t *testing.T) {
|
||||
if readErr != nil || string(raw) != testCase.raw {
|
||||
t.Fatalf("original config changed: %q, %v", raw, readErr)
|
||||
}
|
||||
for _, path := range []string{fixture.hookPath, fixture.runtimeConfig, fixture.privateKey} {
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Fatalf("runtime write occurred at %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
assertNoGrokRuntimeWrites(t, fixture)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrokMissingGuardPreventsWrites(t *testing.T) {
|
||||
fixture := prepareGrokFixture(t, grokFixtureOptions{withoutGuard: true})
|
||||
func TestGrokRejectsRenderedConfigBeyondManagedSourceLimit(t *testing.T) {
|
||||
prefix := `{"padding":"`
|
||||
suffix := `"}`
|
||||
padding := strings.Repeat(
|
||||
"a",
|
||||
maxManagedSourceBytes-len(prefix)-len(suffix),
|
||||
)
|
||||
raw := prefix + padding + suffix
|
||||
fixture := prepareGrokFixture(t, grokFixtureOptions{existingRaw: &raw})
|
||||
err := fixture.plugin.Install(fixture.config)
|
||||
if err == nil || !strings.Contains(err.Error(), "guard runtime is missing") {
|
||||
if err == nil || !strings.Contains(err.Error(), "exceeds") {
|
||||
t.Fatalf("install error = %v", err)
|
||||
}
|
||||
for _, path := range []string{fixture.configPath, fixture.hookPath, fixture.runtimeConfig, fixture.privateKey} {
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Fatalf("write occurred at %s: %v", path, err)
|
||||
}
|
||||
current, readErr := os.ReadFile(fixture.configPath)
|
||||
if readErr != nil || string(current) != raw {
|
||||
t.Fatalf("large source changed: size=%d, err=%v", len(current), readErr)
|
||||
}
|
||||
assertNoGrokRuntimeWrites(t, fixture)
|
||||
}
|
||||
|
||||
func TestGrokAcceptsOfficialZeroTimeoutAndStringEnvironment(t *testing.T) {
|
||||
raw := `{"hooks":{"PreToolUse":[{"hooks":[{"type":"command","command":"user","timeout":0,"env":{"A":"B"}}]}]}}`
|
||||
fixture := prepareGrokFixture(t, grokFixtureOptions{existingRaw: &raw})
|
||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||
t.Fatalf("install Grok hooks with official fields: %v", err)
|
||||
}
|
||||
hooks := requireObject(t, readGrokTestObject(t, fixture.configPath)["hooks"])
|
||||
user := requireObject(t, requireArray(t, requireObject(
|
||||
t,
|
||||
requireArray(t, hooks["PreToolUse"])[0],
|
||||
)["hooks"])[0])
|
||||
if user["timeout"] != float64(0) {
|
||||
t.Fatalf("zero timeout changed: %#v", user)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrokStatusSurfacesMalformedRuntimeMetadata(t *testing.T) {
|
||||
func TestGrokRuntimeConfigOmitsEmptyOptionalToken(t *testing.T) {
|
||||
fixture := prepareGrokFixture(t, grokFixtureOptions{})
|
||||
fixture.config.Token = ""
|
||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||
t.Fatalf("install Grok hooks: %v", err)
|
||||
t.Fatalf("install Grok hooks without token: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(fixture.runtimeConfig, []byte("{ malformed"), 0600); err != nil {
|
||||
t.Fatalf("corrupt runtime config: %v", err)
|
||||
}
|
||||
if _, err := fixture.plugin.Status(); err == nil || !strings.Contains(err.Error(), "parse Elydora runtime config") {
|
||||
t.Fatalf("status error = %v", err)
|
||||
config := readGrokTestObject(t, fixture.runtimeConfig)
|
||||
if _, exists := config["token"]; exists {
|
||||
t.Fatalf("empty optional token was persisted: %#v", config)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrokAtomicWritesLeaveNoTemporaryFiles(t *testing.T) {
|
||||
fixture := prepareGrokFixture(t, grokFixtureOptions{})
|
||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||
t.Fatalf("install Grok hooks: %v", err)
|
||||
}
|
||||
entries, err := os.ReadDir(filepath.Dir(fixture.configPath))
|
||||
if err != nil {
|
||||
t.Fatalf("read Grok config directory: %v", err)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if strings.HasSuffix(entry.Name(), ".tmp") {
|
||||
t.Fatalf("temporary file remains: %s", entry.Name())
|
||||
func TestGrokWindowsAgentIDMatchingIsPlatformCorrect(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
if !sameGrokAgentID("AGENT-1", "agent-1") {
|
||||
t.Fatal("Windows Grok agent IDs should compare case-insensitively")
|
||||
}
|
||||
return
|
||||
}
|
||||
if sameGrokAgentID("AGENT-1", "agent-1") {
|
||||
t.Fatal("POSIX Grok agent IDs should preserve case")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user