182 lines
6.1 KiB
Go
182 lines
6.1 KiB
Go
package plugins
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func installKimiRuntimeFixture(
|
|
t *testing.T,
|
|
status string,
|
|
) (*kimiFixture, *codexTestAPI, *httptest.Server) {
|
|
t.Helper()
|
|
api := &codexTestAPI{status: status}
|
|
server := httptest.NewServer(api)
|
|
fixture := prepareKimiFixture(t, kimiFixtureOptions{withoutLegacyEvidence: true})
|
|
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 Kimi hooks: %v", err)
|
|
}
|
|
return fixture, api, server
|
|
}
|
|
|
|
func TestKimiGeneratedRuntimePreservesSuccessAndFailurePayloads(t *testing.T) {
|
|
fixture, api, server := installKimiRuntimeFixture(t, "active")
|
|
defer server.Close()
|
|
hooks := readKimiTestHooks(t, fixture.modernPath)
|
|
guard := managedKimiTestHook(t, hooks, "PreToolUse")["command"].(string)
|
|
success := managedKimiTestHook(t, hooks, "PostToolUse")["command"].(string)
|
|
failure := managedKimiTestHook(t, hooks, "PostToolUseFailure")["command"].(string)
|
|
|
|
if exitCode, stderr := runKimiCommand(
|
|
t, guard, fixture, kimiOfficialPayload("PreToolUse"),
|
|
); exitCode != 0 || stderr != "" {
|
|
t.Fatalf("guard exit = %d, stderr = %q", exitCode, stderr)
|
|
}
|
|
successPayload := kimiOfficialPayload("PostToolUse")
|
|
failurePayload := kimiOfficialPayload("PostToolUseFailure")
|
|
for _, item := range []struct {
|
|
command string
|
|
payload map[string]any
|
|
}{{success, successPayload}, {failure, failurePayload}} {
|
|
if exitCode, stderr := runKimiCommand(
|
|
t, item.command, fixture, item.payload,
|
|
); exitCode != 0 || stderr != "" {
|
|
t.Fatalf("audit exit = %d, stderr = %q", exitCode, 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("Kimi native operations = %#v", operations)
|
|
}
|
|
if operations[0]["prev_chain_hash"] != "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" ||
|
|
operations[1]["prev_chain_hash"] != operations[0]["chain_hash"] {
|
|
t.Fatalf("Kimi operation chain = %#v", operations)
|
|
}
|
|
}
|
|
|
|
func TestKimiGuardPropagatesOfficialBlockExitCode(t *testing.T) {
|
|
for _, status := range []string{"frozen", "revoked"} {
|
|
t.Run(status, func(t *testing.T) {
|
|
fixture, _, server := installKimiRuntimeFixture(t, status)
|
|
defer server.Close()
|
|
command := managedKimiTestHook(
|
|
t, readKimiTestHooks(t, fixture.modernPath), "PreToolUse",
|
|
)["command"].(string)
|
|
exitCode, stderr := runKimiCommand(
|
|
t, command, fixture, kimiOfficialPayload("PreToolUse"),
|
|
)
|
|
if exitCode != 2 || !strings.Contains(stderr, "Tool execution blocked") {
|
|
t.Fatalf("guard exit = %d, stderr = %q", exitCode, stderr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestKimiFailOpenGuardReportsInputConfigStatusAndAPIFailures(t *testing.T) {
|
|
fixture, api, server := installKimiRuntimeFixture(t, "active")
|
|
command := managedKimiTestHook(
|
|
t, readKimiTestHooks(t, fixture.modernPath), "PreToolUse",
|
|
)["command"].(string)
|
|
runtimeConfig, err := os.ReadFile(fixture.runtimeConfig)
|
|
if err != nil {
|
|
t.Fatalf("read runtime config: %v", err)
|
|
}
|
|
|
|
malformedExit, malformedError := runKimiRawCommand(
|
|
t, command, fixture, []byte("{ malformed"),
|
|
)
|
|
if err := os.WriteFile(fixture.runtimeConfig, []byte("{ malformed"), 0600); err != nil {
|
|
t.Fatalf("corrupt runtime config: %v", err)
|
|
}
|
|
configExit, configError := runKimiCommand(
|
|
t, command, fixture, kimiOfficialPayload("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 := runKimiCommand(
|
|
t, command, fixture, kimiOfficialPayload("PreToolUse"),
|
|
)
|
|
_ = os.Remove(filepath.Join(fixture.agentDir, "status-cache.json"))
|
|
server.Close()
|
|
apiExit, apiError := runKimiCommand(
|
|
t, command, fixture, kimiOfficialPayload("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 TestKimiAuditRecordsMalformedInputAndAPIFailures(t *testing.T) {
|
|
fixture, api, server := installKimiRuntimeFixture(t, "active")
|
|
defer server.Close()
|
|
command := managedKimiTestHook(
|
|
t, readKimiTestHooks(t, fixture.modernPath), "PostToolUseFailure",
|
|
)["command"].(string)
|
|
|
|
malformedExit, malformedError := runKimiRawCommand(
|
|
t, command, fixture, []byte("{ malformed"),
|
|
)
|
|
api.mu.Lock()
|
|
api.postStatus = http.StatusServiceUnavailable
|
|
api.mu.Unlock()
|
|
upstreamExit, upstreamError := runKimiCommand(
|
|
t, command, fixture, kimiOfficialPayload("PostToolUseFailure"),
|
|
)
|
|
log, err := os.ReadFile(filepath.Join(fixture.agentDir, "error.log"))
|
|
if malformedExit != 0 || malformedError != "" || upstreamExit != 0 || upstreamError != "" ||
|
|
err != nil || !strings.Contains(string(log), "invalid JSON") ||
|
|
!strings.Contains(string(log), "HTTP 503") {
|
|
t.Fatalf(
|
|
"audit failures = (%d, %q), (%d, %q), log=%q, err=%v",
|
|
malformedExit, malformedError, upstreamExit, upstreamError, log, err,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestKimiFailurePayloadIncludesOfficialErrorShape(t *testing.T) {
|
|
payload := kimiOfficialPayload("PostToolUseFailure")
|
|
encoded, err := json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatalf("marshal payload: %v", err)
|
|
}
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal(encoded, &decoded); err != nil {
|
|
t.Fatalf("decode payload: %v", err)
|
|
}
|
|
if decoded["hook_event_name"] != "PostToolUseFailure" || decoded["error"] == nil {
|
|
t.Fatalf("failure payload = %#v", decoded)
|
|
}
|
|
}
|