fix(qwen): sync Go hooks with Qwen Code 0.20

This commit is contained in:
ZacharyZhang-NY committed 2026-07-20 16:13:24 -04:00
1 parent 8eecb67722
commit b9ab86c8ee
17 files changed
+2888 -709

No files matched your search

+1 -1
View File
@@ -98,7 +98,7 @@ Auggie installation commits `~/.augment/settings.json`, both platform wrappers,
Factory Droid 0.175 installation follows its whole-source precedence: `~/.factory/hooks.json`, legacy `~/.factory/hooks/hooks.json`, `~/.factory/settings.local.json` `hooks`, then `~/.factory/settings.json` `hooks`. Elydora writes the current `{ "hooks": { ... } }` root contract, preserves JSONC comments and formatting, accepts future hook events, and removes exact legacy Elydora commands during migration. System, project, and nested-folder settings remain read-only policy inputs; `hooksDisabled` and organization-managed hook policy block installation before runtime creation. The guard, native-payload audit runtime, strict config, canonical private key, and every affected hook document commit in one rollback-capable transaction. Windows commands use Factory's native PowerShell execution contract and propagate `$LASTEXITCODE`. Run `/hooks` in Droid after installation to review the effective local and organization-managed hooks. Factory Droid 0.175 installation follows its whole-source precedence: `~/.factory/hooks.json`, legacy `~/.factory/hooks/hooks.json`, `~/.factory/settings.local.json` `hooks`, then `~/.factory/settings.json` `hooks`. Elydora writes the current `{ "hooks": { ... } }` root contract, preserves JSONC comments and formatting, accepts future hook events, and removes exact legacy Elydora commands during migration. System, project, and nested-folder settings remain read-only policy inputs; `hooksDisabled` and organization-managed hook policy block installation before runtime creation. The guard, native-payload audit runtime, strict config, canonical private key, and every affected hook document commit in one rollback-capable transaction. Windows commands use Factory's native PowerShell execution contract and propagate `$LASTEXITCODE`. Run `/hooks` in Droid after installation to review the effective local and organization-managed hooks.
Qwen Code installation writes native user hooks to `$QWEN_HOME/settings.json` with `~/.qwen/settings.json` as the default. Elydora follows Qwen's `.env` discovery order, preserves JSON-with-comments source formatting, keeps workspace settings read-only, and commits runtime and settings changes as one transaction. Run `/hooks` in Qwen Code after installation to review the effective hooks. Qwen Code `0.20.0` installation writes native user hooks to `$QWEN_HOME/settings.json` with `~/.qwen/settings.json` as the default. Elydora follows Qwen's `QWEN_HOME` and `QWEN_RUNTIME_DIR` `.env` bootstrap, reads system defaults, user, trusted workspace, and system override settings in official precedence order, and keeps every non-user source read-only. The adapter preserves JSONC formatting and future event payloads, validates all 21 current events plus command, HTTP, and prompt handlers, applies JavaScript regular-expression semantics only to matcher-enabled events, and owns one named matchless `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` triple. Guard, native-payload audit, strict runtime metadata, canonical private key, and user settings commit in one rollback-capable transaction guarded by source snapshots. Healthy status requires the enabled exact triple and exact generated runtime sources. Run `/hooks` in Qwen Code after installation to review the effective hooks.
### Commands ### Commands
+60 -111
View File
@@ -1,112 +1,115 @@
package plugins package plugins
import ( import "fmt"
"encoding/json"
"fmt"
"os"
"path/filepath"
)
// QwenPlugin manages Qwen Code native user hooks.
type QwenPlugin struct { type QwenPlugin struct {
rename renameFunc rename renameFunc
} }
type qwenRuntimeConfig struct { // ManagesGuardRuntime reports that Qwen commits its guard, audit runtime, and
OrgID string `json:"org_id"` // settings in one transaction.
AgentID string `json:"agent_id"` func (p *QwenPlugin) ManagesGuardRuntime() bool {
KID string `json:"kid"` return true
BaseURL string `json:"base_url"` }
Token string `json:"token"`
AgentName string `json:"agent_name"` // PreflightInstall validates every effective source and runtime identity before
// the CLI writes any managed file.
func (p *QwenPlugin) PreflightInstall(config InstallConfig) error {
sources, err := readQwenSources()
if err != nil {
return err
}
_, _, err = preflightQwenInstallation(config, sources)
return err
}
func qwenInstalledGroups(
nodePath, guardPath, auditPath string,
) map[string]map[string]any {
return map[string]map[string]any{
"PreToolUse": buildQwenGroup(nodePath, guardPath, qwenGuardHookName),
"PostToolUse": buildQwenGroup(nodePath, auditPath, qwenAuditHookName),
"PostToolUseFailure": buildQwenGroup(
nodePath,
auditPath,
qwenAuditHookName,
),
}
} }
func (p *QwenPlugin) Install(config InstallConfig) error { func (p *QwenPlugin) Install(config InstallConfig) error {
runtimeRoot, agentDirectory, err := qwenAgentDirectory(config.AgentID) sources, err := readQwenSources()
if err != nil { if err != nil {
return err return err
} }
document, err := readQwenDocument() paths, nodePath, err := preflightQwenInstallation(config, sources)
if err != nil { if err != nil {
return err return err
} }
expectedGuard := filepath.Join(agentDirectory, qwenGuardScript) rendered, err := renderQwenDocument(
if !sameQwenPath(config.GuardScriptPath, expectedGuard) { sources.user,
return fmt.Errorf("Elydora guard runtime must use the managed agent directory: %s", expectedGuard) "",
} paths.runtimeRoot,
guardExists, err := regularFileExists(config.GuardScriptPath, "Elydora guard runtime") qwenInstalledGroups(nodePath, paths.guardPath, paths.auditPath),
)
if err != nil { if err != nil {
return err return err
} }
if !guardExists { prepared, err := prepareQwenInstallation(config, sources, rendered)
return fmt.Errorf("Elydora guard runtime is missing: %s", config.GuardScriptPath)
}
nodePath, err := resolveNodeRuntime()
if err != nil { if err != nil {
return err return err
} }
if err := validateQwenRegexes(nodePath, document.hooks); err != nil { if err := commitQwenInstallation(prepared, p.rename); err != nil {
return err return err
} }
auditPath := filepath.Join(agentDirectory, qwenAuditScript) fmt.Printf("Qwen Code hooks: %s\n", sources.user.filePath)
rendered, err := renderQwenDocument(document, "", runtimeRoot, map[string]map[string]any{ fmt.Println("Qwen Code verification: run /hooks.")
"PreToolUse": buildQwenGroup(nodePath, config.GuardScriptPath),
"PostToolUse": buildQwenGroup(nodePath, auditPath),
})
if err != nil {
return err
}
changes, err := prepareQwenInstallationChanges(config, agentDirectory, auditPath, rendered)
if err != nil {
return err
}
if err := writeChanges(changes, "Write Qwen Code installation", p.rename); err != nil {
return err
}
fmt.Printf(" Qwen Code: user hooks installed at %s\n", document.filePath)
fmt.Println(" Qwen Code: run /hooks to review the Elydora hook changes.")
return nil return nil
} }
func (p *QwenPlugin) Uninstall(agentID string) error { func (p *QwenPlugin) Uninstall(agentID string) error {
sources, err := readQwenSources()
if err != nil {
return err
}
runtimeRoot, err := qwenRuntimeRoot() runtimeRoot, err := qwenRuntimeRoot()
if err != nil { if err != nil {
return err return err
} }
document, err := readQwenDocument() rendered, err := renderQwenDocument(
sources.user,
agentID,
runtimeRoot,
map[string]map[string]any{},
)
if err != nil { if err != nil {
return err return err
} }
if !document.exists { if !rendered.changed {
return nil return nil
} }
rendered, err := renderQwenDocument(document, agentID, runtimeRoot, nil) change, preconditions, err := prepareQwenUninstall(sources, rendered)
if err != nil { if err != nil {
return err return err
} }
change, err := prepareRenderedQwenChange(rendered) return commitQwenUninstall(change, preconditions, p.rename)
if err != nil {
return err
}
return writeChanges([]*fileChange{change}, "Write Qwen Code settings", p.rename)
} }
func (p *QwenPlugin) Status() (PluginStatus, error) { func (p *QwenPlugin) Status() (PluginStatus, error) {
entry := SupportedAgents[qwenAgentKey] entry := SupportedAgents[qwenAgentKey]
status := PluginStatus{AgentName: qwenAgentKey, DisplayName: entry.Name} status := PluginStatus{AgentName: qwenAgentKey, DisplayName: entry.Name}
document, err := readQwenDocument() sources, err := readQwenSources()
if err != nil { if err != nil {
return status, err return status, err
} }
status.ConfigPath = document.filePath status.ConfigPath = sources.user.filePath
if document.hooksDisabled {
return status, nil
}
runtimeRoot, err := qwenRuntimeRoot() runtimeRoot, err := qwenRuntimeRoot()
if err != nil { if err != nil {
return status, err return status, err
} }
contracts := qwenRuntimeContracts(document.hooks, runtimeRoot) contracts := qwenRuntimeContracts(sources.user.hooks, runtimeRoot)
status.HookConfigured = len(contracts) > 0 status.HookConfigured = !sources.disableControl.disabled && len(contracts) > 0
if !status.HookConfigured { if !status.HookConfigured {
return status, nil return status, nil
} }
@@ -117,57 +120,3 @@ func (p *QwenPlugin) Status() (PluginStatus, error) {
status.Installed = status.HookScriptExists status.Installed = status.HookScriptExists
return status, nil return status, nil
} }
func qwenAgentDirectory(agentID string) (string, string, error) {
runtimeRoot, err := qwenRuntimeRoot()
if err != nil {
return "", "", err
}
agentDirectory, err := ResolveAgentRuntimeDirectory(agentID)
if err != nil {
return "", "", err
}
return runtimeRoot, agentDirectory, nil
}
func prepareQwenInstallationChanges(
config InstallConfig,
agentDirectory, auditPath string,
rendered *qwenRenderedDocument,
) ([]*fileChange, error) {
baseURL := config.BaseURL
if baseURL == "" {
baseURL = "https://api.elydora.com"
}
runtimeConfig, err := json.MarshalIndent(qwenRuntimeConfig{
OrgID: config.OrgID, AgentID: config.AgentID, KID: config.KID,
BaseURL: baseURL, Token: config.Token, AgentName: qwenAgentKey,
}, "", " ")
if err != nil {
return nil, fmt.Errorf("encode Elydora runtime config: %w", err)
}
runtimeConfig = append(runtimeConfig, '\n')
items := []struct {
path, label string
content []byte
mode os.FileMode
}{
{filepath.Join(agentDirectory, "config.json"), "Elydora runtime config", runtimeConfig, 0600},
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", []byte(config.PrivateKey), 0600},
{auditPath, "Elydora audit runtime", []byte(buildHookScript(qwenAgentKey, config.AgentID)), 0700},
}
changes := make([]*fileChange, 0, len(items)+1)
for _, item := range items {
change, changeErr := prepareFileChange(item.path, item.label, item.content, item.mode)
if changeErr != nil {
return nil, changeErr
}
changes = append(changes, change)
}
settingsChange, err := prepareRenderedQwenChange(rendered)
if err != nil {
return nil, err
}
changes = append(changes, settingsChange)
return changes, nil
}
+116
View File
@@ -0,0 +1,116 @@
package plugins
import (
"path/filepath"
"runtime"
"strings"
)
const qwenPOSIXApostrophe = `'"'"'`
type qwenRuntimeReference struct {
agentID string
scriptPath string
}
func buildQwenCommand(nodePath, scriptPath string) string {
if runtime.GOOS == "windows" {
return "& " + quoteQwenPowerShell(nodePath) + " " +
quoteQwenPowerShell(scriptPath) + "; exit $LASTEXITCODE"
}
return quotePOSIXArgument(nodePath) + " " + quotePOSIXArgument(scriptPath)
}
func quoteQwenPowerShell(value string) string {
return "'" + strings.ReplaceAll(value, "'", "''") + "'"
}
func parseQwenCommand(command string) (string, string, bool) {
start := 0
suffix := ""
if runtime.GOOS == "windows" {
if !strings.HasPrefix(command, "& ") {
return "", "", false
}
start = 2
suffix = "; exit $LASTEXITCODE"
}
executable, next, ok := readQwenQuotedArgument(command, start)
if !ok || next >= len(command) || command[next] != ' ' {
return "", "", false
}
script, end, ok := readQwenQuotedArgument(command, next+1)
return executable, script, ok && command[end:] == suffix &&
executable != "" && script != ""
}
func readQwenQuotedArgument(command string, start int) (string, int, bool) {
if start >= len(command) || command[start] != '\'' {
return "", start, false
}
var value strings.Builder
for index := start + 1; index < len(command); {
if runtime.GOOS == "windows" && strings.HasPrefix(command[index:], "''") {
value.WriteByte('\'')
index += 2
continue
}
if runtime.GOOS != "windows" && strings.HasPrefix(command[index:], qwenPOSIXApostrophe) {
value.WriteByte('\'')
index += len(qwenPOSIXApostrophe)
continue
}
if command[index] == '\'' {
return value.String(), index + 1, true
}
value.WriteByte(command[index])
index++
}
return "", start, false
}
func sameQwenPath(left, right string) bool {
left = filepath.Clean(left)
right = filepath.Clean(right)
if absolute, err := filepath.Abs(left); err == nil {
left = absolute
}
if absolute, err := filepath.Abs(right); err == nil {
right = absolute
}
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func sameQwenAgentID(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func isQwenNodeExecutable(filePath string) bool {
name := strings.ToLower(filepath.Base(filePath))
return name == "node" || name == "node.exe"
}
func qwenRuntimeReferenceForCommand(
command, scriptName, runtimeRoot string,
) (qwenRuntimeReference, bool) {
executable, scriptPath, ok := parseQwenCommand(command)
if !ok || !filepath.IsAbs(executable) || !filepath.IsAbs(scriptPath) ||
!isQwenNodeExecutable(executable) || filepath.Base(scriptPath) != scriptName {
return qwenRuntimeReference{}, false
}
agentDirectory := filepath.Dir(scriptPath)
if !sameQwenPath(filepath.Dir(agentDirectory), runtimeRoot) {
return qwenRuntimeReference{}, false
}
agentID := filepath.Base(agentDirectory)
if agentID == "" || agentID == "." || agentID == ".." {
return qwenRuntimeReference{}, false
}
return qwenRuntimeReference{agentID: agentID, scriptPath: scriptPath}, true
}
+126 -20
View File
@@ -6,14 +6,24 @@ import (
"sort" "sort"
) )
const (
qwenSystemDefaultsKind = "system-defaults"
qwenUserKind = "user"
qwenWorkspaceKind = "workspace"
qwenSystemKind = "system"
)
type qwenDocument struct { type qwenDocument struct {
kind string
filePath string filePath string
exists bool exists bool
raw []byte raw []byte
snapshot *managedFileSnapshot
root map[string]any root map[string]any
hooks qwenHookSettings hooks qwenHookSettings
hasHooksContainer bool hasHooksContainer bool
hooksDisabled bool disableAllHooks *bool
folderTrustEnabled *bool
ownedFile bool ownedFile bool
} }
@@ -24,23 +34,79 @@ type qwenRenderedDocument struct {
remove bool remove bool
} }
func qwenDocumentLabel(filePath string) string { func qwenSourceLabel(kind string) string {
return fmt.Sprintf("Qwen Code settings at %s", filePath) switch kind {
case qwenSystemDefaultsKind:
return "Qwen Code system defaults"
case qwenWorkspaceKind:
return "Qwen Code workspace settings"
case qwenSystemKind:
return "Qwen Code system override settings"
default:
return "Qwen Code user settings"
}
} }
func parseQwenDocument(exists bool, filePath string, raw []byte) (*qwenDocument, error) { func qwenDocumentLabel(document *qwenDocument) string {
label := qwenDocumentLabel(filePath) if document == nil {
return "Qwen Code settings"
}
return qwenSourceLabel(document.kind)
}
func readQwenFolderTrust(
root map[string]any,
label string,
) (*bool, error) {
securityValue, exists := root["security"]
if !exists {
return nil, nil
}
security, ok := securityValue.(map[string]any)
if !ok || security == nil {
return nil, fmt.Errorf(`%s field "security" must be an object`, label)
}
folderTrustValue, exists := security["folderTrust"]
if !exists {
return nil, nil
}
folderTrust, ok := folderTrustValue.(map[string]any)
if !ok || folderTrust == nil {
return nil, fmt.Errorf(`%s field "security.folderTrust" must be an object`, label)
}
enabledValue, exists := folderTrust["enabled"]
if !exists {
return nil, nil
}
enabled, ok := enabledValue.(bool)
if !ok {
return nil, fmt.Errorf(
`%s field "security.folderTrust.enabled" must be a boolean`,
label,
)
}
return &enabled, nil
}
func parseQwenDocument(
kind string,
exists bool,
filePath string,
raw []byte,
snapshot *managedFileSnapshot,
) (*qwenDocument, error) {
label := fmt.Sprintf("%s at %s", qwenSourceLabel(kind), filePath)
root, err := decodeJSONCObject(raw, label, false) root, err := decodeJSONCObject(raw, label, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
disabled := false var disabled *bool
if value, hasFlag := root["disableAllHooks"]; hasFlag { if value, hasFlag := root["disableAllHooks"]; hasFlag {
flag, ok := value.(bool) flag, ok := value.(bool)
if !ok { if !ok {
return nil, fmt.Errorf(`%s field "disableAllHooks" must be a boolean`, label) return nil, fmt.Errorf(`%s field "disableAllHooks" must be a boolean`, label)
} }
disabled = flag disabled = &flag
} }
hooks := qwenHookSettings{} hooks := qwenHookSettings{}
hooksValue, hasHooks := root["hooks"] hooksValue, hasHooks := root["hooks"]
@@ -50,15 +116,25 @@ func parseQwenDocument(exists bool, filePath string, raw []byte) (*qwenDocument,
return nil, err return nil, err
} }
} }
folderTrustEnabled, err := readQwenFolderTrust(root, label)
if err != nil {
return nil, err
}
return &qwenDocument{ return &qwenDocument{
filePath: filePath, exists: exists, raw: append([]byte(nil), raw...), root: root, kind: kind, filePath: filePath, exists: exists,
hooks: hooks, hasHooksContainer: hasHooks, hooksDisabled: disabled, raw: append([]byte(nil), raw...), snapshot: snapshot, root: root,
ownedFile: bytes.HasPrefix(raw, []byte(qwenOwnedFileMarker)), hooks: hooks, hasHooksContainer: hasHooks,
disableAllHooks: disabled, folderTrustEnabled: folderTrustEnabled,
ownedFile: kind == qwenUserKind && bytes.HasPrefix(raw, []byte(qwenOwnedFileMarker)),
}, nil }, nil
} }
func createOwnedQwenDocument(filePath string) (*qwenDocument, error) { func createQwenDocument(kind, filePath string) (*qwenDocument, error) {
return parseQwenDocument(false, filePath, []byte(qwenOwnedFileMarker+"\n{}\n")) raw := []byte("{}\n")
if kind == qwenUserKind {
raw = []byte(qwenOwnedFileMarker + "\n{}\n")
}
return parseQwenDocument(kind, false, filePath, raw, nil)
} }
func renderQwenDocument( func renderQwenDocument(
@@ -66,13 +142,16 @@ func renderQwenDocument(
agentID, runtimeRoot string, agentID, runtimeRoot string,
additions map[string]map[string]any, additions map[string]map[string]any,
) (*qwenRenderedDocument, error) { ) (*qwenRenderedDocument, error) {
label := qwenDocumentLabel(document.filePath) if document == nil {
return nil, fmt.Errorf("Qwen Code user settings are required")
}
label := fmt.Sprintf("%s at %s", qwenDocumentLabel(document), document.filePath)
editor, err := newJSONCEditor(document.raw, label, false) editor, err := newJSONCEditor(document.raw, label, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
removals := managedQwenRemovals(document.hooks, agentID, runtimeRoot) removals := managedQwenRemovals(document.hooks, agentID, runtimeRoot)
for _, event := range qwenToolEvents { for _, event := range qwenManagedEvents {
eventRemovals := make([]qwenManagedRemoval, 0) eventRemovals := make([]qwenManagedRemoval, 0)
for _, removal := range removals { for _, removal := range removals {
if removal.event == event { if removal.event == event {
@@ -98,7 +177,13 @@ func renderQwenDocument(
} }
} }
if len(eventRemovals) > 0 { if len(eventRemovals) > 0 {
current, parseErr := parseQwenDocument(document.exists, document.filePath, editor.pack()) current, parseErr := parseQwenDocument(
document.kind,
document.exists,
document.filePath,
editor.pack(),
document.snapshot,
)
if parseErr != nil { if parseErr != nil {
return nil, parseErr return nil, parseErr
} }
@@ -109,7 +194,13 @@ func renderQwenDocument(
} }
} }
} }
current, err := parseQwenDocument(document.exists, document.filePath, editor.pack()) current, err := parseQwenDocument(
document.kind,
document.exists,
document.filePath,
editor.pack(),
document.snapshot,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -118,12 +209,18 @@ func renderQwenDocument(
return nil, err return nil, err
} }
} }
for _, event := range qwenToolEvents { for _, event := range qwenManagedEvents {
group, exists := additions[event] group, exists := additions[event]
if !exists { if !exists {
continue continue
} }
current, err = parseQwenDocument(document.exists, document.filePath, editor.pack()) current, err = parseQwenDocument(
document.kind,
document.exists,
document.filePath,
editor.pack(),
document.snapshot,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -140,13 +237,22 @@ func renderQwenDocument(
} }
} }
next := editor.pack() next := editor.pack()
nextDocument, err := parseQwenDocument(document.exists, document.filePath, next) nextDocument, err := parseQwenDocument(
document.kind,
document.exists,
document.filePath,
next,
document.snapshot,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
remove := len(additions) == 0 && document.ownedFile && len(nextDocument.root) == 0 remove := len(additions) == 0 && document.ownedFile && len(nextDocument.root) == 0
return &qwenRenderedDocument{ return &qwenRenderedDocument{
document: document, changed: remove || !bytes.Equal(next, document.raw), next: next, remove: remove, document: document,
changed: remove || !bytes.Equal(next, document.raw),
next: next,
remove: remove,
}, nil }, nil
} }
+170 -165
View File
@@ -1,8 +1,6 @@
package plugins package plugins
import ( import (
"fmt"
"os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"sort" "sort"
@@ -13,12 +11,25 @@ const (
qwenAgentKey = "qwen" qwenAgentKey = "qwen"
qwenGuardScript = "guard.js" qwenGuardScript = "guard.js"
qwenAuditScript = "hook.js" qwenAuditScript = "hook.js"
qwenGuardHookName = "elydora-guard"
qwenAuditHookName = "elydora-audit"
qwenHookTimeout = float64(10_000) qwenHookTimeout = float64(10_000)
qwenOwnedFileMarker = "// Managed by Elydora" qwenOwnedFileMarker = "// Managed by Elydora"
qwenPOSIXApostrophe = `'"'"'`
) )
var qwenToolEvents = [...]string{"PreToolUse", "PostToolUse"} var qwenManagedEvents = [...]string{
"PreToolUse",
"PostToolUse",
"PostToolUseFailure",
}
var qwenEventContracts = [...]struct {
event, script, name string
}{
{"PreToolUse", qwenGuardScript, qwenGuardHookName},
{"PostToolUse", qwenAuditScript, qwenAuditHookName},
{"PostToolUseFailure", qwenAuditScript, qwenAuditHookName},
}
type qwenHookSettings map[string]any type qwenHookSettings map[string]any
@@ -35,214 +46,208 @@ type qwenRuntimeContract struct {
auditPath string auditPath string
} }
func buildQwenCommand(nodePath, scriptPath string) string { func qwenExpectedShell() string {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
return "& " + quoteQwenPowerShell(nodePath) + " " + quoteQwenPowerShell(scriptPath) + "; exit $LASTEXITCODE" return "powershell"
} }
return quotePOSIXArgument(nodePath) + " " + quotePOSIXArgument(scriptPath) return "bash"
} }
func quoteQwenPowerShell(value string) string { func buildQwenGroup(nodePath, scriptPath, name string) map[string]any {
return "'" + strings.ReplaceAll(value, "'", "''") + "'"
}
func buildQwenGroup(nodePath, scriptPath string) map[string]any {
shell := "bash"
if runtime.GOOS == "windows" {
shell = "powershell"
}
return map[string]any{ return map[string]any{
"matcher": "*",
"hooks": []any{map[string]any{ "hooks": []any{map[string]any{
"type": "command", "command": buildQwenCommand(nodePath, scriptPath), "type": "command",
"shell": shell, "timeout": qwenHookTimeout, "name": name,
"command": buildQwenCommand(nodePath, scriptPath),
"shell": qwenExpectedShell(),
"timeout": qwenHookTimeout,
}}, }},
} }
} }
func parseQwenCommand(command string) (string, string, bool) { func currentQwenReference(
start := 0 handler map[string]any,
if runtime.GOOS == "windows" { scriptName, hookName, runtimeRoot string,
if !strings.HasPrefix(command, "& ") { ) (qwenRuntimeReference, bool) {
return "", "", false if len(handler) != 5 || handler["type"] != "command" ||
} handler["name"] != hookName || handler["shell"] != qwenExpectedShell() ||
start = 2 handler["timeout"] != qwenHookTimeout {
} return qwenRuntimeReference{}, false
executable, next, ok := readQwenQuotedArgument(command, start)
if !ok || next >= len(command) || command[next] != ' ' {
return "", "", false
}
script, end, ok := readQwenQuotedArgument(command, next+1)
expectedSuffix := ""
if runtime.GOOS == "windows" {
expectedSuffix = "; exit $LASTEXITCODE"
}
return executable, script, ok && command[end:] == expectedSuffix && executable != "" && script != ""
}
func readQwenQuotedArgument(command string, start int) (string, int, bool) {
if start >= len(command) || command[start] != '\'' {
return "", start, false
}
var value strings.Builder
for index := start + 1; index < len(command); {
if runtime.GOOS == "windows" && strings.HasPrefix(command[index:], "''") {
value.WriteByte('\'')
index += 2
continue
}
if runtime.GOOS != "windows" && strings.HasPrefix(command[index:], qwenPOSIXApostrophe) {
value.WriteByte('\'')
index += len(qwenPOSIXApostrophe)
continue
}
if command[index] == '\'' {
return value.String(), index + 1, true
}
value.WriteByte(command[index])
index++
}
return "", start, false
}
func sameQwenPath(left, right string) bool {
left = filepath.Clean(left)
right = filepath.Clean(right)
if absolute, err := filepath.Abs(left); err == nil {
left = absolute
}
if absolute, err := filepath.Abs(right); err == nil {
right = absolute
}
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func sameQwenAgentID(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func qwenRuntimeRoot() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolve home directory: %w", err)
}
return filepath.Join(home, ".elydora"), nil
}
func managedQwenAgentID(handler map[string]any, scriptName, runtimeRoot string) (string, bool) {
if len(handler) != 4 || handler["type"] != "command" || handler["timeout"] != qwenHookTimeout {
return "", false
}
expectedShell := "bash"
if runtime.GOOS == "windows" {
expectedShell = "powershell"
}
if handler["shell"] != expectedShell {
return "", false
} }
command, ok := handler["command"].(string) command, ok := handler["command"].(string)
if !ok { if !ok {
return "", false return qwenRuntimeReference{}, false
} }
executable, scriptPath, ok := parseQwenCommand(command) return qwenRuntimeReferenceForCommand(command, scriptName, runtimeRoot)
if !ok || !filepath.IsAbs(executable) || !filepath.IsAbs(scriptPath) ||
!isQwenNodeExecutable(executable) || !sameQwenFileName(filepath.Base(scriptPath), scriptName) {
return "", false
}
agentDirectory := filepath.Dir(scriptPath)
if !sameQwenPath(filepath.Dir(agentDirectory), runtimeRoot) {
return "", false
}
agentID := filepath.Base(agentDirectory)
return agentID, agentID != "" && agentID != "." && agentID != ".."
} }
func isQwenNodeExecutable(path string) bool { func legacyQwenReference(
name := filepath.Base(path) handler map[string]any,
if runtime.GOOS == "windows" { scriptName, runtimeRoot string,
return strings.EqualFold(name, "node.exe") ) (qwenRuntimeReference, bool) {
if len(handler) != 4 || handler["type"] != "command" ||
handler["shell"] != qwenExpectedShell() ||
handler["timeout"] != qwenHookTimeout {
return qwenRuntimeReference{}, false
} }
return name == "node" command, ok := handler["command"].(string)
if !ok {
return qwenRuntimeReference{}, false
}
return qwenRuntimeReferenceForCommand(command, scriptName, runtimeRoot)
} }
func sameQwenFileName(left, right string) bool { func managedQwenReference(
if runtime.GOOS == "windows" { handler map[string]any,
return strings.EqualFold(left, right) scriptName, hookName, runtimeRoot string,
includeLegacy bool,
) (qwenRuntimeReference, bool) {
if reference, ok := currentQwenReference(
handler,
scriptName,
hookName,
runtimeRoot,
); ok {
return reference, true
} }
return left == right if includeLegacy {
return legacyQwenReference(handler, scriptName, runtimeRoot)
}
return qwenRuntimeReference{}, false
} }
func managedQwenRemovals(settings qwenHookSettings, agentID, runtimeRoot string) []qwenManagedRemoval { func exactCurrentQwenGroup(group map[string]any) bool {
_, hasHooks := group["hooks"]
return len(group) == 1 && hasHooks
}
func exactLegacyQwenGroup(group map[string]any) bool {
_, hasHooks := group["hooks"]
return len(group) == 2 && hasHooks && group["matcher"] == "*"
}
func managedQwenRemovals(
settings qwenHookSettings,
agentID, runtimeRoot string,
) []qwenManagedRemoval {
removals := make([]qwenManagedRemoval, 0) removals := make([]qwenManagedRemoval, 0)
for _, contract := range []struct{ event, script string }{ for _, contract := range qwenEventContracts {
{"PreToolUse", qwenGuardScript}, {"PostToolUse", qwenAuditScript},
} {
groups, _ := settings[contract.event].([]any) groups, _ := settings[contract.event].([]any)
for groupIndex, groupValue := range groups { for groupIndex, groupValue := range groups {
group := groupValue.(map[string]any) group := groupValue.(map[string]any)
handlers := group["hooks"].([]any) handlers := group["hooks"].([]any)
indexes := make([]int, 0) indexes := make([]int, 0)
for handlerIndex, handlerValue := range handlers { for handlerIndex, handlerValue := range handlers {
managedID, managed := managedQwenAgentID(handlerValue.(map[string]any), contract.script, runtimeRoot) reference, managed := managedQwenReference(
if managed && (agentID == "" || sameQwenAgentID(managedID, agentID)) { handlerValue.(map[string]any),
contract.script,
contract.name,
runtimeRoot,
true,
)
if managed && (agentID == "" || sameQwenAgentID(reference.agentID, agentID)) {
indexes = append(indexes, handlerIndex) indexes = append(indexes, handlerIndex)
} }
} }
if len(indexes) == 0 { if len(indexes) == 0 {
continue continue
} }
_, hasMatcher := group["matcher"] removeGroup := (exactCurrentQwenGroup(group) || exactLegacyQwenGroup(group)) &&
_, hasHooks := group["hooks"] len(indexes) == len(handlers)
exactGroup := len(group) == 2 && hasMatcher && hasHooks && group["matcher"] == "*" && len(indexes) == len(handlers) removals = append(removals, qwenManagedRemoval{
removals = append(removals, qwenManagedRemoval{contract.event, groupIndex, indexes, exactGroup}) event: contract.event, groupIndex: groupIndex,
handlerIndexes: indexes, removeGroup: removeGroup,
})
} }
} }
return removals return removals
} }
func qwenRuntimeContracts(settings qwenHookSettings, runtimeRoot string) []qwenRuntimeContract { func normalizedQwenAgentID(agentID string) string {
guards := managedQwenIDs(settings, "PreToolUse", qwenGuardScript, runtimeRoot)
audits := managedQwenIDs(settings, "PostToolUse", qwenAuditScript, runtimeRoot)
ids := make([]string, 0, len(guards))
for guardID := range guards {
for auditID := range audits {
if sameQwenAgentID(guardID, auditID) {
ids = append(ids, guardID)
break
}
}
}
sort.Slice(ids, func(left, right int) bool {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
return strings.ToLower(ids[left]) < strings.ToLower(ids[right]) return strings.ToLower(agentID)
}
return agentID
}
func qwenReferencesForEvent(
settings qwenHookSettings,
event, scriptName, hookName, runtimeRoot string,
) map[string][]qwenRuntimeReference {
references := map[string][]qwenRuntimeReference{}
groups, _ := settings[event].([]any)
for _, groupValue := range groups {
group := groupValue.(map[string]any)
if !exactCurrentQwenGroup(group) {
continue
}
for _, handlerValue := range group["hooks"].([]any) {
reference, ok := currentQwenReference(
handlerValue.(map[string]any),
scriptName,
hookName,
runtimeRoot,
)
if !ok {
continue
}
key := normalizedQwenAgentID(reference.agentID)
references[key] = append(references[key], reference)
}
}
return references
}
func qwenRuntimeContracts(
settings qwenHookSettings,
runtimeRoot string,
) []qwenRuntimeContract {
guards := qwenReferencesForEvent(
settings,
"PreToolUse",
qwenGuardScript,
qwenGuardHookName,
runtimeRoot,
)
posts := qwenReferencesForEvent(
settings,
"PostToolUse",
qwenAuditScript,
qwenAuditHookName,
runtimeRoot,
)
failures := qwenReferencesForEvent(
settings,
"PostToolUseFailure",
qwenAuditScript,
qwenAuditHookName,
runtimeRoot,
)
contracts := make([]qwenRuntimeContract, 0)
for key, guard := range guards {
post := posts[key]
failure := failures[key]
if len(guard) != 1 || len(post) != 1 || len(failure) != 1 ||
!sameQwenPath(post[0].scriptPath, failure[0].scriptPath) {
continue
} }
return ids[left] < ids[right]
})
contracts := make([]qwenRuntimeContract, 0, len(ids))
for _, agentID := range ids {
root := filepath.Join(runtimeRoot, agentID)
contracts = append(contracts, qwenRuntimeContract{ contracts = append(contracts, qwenRuntimeContract{
agentID, filepath.Join(root, qwenGuardScript), filepath.Join(root, qwenAuditScript), agentID: guard[0].agentID,
guardPath: guard[0].scriptPath,
auditPath: post[0].scriptPath,
}) })
} }
sort.Slice(contracts, func(left, right int) bool {
return normalizedQwenAgentID(contracts[left].agentID) <
normalizedQwenAgentID(contracts[right].agentID)
})
return contracts return contracts
} }
func managedQwenIDs(settings qwenHookSettings, event, script, runtimeRoot string) map[string]bool { func qwenRuntimeRoot() (string, error) {
ids := map[string]bool{} return AgentRuntimeRoot()
groups, _ := settings[event].([]any)
for _, groupValue := range groups {
for _, handlerValue := range groupValue.(map[string]any)["hooks"].([]any) {
if agentID, ok := managedQwenAgentID(handlerValue.(map[string]any), script, runtimeRoot); ok {
ids[agentID] = true
} }
}
} func qwenContractDirectory(contract qwenRuntimeContract) string {
return ids return filepath.Dir(contract.guardPath)
} }
@@ -0,0 +1,305 @@
package plugins
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
type qwenRuntimePaths struct {
runtimeRoot string
agentDirectory string
configPath string
keyPath string
guardPath string
auditPath string
}
type preparedQwenInstallation struct {
changes []*fileChange
preconditions []filePrecondition
paths *qwenRuntimePaths
}
type qwenRuntimeConfig struct {
OrgID string `json:"org_id"`
AgentID string `json:"agent_id"`
KID string `json:"kid"`
BaseURL string `json:"base_url"`
Token string `json:"token,omitempty"`
AgentName string `json:"agent_name"`
}
func validateQwenInstallConfig(config InstallConfig) error {
for _, field := range []struct{ name, value string }{
{"agent name", config.AgentName},
{"organization ID", config.OrgID},
{"agent ID", config.AgentID},
{"key ID", config.KID},
{"private key", config.PrivateKey},
{"base URL", config.BaseURL},
{"guard script path", config.GuardScriptPath},
} {
if field.value == "" {
return fmt.Errorf("%s is required", field.name)
}
}
if config.AgentName != qwenAgentKey {
return fmt.Errorf(
"Qwen Code installation requires agent name %s",
qwenAgentKey,
)
}
if strings.TrimSpace(config.OrgID) == "" {
return fmt.Errorf("organization ID is required")
}
if strings.TrimSpace(config.KID) == "" {
return fmt.Errorf("key ID is required")
}
if config.Token != "" && strings.TrimSpace(config.Token) == "" {
return fmt.Errorf("token must contain a non-whitespace value when provided")
}
if err := validateManagedPrivateKey(config.PrivateKey); err != nil {
return err
}
return validateManagedBaseURL(config.BaseURL)
}
func qwenAgentPaths(config InstallConfig) (*qwenRuntimePaths, error) {
if err := validateQwenInstallConfig(config); err != nil {
return nil, err
}
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return nil, err
}
agentDirectory, err := ResolveAgentRuntimeDirectory(config.AgentID)
if err != nil {
return nil, err
}
paths := &qwenRuntimePaths{
runtimeRoot: runtimeRoot,
agentDirectory: agentDirectory,
configPath: filepath.Join(agentDirectory, "config.json"),
keyPath: filepath.Join(agentDirectory, "private.key"),
guardPath: filepath.Join(agentDirectory, qwenGuardScript),
auditPath: filepath.Join(agentDirectory, qwenAuditScript),
}
if !filepath.IsAbs(config.GuardScriptPath) ||
!sameQwenPath(config.GuardScriptPath, paths.guardPath) {
return nil, fmt.Errorf(
"Elydora guard runtime must use the managed agent directory: %s",
paths.guardPath,
)
}
if config.HookScript != "" && (!filepath.IsAbs(config.HookScript) ||
!sameQwenPath(config.HookScript, paths.auditPath)) {
return nil, fmt.Errorf(
"Elydora audit runtime must use the managed agent directory: %s",
paths.auditPath,
)
}
return paths, nil
}
func preflightQwenInstallation(
config InstallConfig,
sources *qwenSources,
) (*qwenRuntimePaths, string, error) {
if sources == nil || sources.user == nil || sources.user.filePath == "" {
return nil, "", fmt.Errorf("Qwen Code installation requires user settings")
}
if err := requireQwenHooksEnabled(sources); err != nil {
return nil, "", err
}
paths, err := qwenAgentPaths(config)
if err != nil {
return nil, "", err
}
if err := validateQwenRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil {
return nil, "", err
}
nodePath, err := resolveNodeRuntime()
if err != nil {
return nil, "", err
}
if !filepath.IsAbs(nodePath) || !isQwenNodeExecutable(nodePath) {
return nil, "", fmt.Errorf(
"Qwen Code hooks require an absolute Node.js executable path",
)
}
return paths, nodePath, nil
}
func buildQwenRuntimeConfig(config InstallConfig) ([]byte, error) {
encoded, err := json.MarshalIndent(qwenRuntimeConfig{
OrgID: config.OrgID,
AgentID: config.AgentID,
KID: config.KID,
BaseURL: config.BaseURL,
Token: config.Token,
AgentName: qwenAgentKey,
}, "", " ")
if err != nil {
return nil, fmt.Errorf("encode Elydora runtime config: %w", err)
}
encoded = append(encoded, '\n')
if len(encoded) > maxRuntimeConfigBytes {
return nil, fmt.Errorf(
"Elydora runtime config exceeds %d bytes after JSON encoding",
maxRuntimeConfigBytes,
)
}
return encoded, nil
}
func prepareRenderedQwenChange(
rendered *qwenRenderedDocument,
) (*fileChange, error) {
if rendered == nil || !rendered.changed {
return nil, nil
}
return prepareSnapshotSourceChange(
rendered.document.filePath,
qwenDocumentLabel(rendered.document),
rendered.document.snapshot,
rendered.next,
0600,
rendered.remove,
)
}
func qwenReadOnlyPreconditions(
sources *qwenSources,
changedPath string,
) []filePrecondition {
if sources == nil {
return nil
}
result := make([]filePrecondition, 0, len(sources.preconditions))
for _, condition := range sources.preconditions {
if changedPath != "" && sameQwenPath(condition.filePath, changedPath) {
continue
}
result = append(result, condition)
}
return result
}
func prepareQwenInstallation(
config InstallConfig,
sources *qwenSources,
rendered *qwenRenderedDocument,
) (*preparedQwenInstallation, error) {
paths, _, err := preflightQwenInstallation(config, sources)
if err != nil {
return nil, err
}
if rendered == nil || rendered.document == nil ||
!sameQwenPath(rendered.document.filePath, sources.user.filePath) {
return nil, fmt.Errorf("Qwen Code rendered user settings are required")
}
runtimeConfig, err := buildQwenRuntimeConfig(config)
if err != nil {
return nil, err
}
items := []struct {
path, label string
content []byte
mode os.FileMode
}{
{
paths.guardPath,
"Elydora guard runtime",
[]byte(generateGuardScript(qwenAgentKey, config.AgentID, "", false, "")),
0700,
},
{paths.configPath, "Elydora runtime config", runtimeConfig, 0600},
{paths.keyPath, "Elydora private key", []byte(config.PrivateKey), 0600},
{
paths.auditPath,
"Elydora audit runtime",
[]byte(buildHookScriptWithOutput(
qwenAgentKey,
config.AgentID,
"",
false,
true,
)),
0700,
},
}
changes := make([]*fileChange, 0, len(items)+1)
for _, item := range items {
change, changeErr := prepareFileChange(item.path, item.label, item.content, item.mode)
if changeErr != nil {
return nil, changeErr
}
changes = append(changes, change)
}
settingsChange, err := prepareRenderedQwenChange(rendered)
if err != nil {
return nil, err
}
changes = append(changes, settingsChange)
changedPath := ""
if rendered.changed {
changedPath = rendered.document.filePath
}
return &preparedQwenInstallation{
changes: changes,
paths: paths,
preconditions: qwenReadOnlyPreconditions(sources, changedPath),
}, nil
}
func commitQwenInstallation(
prepared *preparedQwenInstallation,
rename renameFunc,
) error {
if prepared == nil || prepared.paths == nil {
return fmt.Errorf("prepared Qwen Code installation is required")
}
if err := EnsurePrivateDirectory(prepared.paths.runtimeRoot); err != nil {
return err
}
if err := EnsurePrivateDirectory(prepared.paths.agentDirectory); err != nil {
return err
}
return writeChanges(
prepared.changes,
"Install Qwen Code hooks",
rename,
prepared.preconditions...,
)
}
func prepareQwenUninstall(
sources *qwenSources,
rendered *qwenRenderedDocument,
) (*fileChange, []filePrecondition, error) {
change, err := prepareRenderedQwenChange(rendered)
if err != nil {
return nil, nil, err
}
changedPath := ""
if rendered != nil && rendered.changed {
changedPath = rendered.document.filePath
}
return change, qwenReadOnlyPreconditions(sources, changedPath), nil
}
func commitQwenUninstall(
change *fileChange,
preconditions []filePrecondition,
rename renameFunc,
) error {
return writeChanges(
[]*fileChange{change},
"Uninstall Qwen Code hooks",
rename,
preconditions...,
)
}
+240 -124
View File
@@ -1,148 +1,264 @@
package plugins package plugins
import ( import (
"encoding/json" "bytes"
"fmt" "fmt"
"os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"sort"
"strings"
) )
var qwenPathSeparatorPattern = regexp.MustCompile(`[/\\]+`) var qwenPathSeparatorPattern = regexp.MustCompile(`[/\\]+`)
func defaultQwenHome() (string, error) { func readQwenRuntimeConfig(path string) (map[string]any, bool, error) {
home, err := os.UserHomeDir() snapshot, err := readManagedFile(path, "Elydora runtime config", maxRuntimeConfigBytes)
if err != nil { if err != nil || snapshot == nil {
return "", fmt.Errorf("resolve home directory: %w", err) return nil, snapshot != nil, err
} }
return filepath.Join(home, ".qwen"), nil label := fmt.Sprintf("Elydora runtime config at %s", path)
config, err := decodeStrictJSONObject(snapshot.contents, label)
return config, true, err
} }
func resolveQwenStoragePath(value string) (string, error) { func requireQwenRuntimeString(
resolved := value config map[string]any,
if value == "~" || len(value) >= 2 && value[0] == '~' && (value[1] == '/' || value[1] == '\\') { field, configPath string,
home, err := os.UserHomeDir() ) (string, error) {
if err != nil { value, ok := config[field].(string)
return "", fmt.Errorf("resolve home directory: %w", err) if !ok || strings.TrimSpace(value) == "" {
} return "", fmt.Errorf(
resolved = home "Elydora runtime config %s is invalid: %s",
if value != "~" { field,
for _, segment := range qwenPathSeparatorPattern.Split(value[2:], -1) { configPath,
if segment != "" {
resolved = filepath.Join(resolved, segment)
}
}
}
}
if filepath.IsAbs(resolved) {
return filepath.Clean(resolved), nil
}
absolute, err := filepath.Abs(resolved)
if err != nil {
return "", fmt.Errorf("resolve Qwen Code storage path %q: %w", value, err)
}
return absolute, nil
}
func qwenHomeFromEnvFile(filePath string) (string, bool, error) {
raw, exists, err := readOptionalFile(filePath, "Qwen home environment")
if err != nil || !exists {
return "", false, err
}
value := parseDotenv(raw)["QWEN_HOME"]
if value == "" {
return "", false, nil
}
resolved, err := resolveQwenStoragePath(value)
return resolved, err == nil, err
}
func resolveQwenHome() (string, error) {
defaultHome, err := defaultQwenHome()
if err != nil {
return "", err
}
value, explicit := os.LookupEnv("QWEN_HOME")
initialHome := defaultHome
if value != "" {
initialHome, err = resolveQwenStoragePath(value)
if err != nil {
return "", err
}
}
if explicit {
return initialHome, nil
}
for _, candidate := range []string{
filepath.Join(initialHome, ".env"), filepath.Join(filepath.Dir(initialHome), ".env"),
} {
if discovered, exists, readErr := qwenHomeFromEnvFile(candidate); readErr != nil {
return "", readErr
} else if exists {
return discovered, nil
}
}
return initialHome, nil
}
func readQwenDocument() (*qwenDocument, error) {
home, err := resolveQwenHome()
if err != nil {
return nil, err
}
configPath := filepath.Join(home, "settings.json")
raw, exists, err := readOptionalFile(configPath, "Qwen Code settings")
if err != nil {
return nil, err
}
if !exists {
return createOwnedQwenDocument(configPath)
}
return parseQwenDocument(true, configPath, raw)
}
func prepareRenderedQwenChange(rendered *qwenRenderedDocument) (*fileChange, error) {
if rendered == nil || !rendered.changed {
return nil, nil
}
return prepareSourceChange(
rendered.document.filePath,
"Qwen Code settings",
rendered.document.raw,
rendered.document.exists,
rendered.next,
os.FileMode(0600),
rendered.remove,
) )
} }
return value, nil
}
func validateQwenRuntimeConfig(
config map[string]any,
expectedAgentID, configPath string,
) error {
supported := stringSet(
"org_id",
"agent_id",
"kid",
"base_url",
"token",
"agent_name",
)
fields := make([]string, 0, len(config))
for field := range config {
fields = append(fields, field)
}
sort.Strings(fields)
for _, field := range fields {
if _, ok := supported[field]; !ok {
return fmt.Errorf(
`Elydora runtime config has unsupported field %q: %s`,
field,
configPath,
)
}
}
if _, err := requireQwenRuntimeString(config, "org_id", configPath); err != nil {
return err
}
if _, err := requireQwenRuntimeString(config, "kid", configPath); err != nil {
return err
}
agentID, err := requireQwenRuntimeString(config, "agent_id", configPath)
if err != nil {
return err
}
if !sameQwenAgentID(agentID, expectedAgentID) || config["agent_name"] != qwenAgentKey {
return fmt.Errorf(
"Elydora runtime identity does not match Qwen Code hooks: %s",
configPath,
)
}
if _, exists := config["token"]; exists {
if _, err := requireQwenRuntimeString(config, "token", configPath); err != nil {
return err
}
}
baseURL, err := requireQwenRuntimeString(config, "base_url", configPath)
if err != nil {
return err
}
if err := validateManagedBaseURL(baseURL); err != nil {
return fmt.Errorf(
"Elydora runtime config base URL is invalid at %s: %w",
configPath,
err,
)
}
return nil
}
func validateQwenRuntimeIdentity(agentDirectory, agentID string) error {
runtimeRoot := filepath.Dir(agentDirectory)
rootExists, err := managedPhysicalDirectoryExists(
runtimeRoot,
"Elydora runtime directory",
)
if err != nil || !rootExists {
return err
}
directoryExists, err := managedPhysicalDirectoryExists(
agentDirectory,
"Elydora agent runtime directory",
)
if err != nil || !directoryExists {
return err
}
configPath := filepath.Join(agentDirectory, "config.json")
config, configExists, err := readQwenRuntimeConfig(configPath)
if err != nil {
return err
}
artifactExists := false
for _, item := range []struct {
path string
label string
limit int64
}{
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes},
{filepath.Join(agentDirectory, qwenGuardScript), "Elydora guard runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, qwenAuditScript), "Elydora audit runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, "chain-state.json"), "Elydora chain state", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "status-cache.json"), "Elydora status cache", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "error.log"), "Elydora error log", maxManagedSourceBytes},
} {
exists, inspectErr := managedPhysicalFileExists(item.path, item.label, item.limit)
if inspectErr != nil {
return inspectErr
}
artifactExists = artifactExists || exists
}
if !configExists {
if artifactExists {
return fmt.Errorf(
"Elydora runtime identity cannot be verified without config.json: %s",
agentDirectory,
)
}
return nil
}
configuredID, ok := config["agent_id"].(string)
if !ok || config["agent_name"] != qwenAgentKey ||
!sameQwenAgentID(configuredID, agentID) {
return fmt.Errorf(
"Elydora runtime config identity does not match Qwen Code agent %s: %s",
agentID,
configPath,
)
}
return nil
}
func validQwenContractPaths(contract qwenRuntimeContract) (bool, error) {
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return false, err
}
agentDirectory := qwenContractDirectory(contract)
return sameQwenPath(filepath.Dir(agentDirectory), runtimeRoot) &&
sameQwenPath(
contract.guardPath,
filepath.Join(agentDirectory, qwenGuardScript),
) &&
sameQwenPath(
contract.auditPath,
filepath.Join(agentDirectory, qwenAuditScript),
), nil
}
func qwenRuntimeContractExists(contract qwenRuntimeContract) (bool, error) {
validPaths, err := validQwenContractPaths(contract)
if err != nil || !validPaths {
return false, err
}
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return false, err
}
agentDirectory := qwenContractDirectory(contract)
rootExists, err := managedPhysicalDirectoryExists(
runtimeRoot,
"Elydora runtime directory",
)
if err != nil || !rootExists {
return false, err
}
directoryExists, err := managedPhysicalDirectoryExists(
agentDirectory,
"Elydora agent runtime directory",
)
if err != nil || !directoryExists {
return false, err
}
configPath := filepath.Join(agentDirectory, "config.json")
keyPath := filepath.Join(agentDirectory, "private.key")
config, configExists, err := readQwenRuntimeConfig(configPath)
if err != nil {
return false, err
}
key, err := readManagedFile(keyPath, "Elydora private key", maxProtectedSecretBytes)
if err != nil {
return false, err
}
guard, err := readManagedFile(
contract.guardPath,
"Elydora guard runtime",
maxManagedSourceBytes,
)
if err != nil {
return false, err
}
audit, err := readManagedFile(
contract.auditPath,
"Elydora audit runtime",
maxManagedSourceBytes,
)
if err != nil {
return false, err
}
if !configExists || key == nil || guard == nil || audit == nil {
return false, nil
}
if err := validateQwenRuntimeConfig(config, contract.agentID, configPath); err != nil {
return false, err
}
if err := validateManagedPrivateKey(string(key.contents)); err != nil {
return false, fmt.Errorf("Elydora private key at %s: %w", keyPath, err)
}
expectedGuard := []byte(generateGuardScript(
qwenAgentKey,
contract.agentID,
"",
false,
"",
))
expectedAudit := []byte(buildHookScriptWithOutput(
qwenAgentKey,
contract.agentID,
"",
false,
true,
))
return bytes.Equal(guard.contents, expectedGuard) &&
bytes.Equal(audit.contents, expectedAudit), nil
}
func qwenRuntimeFilesExist(contracts []qwenRuntimeContract) (bool, error) { func qwenRuntimeFilesExist(contracts []qwenRuntimeContract) (bool, error) {
for _, contract := range contracts { for _, contract := range contracts {
configPath := filepath.Join(filepath.Dir(contract.guardPath), "config.json") exists, err := qwenRuntimeContractExists(contract)
raw, exists, err := readOptionalFile(configPath, "Elydora runtime config")
if err != nil { if err != nil {
return false, err return false, err
} }
if !exists { if exists {
continue
}
var config map[string]any
if err := json.Unmarshal(raw, &config); err != nil {
return false, fmt.Errorf("parse Elydora runtime config at %s: %w", configPath, err)
}
agentID, ok := config["agent_id"].(string)
if !ok || config["agent_name"] != qwenAgentKey || !sameQwenAgentID(agentID, contract.agentID) {
continue
}
guardExists, err := regularFileExists(contract.guardPath, "Elydora guard runtime")
if err != nil {
return false, err
}
auditExists, err := regularFileExists(contract.auditPath, "Elydora audit runtime")
if err != nil {
return false, err
}
if guardExists && auditExists {
return true, nil return true, nil
} }
} }
+193 -56
View File
@@ -8,27 +8,70 @@ import (
"testing" "testing"
) )
func TestQwenInstallRejectsMalformedSettingsBeforeWrites(t *testing.T) { func TestQwenInstallRejectsMalformedKnownSettingsBeforeWrites(t *testing.T) {
tests := []struct { tests := []struct {
name, source, pattern string name, source, pattern string
}{ }{
{"malformed", "{ malformed", "parse Qwen Code settings"}, {"malformed", "{ malformed", "parse Qwen Code user settings"},
{"non-object", "[]", "JSON object"}, {"non-object", "[]", "JSON object"},
{"trailing comma", `{ "owner": true, }`, "trailing comma"}, {"trailing comma", `{ "owner": true, }`, "trailing comma"},
{"duplicate root", `{ "hooks": {}, "hooks": {} }`, "duplicate"}, {"duplicate root", `{ "hooks": {}, "hooks": {} }`, "duplicate"},
{"nested duplicate", `{ "hooks": { "PreToolUse": [{ "hooks": [], "hooks": [] }] } }`, "duplicate"}, {
"nested duplicate",
`{ "hooks": { "PreToolUse": [{ "hooks": [], "hooks": [] }] } }`,
"duplicate",
},
{"invalid disable flag", `{ "disableAllHooks": "yes" }`, "must be a boolean"}, {"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"}, {"hooks array", `{ "hooks": [] }`, "JSON object"},
{"unsupported event", `{ "hooks": { "UnknownEvent": [] } }`, "unsupported field"}, {"null hooks", `{ "hooks": null }`, "JSON object"},
{"null event", `{ "hooks": { "PreToolUse": null } }`, "must be an array"}, {"null event", `{ "hooks": { "PreToolUse": null } }`, "must be an array"},
{"null group", `{ "hooks": { "PreToolUse": [null] } }`, "must be an object"}, {"null group", `{ "hooks": { "PreToolUse": [null] } }`, "must be an object"},
{"invalid matcher", `{ "hooks": { "PreToolUse": [{ "matcher": "[", "hooks": [] }] } }`, "regular expression"}, {
{"invalid sequential", `{ "hooks": { "PreToolUse": [{ "sequential": "yes", "hooks": [] }] } }`, "must be a boolean"}, "invalid matcher type",
{"null handlers", `{ "hooks": { "PreToolUse": [{ "hooks": null }] } }`, "hooks array"}, `{ "hooks": { "PreToolUse": [{ "matcher": 1, "hooks": [] }] } }`,
{"missing command", `{ "hooks": { "PreToolUse": [{ "hooks": [{ "type": "command" }] }] } }`, "non-empty string"}, "matcher must be a string",
{"missing url", `{ "hooks": { "PreToolUse": [{ "hooks": [{ "type": "http" }] }] } }`, "non-empty string"}, },
{"invalid handler type", `{ "hooks": { "PreToolUse": [{ "hooks": [{ "type": "function", "command": "x" }] }] } }`, "command"}, {
{"invalid timeout", `{ "hooks": { "PreToolUse": [{ "hooks": [{ "type": "command", "command": "x", "timeout": "ten" }] }] } }`, "finite number"}, "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 { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
@@ -40,68 +83,156 @@ func TestQwenInstallRejectsMalformedSettingsBeforeWrites(t *testing.T) {
if readQwenTestFile(t, fixture.configPath) != test.source { if readQwenTestFile(t, fixture.configPath) != test.source {
t.Fatal("failed install changed Qwen Code settings") t.Fatal("failed install changed Qwen Code settings")
} }
for _, path := range []string{fixture.hookPath, fixture.runtimeConfig, fixture.privateKey} { for _, path := range []string{
fixture.guardPath,
fixture.hookPath,
fixture.runtimeConfig,
fixture.privateKey,
} {
requireMissingQwenFile(t, path) requireMissingQwenFile(t, path)
} }
}) })
} }
} }
func TestQwenInstallValidatesRoutingAndManagedPathsBeforeWrites(t *testing.T) { func TestQwenPreservesFutureEventsAndLimitsRegexSemantics(t *testing.T) {
t.Run("unreadable home env", func(t *testing.T) { settings := `{
fixture := prepareQwenFixture(t, qwenFixtureOptions{}) "hooks": {
envPath := filepath.Join(fixture.qwenDir, ".env") "FutureEvent": { "shape": "future" },
if err := os.MkdirAll(envPath, 0755); err != nil { "UserPromptSubmit": [{ "matcher": "[", "hooks": [] }]
t.Fatalf("create unreadable env path: %v", err) }
}`
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)
}
} }
err := fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), "Qwen home environment") {
t.Fatalf("install error = %v", err)
} }
requireMissingQwenFile(t, fixture.configPath)
requireMissingQwenFile(t, fixture.runtimeConfig)
})
func TestQwenInstallRejectsMalformedReadOnlySourceBeforeWrites(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
mutate func(*qwenFixture) path func(*qwenFixture) string
pattern string
}{ }{
{ {"system defaults", func(f *qwenFixture) string { return f.systemDefaults }},
name: "missing guard", {"workspace", func(f *qwenFixture) string { return f.workspaceConfig }},
mutate: func(fixture *qwenFixture) { {"system override", func(f *qwenFixture) string { return f.systemConfig }},
if err := os.Remove(fixture.guardPath); err != nil {
t.Fatalf("remove guard: %v", err)
}
},
pattern: "guard runtime is missing",
},
{
name: "invalid agent id",
mutate: func(fixture *qwenFixture) {
fixture.config.AgentID = "../agent"
},
pattern: "single non-empty path segment",
},
{
name: "guard outside managed directory",
mutate: func(fixture *qwenFixture) {
fixture.config.GuardScriptPath = filepath.Join(fixture.homeDir, "guard.js")
},
pattern: "managed agent directory",
},
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
fixture := prepareQwenFixture(t, qwenFixtureOptions{}) fixture := prepareQwenFixture(t, qwenFixtureOptions{})
test.mutate(fixture) filePath := test.path(fixture)
writeOptionalQwenFile(t, filePath, qwenString("{ malformed"))
err := fixture.plugin.Install(fixture.config) err := fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), test.pattern) { if err == nil || !strings.Contains(err.Error(), "parse Qwen Code") {
t.Fatalf("install error = %v, want substring %q", err, test.pattern) t.Fatalf("install error = %v", err)
} }
for _, path := range []string{fixture.configPath, fixture.hookPath, fixture.runtimeConfig, fixture.privateKey} { 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) requireMissingQwenFile(t, path)
} }
requireNoQwenStagingFiles(t, fixture.homeDir)
}) })
} }
} }
@@ -120,13 +251,18 @@ func TestQwenTransactionRollsBackAllFilesAndCleansStaging(t *testing.T) {
return os.Rename(source, destination) return os.Rename(source, destination)
} }
err := fixture.plugin.Install(fixture.config) err := fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), "Write Qwen Code installation") || !failed { if err == nil || !strings.Contains(err.Error(), "Install Qwen Code hooks") || !failed {
t.Fatalf("transaction error = %v, failed = %v", err, failed) t.Fatalf("transaction error = %v, failed = %v", err, failed)
} }
if readQwenTestFile(t, fixture.configPath) != original { if readQwenTestFile(t, fixture.configPath) != original {
t.Fatal("transaction rollback did not restore Qwen Code settings") t.Fatal("transaction rollback did not restore Qwen Code settings")
} }
for _, path := range []string{fixture.hookPath, fixture.runtimeConfig, fixture.privateKey} { for _, path := range []string{
fixture.guardPath,
fixture.hookPath,
fixture.runtimeConfig,
fixture.privateKey,
} {
requireMissingQwenFile(t, path) requireMissingQwenFile(t, path)
} }
requireNoQwenStagingFiles(t, fixture.homeDir) requireNoQwenStagingFiles(t, fixture.homeDir)
@@ -152,7 +288,8 @@ func TestQwenStatusSurfacesMalformedRuntimeConfig(t *testing.T) {
if err := os.WriteFile(fixture.runtimeConfig, []byte("{ malformed"), 0600); err != nil { if err := os.WriteFile(fixture.runtimeConfig, []byte("{ malformed"), 0600); err != nil {
t.Fatalf("corrupt runtime config: %v", err) t.Fatalf("corrupt runtime config: %v", err)
} }
if _, err := fixture.plugin.Status(); err == nil || !strings.Contains(err.Error(), "parse Elydora runtime config") { if _, err := fixture.plugin.Status(); err == nil ||
!strings.Contains(err.Error(), "parse Elydora runtime config") {
t.Fatalf("status error = %v", err) t.Fatalf("status error = %v", err)
} }
} }
@@ -0,0 +1,194 @@
package plugins
import (
"encoding/json"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
func TestQwenCommandsBlockAndForwardNativePayloadByteForByte(t *testing.T) {
fixture := prepareQwenFixture(t, qwenFixtureOptions{})
installQwenFixture(t, fixture)
capturePath := filepath.Join(t.TempDir(), "captured-event.json")
encodedPath, err := json.Marshal(capturePath)
if err != nil {
t.Fatalf("marshal capture path: %v", err)
}
guardScript := "const fs = require('node:fs');\n" +
"fs.readFileSync(0);\n" +
"process.stderr.write('Agent is frozen by Elydora.');\n" +
"process.exit(2);\n"
if err := os.WriteFile(fixture.guardPath, []byte(guardScript), 0700); err != nil {
t.Fatalf("write blocking guard runtime: %v", err)
}
captureScript := "const fs = require('node:fs');\n" +
"fs.writeFileSync(" + string(encodedPath) + ", fs.readFileSync(0));\n"
if err := os.WriteFile(fixture.hookPath, []byte(captureScript), 0700); err != nil {
t.Fatalf("write capture audit runtime: %v", err)
}
settings := readQwenTestObject(t, fixture.configPath)
prePayload := `{"session_id":"session-1","cwd":"C:/workspace","hook_event_name":"PreToolUse","timestamp":"2026-07-19T00:00:00.000Z","tool_name":"run_shell_command","tool_input":{"command":"echo test"}}` + "\n"
guard := qwenManagedHandler(t, settings, "PreToolUse", fixture.guardPath)
guardResult := runQwenHandler(t, guard, fixture.homeDir, prePayload)
if guardResult.exitCode != 2 || guardResult.stdout != "" ||
!strings.Contains(guardResult.stderr, "Agent is frozen by Elydora") {
t.Fatalf("guard result = %#v", guardResult)
}
for _, event := range []string{"PostToolUse", "PostToolUseFailure"} {
payload := strings.Replace(prePayload, "PreToolUse", event, 1)
handler := qwenManagedHandler(t, settings, event, fixture.hookPath)
result := runQwenHandler(t, handler, fixture.homeDir, payload)
if result.exitCode != 0 || result.stdout != "" || result.stderr != "" {
t.Fatalf("%s result = %#v", event, result)
}
if readQwenTestFile(t, capturePath) != payload {
t.Fatalf("%s changed the native Qwen Code payload", event)
}
}
}
func TestQwenPowerShellCommandPropagatesNodeExitCode(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("PowerShell command contract is Windows-specific")
}
fixture := prepareQwenFixture(t, qwenFixtureOptions{})
installQwenFixture(t, fixture)
handler := qwenManagedHandler(
t,
readQwenTestObject(t, fixture.configPath),
"PreToolUse",
fixture.guardPath,
)
command := handler["command"].(string)
if !strings.HasPrefix(command, "& '") ||
!strings.HasSuffix(command, "; exit $LASTEXITCODE") {
t.Fatalf("PowerShell command = %q", command)
}
}
func TestQwenStatusRequiresEnabledExactTripleAndRuntimeFiles(t *testing.T) {
fixture := prepareQwenFixture(t, qwenFixtureOptions{})
installQwenFixture(t, fixture)
status, err := fixture.plugin.Status()
if err != nil || !status.Installed || !status.HookConfigured ||
!status.HookScriptExists {
t.Fatalf("installed status = %#v, %v", status, err)
}
settings := readQwenTestObject(t, fixture.configPath)
hooks := requireQwenObject(t, settings["hooks"])
failureGroups := requireQwenArray(t, hooks["PostToolUseFailure"])
hooks["PostToolUseFailure"] = append(failureGroups, failureGroups[len(failureGroups)-1])
writeQwenTestObject(t, fixture.configPath, settings)
status, err = fixture.plugin.Status()
if err != nil || status.HookConfigured || status.Installed {
t.Fatalf("duplicate status = %#v, %v", status, err)
}
installQwenFixture(t, fixture)
settings = readQwenTestObject(t, fixture.configPath)
hooks = requireQwenObject(t, settings["hooks"])
delete(hooks, "PostToolUseFailure")
writeQwenTestObject(t, fixture.configPath, settings)
status, err = fixture.plugin.Status()
if err != nil || status.HookConfigured || status.Installed {
t.Fatalf("incomplete status = %#v, %v", status, err)
}
installQwenFixture(t, fixture)
writeOptionalQwenFile(
t,
fixture.systemConfig,
qwenJSON(map[string]any{"disableAllHooks": true}),
)
status, err = fixture.plugin.Status()
if err != nil || status.HookConfigured || status.Installed {
t.Fatalf("disabled status = %#v, %v", status, err)
}
if err := os.Remove(fixture.systemConfig); err != nil {
t.Fatalf("remove system settings: %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.HookConfigured || status.HookScriptExists || status.Installed {
t.Fatalf("missing runtime status = %#v, %v", status, err)
}
}
func TestQwenStatusRequiresCanonicalRuntimeContents(t *testing.T) {
tests := []struct {
name string
mutate func(*testing.T, *qwenFixture)
wantErr string
}{
{
"guard content",
func(t *testing.T, f *qwenFixture) {
if err := os.WriteFile(f.guardPath, []byte("tampered\n"), 0700); err != nil {
t.Fatalf("tamper guard: %v", err)
}
},
"",
},
{
"audit content",
func(t *testing.T, f *qwenFixture) {
if err := os.WriteFile(f.hookPath, []byte("tampered\n"), 0700); err != nil {
t.Fatalf("tamper audit: %v", err)
}
},
"",
},
{
"private key",
func(t *testing.T, f *qwenFixture) {
if err := os.WriteFile(f.privateKey, []byte("invalid"), 0600); err != nil {
t.Fatalf("tamper private key: %v", err)
}
},
"private key",
},
{
"extra config field",
func(t *testing.T, f *qwenFixture) {
config := readQwenTestObject(t, f.runtimeConfig)
config["extra"] = true
writeQwenTestObject(t, f.runtimeConfig, config)
},
"unsupported field",
},
{
"runtime identity",
func(t *testing.T, f *qwenFixture) {
config := readQwenTestObject(t, f.runtimeConfig)
config["agent_id"] = "other-agent"
writeQwenTestObject(t, f.runtimeConfig, config)
},
"identity does not match",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fixture := prepareQwenFixture(t, qwenFixtureOptions{})
installQwenFixture(t, fixture)
test.mutate(t, fixture)
status, err := fixture.plugin.Status()
if test.wantErr == "" {
if err != nil || status.Installed || status.HookScriptExists {
t.Fatalf("status = %#v, %v", status, err)
}
return
}
if err == nil || !strings.Contains(err.Error(), test.wantErr) ||
status.Installed || status.HookScriptExists {
t.Fatalf("status = %#v, %v", status, err)
}
})
}
}
+79 -35
View File
@@ -11,18 +11,46 @@ import (
"time" "time"
) )
var qwenEventNames = map[string]bool{ var qwenEventNames = stringSet(
"PreToolUse": true, "PostToolUse": true, "PostToolUseFailure": true, "PreToolUse",
"PostToolBatch": true, "Notification": true, "UserPromptSubmit": true, "PostToolUse",
"UserPromptExpansion": true, "SessionStart": true, "Stop": true, "PostToolUseFailure",
"MessageDisplay": true, "SubagentStart": true, "SubagentStop": true, "PostToolBatch",
"PreCompact": true, "PostCompact": true, "SessionEnd": true, "Notification",
"PermissionRequest": true, "PermissionDenied": true, "StopFailure": true, "UserPromptSubmit",
"TodoCreated": true, "TodoCompleted": true, "InstructionsLoaded": true, "UserPromptExpansion",
} "SessionStart",
"Stop",
"MessageDisplay",
"SubagentStart",
"SubagentStop",
"PreCompact",
"PostCompact",
"SessionEnd",
"PermissionRequest",
"PermissionDenied",
"StopFailure",
"TodoCreated",
"TodoCompleted",
"InstructionsLoaded",
)
var qwenConfigFields = map[string]bool{ var qwenRegexMatcherEvents = [...]string{
"enabled": true, "disabled": true, "notifications": true, "PreToolUse",
"PostToolUse",
"PostToolUseFailure",
"PermissionRequest",
"PermissionDenied",
"SubagentStart",
"SubagentStop",
"PreCompact",
"PostCompact",
"SessionStart",
"SessionEnd",
"StopFailure",
"Notification",
"InstructionsLoaded",
"UserPromptExpansion",
} }
type qwenRegexEntry struct { type qwenRegexEntry struct {
@@ -36,24 +64,21 @@ func readQwenHookSettings(value any, label string) (qwenHookSettings, error) {
return nil, fmt.Errorf("%s must contain a JSON object", label) return nil, fmt.Errorf("%s must contain a JSON object", label)
} }
settings := make(qwenHookSettings, len(object)) settings := make(qwenHookSettings, len(object))
for key, item := range object { for event, item := range object {
if qwenConfigFields[key] { if _, known := qwenEventNames[event]; !known {
settings[key] = item settings[event] = item
continue continue
} }
if !qwenEventNames[key] {
return nil, fmt.Errorf(`%s contains unsupported field %q`, label, key)
}
groups, ok := item.([]any) groups, ok := item.([]any)
if !ok { if !ok {
return nil, fmt.Errorf(`%s field %q must be an array`, label, key) return nil, fmt.Errorf(`%s field %q must be an array`, label, event)
} }
for groupIndex, group := range groups { for groupIndex, group := range groups {
if err := validateQwenGroup(group, label, key, groupIndex); err != nil { if err := validateQwenGroup(group, label, event, groupIndex); err != nil {
return nil, err return nil, err
} }
} }
settings[key] = item settings[event] = item
} }
return settings, nil return settings, nil
} }
@@ -94,7 +119,7 @@ func validateQwenHandler(value any, groupLabel string, handlerIndex int) error {
} }
kind, _ := handler["type"].(string) kind, _ := handler["type"].(string)
if kind != "command" && kind != "http" && kind != "prompt" { if kind != "command" && kind != "http" && kind != "prompt" {
return fmt.Errorf(`%s type must be "command", "http", or "prompt"`, location) return fmt.Errorf("%s has unsupported type %q", location, kind)
} }
if timeout, exists := handler["timeout"]; exists { if timeout, exists := handler["timeout"]; exists {
number, ok := timeout.(float64) number, ok := timeout.(float64)
@@ -111,7 +136,7 @@ func validateQwenHandler(value any, groupLabel string, handlerIndex int) error {
case "command": case "command":
command, ok := handler["command"].(string) command, ok := handler["command"].(string)
if !ok || command == "" { if !ok || command == "" {
return fmt.Errorf("%s command must be a non-empty string", location) return fmt.Errorf("%s requires a non-empty command", location)
} }
if err := optionalQwenStringMap(handler, "env", location); err != nil { if err := optionalQwenStringMap(handler, "env", location); err != nil {
return err return err
@@ -125,7 +150,7 @@ func validateQwenHandler(value any, groupLabel string, handlerIndex int) error {
case "http": case "http":
url, ok := handler["url"].(string) url, ok := handler["url"].(string)
if !ok || url == "" { if !ok || url == "" {
return fmt.Errorf("%s url must be a non-empty string", location) return fmt.Errorf("%s requires a non-empty url", location)
} }
if err := optionalQwenStringMap(handler, "headers", location); err != nil { if err := optionalQwenStringMap(handler, "headers", location); err != nil {
return err return err
@@ -145,7 +170,7 @@ func validateQwenHandler(value any, groupLabel string, handlerIndex int) error {
case "prompt": case "prompt":
prompt, ok := handler["prompt"].(string) prompt, ok := handler["prompt"].(string)
if !ok || prompt == "" { if !ok || prompt == "" {
return fmt.Errorf("%s prompt must be a non-empty string", location) return fmt.Errorf("%s requires a non-empty prompt", location)
} }
if err := optionalQwenString(handler, "model", location); err != nil { if err := optionalQwenString(handler, "model", location); err != nil {
return err return err
@@ -179,11 +204,11 @@ func optionalQwenStringMap(value map[string]any, key, label string) error {
} }
entries, ok := item.(map[string]any) entries, ok := item.(map[string]any)
if !ok { if !ok {
return fmt.Errorf(`%s field %q must contain string values`, label, key) return fmt.Errorf(`%s field %q must map names to strings`, label, key)
} }
for _, entry := range entries { for _, entry := range entries {
if _, ok := entry.(string); !ok { if _, ok := entry.(string); !ok {
return fmt.Errorf(`%s field %q must contain string values`, label, key) return fmt.Errorf(`%s field %q must map names to strings`, label, key)
} }
} }
return nil return nil
@@ -198,25 +223,41 @@ func allQwenStrings(values []any) bool {
return true return true
} }
func validateQwenRegexes(nodePath string, settings qwenHookSettings) error { func collectQwenRegexes(sources []qwenHookSettings) []qwenRegexEntry {
entries := make([]qwenRegexEntry, 0) entries := make([]qwenRegexEntry, 0)
for event, value := range settings { for sourceIndex, settings := range sources {
if qwenConfigFields[event] { for _, event := range qwenRegexMatcherEvents {
continue groups, _ := settings[event].([]any)
} for groupIndex, groupValue := range groups {
for index, groupValue := range value.([]any) {
group := groupValue.(map[string]any) group := groupValue.(map[string]any)
matcher, ok := group["matcher"].(string) matcher, ok := group["matcher"].(string)
if ok && strings.TrimSpace(matcher) != "" && strings.TrimSpace(matcher) != "*" { if !ok || strings.TrimSpace(matcher) == "" || strings.TrimSpace(matcher) == "*" {
continue
}
entries = append(entries, qwenRegexEntry{ entries = append(entries, qwenRegexEntry{
Label: fmt.Sprintf(`Qwen Code hooks field %q[%d] matcher`, event, index), Pattern: matcher, Label: fmt.Sprintf(
`Qwen Code hook source %d field %q[%d] matcher`,
sourceIndex,
event,
groupIndex,
),
Pattern: matcher,
}) })
} }
} }
} }
return entries
}
func validateQwenJavaScriptMatchers(sources []qwenHookSettings) error {
entries := collectQwenRegexes(sources)
if len(entries) == 0 { if len(entries) == 0 {
return nil return nil
} }
nodePath, err := resolveNodeRuntime()
if err != nil {
return err
}
payload, err := json.Marshal(entries) payload, err := json.Marshal(entries)
if err != nil { if err != nil {
return fmt.Errorf("encode Qwen Code regular expressions: %w", err) return fmt.Errorf("encode Qwen Code regular expressions: %w", err)
@@ -244,7 +285,10 @@ for (const entry of entries) {
if message == "" { if message == "" {
message = runErr.Error() message = runErr.Error()
} }
return fmt.Errorf("Qwen Code matcher must be a valid JavaScript regular expression: %s", message) return fmt.Errorf(
"Qwen Code matcher must be a valid JavaScript regular expression: %s",
message,
)
} }
return nil return nil
} }
@@ -0,0 +1,179 @@
package plugins
import (
"strings"
"testing"
)
func TestQwenSchemaTracksOfficialEventAndMatcherSets(t *testing.T) {
expectedEvents := []string{
"PreToolUse",
"PostToolUse",
"PostToolUseFailure",
"PostToolBatch",
"Notification",
"UserPromptSubmit",
"UserPromptExpansion",
"SessionStart",
"Stop",
"MessageDisplay",
"SubagentStart",
"SubagentStop",
"PreCompact",
"PostCompact",
"SessionEnd",
"PermissionRequest",
"PermissionDenied",
"StopFailure",
"TodoCreated",
"TodoCompleted",
"InstructionsLoaded",
}
if len(qwenEventNames) != len(expectedEvents) {
t.Fatalf("known event count = %d", len(qwenEventNames))
}
for _, event := range expectedEvents {
if _, exists := qwenEventNames[event]; !exists {
t.Fatalf("official event is missing: %s", event)
}
}
if len(qwenRegexMatcherEvents) != 15 {
t.Fatalf("matcher event count = %d", len(qwenRegexMatcherEvents))
}
hooks := qwenHookSettings{}
for _, event := range expectedEvents {
hooks[event] = []any{map[string]any{
"matcher": "tool-(?<name>.+)",
"hooks": []any{},
}}
}
entries := collectQwenRegexes([]qwenHookSettings{hooks})
if len(entries) != 15 {
t.Fatalf("collected matcher count = %d", len(entries))
}
}
func TestQwenMatcherValidationUsesJavaScriptRegExp(t *testing.T) {
t.Run("JavaScript named capture", func(t *testing.T) {
fixture := prepareQwenFixture(t, qwenFixtureOptions{
settings: qwenString(`{
"hooks": {
"PreToolUse": [{ "matcher": "(?<tool>read|write)", "hooks": [] }]
}
}`),
})
installQwenFixture(t, fixture)
})
t.Run("Python named capture", func(t *testing.T) {
fixture := prepareQwenFixture(t, qwenFixtureOptions{
settings: qwenString(`{
"hooks": {
"PreToolUse": [{ "matcher": "(?P<tool>read|write)", "hooks": [] }]
}
}`),
})
err := fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), "JavaScript regular expression") {
t.Fatalf("install error = %v", err)
}
requireMissingQwenFile(t, fixture.guardPath)
})
}
func TestQwenSchemaAcceptsOfficialCommandHTTPAndPromptHandlers(t *testing.T) {
settings := `{
"hooks": {
"PreToolUse": [{
"matcher": "read|write",
"sequential": true,
"hooks": [
{
"type": "command",
"name": "command-hook",
"command": "echo test",
"shell": "powershell",
"timeout": 1000,
"env": { "MODE": "test" },
"async": false,
"description": "command"
},
{
"type": "http",
"name": "http-hook",
"url": "https://hooks.example.test",
"headers": { "Authorization": "Bearer token" },
"allowedEnvVars": ["HOME"],
"once": true,
"if": "always"
},
{
"type": "prompt",
"name": "prompt-hook",
"prompt": "Review this tool call",
"model": "qwen3-coder"
}
]
}]
}
}`
fixture := prepareQwenFixture(t, qwenFixtureOptions{settings: qwenString(settings)})
installQwenFixture(t, fixture)
raw := readQwenTestFile(t, fixture.configPath)
for _, marker := range []string{"command-hook", "http-hook", "prompt-hook"} {
if !strings.Contains(raw, marker) {
t.Fatalf("handler %q was lost", marker)
}
}
}
func TestQwenSchemaRejectsInvalidOfficialHandlerFields(t *testing.T) {
tests := []struct {
name, handler, pattern string
}{
{
"command env",
`{"type":"command","command":"x","env":{"A":1}}`,
"map names to strings",
},
{
"command async",
`{"type":"command","command":"x","async":"yes"}`,
"must be a boolean",
},
{
"command shell",
`{"type":"command","command":"x","shell":"cmd"}`,
"shell must be",
},
{
"http headers",
`{"type":"http","url":"https://x.test","headers":{"A":1}}`,
"map names to strings",
},
{
"http environment",
`{"type":"http","url":"https://x.test","allowedEnvVars":[1]}`,
"array of strings",
},
{
"prompt model",
`{"type":"prompt","prompt":"x","model":1}`,
"must be a string",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
source := `{"hooks":{"PreToolUse":[{"hooks":[` + test.handler + `]}]}}`
fixture := prepareQwenFixture(
t,
qwenFixtureOptions{settings: qwenString(source)},
)
err := fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), test.pattern) {
t.Fatalf("install error = %v", err)
}
requireMissingQwenFile(t, fixture.guardPath)
})
}
}
@@ -0,0 +1,196 @@
package plugins
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestQwenInstallValidatesCredentialsAndManagedPathsBeforeWrites(t *testing.T) {
tests := []struct {
name string
mutate func(*qwenFixture)
pattern string
}{
{
"agent name",
func(f *qwenFixture) { f.config.AgentName = "other" },
"requires agent name qwen",
},
{
"organization",
func(f *qwenFixture) { f.config.OrgID = " " },
"organization ID is required",
},
{
"agent id",
func(f *qwenFixture) { f.config.AgentID = "../agent" },
"single non-empty path segment",
},
{
"key id",
func(f *qwenFixture) { f.config.KID = " " },
"key ID is required",
},
{
"private key",
func(f *qwenFixture) { f.config.PrivateKey = "invalid" },
"private key",
},
{
"base URL",
func(f *qwenFixture) { f.config.BaseURL = "https://api.test/path?secret=x" },
"base URL",
},
{
"token",
func(f *qwenFixture) { f.config.Token = " " },
"token must contain",
},
{
"relative guard",
func(f *qwenFixture) { f.config.GuardScriptPath = "guard.js" },
"managed agent directory",
},
{
"guard outside runtime",
func(f *qwenFixture) {
f.config.GuardScriptPath = filepath.Join(f.homeDir, "guard.js")
},
"managed agent directory",
},
{
"audit outside runtime",
func(f *qwenFixture) {
f.config.HookScript = filepath.Join(f.homeDir, "hook.js")
},
"audit runtime must use",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fixture := prepareQwenFixture(t, qwenFixtureOptions{})
test.mutate(fixture)
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)
}
for _, path := range []string{
fixture.configPath,
fixture.guardPath,
fixture.hookPath,
fixture.runtimeConfig,
fixture.privateKey,
} {
requireMissingQwenFile(t, path)
}
})
}
}
func TestQwenInstallRejectsNonPhysicalSources(t *testing.T) {
t.Run("home environment directory", func(t *testing.T) {
fixture := prepareQwenFixture(t, qwenFixtureOptions{})
envPath := filepath.Join(fixture.qwenDir, ".env")
if err := os.MkdirAll(envPath, 0755); err != nil {
t.Fatalf("create environment directory: %v", err)
}
err := fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), "physical file") {
t.Fatalf("install error = %v", err)
}
requireMissingQwenFile(t, fixture.guardPath)
})
t.Run("user settings symlink", func(t *testing.T) {
fixture := prepareQwenFixture(t, qwenFixtureOptions{})
target := filepath.Join(filepath.Dir(fixture.homeDir), "settings-target.json")
writeOptionalQwenFile(t, target, qwenString("{}\n"))
if err := os.MkdirAll(filepath.Dir(fixture.configPath), 0755); err != nil {
t.Fatalf("create settings directory: %v", err)
}
if err := os.Symlink(target, fixture.configPath); err != nil {
t.Skipf("create settings symlink: %v", err)
}
err := fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), "physical file") {
t.Fatalf("install error = %v", err)
}
requireMissingQwenFile(t, fixture.guardPath)
})
t.Run("agent runtime symlink", func(t *testing.T) {
fixture := prepareQwenFixture(t, qwenFixtureOptions{})
runtimeRoot := filepath.Dir(fixture.agentDir)
target := filepath.Join(filepath.Dir(fixture.homeDir), "agent-target")
for _, directory := range []string{runtimeRoot, target} {
if err := os.MkdirAll(directory, 0700); err != nil {
t.Fatalf("create runtime directory: %v", err)
}
}
if err := os.Symlink(target, fixture.agentDir); err != nil {
t.Skipf("create runtime symlink: %v", err)
}
err := fixture.plugin.PreflightInstall(fixture.config)
if err == nil || !strings.Contains(err.Error(), "physical directory") {
t.Fatalf("preflight error = %v", err)
}
})
}
func TestQwenPreflightProtectsExistingRuntimeIdentity(t *testing.T) {
tests := []struct {
name string
configure func(*testing.T, *qwenFixture)
pattern string
}{
{
"orphan artifact",
func(t *testing.T, f *qwenFixture) {
writeOptionalQwenFile(t, f.guardPath, qwenString("external\n"))
},
"cannot be verified without config.json",
},
{
"different identity",
func(t *testing.T, f *qwenFixture) {
writeOptionalQwenFile(t, f.runtimeConfig, qwenJSON(map[string]any{
"org_id": "org-1",
"agent_id": "other-agent",
"kid": "kid-1",
"base_url": "https://api.elydora.test",
"agent_name": qwenAgentKey,
}))
},
"identity does not match",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fixture := prepareQwenFixture(t, qwenFixtureOptions{})
test.configure(t, fixture)
err := fixture.plugin.PreflightInstall(fixture.config)
if err == nil || !strings.Contains(err.Error(), test.pattern) {
t.Fatalf("preflight error = %v", err)
}
})
}
}
func TestQwenStatusRejectsRuntimeSymlink(t *testing.T) {
fixture := prepareQwenFixture(t, qwenFixtureOptions{})
installQwenFixture(t, fixture)
target := filepath.Join(filepath.Dir(fixture.homeDir), "guard-target.js")
writeOptionalQwenFile(t, target, qwenString(readQwenTestFile(t, fixture.guardPath)))
if err := os.Remove(fixture.guardPath); err != nil {
t.Fatalf("remove guard runtime: %v", err)
}
if err := os.Symlink(target, fixture.guardPath); err != nil {
t.Skipf("create guard symlink: %v", err)
}
if _, err := fixture.plugin.Status(); err == nil ||
!strings.Contains(err.Error(), "physical file") {
t.Fatalf("status error = %v", err)
}
}
+478
View File
@@ -0,0 +1,478 @@
package plugins
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
)
var qwenHomeEnvironmentKeys = [...]string{"QWEN_HOME", "QWEN_RUNTIME_DIR"}
type qwenDisableControl struct {
disabled bool
source *qwenDocument
}
type qwenSources struct {
qwenHome string
systemDefaults *qwenDocument
user *qwenDocument
workspace *qwenDocument
system *qwenDocument
workspaceActive bool
workspaceTrusted bool
disableControl qwenDisableControl
preconditions []filePrecondition
}
type qwenRoutingResult struct {
qwenHome string
preconditions []filePrecondition
}
func defaultQwenHome() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolve home directory: %w", err)
}
return filepath.Join(home, ".qwen"), nil
}
func resolveQwenStoragePath(value string) (string, error) {
resolved := value
if value == "~" || len(value) >= 2 && value[0] == '~' &&
(value[1] == '/' || value[1] == '\\') {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolve home directory: %w", err)
}
resolved = home
if value != "~" {
for _, segment := range qwenPathSeparatorPattern.Split(value[2:], -1) {
if segment != "" {
resolved = filepath.Join(resolved, segment)
}
}
}
}
if filepath.IsAbs(resolved) {
return filepath.Clean(resolved), nil
}
absolute, err := filepath.Abs(resolved)
if err != nil {
return "", fmt.Errorf("resolve Qwen Code storage path %q: %w", value, err)
}
return absolute, nil
}
func qwenComparisonPath(filePath string) string {
resolved, err := filepath.Abs(filePath)
if err != nil {
resolved = filepath.Clean(filePath)
}
if runtime.GOOS == "windows" {
return strings.ToLower(resolved)
}
return resolved
}
func resolveQwenRouting() (*qwenRoutingResult, error) {
values := map[string]string{}
owned := map[string]bool{}
for _, key := range qwenHomeEnvironmentKeys {
value, present := os.LookupEnv(key)
values[key] = value
owned[key] = present
}
if values["QWEN_HOME"] != "" && values["QWEN_RUNTIME_DIR"] != "" {
home, err := resolveQwenStoragePath(values["QWEN_HOME"])
return &qwenRoutingResult{qwenHome: home}, err
}
defaultHome, err := defaultQwenHome()
if err != nil {
return nil, err
}
initialHome := values["QWEN_HOME"]
initialDirectory := defaultHome
if initialHome != "" {
initialDirectory, err = resolveQwenStoragePath(initialHome)
if err != nil {
return nil, err
}
}
candidates := []string{filepath.Join(initialDirectory, ".env")}
if initialHome == "" {
candidates = append(candidates, filepath.Join(filepath.Dir(initialDirectory), ".env"))
}
preconditions := make([]filePrecondition, 0, 3)
visited := map[string]bool{}
readCandidate := func(filePath string) error {
resolved, resolveErr := filepath.Abs(filePath)
if resolveErr != nil {
return fmt.Errorf("resolve Qwen Code home environment path: %w", resolveErr)
}
key := qwenComparisonPath(resolved)
if visited[key] {
return nil
}
visited[key] = true
snapshot, readErr := readManagedFile(
resolved,
"Qwen Code home environment",
maxManagedSourceBytes,
)
if readErr != nil {
return readErr
}
preconditions = append(preconditions, filePrecondition{
filePath: resolved, label: "Qwen Code home environment",
snapshot: snapshot, maximumSize: maxManagedSourceBytes,
})
if snapshot == nil {
return nil
}
parsed := parseDotenv(snapshot.contents)
for _, envKey := range qwenHomeEnvironmentKeys {
value := parsed[envKey]
if value != "" && !owned[envKey] {
values[envKey] = value
owned[envKey] = true
}
}
return nil
}
for _, candidate := range candidates {
if err := readCandidate(candidate); err != nil {
return nil, err
}
}
discoveredHome := values["QWEN_HOME"]
if discoveredHome != "" && discoveredHome != initialHome {
discoveredDirectory, resolveErr := resolveQwenStoragePath(discoveredHome)
if resolveErr != nil {
return nil, resolveErr
}
if !sameQwenPath(discoveredDirectory, initialDirectory) {
if err := readCandidate(filepath.Join(discoveredDirectory, ".env")); err != nil {
return nil, err
}
}
}
resolvedHome := defaultHome
if values["QWEN_HOME"] != "" {
resolvedHome, err = resolveQwenStoragePath(values["QWEN_HOME"])
if err != nil {
return nil, err
}
}
return &qwenRoutingResult{
qwenHome: resolvedHome,
preconditions: preconditions,
}, nil
}
func resolveQwenHome() (string, error) {
routing, err := resolveQwenRouting()
if err != nil {
return "", err
}
return routing.qwenHome, nil
}
func qwenSystemSettingsPath() (string, error) {
if configured := os.Getenv("QWEN_CODE_SYSTEM_SETTINGS_PATH"); configured != "" {
return filepath.Abs(configured)
}
switch runtime.GOOS {
case "darwin":
return "/Library/Application Support/QwenCode/settings.json", nil
case "windows":
return `C:\ProgramData\qwen-code\settings.json`, nil
default:
return "/etc/qwen-code/settings.json", nil
}
}
func qwenSystemDefaultsPath(systemPath string) (string, error) {
if configured := os.Getenv("QWEN_CODE_SYSTEM_DEFAULTS_PATH"); configured != "" {
return filepath.Abs(configured)
}
return filepath.Join(filepath.Dir(systemPath), "system-defaults.json"), nil
}
func readQwenDocument(kind, filePath string) (*qwenDocument, error) {
snapshot, err := readManagedFile(filePath, qwenSourceLabel(kind), maxManagedSourceBytes)
if err != nil {
return nil, err
}
if snapshot == nil {
return createQwenDocument(kind, filePath)
}
return parseQwenDocument(kind, true, filePath, snapshot.contents, snapshot)
}
func canonicalQwenPath(filePath string) string {
canonical, err := filepath.EvalSymlinks(filePath)
if err == nil {
return canonical
}
absolute, absErr := filepath.Abs(filePath)
if absErr == nil {
return absolute
}
return filepath.Clean(filePath)
}
func qwenPathWithin(child, parent string) bool {
relative, err := filepath.Rel(qwenComparisonPath(parent), qwenComparisonPath(child))
if err != nil {
return false
}
return relative == "." ||
(relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator)) &&
!filepath.IsAbs(relative))
}
func qwenWorkspaceTrust(
system, user *qwenDocument,
qwenHome, workspacePath string,
) (bool, *filePrecondition, error) {
var enabled *bool
if user != nil && user.folderTrustEnabled != nil {
enabled = user.folderTrustEnabled
} else if system != nil {
enabled = system.folderTrustEnabled
}
if enabled == nil || !*enabled {
return true, nil, nil
}
filePath := os.Getenv("QWEN_CODE_TRUSTED_FOLDERS_PATH")
var err error
if filePath != "" {
filePath, err = filepath.Abs(filePath)
if err != nil {
return false, nil, fmt.Errorf("resolve Qwen Code trusted folders path: %w", err)
}
} else {
filePath = filepath.Join(qwenHome, "trustedFolders.json")
}
snapshot, err := readManagedFile(
filePath,
"Qwen Code trusted folders",
maxManagedSourceBytes,
)
if err != nil {
return false, nil, err
}
precondition := &filePrecondition{
filePath: filePath, label: "Qwen Code trusted folders",
snapshot: snapshot, maximumSize: maxManagedSourceBytes,
}
if snapshot == nil {
return true, precondition, nil
}
rules, err := decodeJSONCObject(
snapshot.contents,
fmt.Sprintf("Qwen Code trusted folders at %s", filePath),
false,
)
if err != nil {
return false, nil, err
}
for rulePath, level := range rules {
if level != "TRUST_FOLDER" && level != "TRUST_PARENT" && level != "DO_NOT_TRUST" {
return false, nil, fmt.Errorf(
"Qwen Code trusted folders has invalid trust level for %q",
rulePath,
)
}
}
workspace := canonicalQwenPath(workspacePath)
for rulePath, level := range rules {
canonicalRule := canonicalQwenPath(rulePath)
trustRoot := canonicalRule
if level == "TRUST_PARENT" {
trustRoot = filepath.Dir(canonicalRule)
}
if (level == "TRUST_FOLDER" || level == "TRUST_PARENT") &&
qwenPathWithin(workspace, trustRoot) {
return true, precondition, nil
}
}
for rulePath, level := range rules {
if level == "DO_NOT_TRUST" &&
qwenComparisonPath(workspace) == qwenComparisonPath(canonicalQwenPath(rulePath)) {
return false, precondition, nil
}
}
return true, precondition, nil
}
func effectiveQwenDisable(
systemDefaults, user, workspace, system *qwenDocument,
useWorkspace bool,
) qwenDisableControl {
control := qwenDisableControl{}
documents := []*qwenDocument{systemDefaults, user}
if useWorkspace {
documents = append(documents, workspace)
}
documents = append(documents, system)
for _, document := range documents {
if document == nil || document.disableAllHooks == nil {
continue
}
control.disabled = *document.disableAllHooks
control.source = document
}
return control
}
func qwenSourcePrecondition(document *qwenDocument) filePrecondition {
return filePrecondition{
filePath: document.filePath,
label: qwenDocumentLabel(document),
snapshot: document.snapshot,
maximumSize: maxManagedSourceBytes,
}
}
func deduplicateQwenPreconditions(values []filePrecondition) []filePrecondition {
result := make([]filePrecondition, 0, len(values))
seen := map[string]bool{}
for _, value := range values {
key := qwenComparisonPath(value.filePath)
if seen[key] {
continue
}
seen[key] = true
result = append(result, value)
}
return result
}
func readQwenSources() (*qwenSources, error) {
routing, err := resolveQwenRouting()
if err != nil {
return nil, err
}
systemPath, err := qwenSystemSettingsPath()
if err != nil {
return nil, err
}
defaultsPath, err := qwenSystemDefaultsPath(systemPath)
if err != nil {
return nil, err
}
workspaceDirectory, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("resolve Qwen Code workspace: %w", err)
}
workspacePath := filepath.Join(workspaceDirectory, ".qwen", "settings.json")
system, err := readQwenDocument(qwenSystemKind, systemPath)
if err != nil {
return nil, err
}
systemDefaults, err := readQwenDocument(qwenSystemDefaultsKind, defaultsPath)
if err != nil {
return nil, err
}
user, err := readQwenDocument(
qwenUserKind,
filepath.Join(routing.qwenHome, "settings.json"),
)
if err != nil {
return nil, err
}
home, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("resolve home directory: %w", err)
}
canonicalWorkspace := canonicalQwenPath(workspaceDirectory)
workspaceActive := qwenComparisonPath(canonicalWorkspace) !=
qwenComparisonPath(canonicalQwenPath(home))
workspace, err := createQwenDocument(qwenWorkspaceKind, workspacePath)
if err != nil {
return nil, err
}
if workspaceActive {
workspace, err = readQwenDocument(qwenWorkspaceKind, workspacePath)
if err != nil {
return nil, err
}
}
workspaceTrusted := false
var trustPrecondition *filePrecondition
if workspaceActive {
workspaceTrusted, trustPrecondition, err = qwenWorkspaceTrust(
system,
user,
routing.qwenHome,
canonicalWorkspace,
)
if err != nil {
return nil, err
}
}
if err := validateQwenJavaScriptMatchers([]qwenHookSettings{
systemDefaults.hooks,
user.hooks,
workspace.hooks,
system.hooks,
}); err != nil {
return nil, err
}
preconditions := append([]filePrecondition(nil), routing.preconditions...)
preconditions = append(
preconditions,
qwenSourcePrecondition(systemDefaults),
qwenSourcePrecondition(user),
)
if workspaceActive {
preconditions = append(preconditions, qwenSourcePrecondition(workspace))
}
preconditions = append(preconditions, qwenSourcePrecondition(system))
if trustPrecondition != nil {
preconditions = append(preconditions, *trustPrecondition)
}
return &qwenSources{
qwenHome: routing.qwenHome,
systemDefaults: systemDefaults,
user: user,
workspace: workspace,
system: system,
workspaceActive: workspaceActive,
workspaceTrusted: workspaceTrusted,
disableControl: effectiveQwenDisable(
systemDefaults,
user,
workspace,
system,
workspaceActive && workspaceTrusted,
),
preconditions: deduplicateQwenPreconditions(preconditions),
}, nil
}
func requireQwenHooksEnabled(sources *qwenSources) error {
if sources == nil {
return fmt.Errorf("Qwen Code settings sources are required")
}
if !sources.disableControl.disabled {
return nil
}
location := "effective settings"
if sources.disableControl.source != nil {
location = fmt.Sprintf(
"%s at %s",
qwenDocumentLabel(sources.disableControl.source),
sources.disableControl.source.filePath,
)
}
return fmt.Errorf(
"Qwen Code hooks are disabled by disableAllHooks in %s",
location,
)
}
@@ -0,0 +1,209 @@
package plugins
import (
"os"
"path/filepath"
"testing"
)
func TestQwenHomeUsesOfficialEnvironmentDiscovery(t *testing.T) {
fixture := prepareQwenFixture(t, qwenFixtureOptions{})
firstHome := filepath.Join(filepath.Dir(fixture.homeDir), "first # qwen home")
secondHome := filepath.Join(filepath.Dir(fixture.homeDir), "second qwen home")
writeOptionalQwenFile(
t,
filepath.Join(fixture.qwenDir, ".env"),
qwenString(
"export QWEN_HOME = \""+firstHome+"\" # selected by Qwen\n"+
"QWEN_RUNTIME_DIR=runtime-one\n",
),
)
writeOptionalQwenFile(
t,
filepath.Join(fixture.homeDir, ".env"),
qwenString("QWEN_HOME="+secondHome+"\n"),
)
writeOptionalQwenFile(
t,
filepath.Join(firstHome, ".env"),
qwenString("QWEN_HOME="+secondHome+"\nQWEN_RUNTIME_DIR=runtime-two\n"),
)
installQwenFixture(t, fixture)
selected := filepath.Join(firstHome, "settings.json")
qwenManagedHandler(t, readQwenTestObject(t, selected), "PreToolUse", fixture.guardPath)
requireMissingQwenFile(t, filepath.Join(secondHome, "settings.json"))
requireMissingQwenFile(t, fixture.configPath)
status, err := fixture.plugin.Status()
if err != nil || status.ConfigPath != selected || !status.Installed {
t.Fatalf("Qwen status = %#v, %v", status, err)
}
}
func TestQwenExplicitHomeOwnershipSupportsRelativeTildeAndEmptyValues(t *testing.T) {
tests := []struct {
name, value string
expected func(*qwenFixture) string
}{
{
"relative",
"relative-qwen",
func(f *qwenFixture) string {
return filepath.Join(f.workspaceDir, "relative-qwen", "settings.json")
},
},
{
"tilde",
"~/custom-qwen",
func(f *qwenFixture) string {
return filepath.Join(f.homeDir, "custom-qwen", "settings.json")
},
},
{"empty", "", func(f *qwenFixture) string { return f.configPath }},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fixture := prepareQwenFixture(t, qwenFixtureOptions{})
ignored := filepath.Join(filepath.Dir(fixture.homeDir), "ignored-qwen-home")
writeOptionalQwenFile(
t,
filepath.Join(fixture.qwenDir, ".env"),
qwenString("QWEN_HOME="+ignored+"\n"),
)
t.Setenv("QWEN_HOME", test.value)
installQwenFixture(t, fixture)
selected := test.expected(fixture)
qwenManagedHandler(
t,
readQwenTestObject(t, selected),
"PreToolUse",
fixture.guardPath,
)
requireMissingQwenFile(t, filepath.Join(ignored, "settings.json"))
})
}
}
func TestQwenRoutingReadsDiscoveredHomeEnvironment(t *testing.T) {
fixture := prepareQwenFixture(t, qwenFixtureOptions{})
discovered := filepath.Join(filepath.Dir(fixture.homeDir), "discovered")
writeOptionalQwenFile(
t,
filepath.Join(fixture.qwenDir, ".env"),
qwenString("QWEN_HOME="+discovered+"\n"),
)
discoveredEnv := filepath.Join(discovered, ".env")
writeOptionalQwenFile(
t,
discoveredEnv,
qwenString("QWEN_RUNTIME_DIR=runtime-output\n"),
)
routing, err := resolveQwenRouting()
if err != nil {
t.Fatalf("resolve Qwen routing: %v", err)
}
if !sameQwenPath(routing.qwenHome, discovered) {
t.Fatalf("Qwen home = %s", routing.qwenHome)
}
found := false
for _, condition := range routing.preconditions {
found = found || sameQwenPath(condition.filePath, discoveredEnv)
}
if !found {
t.Fatalf("discovered environment is absent from preconditions: %#v", routing.preconditions)
}
}
func TestQwenRoutingSkipsDotenvWhenBothEnvironmentValuesExist(t *testing.T) {
fixture := prepareQwenFixture(t, qwenFixtureOptions{})
explicit := filepath.Join(filepath.Dir(fixture.homeDir), "explicit")
writeOptionalQwenFile(
t,
filepath.Join(fixture.qwenDir, ".env"),
qwenString("QWEN_HOME=ignored\n"),
)
t.Setenv("QWEN_HOME", explicit)
t.Setenv("QWEN_RUNTIME_DIR", "runtime")
routing, err := resolveQwenRouting()
if err != nil {
t.Fatalf("resolve Qwen routing: %v", err)
}
if !sameQwenPath(routing.qwenHome, explicit) || len(routing.preconditions) != 0 {
t.Fatalf("routing = %#v", routing)
}
}
func TestQwenEffectiveDisableUsesOfficialLayerOrderAndTrust(t *testing.T) {
tests := []struct {
name string
trustLevel string
systemDisabled *bool
wantDisabled bool
wantSource string
}{
{"untrusted workspace", "DO_NOT_TRUST", nil, false, qwenUserKind},
{"trusted workspace", "TRUST_FOLDER", nil, true, qwenWorkspaceKind},
{"system override", "TRUST_FOLDER", qwenBool(false), false, qwenSystemKind},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fixture := prepareQwenFixture(t, qwenFixtureOptions{
settings: qwenJSON(map[string]any{
"disableAllHooks": false,
"security": map[string]any{
"folderTrust": map[string]any{"enabled": true},
},
}),
})
writeOptionalQwenFile(
t,
fixture.systemDefaults,
qwenJSON(map[string]any{"disableAllHooks": true}),
)
writeOptionalQwenFile(
t,
fixture.workspaceConfig,
qwenJSON(map[string]any{"disableAllHooks": true}),
)
if test.systemDisabled != nil {
writeOptionalQwenFile(
t,
fixture.systemConfig,
qwenJSON(map[string]any{"disableAllHooks": *test.systemDisabled}),
)
}
writeOptionalQwenFile(
t,
fixture.trustedFolders,
qwenJSON(map[string]any{fixture.workspaceDir: test.trustLevel}),
)
sources, err := readQwenSources()
if err != nil {
t.Fatalf("read Qwen sources: %v", err)
}
if sources.workspaceTrusted != (test.trustLevel == "TRUST_FOLDER") ||
sources.disableControl.disabled != test.wantDisabled ||
sources.disableControl.source == nil ||
sources.disableControl.source.kind != test.wantSource {
t.Fatalf("sources = %#v", sources)
}
})
}
}
func TestQwenWorkspaceAtHomeIsInactive(t *testing.T) {
fixture := prepareQwenFixture(t, qwenFixtureOptions{})
if err := os.Chdir(fixture.homeDir); err != nil {
t.Fatalf("enter home directory: %v", err)
}
sources, err := readQwenSources()
if err != nil {
t.Fatalf("read Qwen sources: %v", err)
}
if sources.workspaceActive || sources.workspaceTrusted {
t.Fatalf("workspace state = active:%v trusted:%v", sources.workspaceActive, sources.workspaceTrusted)
}
}
func qwenBool(value bool) *bool {
return &value
}
+129 -160
View File
@@ -1,7 +1,6 @@
package plugins package plugins
import ( import (
"encoding/json"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
@@ -10,36 +9,55 @@ import (
"testing" "testing"
) )
func TestQwenRegistryUsesOfficialUserSettings(t *testing.T) { func TestQwenRegistryAndRuntimeOwnership(t *testing.T) {
entry := SupportedAgents[qwenAgentKey] entry := SupportedAgents[qwenAgentKey]
if entry.Name != "Qwen Code" || entry.ConfigDir != "~/.qwen" || entry.ConfigFile != "settings.json" { if entry.Name != "Qwen Code" || entry.ConfigDir != "~/.qwen" ||
entry.ConfigFile != "settings.json" {
t.Fatalf("Qwen Code registry entry = %#v", entry) t.Fatalf("Qwen Code registry entry = %#v", entry)
} }
if _, ok := NewPlugin(qwenAgentKey).(*QwenPlugin); !ok { plugin, ok := NewPlugin(qwenAgentKey).(*QwenPlugin)
if !ok {
t.Fatal("Qwen Code plugin is not registered") 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 TestQwenInstallPreservesJSONCommentsAndIsIdempotent(t *testing.T) { func TestQwenInstallPreservesSourcesAndIsIdempotent(t *testing.T) {
settings := "{\r\n" + settings := "{\r\n" +
" // Keep this user preference.\r\n" + " // Keep this user preference.\r\n" +
" \"theme\": \"GitHub\",\r\n" + " \"theme\": \"GitHub\",\r\n" +
" \"hooks\": {\r\n" + " \"hooks\": {\r\n" +
" \"FutureEvent\": { \"opaque\": true },\r\n" +
" \"SessionStart\": [{ \"hooks\": [{ \"type\": \"command\", \"command\": \"session-hook\" }] }],\r\n" + " \"SessionStart\": [{ \"hooks\": [{ \"type\": \"command\", \"command\": \"session-hook\" }] }],\r\n" +
" \"PreToolUse\": [{ \"matcher\": \"read_file\", \"hooks\": [{ \"type\": \"command\", \"command\": \"user-hook\" }] }]\r\n" + " \"PreToolUse\": [{ \"matcher\": \"read_file\", \"hooks\": [{ \"type\": \"command\", \"command\": \"user-hook\" }] }]\r\n" +
" }\r\n" + " }\r\n" +
"}\r\n" "}\r\n"
fixture := prepareQwenFixture(t, qwenFixtureOptions{settings: qwenString(settings)}) fixture := prepareQwenFixture(t, qwenFixtureOptions{settings: qwenString(settings)})
workspaceSettings := filepath.Join(fixture.workspaceDir, ".qwen", "settings.json") writeOptionalQwenFile(
writeOptionalQwenFile(t, workspaceSettings, qwenString("{ \"owner\": \"workspace\" }\n")) t,
fixture.workspaceConfig,
qwenString("{ \"owner\": \"workspace\" }\n"),
)
workspaceBefore := readQwenTestFile(t, fixture.workspaceConfig)
installQwenFixture(t, fixture) installQwenFixture(t, fixture)
first := readQwenTestFile(t, fixture.configPath) firstSettings := readQwenTestFile(t, fixture.configPath)
firstGuard := readQwenTestFile(t, fixture.guardPath)
firstAudit := readQwenTestFile(t, fixture.hookPath)
installQwenFixture(t, fixture) installQwenFixture(t, fixture)
if readQwenTestFile(t, fixture.configPath) != first { if readQwenTestFile(t, fixture.configPath) != firstSettings ||
t.Fatal("idempotent install changed Qwen Code settings") readQwenTestFile(t, fixture.guardPath) != firstGuard ||
readQwenTestFile(t, fixture.hookPath) != firstAudit {
t.Fatal("idempotent install changed Qwen Code installation")
} }
if !strings.Contains(first, "Keep this user preference") || !strings.Contains(first, "\r\n") { if !strings.Contains(firstSettings, "Keep this user preference") ||
t.Fatalf("settings comments or line endings were lost:\n%s", first) !strings.Contains(firstSettings, "\r\n") ||
!strings.Contains(firstSettings, "FutureEvent") {
t.Fatalf("settings source fidelity was lost:\n%s", firstSettings)
} }
root := readQwenTestObject(t, fixture.configPath) root := readQwenTestObject(t, fixture.configPath)
if root["theme"] != "GitHub" { if root["theme"] != "GitHub" {
@@ -48,28 +66,52 @@ func TestQwenInstallPreservesJSONCommentsAndIsIdempotent(t *testing.T) {
hooks := requireQwenObject(t, root["hooks"]) hooks := requireQwenObject(t, root["hooks"])
if len(requireQwenArray(t, hooks["SessionStart"])) != 1 || if len(requireQwenArray(t, hooks["SessionStart"])) != 1 ||
len(requireQwenArray(t, hooks["PreToolUse"])) != 2 || len(requireQwenArray(t, hooks["PreToolUse"])) != 2 ||
len(requireQwenArray(t, hooks["PostToolUse"])) != 1 { len(requireQwenArray(t, hooks["PostToolUse"])) != 1 ||
len(requireQwenArray(t, hooks["PostToolUseFailure"])) != 1 {
t.Fatalf("Qwen hooks = %#v", hooks) t.Fatalf("Qwen hooks = %#v", hooks)
} }
for _, item := range []struct{ event, path string }{ for _, item := range []struct {
{"PreToolUse", fixture.guardPath}, {"PostToolUse", fixture.hookPath}, 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) handler := qwenManagedHandler(t, root, item.event, item.path)
if len(handler) != 4 || handler["type"] != "command" || if len(handler) != 5 || handler["type"] != "command" ||
handler["shell"] != expectedQwenShell() || handler["timeout"] != float64(10_000) { handler["name"] != item.name || handler["shell"] != qwenExpectedShell() ||
handler["timeout"] != qwenHookTimeout {
t.Fatalf("managed handler = %#v", handler) t.Fatalf("managed handler = %#v", handler)
} }
} }
if readQwenTestObject(t, workspaceSettings)["owner"] != "workspace" { if readQwenTestFile(t, fixture.workspaceConfig) != workspaceBefore {
t.Fatal("workspace settings changed") 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) runtimeConfig := readQwenTestObject(t, fixture.runtimeConfig)
if runtimeConfig["agent_name"] != qwenAgentKey { if runtimeConfig["agent_name"] != qwenAgentKey ||
runtimeConfig["agent_id"] != qwenTestAgentID ||
runtimeConfig["base_url"] != fixture.config.BaseURL {
t.Fatalf("runtime config = %#v", runtimeConfig) t.Fatalf("runtime config = %#v", runtimeConfig)
} }
} }
func TestQwenInstallReportsReviewCommand(t *testing.T) { func TestQwenInstallReportsOfficialReviewCommand(t *testing.T) {
fixture := prepareQwenFixture(t, qwenFixtureOptions{}) fixture := prepareQwenFixture(t, qwenFixtureOptions{})
reader, writer, err := os.Pipe() reader, writer, err := os.Pipe()
if err != nil { if err != nil {
@@ -83,7 +125,12 @@ func TestQwenInstallReportsReviewCommand(t *testing.T) {
output, readErr := io.ReadAll(reader) output, readErr := io.ReadAll(reader)
_ = reader.Close() _ = reader.Close()
if installErr != nil || closeErr != nil || readErr != nil { if installErr != nil || closeErr != nil || readErr != nil {
t.Fatalf("capture install output: install=%v close=%v read=%v", installErr, closeErr, readErr) t.Fatalf(
"capture install output: install=%v close=%v read=%v",
installErr,
closeErr,
readErr,
)
} }
text := string(output) text := string(output)
for _, marker := range []string{fixture.configPath, "/hooks"} { for _, marker := range []string{fixture.configPath, "/hooks"} {
@@ -93,109 +140,69 @@ func TestQwenInstallReportsReviewCommand(t *testing.T) {
} }
} }
func TestQwenHomeUsesOfficialUserEnvironmentPrecedence(t *testing.T) { func legacyQwenGroupForTest(
fixture := prepareQwenFixture(t, qwenFixtureOptions{}) t *testing.T,
firstHome := filepath.Join(filepath.Dir(fixture.homeDir), "first # qwen home") event, scriptPath string,
secondHome := filepath.Join(filepath.Dir(fixture.homeDir), "second qwen home") ) map[string]any {
writeOptionalQwenFile( t.Helper()
t, nodePath, err := resolveNodeRuntime()
filepath.Join(fixture.qwenDir, ".env"),
qwenString("export QWEN_HOME = \""+firstHome+"\" # selected by Qwen\n"),
)
writeOptionalQwenFile(t, filepath.Join(fixture.homeDir, ".env"), qwenString("QWEN_HOME="+secondHome+"\n"))
installQwenFixture(t, fixture)
selected := filepath.Join(firstHome, "settings.json")
qwenManagedHandler(t, readQwenTestObject(t, selected), "PreToolUse", fixture.guardPath)
requireMissingQwenFile(t, filepath.Join(secondHome, "settings.json"))
requireMissingQwenFile(t, fixture.configPath)
status, err := fixture.plugin.Status()
if err != nil || status.ConfigPath != selected || !status.Installed {
t.Fatalf("Qwen status = %#v, %v", status, err)
}
}
func TestQwenExplicitHomeSupportsRelativeTildeAndEmptyValues(t *testing.T) {
tests := []struct {
name, value string
expected func(*qwenFixture) string
}{
{"relative", "relative-qwen", func(f *qwenFixture) string {
return filepath.Join(f.workspaceDir, "relative-qwen", "settings.json")
}},
{"tilde", "~/custom-qwen", func(f *qwenFixture) string {
return filepath.Join(f.homeDir, "custom-qwen", "settings.json")
}},
{"empty", "", func(f *qwenFixture) string { return f.configPath }},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fixture := prepareQwenFixture(t, qwenFixtureOptions{})
ignored := filepath.Join(filepath.Dir(fixture.homeDir), "ignored-qwen-home")
writeOptionalQwenFile(
t, filepath.Join(fixture.qwenDir, ".env"), qwenString("QWEN_HOME="+ignored+"\n"),
)
t.Setenv("QWEN_HOME", test.value)
installQwenFixture(t, fixture)
qwenManagedHandler(t, readQwenTestObject(t, test.expected(fixture)), "PreToolUse", fixture.guardPath)
requireMissingQwenFile(t, filepath.Join(ignored, "settings.json"))
})
}
}
func TestQwenCommandsBlockAndForwardOfficialInputByteForByte(t *testing.T) {
fixture := prepareQwenFixture(t, qwenFixtureOptions{})
installQwenFixture(t, fixture)
capturePath := filepath.Join(t.TempDir(), "captured-event.json")
encodedPath, err := json.Marshal(capturePath)
if err != nil { if err != nil {
t.Fatalf("marshal capture path: %v", err) t.Fatalf("resolve Node.js: %v", err)
} }
captureScript := "const fs = require('node:fs'); const chunks = []; " + return map[string]any{
"process.stdin.on('data', chunk => chunks.push(chunk)); " + "matcher": "*",
"process.stdin.on('end', () => fs.writeFileSync(" + string(encodedPath) + ", Buffer.concat(chunks)));\n" "hooks": []any{map[string]any{
if err := os.WriteFile(fixture.hookPath, []byte(captureScript), 0700); err != nil { "type": "command",
t.Fatalf("write capture audit runtime: %v", err) "command": buildQwenCommand(nodePath, scriptPath),
} "shell": qwenExpectedShell(),
settings := readQwenTestObject(t, fixture.configPath) "timeout": qwenHookTimeout,
guard := qwenManagedHandler(t, settings, "PreToolUse", fixture.guardPath) }},
audit := qwenManagedHandler(t, settings, "PostToolUse", fixture.hookPath) "event": event,
prePayload := `{"session_id":"session-1","cwd":"C:/workspace","hook_event_name":"PreToolUse","timestamp":"2026-07-19T00:00:00.000Z","tool_name":"run_shell_command","tool_input":{"command":"echo test"}}`
guardResult := runQwenHandler(t, guard, fixture.homeDir, prePayload)
if guardResult.exitCode != 2 || !strings.Contains(guardResult.stderr, "Agent is frozen by Elydora") {
t.Fatalf("guard result = %#v", guardResult)
}
postPayload := strings.Replace(prePayload, "PreToolUse", "PostToolUse", 1)
auditResult := runQwenHandler(t, audit, fixture.homeDir, postPayload)
if auditResult.exitCode != 0 {
t.Fatalf("audit result = %#v", auditResult)
}
if readQwenTestFile(t, capturePath) != postPayload {
t.Fatal("audit hook changed the official Qwen Code payload")
} }
} }
func TestQwenStatusRequiresEnabledPairAndRuntimeFiles(t *testing.T) { 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{}) 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) installQwenFixture(t, fixture)
status, err := fixture.plugin.Status() root := readQwenTestObject(t, fixture.configPath)
if err != nil || !status.Installed || !status.HookConfigured || !status.HookScriptExists { for _, item := range []struct{ event, path string }{
t.Fatalf("installed status = %#v, %v", status, err) {"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)
} }
settings := readQwenTestObject(t, fixture.configPath)
settings["disableAllHooks"] = true
writeQwenTestObject(t, fixture.configPath, settings)
status, err = fixture.plugin.Status()
if err != nil || status.Installed || status.HookConfigured {
t.Fatalf("disabled status = %#v, %v", status, err)
} }
settings["disableAllHooks"] = false preserved := false
writeQwenTestObject(t, fixture.configPath, settings) for _, groupValue := range requireQwenArray(t, requireQwenObject(t, root["hooks"])["PreToolUse"]) {
if err := os.Remove(fixture.guardPath); err != nil { for _, handlerValue := range requireQwenArray(t, requireQwenObject(t, groupValue)["hooks"]) {
t.Fatalf("remove guard runtime: %v", err) preserved = preserved || requireQwenObject(t, handlerValue)["timeout"] == float64(9_000)
} }
status, err = fixture.plugin.Status() }
if err != nil || status.Installed || !status.HookConfigured || status.HookScriptExists { if !preserved {
t.Fatalf("missing-runtime status = %#v, %v", status, err) t.Fatal("legacy ownership lookalike was removed")
} }
} }
@@ -209,18 +216,12 @@ func TestQwenUninstallPreservesExternalMutationsAndExactOwnership(t *testing.T)
group["hooks"] = append(requireQwenArray(t, group["hooks"]), map[string]any{ group["hooks"] = append(requireQwenArray(t, group["hooks"]), map[string]any{
"type": "command", "command": "user-command", "type": "command", "command": "user-command",
}) })
nodePath, err := resolveNodeRuntime() otherAgent := filepath.Join(filepath.Dir(fixture.agentDir), "agent-10")
if err != nil {
t.Fatalf("resolve Node.js: %v", err)
}
hooks := requireQwenObject(t, settings["hooks"]) hooks := requireQwenObject(t, settings["hooks"])
hooks["PreToolUse"] = append(requireQwenArray(t, hooks["PreToolUse"]), map[string]any{ hooks["PreToolUse"] = append(
"matcher": "*", requireQwenArray(t, hooks["PreToolUse"]),
"hooks": []any{map[string]any{ legacyQwenGroupValue(t, filepath.Join(otherAgent, qwenGuardScript)),
"type": "command", "command": buildQwenCommand(nodePath, filepath.Join(filepath.Dir(fixture.agentDir), "agent-10", qwenGuardScript)), )
"shell": expectedQwenShell(), "timeout": float64(10_000),
}},
})
writeQwenTestObject(t, fixture.configPath, settings) writeQwenTestObject(t, fixture.configPath, settings)
before := readQwenTestFile(t, fixture.configPath) before := readQwenTestFile(t, fixture.configPath)
if err := fixture.plugin.Uninstall("other-agent"); err != nil { if err := fixture.plugin.Uninstall("other-agent"); err != nil {
@@ -250,6 +251,9 @@ func TestQwenUninstallPreservesExternalMutationsAndExactOwnership(t *testing.T)
if _, exists := remainingHooks["PostToolUse"]; exists { if _, exists := remainingHooks["PostToolUse"]; exists {
t.Fatalf("PostToolUse remains: %#v", remainingHooks) t.Fatalf("PostToolUse remains: %#v", remainingHooks)
} }
if _, exists := remainingHooks["PostToolUseFailure"]; exists {
t.Fatalf("PostToolUseFailure remains: %#v", remainingHooks)
}
} }
func TestQwenUninstallDeletesOwnedEmptySettings(t *testing.T) { func TestQwenUninstallDeletesOwnedEmptySettings(t *testing.T) {
@@ -263,38 +267,3 @@ func TestQwenUninstallDeletesOwnedEmptySettings(t *testing.T) {
} }
requireMissingQwenFile(t, fixture.configPath) requireMissingQwenFile(t, fixture.configPath)
} }
func TestQwenDotenvParserMatchesOfficialGrammar(t *testing.T) {
tests := []struct {
source string
want map[string]string
}{
{"QWEN_HOME=plain\n", map[string]string{"QWEN_HOME": "plain"}},
{"export QWEN_HOME = \"path # one\" # comment\n", map[string]string{"QWEN_HOME": "path # one"}},
{"QWEN_HOME: path with spaces\n", map[string]string{"QWEN_HOME": "path with spaces"}},
{"QWEN_HOME=\n", map[string]string{"QWEN_HOME": ""}},
{"QWEN_HOME='single quoted'\r\n", map[string]string{"QWEN_HOME": "single quoted"}},
{"QWEN_HOME=`backtick value`\n", map[string]string{"QWEN_HOME": "backtick value"}},
{"QWEN_HOME=first\nQWEN_HOME=second\n", map[string]string{"QWEN_HOME": "second"}},
{"OTHER=x\nQWEN_HOME=\"line\\nnext\"\n", map[string]string{"OTHER": "x", "QWEN_HOME": "line\nnext"}},
}
for _, test := range tests {
got := parseDotenv([]byte(test.source))
encodedGot, _ := json.Marshal(got)
encodedWant, _ := json.Marshal(test.want)
if string(encodedGot) != string(encodedWant) {
t.Fatalf("parseDotenv(%q) = %s, want %s", test.source, encodedGot, encodedWant)
}
}
}
func writeQwenTestObject(t *testing.T, path string, value map[string]any) {
t.Helper()
encoded, err := json.MarshalIndent(value, "", " ")
if err != nil {
t.Fatalf("encode %s: %v", path, err)
}
if err := os.WriteFile(path, append(encoded, '\n'), 0600); err != nil {
t.Fatalf("write %s: %v", path, err)
}
}
@@ -8,7 +8,6 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"testing" "testing"
) )
@@ -17,7 +16,6 @@ const qwenTestAgentID = "agent-1"
type qwenFixtureOptions struct { type qwenFixtureOptions struct {
settings *string settings *string
skipGuard bool
} }
type qwenFixture struct { type qwenFixture struct {
@@ -27,6 +25,10 @@ type qwenFixture struct {
workspaceDir string workspaceDir string
qwenDir string qwenDir string
configPath string configPath string
workspaceConfig string
systemConfig string
systemDefaults string
trustedFolders string
agentDir string agentDir string
guardPath string guardPath string
hookPath string hookPath string
@@ -36,6 +38,7 @@ type qwenFixture struct {
type qwenCommandResult struct { type qwenCommandResult struct {
exitCode int exitCode int
stdout string
stderr string stderr string
} }
@@ -58,24 +61,25 @@ func prepareQwenFixture(t *testing.T, options qwenFixtureOptions) *qwenFixture {
workspaceDir := filepath.Join(homeDir, "workspace") workspaceDir := filepath.Join(homeDir, "workspace")
qwenDir := filepath.Join(homeDir, ".qwen") qwenDir := filepath.Join(homeDir, ".qwen")
configPath := filepath.Join(qwenDir, "settings.json") configPath := filepath.Join(qwenDir, "settings.json")
workspaceConfig := filepath.Join(workspaceDir, ".qwen", "settings.json")
systemDirectory := filepath.Join(rootDir, "system")
systemConfig := filepath.Join(systemDirectory, "settings.json")
systemDefaults := filepath.Join(systemDirectory, "system-defaults.json")
trustedFolders := filepath.Join(rootDir, "trustedFolders.json")
agentDir := filepath.Join(homeDir, ".elydora", qwenTestAgentID) agentDir := filepath.Join(homeDir, ".elydora", qwenTestAgentID)
guardPath := filepath.Join(agentDir, qwenGuardScript) guardPath := filepath.Join(agentDir, qwenGuardScript)
hookPath := filepath.Join(agentDir, qwenAuditScript) hookPath := filepath.Join(agentDir, qwenAuditScript)
for _, directory := range []string{workspaceDir, agentDir} { if err := os.MkdirAll(workspaceDir, 0755); err != nil {
if err := os.MkdirAll(directory, 0755); err != nil { t.Fatalf("create fixture workspace: %v", err)
t.Fatalf("create fixture directory %s: %v", directory, err)
}
}
if !options.skipGuard {
guard := "process.stdin.resume(); process.stderr.write('Agent is frozen by Elydora.\\n'); process.exit(2);\n"
if err := os.WriteFile(guardPath, []byte(guard), 0700); err != nil {
t.Fatalf("write guard runtime: %v", err)
}
} }
writeOptionalQwenFile(t, configPath, options.settings) writeOptionalQwenFile(t, configPath, options.settings)
t.Setenv("HOME", homeDir) t.Setenv("HOME", homeDir)
t.Setenv("USERPROFILE", homeDir) t.Setenv("USERPROFILE", homeDir)
unsetQwenTestEnv(t, "QWEN_HOME") unsetQwenTestEnv(t, "QWEN_HOME")
unsetQwenTestEnv(t, "QWEN_RUNTIME_DIR")
t.Setenv("QWEN_CODE_SYSTEM_SETTINGS_PATH", systemConfig)
t.Setenv("QWEN_CODE_SYSTEM_DEFAULTS_PATH", systemDefaults)
t.Setenv("QWEN_CODE_TRUSTED_FOLDERS_PATH", trustedFolders)
previousDirectory, err := os.Getwd() previousDirectory, err := os.Getwd()
if err != nil { if err != nil {
t.Fatalf("read current directory: %v", err) t.Fatalf("read current directory: %v", err)
@@ -94,6 +98,10 @@ func prepareQwenFixture(t *testing.T, options qwenFixtureOptions) *qwenFixture {
workspaceDir: workspaceDir, workspaceDir: workspaceDir,
qwenDir: qwenDir, qwenDir: qwenDir,
configPath: configPath, configPath: configPath,
workspaceConfig: workspaceConfig,
systemConfig: systemConfig,
systemDefaults: systemDefaults,
trustedFolders: trustedFolders,
agentDir: agentDir, agentDir: agentDir,
guardPath: guardPath, guardPath: guardPath,
hookPath: hookPath, hookPath: hookPath,
@@ -126,16 +134,16 @@ func unsetQwenTestEnv(t *testing.T, name string) {
}) })
} }
func writeOptionalQwenFile(t *testing.T, path string, source *string) { func writeOptionalQwenFile(t *testing.T, filePath string, source *string) {
t.Helper() t.Helper()
if source == nil { if source == nil {
return return
} }
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
t.Fatalf("create directory for %s: %v", path, err) t.Fatalf("create directory for %s: %v", filePath, err)
} }
if err := os.WriteFile(path, []byte(*source), 0600); err != nil { if err := os.WriteFile(filePath, []byte(*source), 0600); err != nil {
t.Fatalf("write %s: %v", path, err) t.Fatalf("write %s: %v", filePath, err)
} }
} }
@@ -146,22 +154,24 @@ func installQwenFixture(t *testing.T, fixture *qwenFixture) {
} }
} }
func readQwenTestFile(t *testing.T, path string) string { func readQwenTestFile(t *testing.T, filePath string) string {
t.Helper() t.Helper()
raw, err := os.ReadFile(path) raw, err := os.ReadFile(filePath)
if err != nil { if err != nil {
t.Fatalf("read %s: %v", path, err) t.Fatalf("read %s: %v", filePath, err)
} }
return string(raw) return string(raw)
} }
func readQwenTestObject(t *testing.T, path string) map[string]any { func readQwenTestObject(t *testing.T, filePath string) map[string]any {
t.Helper() t.Helper()
object, err := decodeJSONCObject( object, err := decodeJSONCObject(
[]byte(readQwenTestFile(t, path)), "Qwen test settings", false, []byte(readQwenTestFile(t, filePath)),
"Qwen test settings",
false,
) )
if err != nil { if err != nil {
t.Fatalf("decode %s: %v", path, err) t.Fatalf("decode %s: %v", filePath, err)
} }
return object return object
} }
@@ -184,8 +194,16 @@ func requireQwenArray(t *testing.T, value any) []any {
return array return array
} }
func qwenManagedGroup(t *testing.T, settings map[string]any, event, scriptPath string) map[string]any { func qwenManagedGroup(
t *testing.T,
settings map[string]any,
event, scriptPath string,
) map[string]any {
t.Helper() t.Helper()
expectedName := qwenAuditHookName
if event == "PreToolUse" {
expectedName = qwenGuardHookName
}
hooks := requireQwenObject(t, settings["hooks"]) hooks := requireQwenObject(t, settings["hooks"])
for _, groupValue := range requireQwenArray(t, hooks[event]) { for _, groupValue := range requireQwenArray(t, hooks[event]) {
group := requireQwenObject(t, groupValue) group := requireQwenObject(t, groupValue)
@@ -193,7 +211,7 @@ func qwenManagedGroup(t *testing.T, settings map[string]any, event, scriptPath s
handler := requireQwenObject(t, handlerValue) handler := requireQwenObject(t, handlerValue)
command, _ := handler["command"].(string) command, _ := handler["command"].(string)
_, candidate, managed := parseQwenCommand(command) _, candidate, managed := parseQwenCommand(command)
if managed && sameQwenPath(candidate, scriptPath) { if managed && handler["name"] == expectedName && sameQwenPath(candidate, scriptPath) {
return group return group
} }
} }
@@ -202,7 +220,11 @@ func qwenManagedGroup(t *testing.T, settings map[string]any, event, scriptPath s
return nil return nil
} }
func qwenManagedHandler(t *testing.T, settings map[string]any, event, scriptPath string) map[string]any { func qwenManagedHandler(
t *testing.T,
settings map[string]any,
event, scriptPath string,
) map[string]any {
t.Helper() t.Helper()
group := qwenManagedGroup(t, settings, event, scriptPath) group := qwenManagedGroup(t, settings, event, scriptPath)
for _, handlerValue := range requireQwenArray(t, group["hooks"]) { for _, handlerValue := range requireQwenArray(t, group["hooks"]) {
@@ -217,35 +239,49 @@ func qwenManagedHandler(t *testing.T, settings map[string]any, event, scriptPath
return nil return nil
} }
func runQwenHandler(t *testing.T, handler map[string]any, homeDir, payload string) qwenCommandResult { func runQwenHandler(
t *testing.T,
handler map[string]any,
homeDir, payload string,
) qwenCommandResult {
t.Helper() t.Helper()
command := handler["command"].(string) command := handler["command"].(string)
var process *exec.Cmd var process *exec.Cmd
if handler["shell"] == "powershell" { if handler["shell"] == "powershell" {
process = exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", command) process = exec.Command(
"powershell",
"-NoProfile",
"-NonInteractive",
"-Command",
command,
)
} else { } else {
process = exec.Command("bash", "-c", command) process = exec.Command("bash", "-c", command)
} }
process.Env = append(os.Environ(), "HOME="+homeDir, "USERPROFILE="+homeDir) process.Env = append(os.Environ(), "HOME="+homeDir, "USERPROFILE="+homeDir)
process.Stdin = bytes.NewBufferString(payload) process.Stdin = bytes.NewBufferString(payload)
var stdout bytes.Buffer
var stderr bytes.Buffer var stderr bytes.Buffer
process.Stdout = &stdout
process.Stderr = &stderr process.Stderr = &stderr
err := process.Run() err := process.Run()
result := qwenCommandResult{stdout: stdout.String(), stderr: stderr.String()}
if err == nil { if err == nil {
return qwenCommandResult{stderr: stderr.String()} return result
} }
var exitError *exec.ExitError var exitError *exec.ExitError
if errors.As(err, &exitError) { if errors.As(err, &exitError) {
return qwenCommandResult{exitCode: exitError.ExitCode(), stderr: stderr.String()} result.exitCode = exitError.ExitCode()
return result
} }
t.Fatalf("run Qwen Code hook command: %v", err) t.Fatalf("run Qwen Code hook command: %v", err)
return qwenCommandResult{} return qwenCommandResult{}
} }
func requireMissingQwenFile(t *testing.T, path string) { func requireMissingQwenFile(t *testing.T, filePath string) {
t.Helper() t.Helper()
if _, err := os.Stat(path); !os.IsNotExist(err) { if _, err := os.Stat(filePath); !os.IsNotExist(err) {
t.Fatalf("expected %s to be missing, got %v", path, err) t.Fatalf("expected %s to be missing, got %v", filePath, err)
} }
} }
@@ -265,9 +301,16 @@ func requireNoQwenStagingFiles(t *testing.T, root string) {
} }
} }
func expectedQwenShell() string { func writeQwenTestObject(t *testing.T, filePath string, value map[string]any) {
if runtime.GOOS == "windows" { t.Helper()
return "powershell" encoded, err := json.MarshalIndent(value, "", " ")
if err != nil {
t.Fatalf("encode %s: %v", filePath, err)
}
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
t.Fatalf("create directory for %s: %v", filePath, err)
}
if err := os.WriteFile(filePath, append(encoded, '\n'), 0600); err != nil {
t.Fatalf("write %s: %v", filePath, err)
} }
return "bash"
} }
+133
View File
@@ -0,0 +1,133 @@
package main
import (
"bytes"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
)
type qwenCLIResult struct {
stdout string
stderr string
err error
}
func runQwenCLI(
t *testing.T,
binary, directory string,
environment []string,
arguments ...string,
) qwenCLIResult {
t.Helper()
command := exec.Command(binary, arguments...)
command.Dir = directory
command.Env = environment
var stdout bytes.Buffer
var stderr bytes.Buffer
command.Stdout = &stdout
command.Stderr = &stderr
err := command.Run()
return qwenCLIResult{stdout: stdout.String(), stderr: stderr.String(), err: err}
}
func TestQwenCLIInstallStatusAndUninstallEndToEnd(t *testing.T) {
root := t.TempDir()
binary := filepath.Join(root, "elydora")
if runtime.GOOS == "windows" {
binary += ".exe"
}
build := exec.Command("go", "build", "-o", binary, ".")
if output, err := build.CombinedOutput(); err != nil {
t.Fatalf("build Elydora CLI: %v\n%s", err, output)
}
home := filepath.Join(root, "home")
workspace := filepath.Join(home, "workspace")
if err := os.MkdirAll(workspace, 0755); err != nil {
t.Fatalf("create workspace: %v", err)
}
privateKeyPath := filepath.Join(root, "private.key")
tokenPath := filepath.Join(root, "token.txt")
if err := os.WriteFile(
privateKeyPath,
[]byte("AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE\n"),
0600,
); err != nil {
t.Fatalf("write private key: %v", err)
}
if err := os.WriteFile(tokenPath, []byte("token-1\n"), 0600); err != nil {
t.Fatalf("write token: %v", err)
}
environment := append(
os.Environ(),
"HOME="+home,
"USERPROFILE="+home,
"QWEN_HOME=",
"QWEN_RUNTIME_DIR=",
"QWEN_CODE_SYSTEM_SETTINGS_PATH="+filepath.Join(root, "system.json"),
"QWEN_CODE_SYSTEM_DEFAULTS_PATH="+filepath.Join(root, "defaults.json"),
"QWEN_CODE_TRUSTED_FOLDERS_PATH="+filepath.Join(root, "trusted.json"),
)
install := runQwenCLI(
t,
binary,
workspace,
environment,
"install",
"--agent", "qwen",
"--org-id", "org-1",
"--agent-id", qwenTestCLIAgentID,
"--kid", "kid-1",
"--private-key-file", privateKeyPath,
"--token-file", tokenPath,
"--base-url", "https://api.elydora.test",
)
if install.err != nil {
t.Fatalf("install Qwen CLI hooks: %v\n%s", install.err, install.stderr)
}
settingsPath := filepath.Join(home, ".qwen", "settings.json")
settings, err := os.ReadFile(settingsPath)
if err != nil {
t.Fatalf("read Qwen settings: %v", err)
}
for _, marker := range []string{
"PreToolUse",
"PostToolUse",
"PostToolUseFailure",
"elydora-guard",
"elydora-audit",
} {
if !strings.Contains(string(settings), marker) {
t.Fatalf("installed settings are missing %q", marker)
}
}
status := runQwenCLI(t, binary, workspace, environment, "status", "--agent", "qwen")
if status.err != nil || !strings.Contains(status.stdout, "installed") {
t.Fatalf("Qwen status: %v\n%s\n%s", status.err, status.stdout, status.stderr)
}
uninstall := runQwenCLI(
t,
binary,
workspace,
environment,
"uninstall",
"--agent", "qwen",
"--agent-id", qwenTestCLIAgentID,
)
if uninstall.err != nil {
t.Fatalf("uninstall Qwen CLI hooks: %v\n%s", uninstall.err, uninstall.stderr)
}
for _, path := range []string{
settingsPath,
filepath.Join(home, ".elydora", qwenTestCLIAgentID),
} {
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Fatalf("uninstall left %s: %v", path, err)
}
}
}
const qwenTestCLIAgentID = "qwen-e2e-agent"