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
270 lines
9.1 KiB
Go
270 lines
9.1 KiB
Go
package plugins
|
|
|
|
import (
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestQwenRegistryAndRuntimeOwnership(t *testing.T) {
|
|
entry := SupportedAgents[qwenAgentKey]
|
|
if entry.Name != "Qwen Code" || entry.ConfigDir != "~/.qwen" ||
|
|
entry.ConfigFile != "settings.json" {
|
|
t.Fatalf("Qwen Code registry entry = %#v", entry)
|
|
}
|
|
plugin, ok := NewPlugin(qwenAgentKey).(*QwenPlugin)
|
|
if !ok {
|
|
t.Fatal("Qwen Code plugin is not registered")
|
|
}
|
|
if !plugin.ManagesGuardRuntime() {
|
|
t.Fatal("Qwen Code plugin must own its runtime transaction")
|
|
}
|
|
if _, ok := any(plugin).(InstallPreflighter); !ok {
|
|
t.Fatal("Qwen Code plugin must preflight before CLI writes")
|
|
}
|
|
}
|
|
|
|
func TestQwenInstallPreservesSourcesAndIsIdempotent(t *testing.T) {
|
|
settings := "{\r\n" +
|
|
" // Keep this user preference.\r\n" +
|
|
" \"theme\": \"GitHub\",\r\n" +
|
|
" \"hooks\": {\r\n" +
|
|
" \"FutureEvent\": { \"opaque\": true },\r\n" +
|
|
" \"SessionStart\": [{ \"hooks\": [{ \"type\": \"command\", \"command\": \"session-hook\" }] }],\r\n" +
|
|
" \"PreToolUse\": [{ \"matcher\": \"read_file\", \"hooks\": [{ \"type\": \"command\", \"command\": \"user-hook\" }] }]\r\n" +
|
|
" }\r\n" +
|
|
"}\r\n"
|
|
fixture := prepareQwenFixture(t, qwenFixtureOptions{settings: qwenString(settings)})
|
|
writeOptionalQwenFile(
|
|
t,
|
|
fixture.workspaceConfig,
|
|
qwenString("{ \"owner\": \"workspace\" }\n"),
|
|
)
|
|
workspaceBefore := readQwenTestFile(t, fixture.workspaceConfig)
|
|
installQwenFixture(t, fixture)
|
|
firstSettings := readQwenTestFile(t, fixture.configPath)
|
|
firstGuard := readQwenTestFile(t, fixture.guardPath)
|
|
firstAudit := readQwenTestFile(t, fixture.hookPath)
|
|
installQwenFixture(t, fixture)
|
|
if readQwenTestFile(t, fixture.configPath) != firstSettings ||
|
|
readQwenTestFile(t, fixture.guardPath) != firstGuard ||
|
|
readQwenTestFile(t, fixture.hookPath) != firstAudit {
|
|
t.Fatal("idempotent install changed Qwen Code installation")
|
|
}
|
|
if !strings.Contains(firstSettings, "Keep this user preference") ||
|
|
!strings.Contains(firstSettings, "\r\n") ||
|
|
!strings.Contains(firstSettings, "FutureEvent") {
|
|
t.Fatalf("settings source fidelity was lost:\n%s", firstSettings)
|
|
}
|
|
root := readQwenTestObject(t, fixture.configPath)
|
|
if root["theme"] != "GitHub" {
|
|
t.Fatalf("theme = %#v", root["theme"])
|
|
}
|
|
hooks := requireQwenObject(t, root["hooks"])
|
|
if len(requireQwenArray(t, hooks["SessionStart"])) != 1 ||
|
|
len(requireQwenArray(t, hooks["PreToolUse"])) != 2 ||
|
|
len(requireQwenArray(t, hooks["PostToolUse"])) != 1 ||
|
|
len(requireQwenArray(t, hooks["PostToolUseFailure"])) != 1 {
|
|
t.Fatalf("Qwen hooks = %#v", hooks)
|
|
}
|
|
for _, item := range []struct {
|
|
event, path, name string
|
|
}{
|
|
{"PreToolUse", fixture.guardPath, qwenGuardHookName},
|
|
{"PostToolUse", fixture.hookPath, qwenAuditHookName},
|
|
{"PostToolUseFailure", fixture.hookPath, qwenAuditHookName},
|
|
} {
|
|
group := qwenManagedGroup(t, root, item.event, item.path)
|
|
if len(group) != 1 {
|
|
t.Fatalf("managed group = %#v", group)
|
|
}
|
|
handler := qwenManagedHandler(t, root, item.event, item.path)
|
|
if len(handler) != 5 || handler["type"] != "command" ||
|
|
handler["name"] != item.name || handler["shell"] != qwenExpectedShell() ||
|
|
handler["timeout"] != qwenHookTimeout {
|
|
t.Fatalf("managed handler = %#v", handler)
|
|
}
|
|
}
|
|
if readQwenTestFile(t, fixture.workspaceConfig) != workspaceBefore {
|
|
t.Fatal("workspace settings changed")
|
|
}
|
|
if firstGuard != generateGuardScript(qwenAgentKey, qwenTestAgentID, "", false, "") {
|
|
t.Fatal("guard runtime differs from the canonical generator")
|
|
}
|
|
if firstAudit != buildHookScriptWithOutput(
|
|
qwenAgentKey,
|
|
qwenTestAgentID,
|
|
"",
|
|
false,
|
|
true,
|
|
) {
|
|
t.Fatal("audit runtime differs from the native-payload generator")
|
|
}
|
|
runtimeConfig := readQwenTestObject(t, fixture.runtimeConfig)
|
|
if runtimeConfig["agent_name"] != qwenAgentKey ||
|
|
runtimeConfig["agent_id"] != qwenTestAgentID ||
|
|
runtimeConfig["base_url"] != fixture.config.BaseURL {
|
|
t.Fatalf("runtime config = %#v", runtimeConfig)
|
|
}
|
|
}
|
|
|
|
func TestQwenInstallReportsOfficialReviewCommand(t *testing.T) {
|
|
fixture := prepareQwenFixture(t, qwenFixtureOptions{})
|
|
reader, writer, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatalf("create output pipe: %v", err)
|
|
}
|
|
original := os.Stdout
|
|
os.Stdout = writer
|
|
installErr := fixture.plugin.Install(fixture.config)
|
|
os.Stdout = original
|
|
closeErr := writer.Close()
|
|
output, readErr := io.ReadAll(reader)
|
|
_ = reader.Close()
|
|
if installErr != nil || closeErr != nil || readErr != nil {
|
|
t.Fatalf(
|
|
"capture install output: install=%v close=%v read=%v",
|
|
installErr,
|
|
closeErr,
|
|
readErr,
|
|
)
|
|
}
|
|
text := string(output)
|
|
for _, marker := range []string{fixture.configPath, "/hooks"} {
|
|
if !strings.Contains(text, marker) {
|
|
t.Fatalf("install output is missing %q: %s", marker, text)
|
|
}
|
|
}
|
|
}
|
|
|
|
func legacyQwenGroupForTest(
|
|
t *testing.T,
|
|
event, scriptPath string,
|
|
) map[string]any {
|
|
t.Helper()
|
|
nodePath, err := resolveNodeRuntime()
|
|
if err != nil {
|
|
t.Fatalf("resolve Node.js: %v", err)
|
|
}
|
|
return map[string]any{
|
|
"matcher": "*",
|
|
"hooks": []any{map[string]any{
|
|
"type": "command",
|
|
"command": buildShellCommand(nodePath, scriptPath),
|
|
"shell": qwenExpectedShell(),
|
|
"timeout": qwenHookTimeout,
|
|
}},
|
|
"event": event,
|
|
}
|
|
}
|
|
|
|
func legacyQwenGroupValue(t *testing.T, scriptPath string) map[string]any {
|
|
t.Helper()
|
|
group := legacyQwenGroupForTest(t, "", scriptPath)
|
|
delete(group, "event")
|
|
return group
|
|
}
|
|
|
|
func TestQwenInstallMigratesLegacyPairToCurrentTriple(t *testing.T) {
|
|
fixture := prepareQwenFixture(t, qwenFixtureOptions{})
|
|
lookalike := legacyQwenGroupValue(t, fixture.guardPath)
|
|
requireQwenObject(t, requireQwenArray(t, lookalike["hooks"])[0])["timeout"] = float64(9_000)
|
|
settings := map[string]any{
|
|
"owner": "user",
|
|
"hooks": map[string]any{
|
|
"PreToolUse": []any{
|
|
legacyQwenGroupValue(t, fixture.guardPath),
|
|
lookalike,
|
|
},
|
|
"PostToolUse": []any{legacyQwenGroupValue(t, fixture.hookPath)},
|
|
},
|
|
}
|
|
writeQwenTestObject(t, fixture.configPath, settings)
|
|
installQwenFixture(t, fixture)
|
|
root := readQwenTestObject(t, fixture.configPath)
|
|
for _, item := range []struct{ event, path string }{
|
|
{"PreToolUse", fixture.guardPath},
|
|
{"PostToolUse", fixture.hookPath},
|
|
{"PostToolUseFailure", fixture.hookPath},
|
|
} {
|
|
group := qwenManagedGroup(t, root, item.event, item.path)
|
|
if len(group) != 1 {
|
|
t.Fatalf("legacy group remains for %s: %#v", item.event, group)
|
|
}
|
|
}
|
|
preserved := false
|
|
for _, groupValue := range requireQwenArray(t, requireQwenObject(t, root["hooks"])["PreToolUse"]) {
|
|
for _, handlerValue := range requireQwenArray(t, requireQwenObject(t, groupValue)["hooks"]) {
|
|
preserved = preserved || requireQwenObject(t, handlerValue)["timeout"] == float64(9_000)
|
|
}
|
|
}
|
|
if !preserved {
|
|
t.Fatal("legacy ownership lookalike was removed")
|
|
}
|
|
}
|
|
|
|
func TestQwenUninstallPreservesExternalMutationsAndExactOwnership(t *testing.T) {
|
|
fixture := prepareQwenFixture(t, qwenFixtureOptions{
|
|
settings: qwenString(`{"$version":4,"owner":"user"}`),
|
|
})
|
|
installQwenFixture(t, fixture)
|
|
settings := readQwenTestObject(t, fixture.configPath)
|
|
group := qwenManagedGroup(t, settings, "PreToolUse", fixture.guardPath)
|
|
group["hooks"] = append(requireQwenArray(t, group["hooks"]), map[string]any{
|
|
"type": "command", "command": "user-command",
|
|
})
|
|
otherAgent := filepath.Join(filepath.Dir(fixture.agentDir), "agent-10")
|
|
hooks := requireQwenObject(t, settings["hooks"])
|
|
hooks["PreToolUse"] = append(
|
|
requireQwenArray(t, hooks["PreToolUse"]),
|
|
legacyQwenGroupValue(t, filepath.Join(otherAgent, qwenGuardScript)),
|
|
)
|
|
writeQwenTestObject(t, fixture.configPath, settings)
|
|
before := readQwenTestFile(t, fixture.configPath)
|
|
if err := fixture.plugin.Uninstall("other-agent"); err != nil {
|
|
t.Fatalf("uninstall other agent: %v", err)
|
|
}
|
|
if readQwenTestFile(t, fixture.configPath) != before {
|
|
t.Fatal("uninstall changed an unrelated installation")
|
|
}
|
|
agentID := qwenTestAgentID
|
|
if runtime.GOOS == "windows" {
|
|
agentID = "AGENT-1"
|
|
}
|
|
if err := fixture.plugin.Uninstall(agentID); err != nil {
|
|
t.Fatalf("uninstall Qwen Code hooks: %v", err)
|
|
}
|
|
remaining := readQwenTestObject(t, fixture.configPath)
|
|
if remaining["$version"] != float64(4) || remaining["owner"] != "user" {
|
|
t.Fatalf("root metadata = %#v", remaining)
|
|
}
|
|
remainingRaw := readQwenTestFile(t, fixture.configPath)
|
|
for _, marker := range []string{"user-command", "agent-10"} {
|
|
if !strings.Contains(remainingRaw, marker) {
|
|
t.Fatalf("uninstall removed %q", marker)
|
|
}
|
|
}
|
|
remainingHooks := requireQwenObject(t, remaining["hooks"])
|
|
if _, exists := remainingHooks["PostToolUse"]; exists {
|
|
t.Fatalf("PostToolUse remains: %#v", remainingHooks)
|
|
}
|
|
if _, exists := remainingHooks["PostToolUseFailure"]; exists {
|
|
t.Fatalf("PostToolUseFailure remains: %#v", remainingHooks)
|
|
}
|
|
}
|
|
|
|
func TestQwenUninstallDeletesOwnedEmptySettings(t *testing.T) {
|
|
fixture := prepareQwenFixture(t, qwenFixtureOptions{})
|
|
installQwenFixture(t, fixture)
|
|
if !strings.HasPrefix(readQwenTestFile(t, fixture.configPath), qwenOwnedFileMarker) {
|
|
t.Fatal("created settings are missing the ownership marker")
|
|
}
|
|
if err := fixture.plugin.Uninstall(qwenTestAgentID); err != nil {
|
|
t.Fatalf("uninstall Qwen Code hooks: %v", err)
|
|
}
|
|
requireMissingQwenFile(t, fixture.configPath)
|
|
}
|