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

This commit is contained in:
ZacharyZhang-NY committed 2026-07-20 16:06:52 -04:00
1 parent 4521cbe32f
commit dd72793cdb
18 files changed
+2921 -741

No files matched your search

+3 -2
View File
@@ -41,8 +41,9 @@ This repository owns the public Go SDK, the `cmd/elydora` CLI, local signing beh
- Factory Droid `droid@0.175.0` selects one complete user hook source in this order: `~/.factory/hooks.json`, legacy `~/.factory/hooks/hooks.json`, `~/.factory/settings.local.json` `hooks`, then `~/.factory/settings.json` `hooks`. Write the current root `{ "hooks": { ... } }` container, accept unknown future event arrays, preserve JSONC syntax, and remove exact Elydora entries from every discovered user source during migration or uninstall. - Factory Droid `droid@0.175.0` selects one complete user hook source in this order: `~/.factory/hooks.json`, legacy `~/.factory/hooks/hooks.json`, `~/.factory/settings.local.json` `hooks`, then `~/.factory/settings.json` `hooks`. Write the current root `{ "hooks": { ... } }` container, accept unknown future event arrays, preserve JSONC syntax, and remove exact Elydora entries from every discovered user source during migration or uninstall.
- Resolve Factory `hooksDisabled` through system, project, nested-folder, and matching `settings.local.json` extension-only precedence before user settings. Treat system `allowManagedHooksOnly` as an installation gate, snapshot every read-only policy layer as a transaction precondition, and direct users to `/hooks` for local and organization-managed review. - Resolve Factory `hooksDisabled` through system, project, nested-folder, and matching `settings.local.json` extension-only precedence before user settings. Treat system `allowManagedHooksOnly` as an installation gate, snapshot every read-only policy layer as a transaction precondition, and direct users to `/hooks` for local and organization-managed review.
- Generate Factory Droid guard, strict config, canonical private key, and native-payload audit runtime with every affected hook document in one rollback-capable transaction. Windows commands use Factory's native PowerShell contract with explicit `$LASTEXITCODE` propagation. Healthy status requires one exact current `PreToolUse`/`PostToolUse` pair, strict runtime identity, physical files, and exact generated runtime sources. - Generate Factory Droid guard, strict config, canonical private key, and native-payload audit runtime with every affected hook document in one rollback-capable transaction. Windows commands use Factory's native PowerShell contract with explicit `$LASTEXITCODE` propagation. Healthy status requires one exact current `PreToolUse`/`PostToolUse` pair, strict runtime identity, physical files, and exact generated runtime sources.
- Qwen Code `0.20.0` loads user hooks from `$QWEN_HOME/settings.json` with `~/.qwen/settings.json` as the default; follow Qwen's `.env` precedence and keep workspace settings read-only. Preserve comments, reject trailing commas and duplicate keys, express timeouts in milliseconds, and direct users to `/hooks` for review. - Qwen Code `0.20.0` loads user hooks from `$QWEN_HOME/settings.json` with `~/.qwen/settings.json` as the default. Follow the official `QWEN_HOME` and `QWEN_RUNTIME_DIR` `.env` bootstrap, then resolve system defaults, user, trusted workspace, and system override settings in precedence order. Keep every non-user settings source and `trustedFolders.json` read-only and guard each source snapshot through the write transaction.
- Commit Qwen Code runtime config, private key, audit runtime, and user settings in one rollback-capable transaction. Status requires enabled `PreToolUse` and `PostToolUse` hooks with matching managed runtime files. - Preserve JSONC comments, line endings, and unknown future hook fields. Reject trailing commas and duplicate keys, validate all 21 current events plus command, HTTP, and prompt handlers, reject settings-defined function handlers, and apply Node.js `RegExp` semantics only to the 15 matcher-enabled events.
- Own one exact named matchless `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` triple with 10,000 millisecond timeouts. Generate guard, strict runtime config, canonical private key, and native-payload audit runtime with user settings in one rollback-capable transaction. Propagate frozen and revoked agents through exit code `2`; require effective hooks to be enabled, one exact triple, physical runtime files, strict identity, and exact generated sources for healthy status; direct users to `/hooks` for review.
## Code Quality ## Code Quality
+1 -1
View File
@@ -101,7 +101,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
}
+133 -27
View File
@@ -6,15 +6,25 @@ import (
"sort" "sort"
) )
const (
qwenSystemDefaultsKind = "system-defaults"
qwenUserKind = "user"
qwenWorkspaceKind = "workspace"
qwenSystemKind = "system"
)
type qwenDocument struct { type qwenDocument struct {
filePath string kind string
exists bool filePath string
raw []byte exists bool
root map[string]any raw []byte
hooks qwenHookSettings snapshot *managedFileSnapshot
hasHooksContainer bool root map[string]any
hooksDisabled bool hooks qwenHookSettings
ownedFile bool hasHooksContainer bool
disableAllHooks *bool
folderTrustEnabled *bool
ownedFile bool
} }
type qwenRenderedDocument struct { type qwenRenderedDocument struct {
@@ -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
} }
+169 -164
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) if runtime.GOOS == "windows" {
audits := managedQwenIDs(settings, "PostToolUse", qwenAuditScript, runtimeRoot) return strings.ToLower(agentID)
ids := make([]string, 0, len(guards)) }
for guardID := range guards { return agentID
for auditID := range audits { }
if sameQwenAgentID(guardID, auditID) {
ids = append(ids, guardID) func qwenReferencesForEvent(
break 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)
} }
} }
sort.Slice(ids, func(left, right int) bool { return references
if runtime.GOOS == "windows" { }
return strings.ToLower(ids[left]) < strings.ToLower(ids[right])
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) { func qwenContractDirectory(contract qwenRuntimeContract) string {
if agentID, ok := managedQwenAgentID(handlerValue.(map[string]any), script, runtimeRoot); ok { return filepath.Dir(contract.guardPath)
ids[agentID] = true
}
}
}
return ids
} }
+305
View File
@@ -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...,
)
}
+237 -121
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 value, nil
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) { func validateQwenRuntimeConfig(
raw, exists, err := readOptionalFile(filePath, "Qwen home environment") config map[string]any,
if err != nil || !exists { expectedAgentID, configPath string,
return "", false, err ) error {
} supported := stringSet(
value := parseDotenv(raw)["QWEN_HOME"] "org_id",
if value == "" { "agent_id",
return "", false, nil "kid",
} "base_url",
resolved, err := resolveQwenStoragePath(value) "token",
return resolved, err == nil, err "agent_name",
}
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,
) )
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
} }
} }
+195 -58
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)
} }
} }
+194
View File
@@ -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)
}
})
}
}
+81 -37
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
} }
+179
View File
@@ -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)
})
}
}
+196
View File
@@ -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,
)
}
+209
View File
@@ -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
}
+130 -161
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) preserved := false
settings["disableAllHooks"] = true for _, groupValue := range requireQwenArray(t, requireQwenObject(t, root["hooks"])["PreToolUse"]) {
writeQwenTestObject(t, fixture.configPath, settings) for _, handlerValue := range requireQwenArray(t, requireQwenObject(t, groupValue)["hooks"]) {
status, err = fixture.plugin.Status() preserved = preserved || requireQwenObject(t, handlerValue)["timeout"] == float64(9_000)
if err != nil || status.Installed || status.HookConfigured { }
t.Fatalf("disabled status = %#v, %v", status, err)
} }
settings["disableAllHooks"] = false if !preserved {
writeQwenTestObject(t, fixture.configPath, settings) t.Fatal("legacy ownership lookalike was removed")
if err := os.Remove(fixture.guardPath); err != nil {
t.Fatalf("remove guard runtime: %v", err)
}
status, err = fixture.plugin.Status()
if err != nil || status.Installed || !status.HookConfigured || status.HookScriptExists {
t.Fatalf("missing-runtime status = %#v, %v", status, err)
} }
} }
@@ -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)
}
}
+102 -59
View File
@@ -8,7 +8,6 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"testing" "testing"
) )
@@ -16,26 +15,30 @@ import (
const qwenTestAgentID = "agent-1" const qwenTestAgentID = "agent-1"
type qwenFixtureOptions struct { type qwenFixtureOptions struct {
settings *string settings *string
skipGuard bool
} }
type qwenFixture struct { type qwenFixture struct {
plugin *QwenPlugin plugin *QwenPlugin
config InstallConfig config InstallConfig
homeDir string homeDir string
workspaceDir string workspaceDir string
qwenDir string qwenDir string
configPath string configPath string
agentDir string workspaceConfig string
guardPath string systemConfig string
hookPath string systemDefaults string
runtimeConfig string trustedFolders string
privateKey string agentDir string
guardPath string
hookPath string
runtimeConfig string
privateKey string
} }
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)
@@ -89,16 +93,20 @@ func prepareQwenFixture(t *testing.T, options qwenFixtureOptions) *qwenFixture {
} }
}) })
return &qwenFixture{ return &qwenFixture{
plugin: &QwenPlugin{}, plugin: &QwenPlugin{},
homeDir: homeDir, homeDir: homeDir,
workspaceDir: workspaceDir, workspaceDir: workspaceDir,
qwenDir: qwenDir, qwenDir: qwenDir,
configPath: configPath, configPath: configPath,
agentDir: agentDir, workspaceConfig: workspaceConfig,
guardPath: guardPath, systemConfig: systemConfig,
hookPath: hookPath, systemDefaults: systemDefaults,
runtimeConfig: filepath.Join(agentDir, "config.json"), trustedFolders: trustedFolders,
privateKey: filepath.Join(agentDir, "private.key"), agentDir: agentDir,
guardPath: guardPath,
hookPath: hookPath,
runtimeConfig: filepath.Join(agentDir, "config.json"),
privateKey: filepath.Join(agentDir, "private.key"),
config: InstallConfig{ config: InstallConfig{
AgentName: qwenAgentKey, AgentName: qwenAgentKey,
OrgID: "org-1", OrgID: "org-1",
@@ -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"