294 lines
8.5 KiB
Go
294 lines
8.5 KiB
Go
package plugins
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func installClaudeRuntimeFixture(
|
|
t *testing.T,
|
|
status string,
|
|
) (*claudeFixture, *codexTestAPI, *httptest.Server) {
|
|
t.Helper()
|
|
api := &codexTestAPI{status: status}
|
|
server := httptest.NewServer(api)
|
|
fixture := prepareClaudeFixture(t, claudeFixtureOptions{})
|
|
fixture.config.BaseURL = server.URL
|
|
fixture.config.Token = "ely_test_token"
|
|
if err := fixture.plugin.Install(fixture.config); err != nil {
|
|
server.Close()
|
|
t.Fatalf("install Claude hooks: %v", err)
|
|
}
|
|
return fixture, api, server
|
|
}
|
|
|
|
func TestClaudeGeneratedRuntimePreservesNativePayloadAuthAndChain(t *testing.T) {
|
|
fixture, api, server := installClaudeRuntimeFixture(t, "active")
|
|
defer server.Close()
|
|
settings := readClaudeTestObject(t, fixture.configPath)
|
|
guard := claudeTestManagedHandler(
|
|
t,
|
|
settings,
|
|
"PreToolUse",
|
|
claudeGuardScript,
|
|
claudeGuardStatusMessage,
|
|
)
|
|
success := claudeTestManagedHandler(
|
|
t,
|
|
settings,
|
|
"PostToolUse",
|
|
claudeAuditScript,
|
|
claudeAuditStatusMessage,
|
|
)
|
|
failure := claudeTestManagedHandler(
|
|
t,
|
|
settings,
|
|
"PostToolUseFailure",
|
|
claudeAuditScript,
|
|
claudeAuditStatusMessage,
|
|
)
|
|
if exit, stdout, stderr := runClaudeHandler(
|
|
t,
|
|
guard,
|
|
fixture,
|
|
claudeOfficialPayload("PreToolUse"),
|
|
); exit != 0 || stdout != "" || stderr != "" {
|
|
t.Fatalf("guard result = %d, %q, %q", exit, stdout, stderr)
|
|
}
|
|
successPayload := claudeOfficialPayload("PostToolUse")
|
|
failurePayload := claudeOfficialPayload("PostToolUseFailure")
|
|
for _, item := range []struct {
|
|
handler map[string]any
|
|
payload map[string]any
|
|
}{
|
|
{success, successPayload},
|
|
{failure, failurePayload},
|
|
} {
|
|
if exit, stdout, stderr := runClaudeHandler(
|
|
t,
|
|
item.handler,
|
|
fixture,
|
|
item.payload,
|
|
); exit != 0 || stdout != "" || stderr != "" {
|
|
t.Fatalf("audit result = %d, %q, %q", exit, stdout, stderr)
|
|
}
|
|
}
|
|
getAuth, postAuth, operations := api.snapshot()
|
|
if getAuth != "Bearer ely_test_token" || postAuth != "Bearer ely_test_token" {
|
|
t.Fatalf("authorization headers = %q, %q", getAuth, postAuth)
|
|
}
|
|
if len(operations) != 2 ||
|
|
!reflect.DeepEqual(operations[0]["payload"], successPayload) ||
|
|
!reflect.DeepEqual(operations[1]["payload"], failurePayload) {
|
|
t.Fatalf("Claude native operations = %#v", operations)
|
|
}
|
|
if operations[0]["prev_chain_hash"] !=
|
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" ||
|
|
operations[1]["prev_chain_hash"] != operations[0]["chain_hash"] {
|
|
t.Fatalf("Claude operation chain = %#v", operations)
|
|
}
|
|
}
|
|
|
|
func TestClaudeGuardUsesOfficialBlockingExitCode(t *testing.T) {
|
|
for _, status := range []string{"frozen", "revoked"} {
|
|
t.Run(status, func(t *testing.T) {
|
|
fixture, _, server := installClaudeRuntimeFixture(t, status)
|
|
defer server.Close()
|
|
guard := claudeTestManagedHandler(
|
|
t,
|
|
readClaudeTestObject(t, fixture.configPath),
|
|
"PreToolUse",
|
|
claudeGuardScript,
|
|
claudeGuardStatusMessage,
|
|
)
|
|
exit, stdout, stderr := runClaudeHandler(
|
|
t,
|
|
guard,
|
|
fixture,
|
|
claudeOfficialPayload("PreToolUse"),
|
|
)
|
|
if exit != 2 || stdout != "" ||
|
|
!strings.Contains(strings.ToLower(stderr), status) {
|
|
t.Fatalf("guard result = %d, %q, %q", exit, stdout, stderr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClaudeFailOpenGuardReportsInputConfigStatusAndAPIFailures(t *testing.T) {
|
|
fixture, api, server := installClaudeRuntimeFixture(t, "active")
|
|
guard := claudeTestManagedHandler(
|
|
t,
|
|
readClaudeTestObject(t, fixture.configPath),
|
|
"PreToolUse",
|
|
claudeGuardScript,
|
|
claudeGuardStatusMessage,
|
|
)
|
|
runtimeConfig, err := os.ReadFile(fixture.runtimeConfig)
|
|
if err != nil {
|
|
t.Fatalf("read runtime config: %v", err)
|
|
}
|
|
malformedExit, _, malformedError := runClaudeRawHandler(
|
|
t,
|
|
guard,
|
|
fixture,
|
|
[]byte("{ malformed"),
|
|
)
|
|
if err := os.WriteFile(fixture.runtimeConfig, []byte("{ malformed"), 0600); err != nil {
|
|
t.Fatalf("corrupt runtime config: %v", err)
|
|
}
|
|
configExit, _, configError := runClaudeHandler(
|
|
t,
|
|
guard,
|
|
fixture,
|
|
claudeOfficialPayload("PreToolUse"),
|
|
)
|
|
if err := os.WriteFile(fixture.runtimeConfig, runtimeConfig, 0600); err != nil {
|
|
t.Fatalf("restore runtime config: %v", err)
|
|
}
|
|
_ = os.Remove(filepath.Join(fixture.agentDir, "status-cache.json"))
|
|
api.mu.Lock()
|
|
api.status = "unknown"
|
|
api.mu.Unlock()
|
|
statusExit, _, statusError := runClaudeHandler(
|
|
t,
|
|
guard,
|
|
fixture,
|
|
claudeOfficialPayload("PreToolUse"),
|
|
)
|
|
_ = os.Remove(filepath.Join(fixture.agentDir, "status-cache.json"))
|
|
server.Close()
|
|
apiExit, _, apiError := runClaudeHandler(
|
|
t,
|
|
guard,
|
|
fixture,
|
|
claudeOfficialPayload("PreToolUse"),
|
|
)
|
|
for _, result := range []struct {
|
|
exit int
|
|
err string
|
|
want string
|
|
}{
|
|
{malformedExit, malformedError, "invalid JSON"},
|
|
{configExit, configError, "Failed to read agent config"},
|
|
{statusExit, statusError, "invalid agent status"},
|
|
{apiExit, apiError, "Failed to resolve agent status"},
|
|
} {
|
|
if result.exit != 0 || !strings.Contains(result.err, result.want) {
|
|
t.Fatalf("fail-open result = %d, %q; want %q", result.exit, result.err, result.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestClaudeAuditRecordsMalformedInputAndAPIFailures(t *testing.T) {
|
|
fixture, api, server := installClaudeRuntimeFixture(t, "active")
|
|
defer server.Close()
|
|
handler := claudeTestManagedHandler(
|
|
t,
|
|
readClaudeTestObject(t, fixture.configPath),
|
|
"PostToolUseFailure",
|
|
claudeAuditScript,
|
|
claudeAuditStatusMessage,
|
|
)
|
|
malformedExit, malformedOutput, malformedError := runClaudeRawHandler(
|
|
t,
|
|
handler,
|
|
fixture,
|
|
[]byte("{ malformed"),
|
|
)
|
|
api.mu.Lock()
|
|
api.postStatus = http.StatusServiceUnavailable
|
|
api.mu.Unlock()
|
|
upstreamExit, upstreamOutput, upstreamError := runClaudeHandler(
|
|
t,
|
|
handler,
|
|
fixture,
|
|
claudeOfficialPayload("PostToolUseFailure"),
|
|
)
|
|
log, err := os.ReadFile(filepath.Join(fixture.agentDir, "error.log"))
|
|
if malformedExit != 0 || malformedOutput != "" || malformedError != "" ||
|
|
upstreamExit != 0 || upstreamOutput != "" || upstreamError != "" ||
|
|
err != nil || !strings.Contains(string(log), "invalid JSON") ||
|
|
!strings.Contains(string(log), "HTTP 503") {
|
|
t.Fatalf(
|
|
"audit failures = (%d, %q, %q), (%d, %q, %q), log=%q, err=%v",
|
|
malformedExit,
|
|
malformedOutput,
|
|
malformedError,
|
|
upstreamExit,
|
|
upstreamOutput,
|
|
upstreamError,
|
|
log,
|
|
err,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestClaudeStatusRequiresStrictRuntimeFiles(t *testing.T) {
|
|
for _, name := range []string{"config.json", "private.key", "guard.js", "hook.js"} {
|
|
t.Run("missing "+name, func(t *testing.T) {
|
|
fixture := prepareClaudeFixture(t, claudeFixtureOptions{})
|
|
if err := fixture.plugin.Install(fixture.config); err != nil {
|
|
t.Fatalf("install Claude hooks: %v", err)
|
|
}
|
|
if err := os.Remove(filepath.Join(fixture.agentDir, name)); err != nil {
|
|
t.Fatalf("remove %s: %v", name, err)
|
|
}
|
|
status, err := fixture.plugin.Status()
|
|
if err != nil || status.Installed || !status.HookConfigured ||
|
|
status.HookScriptExists {
|
|
t.Fatalf("missing %s status = %#v, %v", name, status, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClaudeStatusSurfacesInvalidRuntimeMetadata(t *testing.T) {
|
|
tests := []struct{ name, source, want string }{
|
|
{"malformed", "{ malformed", "parse Elydora runtime config"},
|
|
{"unsupported", `{"org_id":"o","agent_id":"agent-1","kid":"k","base_url":"https://api.test","agent_name":"claudecode","extra":true}`, "unsupported field"},
|
|
{"identity", `{"org_id":"o","agent_id":"other","kid":"k","base_url":"https://api.test","agent_name":"claudecode"}`, "identity does not match"},
|
|
}
|
|
for _, testCase := range tests {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
fixture := prepareClaudeFixture(t, claudeFixtureOptions{})
|
|
if err := fixture.plugin.Install(fixture.config); err != nil {
|
|
t.Fatalf("install Claude hooks: %v", err)
|
|
}
|
|
if err := os.WriteFile(
|
|
fixture.runtimeConfig,
|
|
[]byte(testCase.source),
|
|
0600,
|
|
); err != nil {
|
|
t.Fatalf("replace runtime config: %v", err)
|
|
}
|
|
_, err := fixture.plugin.Status()
|
|
if err == nil || !strings.Contains(err.Error(), testCase.want) {
|
|
t.Fatalf("status error = %v, want %q", err, testCase.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClaudeRuntimeConfigOmitsEmptyToken(t *testing.T) {
|
|
fixture := prepareClaudeFixture(t, claudeFixtureOptions{})
|
|
fixture.config.Token = ""
|
|
if err := fixture.plugin.Install(fixture.config); err != nil {
|
|
t.Fatalf("install Claude hooks: %v", err)
|
|
}
|
|
config := readClaudeTestObject(t, fixture.runtimeConfig)
|
|
if _, exists := config["token"]; exists {
|
|
t.Fatalf("empty token persisted: %#v", config)
|
|
}
|
|
raw, err := json.Marshal(config)
|
|
if err != nil || len(raw) == 0 {
|
|
t.Fatalf("runtime config encoding = %q, %v", raw, err)
|
|
}
|
|
}
|