Move Kimi, Grok, and Qwen onto managed_runtime.go and shell_command.go; share the encoded and shell command builders and the quoted Windows legacy parser with Droid; split managed_changes.go; dedupe Letta's hook entry filtering and stop swallowing settings read errors. Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
296 lines
9.0 KiB
Go
296 lines
9.0 KiB
Go
package plugins
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestQwenInstallRejectsMalformedKnownSettingsBeforeWrites(t *testing.T) {
|
|
tests := []struct {
|
|
name, source, pattern string
|
|
}{
|
|
{"malformed", "{ malformed", "parse Qwen Code user settings"},
|
|
{"non-object", "[]", "JSON object"},
|
|
{"trailing comma", `{ "owner": true, }`, "trailing comma"},
|
|
{"duplicate root", `{ "hooks": {}, "hooks": {} }`, "duplicate"},
|
|
{
|
|
"nested duplicate",
|
|
`{ "hooks": { "PreToolUse": [{ "hooks": [], "hooks": [] }] } }`,
|
|
"duplicate",
|
|
},
|
|
{"invalid disable flag", `{ "disableAllHooks": "yes" }`, "must be a boolean"},
|
|
{"invalid security", `{ "security": [] }`, "security"},
|
|
{
|
|
"invalid folder trust",
|
|
`{ "security": { "folderTrust": { "enabled": "yes" } } }`,
|
|
"must be a boolean",
|
|
},
|
|
{"hooks array", `{ "hooks": [] }`, "JSON object"},
|
|
{"null hooks", `{ "hooks": null }`, "JSON object"},
|
|
{"null event", `{ "hooks": { "PreToolUse": null } }`, "must be an array"},
|
|
{"null group", `{ "hooks": { "PreToolUse": [null] } }`, "must be an object"},
|
|
{
|
|
"invalid matcher type",
|
|
`{ "hooks": { "PreToolUse": [{ "matcher": 1, "hooks": [] }] } }`,
|
|
"matcher must be a string",
|
|
},
|
|
{
|
|
"invalid matcher regex",
|
|
`{ "hooks": { "PreToolUse": [{ "matcher": "[", "hooks": [] }] } }`,
|
|
"regular expression",
|
|
},
|
|
{
|
|
"invalid sequential",
|
|
`{ "hooks": { "PreToolUse": [{ "sequential": "yes", "hooks": [] }] } }`,
|
|
"must be a boolean",
|
|
},
|
|
{
|
|
"null handlers",
|
|
`{ "hooks": { "PreToolUse": [{ "hooks": null }] } }`,
|
|
"hooks array",
|
|
},
|
|
{
|
|
"missing command",
|
|
`{ "hooks": { "PreToolUse": [{ "hooks": [{ "type": "command" }] }] } }`,
|
|
"non-empty command",
|
|
},
|
|
{
|
|
"missing url",
|
|
`{ "hooks": { "PreToolUse": [{ "hooks": [{ "type": "http" }] }] } }`,
|
|
"non-empty url",
|
|
},
|
|
{
|
|
"settings function",
|
|
`{ "hooks": { "PreToolUse": [{ "hooks": [{ "type": "function" }] }] } }`,
|
|
"unsupported type",
|
|
},
|
|
{
|
|
"invalid timeout",
|
|
`{ "hooks": { "PreToolUse": [{ "hooks": [{ "type": "command", "command": "x", "timeout": "ten" }] }] } }`,
|
|
"finite number",
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
fixture := prepareQwenFixture(t, qwenFixtureOptions{settings: qwenString(test.source)})
|
|
err := fixture.plugin.Install(fixture.config)
|
|
if err == nil || !strings.Contains(err.Error(), test.pattern) {
|
|
t.Fatalf("install error = %v, want substring %q", err, test.pattern)
|
|
}
|
|
if readQwenTestFile(t, fixture.configPath) != test.source {
|
|
t.Fatal("failed install changed Qwen Code settings")
|
|
}
|
|
for _, path := range []string{
|
|
fixture.guardPath,
|
|
fixture.hookPath,
|
|
fixture.runtimeConfig,
|
|
fixture.privateKey,
|
|
} {
|
|
requireMissingQwenFile(t, path)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestQwenPreservesFutureEventsAndLimitsRegexSemantics(t *testing.T) {
|
|
settings := `{
|
|
"hooks": {
|
|
"FutureEvent": { "shape": "future" },
|
|
"UserPromptSubmit": [{ "matcher": "[", "hooks": [] }]
|
|
}
|
|
}`
|
|
fixture := prepareQwenFixture(t, qwenFixtureOptions{settings: qwenString(settings)})
|
|
installQwenFixture(t, fixture)
|
|
raw := readQwenTestFile(t, fixture.configPath)
|
|
for _, marker := range []string{"FutureEvent", `"matcher": "["`} {
|
|
if !strings.Contains(raw, marker) {
|
|
t.Fatalf("future-compatible setting %q was lost: %s", marker, raw)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestQwenInstallRejectsMalformedReadOnlySourceBeforeWrites(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
path func(*qwenFixture) string
|
|
}{
|
|
{"system defaults", func(f *qwenFixture) string { return f.systemDefaults }},
|
|
{"workspace", func(f *qwenFixture) string { return f.workspaceConfig }},
|
|
{"system override", func(f *qwenFixture) string { return f.systemConfig }},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
fixture := prepareQwenFixture(t, qwenFixtureOptions{})
|
|
filePath := test.path(fixture)
|
|
writeOptionalQwenFile(t, filePath, qwenString("{ malformed"))
|
|
err := fixture.plugin.Install(fixture.config)
|
|
if err == nil || !strings.Contains(err.Error(), "parse Qwen Code") {
|
|
t.Fatalf("install error = %v", err)
|
|
}
|
|
if readQwenTestFile(t, filePath) != "{ malformed" {
|
|
t.Fatal("failed install changed the read-only source")
|
|
}
|
|
requireMissingQwenFile(t, fixture.guardPath)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestQwenInstallRejectsEffectiveDisableBeforeWrites(t *testing.T) {
|
|
fixture := prepareQwenFixture(t, qwenFixtureOptions{})
|
|
writeOptionalQwenFile(
|
|
t,
|
|
fixture.systemConfig,
|
|
qwenJSON(map[string]any{"disableAllHooks": true}),
|
|
)
|
|
err := fixture.plugin.Install(fixture.config)
|
|
if err == nil || !strings.Contains(err.Error(), "disableAllHooks") ||
|
|
!strings.Contains(err.Error(), fixture.systemConfig) {
|
|
t.Fatalf("install error = %v", err)
|
|
}
|
|
requireMissingQwenFile(t, fixture.guardPath)
|
|
requireMissingQwenFile(t, fixture.configPath)
|
|
}
|
|
|
|
func TestQwenTransactionProtectsEveryReadOnlySource(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
configure func(*testing.T, *qwenFixture)
|
|
path func(*qwenFixture) string
|
|
content string
|
|
}{
|
|
{
|
|
"home environment",
|
|
func(*testing.T, *qwenFixture) {},
|
|
func(f *qwenFixture) string { return filepath.Join(f.qwenDir, ".env") },
|
|
"QWEN_HOME=external\n",
|
|
},
|
|
{
|
|
"system defaults",
|
|
func(*testing.T, *qwenFixture) {},
|
|
func(f *qwenFixture) string { return f.systemDefaults },
|
|
"{}\n",
|
|
},
|
|
{
|
|
"workspace",
|
|
func(*testing.T, *qwenFixture) {},
|
|
func(f *qwenFixture) string { return f.workspaceConfig },
|
|
"{}\n",
|
|
},
|
|
{
|
|
"system override",
|
|
func(*testing.T, *qwenFixture) {},
|
|
func(f *qwenFixture) string { return f.systemConfig },
|
|
"{}\n",
|
|
},
|
|
{
|
|
"trusted folders",
|
|
func(t *testing.T, f *qwenFixture) {
|
|
writeOptionalQwenFile(t, f.configPath, qwenJSON(map[string]any{
|
|
"security": map[string]any{
|
|
"folderTrust": map[string]any{"enabled": true},
|
|
},
|
|
}))
|
|
},
|
|
func(f *qwenFixture) string { return f.trustedFolders },
|
|
"{}\n",
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
fixture := prepareQwenFixture(t, qwenFixtureOptions{
|
|
settings: qwenJSON(map[string]any{"owner": "user"}),
|
|
})
|
|
test.configure(t, fixture)
|
|
original := readQwenTestFile(t, fixture.configPath)
|
|
sourcePath := test.path(fixture)
|
|
mutated := false
|
|
fixture.plugin.rename = func(source, destination string) error {
|
|
if !mutated {
|
|
mutated = true
|
|
writeOptionalQwenFile(t, sourcePath, qwenString(test.content))
|
|
}
|
|
return os.Rename(source, destination)
|
|
}
|
|
err := fixture.plugin.Install(fixture.config)
|
|
if err == nil || !strings.Contains(err.Error(), "changed during") || !mutated {
|
|
t.Fatalf("transaction error = %v, mutated = %v", err, mutated)
|
|
}
|
|
if readQwenTestFile(t, fixture.configPath) != original {
|
|
t.Fatal("transaction rollback changed user settings")
|
|
}
|
|
if readQwenTestFile(t, sourcePath) != test.content {
|
|
t.Fatal("transaction rollback overwrote the external mutation")
|
|
}
|
|
for _, path := range []string{
|
|
fixture.guardPath,
|
|
fixture.hookPath,
|
|
fixture.runtimeConfig,
|
|
fixture.privateKey,
|
|
} {
|
|
requireMissingQwenFile(t, path)
|
|
}
|
|
requireNoQwenStagingFiles(t, fixture.homeDir)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestQwenTransactionRollsBackAllFilesAndCleansStaging(t *testing.T) {
|
|
fixture := prepareQwenFixture(t, qwenFixtureOptions{
|
|
settings: qwenJSON(map[string]any{"owner": "user"}),
|
|
})
|
|
original := readQwenTestFile(t, fixture.configPath)
|
|
failed := false
|
|
fixture.plugin.rename = func(source, destination string) error {
|
|
if !failed && sameManagedPath(destination, fixture.configPath) {
|
|
failed = true
|
|
return errors.New("simulated settings commit failure")
|
|
}
|
|
return os.Rename(source, destination)
|
|
}
|
|
err := fixture.plugin.Install(fixture.config)
|
|
if err == nil || !strings.Contains(err.Error(), "Install Qwen Code hooks") || !failed {
|
|
t.Fatalf("transaction error = %v, failed = %v", err, failed)
|
|
}
|
|
if readQwenTestFile(t, fixture.configPath) != original {
|
|
t.Fatal("transaction rollback did not restore Qwen Code settings")
|
|
}
|
|
for _, path := range []string{
|
|
fixture.guardPath,
|
|
fixture.hookPath,
|
|
fixture.runtimeConfig,
|
|
fixture.privateKey,
|
|
} {
|
|
requireMissingQwenFile(t, path)
|
|
}
|
|
requireNoQwenStagingFiles(t, fixture.homeDir)
|
|
}
|
|
|
|
func TestQwenRecoveryCommandsDoNotRequireNodeResolution(t *testing.T) {
|
|
fixture := prepareQwenFixture(t, qwenFixtureOptions{})
|
|
installQwenFixture(t, fixture)
|
|
t.Setenv("PATH", "")
|
|
status, err := fixture.plugin.Status()
|
|
if err != nil || !status.Installed {
|
|
t.Fatalf("status without PATH = %#v, %v", status, err)
|
|
}
|
|
if err := fixture.plugin.Uninstall(qwenTestAgentID); err != nil {
|
|
t.Fatalf("uninstall without PATH: %v", err)
|
|
}
|
|
requireMissingQwenFile(t, fixture.configPath)
|
|
}
|
|
|
|
func TestQwenStatusSurfacesMalformedRuntimeConfig(t *testing.T) {
|
|
fixture := prepareQwenFixture(t, qwenFixtureOptions{})
|
|
installQwenFixture(t, fixture)
|
|
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)
|
|
}
|
|
}
|