0xray 3.0.14 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitignore.default +59 -0
- package/.mcp.json +35 -0
- package/.opencode/codex.codex +1 -1
- package/.opencode/commands/dependency-audit.md +3 -3
- package/.opencode/commands/summary-logger.md +4 -4
- package/.opencode/enforcer-config.json +2 -2
- package/.opencode/hooks/post-commit +1 -1
- package/.opencode/init.sh +10 -4
- package/.plugin/plugin.json +11 -0
- package/AGENTS-consumer.md +46 -0
- package/AGENTS.md +10 -10
- package/LICENSE +1 -1
- package/README.md +8 -8
- package/SKILLS.md +140 -0
- package/dist/.gitignore.default +59 -0
- package/dist/AGENTS.md +10 -10
- package/dist/CHANGELOG.md +34 -0
- package/dist/LICENSE +1 -1
- package/dist/README.md +8 -8
- package/dist/SKILLS.md +140 -0
- package/dist/agents/index.js +3 -1
- package/dist/agents/registry.js +13 -0
- package/dist/analytics/routing-refiner.js +1 -1
- package/dist/architect/architectural-integrity.js +3 -3
- package/dist/benchmark/performance-benchmark.js +4 -3
- package/dist/cli/commands/antigravity-status.js +23 -23
- package/dist/cli/commands/credible-init.js +1 -1
- package/dist/cli/commands/hermes-install.js +12 -4
- package/dist/cli/commands/mcp-install.js +1 -1
- package/dist/cli/commands/openclaw-install.js +10 -1
- package/dist/cli/commands/opencode-install.js +22 -0
- package/dist/cli/commands/plugin-commands.js +2 -2
- package/dist/cli/commands/publish-agent.js +5 -5
- package/dist/cli/commands/skill-install.js +39 -2
- package/dist/cli/commands/status.js +8 -3
- package/dist/cli/commands/storyteller.js +2 -2
- package/dist/cli/index.js +17 -11
- package/dist/cli/server.js +4 -4
- package/dist/core/activity-logger.js +4 -4
- package/dist/core/agent-spawn-gate.js +1 -1
- package/dist/core/boot-orchestrator.js +30 -31
- package/dist/core/bridge.mjs +26 -26
- package/dist/core/codex-formatter.js +2 -3
- package/dist/core/codex-injector.js +7 -7
- package/dist/core/config-loader.js +3 -2
- package/dist/core/config-paths.js +38 -31
- package/dist/core/context-loader.js +5 -5
- package/dist/core/features-config.js +1 -1
- package/dist/core/index.js +2 -2
- package/dist/core/logging-config.js +7 -7
- package/dist/core/orchestrator.js +2 -2
- package/dist/core/system-prompt-generator.js +2 -2
- package/dist/core/xray-activation.js +42 -41
- package/dist/delegation/agent-delegator.js +3 -3
- package/dist/enforcement/enforcer-tools.js +4 -4
- package/dist/enforcement/loaders/codex-loader.js +2 -2
- package/dist/execution/opencode-cli-invoker.js +1 -1
- package/dist/governance/governance-service.js +1 -1
- package/dist/governance/llm-governance-provider.js +223 -0
- package/dist/index.js +6 -6
- package/dist/inference/deploy-verifier.js +1 -1
- package/dist/inference/inference-cycle.js +16 -16
- package/dist/integrations/base/README.md +1 -1
- package/dist/integrations/enforcement-gate.js +2 -2
- package/dist/integrations/governance/index.js +1 -1
- package/dist/integrations/grok/grok-cli.js +20 -6
- package/dist/integrations/grok/hooks/pre-tool-use.js +3 -3
- package/dist/integrations/grok/plugin/0xray/.mcp.json +16 -4
- package/dist/integrations/grok/plugin/0xray/hooks/hooks.json +2 -2
- package/dist/integrations/hermes-agent/__init__.py +90 -79
- package/dist/integrations/hermes-agent/after-install.md +14 -14
- package/dist/integrations/hermes-agent/bridge.mjs +60 -25
- package/dist/integrations/hermes-agent/plugin.yaml +5 -5
- package/dist/integrations/hermes-agent/schemas.py +61 -12
- package/dist/integrations/hermes-agent/test_plugin.py +124 -124
- package/dist/integrations/hermes-agent/tools.py +129 -25
- package/dist/integrations/openclaw/README.md +6 -6
- package/dist/integrations/openclaw/config.js +1 -1
- package/dist/integrations/openclaw/index.js +2 -2
- package/dist/integrations/openclaw/types.js +3 -3
- package/dist/integrations/plugins/plugin-integration.js +1 -1
- package/dist/integrations/plugins/plugin-registry.js +3 -3
- package/dist/mcps/architect-tools.server.js +3 -2
- package/dist/mcps/auto-format.server.js +2 -2
- package/dist/mcps/boot-orchestrator.server.js +2 -2
- package/dist/mcps/config/config-loader.js +40 -9
- package/dist/mcps/config/server-config-registry.js +10 -10
- package/dist/mcps/connection/mcp-connection.js +1 -1
- package/dist/mcps/enforcer-tools.server.js +2 -2
- package/dist/mcps/estimation.server.js +3 -2
- package/dist/mcps/framework-compliance-audit.server.js +2 -2
- package/dist/mcps/framework-help.server.js +9 -8
- package/dist/mcps/governance.server.js +8 -4
- package/dist/mcps/knowledge-skills/api-design.server.js +3 -2
- package/dist/mcps/knowledge-skills/architecture-patterns.server.js +3 -2
- package/dist/mcps/knowledge-skills/bug-triage-specialist.server.js +3 -3
- package/dist/mcps/knowledge-skills/code-analyzer.server.js +2 -2
- package/dist/mcps/knowledge-skills/code-review.server.js +14 -39
- package/dist/mcps/knowledge-skills/content-creator.server.js +3 -3
- package/dist/mcps/knowledge-skills/database-design.server.js +2 -2
- package/dist/mcps/knowledge-skills/devops-deployment.server.js +3 -2
- package/dist/mcps/knowledge-skills/git-workflow.server.js +3 -2
- package/dist/mcps/knowledge-skills/growth-strategist.server.js +3 -3
- package/dist/mcps/knowledge-skills/log-monitor.server.js +3 -3
- package/dist/mcps/knowledge-skills/mobile-development.server.js +3 -3
- package/dist/mcps/knowledge-skills/multimodal-looker.server.js +3 -3
- package/dist/mcps/knowledge-skills/performance-optimization.server.js +2 -2
- package/dist/mcps/knowledge-skills/project-analysis.server.js +2 -2
- package/dist/mcps/knowledge-skills/refactoring-strategies.server.js +3 -2
- package/dist/mcps/knowledge-skills/security-audit.server.js +14 -44
- package/dist/mcps/knowledge-skills/seo-consultant.server.js +3 -2
- package/dist/mcps/knowledge-skills/session-management.server.js +3 -2
- package/dist/mcps/knowledge-skills/skill-invocation.server.js +105 -3
- package/dist/mcps/knowledge-skills/strategist.server.js +3 -3
- package/dist/mcps/knowledge-skills/tech-writer.server.js +3 -3
- package/dist/mcps/knowledge-skills/testing-best-practices.server.js +3 -2
- package/dist/mcps/knowledge-skills/testing-strategy.server.js +2 -2
- package/dist/mcps/knowledge-skills/ui-ux-design.server.js +5 -4
- package/dist/mcps/lint.server.js +4 -4
- package/dist/mcps/mcp-client.js +5 -5
- package/dist/mcps/model-health-check.server.js +3 -2
- package/dist/mcps/orchestrator/server.js +4 -3
- package/dist/mcps/orchestrator.server.js +2 -1
- package/dist/mcps/performance-analysis.server.js +2 -2
- package/dist/mcps/processor-pipeline.server.js +3 -2
- package/dist/mcps/registry.json +1 -1
- package/dist/mcps/researcher.server.js +14 -39
- package/dist/mcps/security-scan.server.js +2 -2
- package/dist/mcps/simulation/server-simulations.js +3 -3
- package/dist/mcps/state-manager.server.js +5 -5
- package/dist/metrics/agent-metrics.js +1 -1
- package/dist/orchestrator/enhanced-multi-agent-orchestrator.js +4 -4
- package/dist/orchestrator/intelligent-commit-batcher.js +2 -3
- package/dist/orchestrator/multi-agent-orchestration-coordinator.js +9 -9
- package/dist/orchestrator/orchestrator.js +9 -8
- package/dist/orchestrator/universal-registry-bridge.js +4 -4
- package/dist/postprocessor/PostProcessor.js +9 -9
- package/dist/postprocessor/services/RegressionAnalysisService.js +4 -4
- package/dist/postprocessor/triggers/GitHookTrigger.js +18 -18
- package/dist/processors/implementations/agents-md-validation-processor.js +2 -2
- package/dist/processors/implementations/inference-improvement-processor.js +1 -2
- package/dist/processors/implementations/log-protection-processor.js +2 -2
- package/dist/processors/implementations/publish-preflight-processor.js +2 -3
- package/dist/processors/implementations/regression-testing-processor.js +2 -2
- package/dist/processors/implementations/session-summary-processor.js +2 -2
- package/dist/processors/implementations/storytelling-trigger-processor.js +1 -2
- package/dist/public/about.html +6 -6
- package/dist/public/enterprise.html +1 -1
- package/dist/public/features.html +2 -2
- package/dist/public/index.html +4 -4
- package/dist/reporting/report-formatter.js +1 -1
- package/dist/scripts/activate-kernel-pipeline.js +4 -4
- package/dist/scripts/pre-command +1 -1
- package/dist/services/inference-tuner.js +2 -2
- package/dist/skills/api-design/SKILL.md +1 -1
- package/dist/skills/architect-tools/SKILL.md +1 -1
- package/dist/skills/architecture-patterns/SKILL.md +1 -1
- package/dist/skills/auto-format/SKILL.md +1 -1
- package/dist/skills/boot-orchestrator/SKILL.md +1 -1
- package/dist/skills/bug-triage/SKILL.md +1 -1
- package/dist/skills/code-analyzer/SKILL.md +1 -1
- package/dist/skills/code-review/SKILL.md +1 -1
- package/dist/skills/enforcer/SKILL.md +1 -1
- package/dist/skills/framework-compliance-audit/SKILL.md +1 -1
- package/dist/skills/git-workflow/SKILL.md +1 -1
- package/dist/skills/hermes-agent/SKILL.md +42 -42
- package/dist/skills/inference-improve/SKILL.md +1 -1
- package/dist/skills/lint/SKILL.md +1 -1
- package/dist/skills/log-monitor/SKILL.md +1 -1
- package/dist/skills/model-health-check/SKILL.md +1 -1
- package/dist/skills/multimodal-looker/SKILL.md +1 -1
- package/dist/skills/orchestrator/SKILL.md +1 -1
- package/dist/skills/performance-analysis/SKILL.md +1 -1
- package/dist/skills/performance-optimization/SKILL.md +1 -1
- package/dist/skills/processor-pipeline/SKILL.md +1 -1
- package/dist/skills/project-analysis/SKILL.md +1 -1
- package/dist/skills/refactoring-strategies/SKILL.md +1 -1
- package/dist/skills/registry.json +1 -1
- package/dist/skills/researcher/SKILL.md +1 -1
- package/dist/skills/security-audit/SKILL.md +1 -1
- package/dist/skills/security-scan/SKILL.md +1 -1
- package/dist/skills/session-management/SKILL.md +1 -1
- package/dist/skills/state-manager/SKILL.md +1 -1
- package/dist/skills/storyteller/SKILL.md +1 -1
- package/dist/skills/testing-best-practices/SKILL.md +1 -1
- package/dist/skills/testing-strategy/SKILL.md +1 -1
- package/dist/skills/ui-ux-design/SKILL.md +1 -1
- package/dist/state/index.js +1 -1
- package/dist/state/state-manager.js +3 -4
- package/dist/utils/import-resolver.js +1 -1
- package/dist/utils/path-resolver.js +2 -2
- package/dist/utils/token-manager.js +1 -1
- package/dist/validation/estimation-validator.js +1 -1
- package/package.json +10 -3
- package/scripts/hooks/pre-command +1 -1
- package/scripts/hooks/run-hook.js +2 -2
- package/scripts/node/auto-reflection-generator.mjs +30 -36
- package/scripts/node/govern-reflection.mjs +1 -1
- package/scripts/node/postinstall.cjs +37 -2
- package/scripts/node/pre-publish-guard.js +13 -9
- package/scripts/node/setup-xai-oauth.mjs +244 -0
- package/scripts/node/universal-version-manager.js +8 -8
- package/scripts/node/version-manager.mjs +1 -1
- package/src/integrations/grok/plugin/0xray/.mcp.json +16 -4
- package/src/integrations/grok/plugin/0xray/hooks/hooks.json +2 -2
- package/src/mcps/architect-tools.server.ts +2 -2
- package/src/mcps/auto-format.server.ts +2 -2
- package/src/mcps/boot-orchestrator.server.ts +2 -2
- package/src/mcps/config/__tests__/config-loader.test.ts +29 -33
- package/src/mcps/config/__tests__/server-config-registry.test.ts +12 -12
- package/src/mcps/config/config-loader.ts +50 -11
- package/src/mcps/config/server-config-registry.ts +10 -10
- package/src/mcps/connection/mcp-connection.ts +1 -1
- package/src/mcps/enforcer-tools.server.ts +2 -2
- package/src/mcps/estimation.server.ts +3 -2
- package/src/mcps/framework-compliance-audit.server.ts +2 -2
- package/src/mcps/framework-help.server.ts +9 -8
- package/src/mcps/governance.server.ts +8 -3
- package/src/mcps/knowledge-skills/api-design.server.ts +3 -2
- package/src/mcps/knowledge-skills/architecture-patterns.server.ts +2 -2
- package/src/mcps/knowledge-skills/bug-triage-specialist.server.ts +2 -2
- package/src/mcps/knowledge-skills/code-analyzer.server.ts +2 -2
- package/src/mcps/knowledge-skills/code-review.server.ts +21 -36
- package/src/mcps/knowledge-skills/content-creator.server.ts +3 -3
- package/src/mcps/knowledge-skills/database-design.server.ts +2 -2
- package/src/mcps/knowledge-skills/devops-deployment.server.ts +3 -2
- package/src/mcps/knowledge-skills/git-workflow.server.ts +3 -2
- package/src/mcps/knowledge-skills/growth-strategist.server.ts +3 -3
- package/src/mcps/knowledge-skills/log-monitor.server.ts +3 -3
- package/src/mcps/knowledge-skills/mobile-development.server.ts +3 -3
- package/src/mcps/knowledge-skills/multimodal-looker.server.ts +3 -3
- package/src/mcps/knowledge-skills/performance-optimization.server.ts +2 -2
- package/src/mcps/knowledge-skills/project-analysis.server.ts +2 -2
- package/src/mcps/knowledge-skills/refactoring-strategies.server.ts +3 -2
- package/src/mcps/knowledge-skills/security-audit.server.ts +21 -40
- package/src/mcps/knowledge-skills/seo-consultant.server.ts +3 -2
- package/src/mcps/knowledge-skills/session-management.server.ts +3 -2
- package/src/mcps/knowledge-skills/skill-invocation.server.ts +113 -3
- package/src/mcps/knowledge-skills/strategist.server.ts +3 -3
- package/src/mcps/knowledge-skills/tech-writer.server.ts +3 -3
- package/src/mcps/knowledge-skills/testing-best-practices.server.ts +3 -2
- package/src/mcps/knowledge-skills/testing-strategy.server.ts +2 -2
- package/src/mcps/knowledge-skills/ui-ux-design.server.ts +5 -4
- package/src/mcps/lint.server.ts +4 -4
- package/src/mcps/mcp-client.ts +5 -5
- package/src/mcps/model-health-check.server.ts +2 -2
- package/src/mcps/orchestrator/server.ts +4 -3
- package/src/mcps/orchestrator.server.ts +2 -1
- package/src/mcps/performance-analysis.server.ts +2 -2
- package/src/mcps/processor-pipeline.server.ts +3 -2
- package/src/mcps/registry.json +1 -1
- package/src/mcps/researcher.server.ts +21 -36
- package/src/mcps/security-scan.server.ts +2 -2
- package/src/mcps/simulation/server-simulations.ts +3 -3
- package/src/mcps/state-manager.server.ts +5 -5
- package/src/opencode/codex.codex +1 -1
- package/src/opencode/commands/dependency-audit.md +3 -3
- package/src/opencode/commands/summary-logger.md +4 -4
- package/src/opencode/enforcer-config.json +2 -2
- package/src/skills/api-design/SKILL.md +1 -1
- package/src/skills/architect-tools/SKILL.md +1 -1
- package/src/skills/architecture-patterns/SKILL.md +1 -1
- package/src/skills/auto-format/SKILL.md +1 -1
- package/src/skills/boot-orchestrator/SKILL.md +1 -1
- package/src/skills/bug-triage/SKILL.md +1 -1
- package/src/skills/code-analyzer/SKILL.md +1 -1
- package/src/skills/code-review/SKILL.md +1 -1
- package/src/skills/enforcer/SKILL.md +1 -1
- package/src/skills/framework-compliance-audit/SKILL.md +1 -1
- package/src/skills/git-workflow/SKILL.md +1 -1
- package/src/skills/hermes-agent/SKILL.md +42 -42
- package/src/skills/inference-improve/SKILL.md +1 -1
- package/src/skills/lint/SKILL.md +1 -1
- package/src/skills/log-monitor/SKILL.md +1 -1
- package/src/skills/model-health-check/SKILL.md +1 -1
- package/src/skills/multimodal-looker/SKILL.md +1 -1
- package/src/skills/orchestrator/SKILL.md +1 -1
- package/src/skills/performance-analysis/SKILL.md +1 -1
- package/src/skills/performance-optimization/SKILL.md +1 -1
- package/src/skills/processor-pipeline/SKILL.md +1 -1
- package/src/skills/project-analysis/SKILL.md +1 -1
- package/src/skills/refactoring-strategies/SKILL.md +1 -1
- package/src/skills/registry.json +1 -1
- package/src/skills/researcher/SKILL.md +1 -1
- package/src/skills/security-audit/SKILL.md +1 -1
- package/src/skills/security-scan/SKILL.md +1 -1
- package/src/skills/session-management/SKILL.md +1 -1
- package/src/skills/state-manager/SKILL.md +1 -1
- package/src/skills/storyteller/SKILL.md +1 -1
- package/src/skills/testing-best-practices/SKILL.md +1 -1
- package/src/skills/testing-strategy/SKILL.md +1 -1
- package/src/skills/ui-ux-design/SKILL.md +1 -1
- package/xray/agents_template.md +46 -0
- package/xray/codex.json +598 -0
- package/dist/plugin/xray-codex-injection.js +0 -857
- /package/dist/integrations/openclaw/hooks/{strray-hooks.js → xray-hooks.js} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Comprehensive tests for the
|
|
1
|
+
"""Comprehensive tests for the Xray Hermes Plugin v2.
|
|
2
2
|
|
|
3
3
|
Tests all 3 tools, both hooks, the slash command, bridge integration,
|
|
4
4
|
logging to disk, and the full register() wiring.
|
|
@@ -30,18 +30,18 @@ sys.path.insert(0, PLUGIN_DIR)
|
|
|
30
30
|
|
|
31
31
|
# Force reimport
|
|
32
32
|
for mod in list(sys.modules):
|
|
33
|
-
if "
|
|
33
|
+
if "xray" in mod and ("schemas" in mod or "tools" in mod or "plugin" in mod):
|
|
34
34
|
del sys.modules[mod]
|
|
35
35
|
|
|
36
36
|
schemas = importlib.import_module("schemas")
|
|
37
37
|
tools_mod = importlib.import_module("tools")
|
|
38
38
|
|
|
39
39
|
# Create a fake package for __init__.py execution
|
|
40
|
-
pkg = types.ModuleType("
|
|
40
|
+
pkg = types.ModuleType("xray_hermes_pkg")
|
|
41
41
|
pkg.__path__ = [PLUGIN_DIR]
|
|
42
42
|
pkg.__dict__["schemas"] = schemas
|
|
43
43
|
pkg.__dict__["tools"] = tools_mod
|
|
44
|
-
sys.modules["
|
|
44
|
+
sys.modules["xray_hermes_pkg"] = pkg
|
|
45
45
|
|
|
46
46
|
init_path = os.path.join(PLUGIN_DIR, "__init__.py")
|
|
47
47
|
with open(init_path) as f:
|
|
@@ -56,8 +56,8 @@ pi = pkg # plugin init module
|
|
|
56
56
|
|
|
57
57
|
class TestSchemas(unittest.TestCase):
|
|
58
58
|
def test_validate_schema_has_required_fields(self):
|
|
59
|
-
s = schemas.
|
|
60
|
-
self.assertEqual(s["name"], "
|
|
59
|
+
s = schemas.XRAY_VALIDATE
|
|
60
|
+
self.assertEqual(s["name"], "xray_validate")
|
|
61
61
|
params = s["parameters"]
|
|
62
62
|
self.assertEqual(params["type"], "object")
|
|
63
63
|
self.assertIn("files", params["properties"])
|
|
@@ -66,51 +66,51 @@ class TestSchemas(unittest.TestCase):
|
|
|
66
66
|
self.assertEqual(params["properties"]["files"]["type"], "array")
|
|
67
67
|
|
|
68
68
|
def test_codex_check_schema(self):
|
|
69
|
-
s = schemas.
|
|
70
|
-
self.assertEqual(s["name"], "
|
|
69
|
+
s = schemas.XRAY_CODEX_CHECK
|
|
70
|
+
self.assertEqual(s["name"], "xray_codex_check")
|
|
71
71
|
fa = s["parameters"]["properties"]["focus_areas"]
|
|
72
72
|
self.assertIn("enum", fa["items"])
|
|
73
73
|
self.assertIn("error-handling", fa["items"]["enum"])
|
|
74
74
|
|
|
75
75
|
def test_health_schema_no_required(self):
|
|
76
|
-
s = schemas.
|
|
76
|
+
s = schemas.XRAY_HEALTH
|
|
77
77
|
self.assertEqual(len(s["parameters"].get("required", [])), 0)
|
|
78
78
|
|
|
79
79
|
def test_descriptions_non_empty(self):
|
|
80
|
-
for name, schema in [("v", schemas.
|
|
80
|
+
for name, schema in [("v", schemas.XRAY_VALIDATE), ("c", schemas.XRAY_CODEX_CHECK), ("h", schemas.XRAY_HEALTH)]:
|
|
81
81
|
self.assertTrue(len(schema["description"]) > 20, f"{name}")
|
|
82
82
|
|
|
83
83
|
|
|
84
|
-
class
|
|
84
|
+
class TestRunXrayHelper(unittest.TestCase):
|
|
85
85
|
"""Test the CLI fallback helper (still exists for bridge-less environments)."""
|
|
86
86
|
|
|
87
87
|
def test_successful_command(self):
|
|
88
88
|
with patch("subprocess.run") as m:
|
|
89
89
|
m.return_value = MagicMock(returncode=0, stdout="all good", stderr="")
|
|
90
|
-
r = json.loads(tools_mod.
|
|
90
|
+
r = json.loads(tools_mod._run_xray(["health"]))
|
|
91
91
|
self.assertEqual(r["status"], "ok")
|
|
92
92
|
self.assertEqual(m.call_args[0][0], ["npx", "0xray", "health"])
|
|
93
93
|
|
|
94
94
|
def test_command_failure(self):
|
|
95
95
|
with patch("subprocess.run") as m:
|
|
96
96
|
m.return_value = MagicMock(returncode=1, stdout="", stderr="broke")
|
|
97
|
-
r = json.loads(tools_mod.
|
|
97
|
+
r = json.loads(tools_mod._run_xray(["validate"]))
|
|
98
98
|
self.assertEqual(r["status"], "error")
|
|
99
99
|
|
|
100
100
|
def test_file_not_found(self):
|
|
101
101
|
with patch("subprocess.run", side_effect=FileNotFoundError):
|
|
102
|
-
r = json.loads(tools_mod.
|
|
102
|
+
r = json.loads(tools_mod._run_xray(["health"]))
|
|
103
103
|
self.assertIn("not found", r["error"])
|
|
104
104
|
|
|
105
105
|
def test_timeout(self):
|
|
106
106
|
with patch("subprocess.run", side_effect=subprocess.TimeoutExpired("c", 30)):
|
|
107
|
-
r = json.loads(tools_mod.
|
|
107
|
+
r = json.loads(tools_mod._run_xray(["health"], timeout=15))
|
|
108
108
|
self.assertIn("15s", r["error"])
|
|
109
109
|
|
|
110
110
|
def test_custom_timeout(self):
|
|
111
111
|
with patch("subprocess.run") as m:
|
|
112
112
|
m.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
113
|
-
tools_mod.
|
|
113
|
+
tools_mod._run_xray(["health"], timeout=60)
|
|
114
114
|
self.assertEqual(m.call_args[1]["timeout"], 60)
|
|
115
115
|
|
|
116
116
|
|
|
@@ -143,11 +143,11 @@ class TestBridgeHelper(unittest.TestCase):
|
|
|
143
143
|
self.assertIn("timed out", r["error"])
|
|
144
144
|
|
|
145
145
|
|
|
146
|
-
class
|
|
146
|
+
class TestXrayHealth(unittest.TestCase):
|
|
147
147
|
def test_health_via_bridge(self):
|
|
148
148
|
"""v2: health uses bridge first."""
|
|
149
149
|
with patch.object(tools_mod, "_bridge_call", return_value={"status": "ok", "framework": "loaded", "version": "1.15.0", "components": {}}) as m:
|
|
150
|
-
r = json.loads(tools_mod.
|
|
150
|
+
r = json.loads(tools_mod.xray_health({}))
|
|
151
151
|
self.assertEqual(r["status"], "ok")
|
|
152
152
|
self.assertEqual(r["via"], "bridge")
|
|
153
153
|
m.assert_called_once_with({"command": "health"}, timeout=10)
|
|
@@ -155,21 +155,21 @@ class TestStrrayHealth(unittest.TestCase):
|
|
|
155
155
|
def test_health_fallback_to_cli(self):
|
|
156
156
|
"""v2: falls back to CLI when bridge fails."""
|
|
157
157
|
with patch.object(tools_mod, "_bridge_call", return_value={"error": "node not found"}):
|
|
158
|
-
with patch.object(tools_mod, "
|
|
159
|
-
r = json.loads(tools_mod.
|
|
158
|
+
with patch.object(tools_mod, "_run_xray", return_value='{"status":"ok","output":"healthy"}') as cli:
|
|
159
|
+
r = json.loads(tools_mod.xray_health({}))
|
|
160
160
|
cli.assert_called_once()
|
|
161
161
|
|
|
162
162
|
def test_health_ignores_extra_args(self):
|
|
163
163
|
with patch.object(tools_mod, "_bridge_call", return_value={"status": "ok", "framework": "loaded", "version": "1.0", "components": {}}):
|
|
164
|
-
r = json.loads(tools_mod.
|
|
164
|
+
r = json.loads(tools_mod.xray_health({"x": 1}))
|
|
165
165
|
self.assertEqual(r["status"], "ok")
|
|
166
166
|
|
|
167
167
|
|
|
168
|
-
class
|
|
168
|
+
class TestXrayValidate(unittest.TestCase):
|
|
169
169
|
def test_with_files_via_bridge(self):
|
|
170
170
|
"""v2: validate uses bridge first."""
|
|
171
171
|
with patch.object(tools_mod, "_bridge_call", return_value={"passed": True, "fileResults": []}) as m:
|
|
172
|
-
r = json.loads(tools_mod.
|
|
172
|
+
r = json.loads(tools_mod.xray_validate({"files": ["a.ts", "b.ts"], "operation": "commit"}))
|
|
173
173
|
self.assertEqual(r["status"], "passed")
|
|
174
174
|
self.assertEqual(r["files_checked"], 2)
|
|
175
175
|
self.assertEqual(r["via"], "bridge")
|
|
@@ -177,86 +177,86 @@ class TestStrrayValidate(unittest.TestCase):
|
|
|
177
177
|
|
|
178
178
|
def test_bridge_violations(self):
|
|
179
179
|
with patch.object(tools_mod, "_bridge_call", return_value={"passed": False, "fileResults": [{"file": "a.ts", "passed": False, "violations": ["tests-required"]}]}) as m:
|
|
180
|
-
r = json.loads(tools_mod.
|
|
180
|
+
r = json.loads(tools_mod.xray_validate({"files": ["a.ts"]}))
|
|
181
181
|
self.assertEqual(r["status"], "violations")
|
|
182
182
|
self.assertEqual(r["file_results"][0]["violations"], ["tests-required"])
|
|
183
183
|
|
|
184
184
|
def test_bridge_error_fallback_to_cli(self):
|
|
185
185
|
with patch.object(tools_mod, "_bridge_call", return_value={"error": "node not found"}):
|
|
186
|
-
with patch.object(tools_mod, "
|
|
187
|
-
r = json.loads(tools_mod.
|
|
186
|
+
with patch.object(tools_mod, "_run_xray", return_value='{"status":"ok","output":"valid"}') as cli:
|
|
187
|
+
r = json.loads(tools_mod.xray_validate({"files": ["a.ts"]}))
|
|
188
188
|
self.assertEqual(r["via"], "cli")
|
|
189
189
|
cli.assert_called_once()
|
|
190
190
|
|
|
191
191
|
def test_no_files_error(self):
|
|
192
|
-
r = json.loads(tools_mod.
|
|
192
|
+
r = json.loads(tools_mod.xray_validate({"files": []}))
|
|
193
193
|
self.assertIn("No files", r["error"])
|
|
194
194
|
|
|
195
195
|
def test_no_files_key_error(self):
|
|
196
|
-
r = json.loads(tools_mod.
|
|
196
|
+
r = json.loads(tools_mod.xray_validate({}))
|
|
197
197
|
self.assertIn("error", r)
|
|
198
198
|
|
|
199
199
|
def test_default_operation(self):
|
|
200
200
|
with patch.object(tools_mod, "_bridge_call", return_value={"passed": True, "fileResults": []}):
|
|
201
|
-
r = json.loads(tools_mod.
|
|
201
|
+
r = json.loads(tools_mod.xray_validate({"files": ["a.ts"]}))
|
|
202
202
|
self.assertEqual(r["operation"], "commit")
|
|
203
203
|
|
|
204
204
|
def test_100_files(self):
|
|
205
205
|
with patch.object(tools_mod, "_bridge_call", return_value={"passed": True, "fileResults": []}) as m:
|
|
206
206
|
fs = [f"f{i}.ts" for i in range(100)]
|
|
207
|
-
r = json.loads(tools_mod.
|
|
207
|
+
r = json.loads(tools_mod.xray_validate({"files": fs}))
|
|
208
208
|
self.assertEqual(r["files_checked"], 100)
|
|
209
209
|
|
|
210
210
|
|
|
211
|
-
class
|
|
211
|
+
class TestXrayCodexCheck(unittest.TestCase):
|
|
212
212
|
def test_with_code_via_bridge(self):
|
|
213
213
|
"""v2: codex check uses bridge for real quality gate analysis."""
|
|
214
214
|
with patch.object(tools_mod, "_bridge_call", return_value={"passed": True, "violations": [], "checks": []}) as m:
|
|
215
|
-
r = json.loads(tools_mod.
|
|
215
|
+
r = json.loads(tools_mod.xray_codex_check({"code": "const x = null;", "operation": "create"}))
|
|
216
216
|
self.assertEqual(r["status"], "passed")
|
|
217
217
|
self.assertEqual(r["via"], "bridge")
|
|
218
218
|
m.assert_called_once()
|
|
219
219
|
|
|
220
220
|
def test_with_code_bridge_violations(self):
|
|
221
221
|
with patch.object(tools_mod, "_bridge_call", return_value={"passed": False, "violations": ["console.log found"], "checks": []}):
|
|
222
|
-
r = json.loads(tools_mod.
|
|
222
|
+
r = json.loads(tools_mod.xray_codex_check({"code": "console.log(x)", "operation": "create"}))
|
|
223
223
|
self.assertEqual(r["status"], "violations")
|
|
224
224
|
self.assertEqual(r["violations"], ["console.log found"])
|
|
225
225
|
|
|
226
226
|
def test_with_focus_areas(self):
|
|
227
227
|
with patch.object(tools_mod, "_bridge_call", return_value={"passed": True, "violations": [], "checks": []}) as m:
|
|
228
|
-
tools_mod.
|
|
228
|
+
tools_mod.xray_codex_check({"code": "eval()", "operation": "modify", "focus_areas": ["security"]})
|
|
229
229
|
self.assertEqual(m.call_args[0][0]["focusAreas"], ["security"])
|
|
230
230
|
|
|
231
231
|
def test_empty_string_code_treated_as_code(self):
|
|
232
232
|
"""BUG FIX: empty string '' should still be treated as code (is not None)."""
|
|
233
233
|
with patch.object(tools_mod, "_bridge_call", return_value={"passed": True, "violations": [], "checks": []}) as m:
|
|
234
|
-
r = json.loads(tools_mod.
|
|
234
|
+
r = json.loads(tools_mod.xray_codex_check({"code": "", "operation": "create"}))
|
|
235
235
|
self.assertEqual(r["status"], "passed")
|
|
236
236
|
self.assertEqual(r["code_length"], 0)
|
|
237
237
|
|
|
238
238
|
def test_no_code_bridge_health(self):
|
|
239
239
|
"""No code provided — bridge health check."""
|
|
240
240
|
with patch.object(tools_mod, "_bridge_call", return_value={"framework": "loaded", "version": "1.15.0", "components": {}}) as m:
|
|
241
|
-
r = json.loads(tools_mod.
|
|
241
|
+
r = json.loads(tools_mod.xray_codex_check({"operation": "refactor"}))
|
|
242
242
|
self.assertEqual(r["status"], "ok")
|
|
243
243
|
self.assertIn("Pass", r["note"])
|
|
244
244
|
|
|
245
245
|
def test_no_code_bridge_error_fallback(self):
|
|
246
246
|
with patch.object(tools_mod, "_bridge_call", return_value={"error": "node not found"}):
|
|
247
|
-
with patch.object(tools_mod, "
|
|
248
|
-
r = json.loads(tools_mod.
|
|
247
|
+
with patch.object(tools_mod, "_run_xray", return_value='{"status":"ok","output":"healthy"}') as cli:
|
|
248
|
+
r = json.loads(tools_mod.xray_codex_check({"operation": "create"}))
|
|
249
249
|
cli.assert_called_once()
|
|
250
250
|
|
|
251
251
|
def test_default_operation(self):
|
|
252
252
|
with patch.object(tools_mod, "_bridge_call", return_value={"passed": True, "violations": [], "checks": []}):
|
|
253
|
-
r = json.loads(tools_mod.
|
|
253
|
+
r = json.loads(tools_mod.xray_codex_check({"code": "x"}))
|
|
254
254
|
self.assertEqual(r["operation"], "create")
|
|
255
255
|
|
|
256
256
|
def test_multiline_code(self):
|
|
257
257
|
code = "function foo() {\n return null;\n}\n"
|
|
258
258
|
with patch.object(tools_mod, "_bridge_call", return_value={"passed": True, "violations": [], "checks": []}):
|
|
259
|
-
r = json.loads(tools_mod.
|
|
259
|
+
r = json.loads(tools_mod.xray_codex_check({"code": code, "operation": "create"}))
|
|
260
260
|
self.assertEqual(r["code_length"], len(code))
|
|
261
261
|
|
|
262
262
|
|
|
@@ -270,10 +270,10 @@ class TestPreToolCallHook(unittest.TestCase):
|
|
|
270
270
|
pi._session_stats["session_id"] = None
|
|
271
271
|
|
|
272
272
|
@patch.object(pi, "_call_bridge")
|
|
273
|
-
def
|
|
274
|
-
"""
|
|
275
|
-
pi._on_pre_tool_call("
|
|
276
|
-
self.assertEqual(pi._session_stats["
|
|
273
|
+
def test_xray_mcp_no_bridge(self, mock_bridge):
|
|
274
|
+
"""Xray MCP tools skip bridge entirely."""
|
|
275
|
+
pi._on_pre_tool_call("mcp_xray_lint_lint", {}, "t1")
|
|
276
|
+
self.assertEqual(pi._session_stats["xray_mcp_calls"], 1)
|
|
277
277
|
self.assertEqual(pi._session_stats["native_tool_calls"], 0)
|
|
278
278
|
mock_bridge.assert_not_called()
|
|
279
279
|
|
|
@@ -316,26 +316,26 @@ class TestPreToolCallHook(unittest.TestCase):
|
|
|
316
316
|
"""Terminal nudge only fires when command matches a known pattern."""
|
|
317
317
|
# No command arg — no nudge
|
|
318
318
|
with self.assertRaises(AssertionError):
|
|
319
|
-
with self.assertLogs("
|
|
319
|
+
with self.assertLogs("xray-hermes", level="INFO"):
|
|
320
320
|
pi._on_pre_tool_call("terminal", {}, "t1")
|
|
321
321
|
|
|
322
322
|
# Generic command (git, ls) — no nudge
|
|
323
323
|
with self.assertRaises(AssertionError):
|
|
324
|
-
with self.assertLogs("
|
|
324
|
+
with self.assertLogs("xray-hermes", level="INFO"):
|
|
325
325
|
pi._on_pre_tool_call("terminal", {"command": "git status"}, "t1")
|
|
326
326
|
|
|
327
327
|
# Grep command — should nudge
|
|
328
|
-
with self.assertLogs("
|
|
328
|
+
with self.assertLogs("xray-hermes", level="INFO") as cm:
|
|
329
329
|
pi._on_pre_tool_call("terminal", {"command": "grep -r 'pattern' src/"}, "t1")
|
|
330
330
|
self.assertTrue(any("grep" in m for m in cm.output))
|
|
331
331
|
|
|
332
332
|
# npm audit — should nudge
|
|
333
|
-
with self.assertLogs("
|
|
333
|
+
with self.assertLogs("xray-hermes", level="INFO") as cm:
|
|
334
334
|
pi._on_pre_tool_call("terminal", {"command": "npm audit"}, "t1")
|
|
335
335
|
self.assertTrue(any("audit" in m for m in cm.output))
|
|
336
336
|
|
|
337
337
|
def test_nudge_search_files(self):
|
|
338
|
-
with self.assertLogs("
|
|
338
|
+
with self.assertLogs("xray-hermes", level="INFO") as cm:
|
|
339
339
|
pi._on_pre_tool_call("search_files", {}, "t1")
|
|
340
340
|
self.assertTrue(any("Tip" in m for m in cm.output))
|
|
341
341
|
|
|
@@ -343,7 +343,7 @@ class TestPreToolCallHook(unittest.TestCase):
|
|
|
343
343
|
"""write_file is a code tool — no nudge, gets bridge instead."""
|
|
344
344
|
with patch.object(pi, "_call_bridge", return_value={"passed": True, "qualityGate": {"passed": True}, "processors": {"ran": False}}):
|
|
345
345
|
with self.assertRaises(AssertionError):
|
|
346
|
-
with self.assertLogs("
|
|
346
|
+
with self.assertLogs("xray-hermes", level="INFO"):
|
|
347
347
|
pi._on_pre_tool_call("write_file", {}, "t1")
|
|
348
348
|
|
|
349
349
|
def test_accumulates(self):
|
|
@@ -352,12 +352,12 @@ class TestPreToolCallHook(unittest.TestCase):
|
|
|
352
352
|
pi._on_pre_tool_call("terminal", {}, "t1")
|
|
353
353
|
self.assertEqual(pi._session_stats["total_tool_calls"], 5)
|
|
354
354
|
|
|
355
|
-
def
|
|
356
|
-
self.assertTrue(pi.
|
|
357
|
-
self.assertTrue(pi.
|
|
358
|
-
self.assertFalse(pi.
|
|
359
|
-
self.assertFalse(pi.
|
|
360
|
-
self.assertFalse(pi.
|
|
355
|
+
def test_is_xray_mcp(self):
|
|
356
|
+
self.assertTrue(pi._is_xray_mcp("mcp_xray_lint_lint"))
|
|
357
|
+
self.assertTrue(pi._is_xray_mcp("mcp_xray_enforcer_codex_enforcement"))
|
|
358
|
+
self.assertFalse(pi._is_xray_mcp("terminal"))
|
|
359
|
+
self.assertFalse(pi._is_xray_mcp("xray_validate"))
|
|
360
|
+
self.assertFalse(pi._is_xray_mcp("mcp_other_tool"))
|
|
361
361
|
|
|
362
362
|
|
|
363
363
|
class TestPostToolCallHook(unittest.TestCase):
|
|
@@ -415,7 +415,7 @@ class TestSlashCommand(unittest.TestCase):
|
|
|
415
415
|
"session_id": "test-session",
|
|
416
416
|
"code_operations": 5,
|
|
417
417
|
"total_tool_calls": 20,
|
|
418
|
-
"
|
|
418
|
+
"xray_mcp_calls": 12,
|
|
419
419
|
"native_tool_calls": 8,
|
|
420
420
|
"quality_gate_runs": 10,
|
|
421
421
|
"quality_gate_blocks": 2,
|
|
@@ -429,7 +429,7 @@ class TestSlashCommand(unittest.TestCase):
|
|
|
429
429
|
}
|
|
430
430
|
|
|
431
431
|
def test_stats(self):
|
|
432
|
-
o = pi.
|
|
432
|
+
o = pi._xray_command("stats")
|
|
433
433
|
self.assertIn("20", o)
|
|
434
434
|
self.assertIn("12", o)
|
|
435
435
|
self.assertIn("8", o)
|
|
@@ -440,32 +440,32 @@ class TestSlashCommand(unittest.TestCase):
|
|
|
440
440
|
self.assertIn("15", o) # bridge_calls
|
|
441
441
|
|
|
442
442
|
def test_help(self):
|
|
443
|
-
o = pi.
|
|
443
|
+
o = pi._xray_command("help")
|
|
444
444
|
self.assertIn("status", o)
|
|
445
445
|
self.assertIn("help", o)
|
|
446
446
|
|
|
447
447
|
def test_status_via_bridge(self):
|
|
448
|
-
"""v2: status uses bridge, not tools.
|
|
448
|
+
"""v2: status uses bridge, not tools.xray_health."""
|
|
449
449
|
with patch.object(pi, "_call_bridge", return_value={"framework": "loaded", "version": "1.15.0", "components": {"qualityGate": True, "processorManager": True}}) as m:
|
|
450
|
-
o = pi.
|
|
450
|
+
o = pi._xray_command("status")
|
|
451
451
|
self.assertIn("loaded", o)
|
|
452
452
|
self.assertIn("1.15.0", o)
|
|
453
453
|
m.assert_called_once_with({"command": "health"}, timeout=10)
|
|
454
454
|
|
|
455
455
|
def test_status_bridge_error(self):
|
|
456
456
|
with patch.object(pi, "_call_bridge", return_value={"error": "node not found"}):
|
|
457
|
-
o = pi.
|
|
457
|
+
o = pi._xray_command("status")
|
|
458
458
|
self.assertIn("node not found", o)
|
|
459
459
|
|
|
460
460
|
def test_default_status(self):
|
|
461
461
|
with patch.object(pi, "_call_bridge", return_value={"framework": "loaded", "version": "1.0", "components": {}}):
|
|
462
|
-
o = pi.
|
|
462
|
+
o = pi._xray_command("")
|
|
463
463
|
self.assertIn("loaded", o)
|
|
464
464
|
|
|
465
465
|
def test_stats_null_started_at(self):
|
|
466
466
|
pi._session_stats["started_at"] = None
|
|
467
467
|
pi._session_stats["session_id"] = None
|
|
468
|
-
o = pi.
|
|
468
|
+
o = pi._xray_command("stats")
|
|
469
469
|
self.assertIn("N/A", o)
|
|
470
470
|
|
|
471
471
|
|
|
@@ -484,7 +484,7 @@ class TestSessionStartHook(unittest.TestCase):
|
|
|
484
484
|
self.assertEqual(pi._session_stats["session_id"], "s1")
|
|
485
485
|
|
|
486
486
|
def test_logs(self):
|
|
487
|
-
with self.assertLogs("
|
|
487
|
+
with self.assertLogs("xray-hermes", level="INFO") as cm:
|
|
488
488
|
pi._on_session_start("s1", "telegram")
|
|
489
489
|
self.assertTrue(any("s1" in m for m in cm.output))
|
|
490
490
|
self.assertTrue(any("telegram" in m for m in cm.output))
|
|
@@ -495,29 +495,29 @@ class TestRegisterIntegration(unittest.TestCase):
|
|
|
495
495
|
ctx = MagicMock()
|
|
496
496
|
pi.register(ctx)
|
|
497
497
|
names = [c[1]["name"] for c in ctx.register_tool.call_args_list]
|
|
498
|
-
self.assertEqual(set(names), {"
|
|
498
|
+
self.assertEqual(set(names), {"xray_validate", "xray_codex_check", "xray_health", "xray_hooks"})
|
|
499
499
|
|
|
500
500
|
def test_toolset_name(self):
|
|
501
501
|
ctx = MagicMock()
|
|
502
502
|
pi.register(ctx)
|
|
503
503
|
for c in ctx.register_tool.call_args_list:
|
|
504
|
-
self.assertEqual(c[1]["toolset"], "
|
|
504
|
+
self.assertEqual(c[1]["toolset"], "xray-hermes")
|
|
505
505
|
|
|
506
506
|
def test_schemas_wired(self):
|
|
507
507
|
ctx = MagicMock()
|
|
508
508
|
pi.register(ctx)
|
|
509
509
|
sm = {c[1]["name"]: c[1]["schema"] for c in ctx.register_tool.call_args_list}
|
|
510
|
-
self.assertIs(sm["
|
|
511
|
-
self.assertIs(sm["
|
|
512
|
-
self.assertIs(sm["
|
|
510
|
+
self.assertIs(sm["xray_validate"], schemas.XRAY_VALIDATE)
|
|
511
|
+
self.assertIs(sm["xray_codex_check"], schemas.XRAY_CODEX_CHECK)
|
|
512
|
+
self.assertIs(sm["xray_health"], schemas.XRAY_HEALTH)
|
|
513
513
|
|
|
514
514
|
def test_handlers_wired(self):
|
|
515
515
|
ctx = MagicMock()
|
|
516
516
|
pi.register(ctx)
|
|
517
517
|
hm = {c[1]["name"]: c[1]["handler"] for c in ctx.register_tool.call_args_list}
|
|
518
|
-
self.assertIs(hm["
|
|
519
|
-
self.assertIs(hm["
|
|
520
|
-
self.assertIs(hm["
|
|
518
|
+
self.assertIs(hm["xray_validate"], tools_mod.xray_validate)
|
|
519
|
+
self.assertIs(hm["xray_codex_check"], tools_mod.xray_codex_check)
|
|
520
|
+
self.assertIs(hm["xray_health"], tools_mod.xray_health)
|
|
521
521
|
|
|
522
522
|
def test_hooks_registered(self):
|
|
523
523
|
ctx = MagicMock()
|
|
@@ -551,7 +551,7 @@ class TestRegisterIntegration(unittest.TestCase):
|
|
|
551
551
|
|
|
552
552
|
def test_logs_on_load(self):
|
|
553
553
|
ctx = MagicMock()
|
|
554
|
-
with self.assertLogs("
|
|
554
|
+
with self.assertLogs("xray-hermes", level="INFO") as cm:
|
|
555
555
|
pi.register(ctx)
|
|
556
556
|
self.assertTrue(any("Plugin" in m and "loaded" in m for m in cm.output))
|
|
557
557
|
|
|
@@ -656,9 +656,9 @@ class TestLiveBridge(unittest.TestCase):
|
|
|
656
656
|
if not bridge_path.exists():
|
|
657
657
|
self.skipTest("bridge.mjs not built yet")
|
|
658
658
|
|
|
659
|
-
# Use
|
|
660
|
-
|
|
661
|
-
cwd =
|
|
659
|
+
# Use xray dev repo as project root (has package.json)
|
|
660
|
+
xray_root = str(Path(PLUGIN_DIR).parent.parent.parent / "dev" / "xray")
|
|
661
|
+
cwd = xray_root if Path(xray_root).exists() else PLUGIN_DIR
|
|
662
662
|
|
|
663
663
|
r = subprocess.run(
|
|
664
664
|
["node", str(bridge_path), "health", "--cwd", cwd],
|
|
@@ -675,8 +675,8 @@ class TestLiveBridge(unittest.TestCase):
|
|
|
675
675
|
if not bridge_path.exists():
|
|
676
676
|
self.skipTest("bridge.mjs not built yet")
|
|
677
677
|
|
|
678
|
-
|
|
679
|
-
cwd =
|
|
678
|
+
xray_root = str(Path(PLUGIN_DIR).parent.parent.parent / "dev" / "xray")
|
|
679
|
+
cwd = xray_root if Path(xray_root).exists() else PLUGIN_DIR
|
|
680
680
|
|
|
681
681
|
r = subprocess.run(
|
|
682
682
|
["node", str(bridge_path), "stats", "--cwd", cwd],
|
|
@@ -692,8 +692,8 @@ class TestLiveBridge(unittest.TestCase):
|
|
|
692
692
|
if not bridge_path.exists():
|
|
693
693
|
self.skipTest("bridge.mjs not built yet")
|
|
694
694
|
|
|
695
|
-
|
|
696
|
-
cwd =
|
|
695
|
+
xray_root = str(Path(PLUGIN_DIR).parent.parent.parent / "dev" / "xray")
|
|
696
|
+
cwd = xray_root if Path(xray_root).exists() else PLUGIN_DIR
|
|
697
697
|
|
|
698
698
|
r = subprocess.run(
|
|
699
699
|
["node", str(bridge_path), "validate", "--cwd", cwd,
|
|
@@ -710,8 +710,8 @@ class TestLiveBridge(unittest.TestCase):
|
|
|
710
710
|
if not bridge_path.exists():
|
|
711
711
|
self.skipTest("bridge.mjs not built yet")
|
|
712
712
|
|
|
713
|
-
|
|
714
|
-
cwd =
|
|
713
|
+
xray_root = str(Path(PLUGIN_DIR).parent.parent.parent / "dev" / "xray")
|
|
714
|
+
cwd = xray_root if Path(xray_root).exists() else PLUGIN_DIR
|
|
715
715
|
|
|
716
716
|
r = subprocess.run(
|
|
717
717
|
["node", str(bridge_path), "validate", "--cwd", cwd,
|
|
@@ -739,32 +739,32 @@ class TestBridgeErrorPaths(unittest.TestCase):
|
|
|
739
739
|
r = tools_mod._bridge_call({"command": "health"})
|
|
740
740
|
self.assertIn("error", r)
|
|
741
741
|
|
|
742
|
-
def
|
|
743
|
-
"""
|
|
742
|
+
def test_bridge_generic_exception_in_run_xray(self):
|
|
743
|
+
"""_run_xray catches non-standard exceptions."""
|
|
744
744
|
with patch("subprocess.run", side_effect=RuntimeError("unexpected")):
|
|
745
|
-
r = json.loads(tools_mod.
|
|
745
|
+
r = json.loads(tools_mod._run_xray(["health"]))
|
|
746
746
|
self.assertIn("unexpected", r["error"])
|
|
747
747
|
|
|
748
748
|
def test_validate_cli_fallback_error(self):
|
|
749
|
-
"""
|
|
749
|
+
"""xray_validate CLI fallback when CLI returns an error JSON."""
|
|
750
750
|
with patch.object(tools_mod, "_bridge_call", return_value={"error": "bridge down"}):
|
|
751
|
-
with patch.object(tools_mod, "
|
|
752
|
-
r = json.loads(tools_mod.
|
|
751
|
+
with patch.object(tools_mod, "_run_xray", return_value='{"error": "validation failed"}'):
|
|
752
|
+
r = json.loads(tools_mod.xray_validate({"files": ["a.ts"]}))
|
|
753
753
|
# CLI error path returns raw result without "via" key
|
|
754
754
|
self.assertIn("error", r)
|
|
755
755
|
|
|
756
756
|
def test_codex_check_static_fallback(self):
|
|
757
|
-
"""
|
|
757
|
+
"""xray_codex_check with code but bridge down falls back to static analysis."""
|
|
758
758
|
with patch.object(tools_mod, "_bridge_call", return_value={"error": "bridge down"}):
|
|
759
|
-
r = json.loads(tools_mod.
|
|
759
|
+
r = json.loads(tools_mod.xray_codex_check({"code": "console.log(1)", "operation": "create"}))
|
|
760
760
|
self.assertEqual(r["via"], "static")
|
|
761
761
|
self.assertIn("basic analysis", r["note"])
|
|
762
762
|
|
|
763
763
|
def test_codex_check_cli_health_error(self):
|
|
764
|
-
"""
|
|
764
|
+
"""xray_codex_check no-code path: bridge error + CLI also errors."""
|
|
765
765
|
with patch.object(tools_mod, "_bridge_call", return_value={"error": "no node"}):
|
|
766
|
-
with patch.object(tools_mod, "
|
|
767
|
-
r = json.loads(tools_mod.
|
|
766
|
+
with patch.object(tools_mod, "_run_xray", return_value='{"error": "0xray not found"}'):
|
|
767
|
+
r = json.loads(tools_mod.xray_codex_check({"operation": "create"}))
|
|
768
768
|
self.assertIn("error", r)
|
|
769
769
|
|
|
770
770
|
|
|
@@ -850,17 +850,17 @@ class TestPreToolCallEdgeCases(unittest.TestCase):
|
|
|
850
850
|
self.assertEqual(pi._session_stats["code_operations"], 1, f"{tool} should be a code tool")
|
|
851
851
|
|
|
852
852
|
@patch.object(pi, "_call_bridge")
|
|
853
|
-
def
|
|
853
|
+
def test_unknown_tool_not_xray_mcp(self, mock_bridge):
|
|
854
854
|
"""Unknown tools should be treated as native tools."""
|
|
855
855
|
pi._on_pre_tool_call("some_random_tool", {}, "t1")
|
|
856
856
|
self.assertEqual(pi._session_stats["native_tool_calls"], 1)
|
|
857
857
|
mock_bridge.assert_not_called()
|
|
858
858
|
|
|
859
|
-
def
|
|
860
|
-
"""
|
|
861
|
-
pi._on_pre_tool_call("
|
|
859
|
+
def test_xray_validate_tool_not_treated_as_mcp(self):
|
|
860
|
+
"""xray_validate is a native tool, not an MCP tool."""
|
|
861
|
+
pi._on_pre_tool_call("xray_validate", {}, "t1")
|
|
862
862
|
self.assertEqual(pi._session_stats["native_tool_calls"], 1)
|
|
863
|
-
self.assertEqual(pi._session_stats["
|
|
863
|
+
self.assertEqual(pi._session_stats["xray_mcp_calls"], 0)
|
|
864
864
|
|
|
865
865
|
@patch.object(pi, "_call_bridge", return_value={"passed": True, "qualityGate": {"passed": True, "violations": []}, "processors": {"ran": True, "success": True, "processorCount": 3, "details": [{"name": "p1", "success": True}, {"name": "p2", "success": True}, {"name": "p3", "success": False, "error": "failed"}]}})
|
|
866
866
|
def test_pre_processor_partial_failure(self, mock_bridge):
|
|
@@ -875,12 +875,12 @@ class TestSlashCommandEdgeCases(unittest.TestCase):
|
|
|
875
875
|
def test_unknown_command_defaults_to_status(self):
|
|
876
876
|
"""Unknown args default to status."""
|
|
877
877
|
with patch.object(pi, "_call_bridge", return_value={"framework": "loaded", "version": "1.0", "components": {}}) as m:
|
|
878
|
-
pi.
|
|
878
|
+
pi._xray_command("something-random")
|
|
879
879
|
m.assert_called_once_with({"command": "health"}, timeout=10)
|
|
880
880
|
|
|
881
881
|
def test_case_insensitive(self):
|
|
882
882
|
"""Command arg is lowercased."""
|
|
883
|
-
o = pi.
|
|
883
|
+
o = pi._xray_command(" STATS ")
|
|
884
884
|
self.assertIn("Session", o)
|
|
885
885
|
|
|
886
886
|
|
|
@@ -945,8 +945,8 @@ class TestBridgeHelperTimeoutDefault(unittest.TestCase):
|
|
|
945
945
|
self.assertEqual(m.call_args[1]["timeout"], 5)
|
|
946
946
|
|
|
947
947
|
|
|
948
|
-
class
|
|
949
|
-
"""Tests for the
|
|
948
|
+
class TestXrayHooksTool(unittest.TestCase):
|
|
949
|
+
"""Tests for the xray_hooks tool."""
|
|
950
950
|
|
|
951
951
|
def test_list_via_bridge(self):
|
|
952
952
|
"""list action uses bridge when available."""
|
|
@@ -954,7 +954,7 @@ class TestStrrayHooksTool(unittest.TestCase):
|
|
|
954
954
|
"status": "ok", "action": "list",
|
|
955
955
|
"hooks": {"managed": ["pre-commit"], "missing": [], "external": [], "stale": []},
|
|
956
956
|
}) as m:
|
|
957
|
-
r = json.loads(tools_mod.
|
|
957
|
+
r = json.loads(tools_mod.xray_hooks({"action": "list"}))
|
|
958
958
|
self.assertEqual(r["status"], "ok")
|
|
959
959
|
self.assertEqual(r["via"], "bridge")
|
|
960
960
|
m.assert_called_once()
|
|
@@ -968,7 +968,7 @@ class TestStrrayHooksTool(unittest.TestCase):
|
|
|
968
968
|
"status": "ok", "action": "install", "installed": ["pre-commit", "post-commit"],
|
|
969
969
|
"skipped": [], "errors": [],
|
|
970
970
|
}) as m:
|
|
971
|
-
r = json.loads(tools_mod.
|
|
971
|
+
r = json.loads(tools_mod.xray_hooks({"action": "install"}))
|
|
972
972
|
self.assertEqual(r["via"], "bridge")
|
|
973
973
|
self.assertEqual(len(r["result"]["installed"]), 2)
|
|
974
974
|
|
|
@@ -977,14 +977,14 @@ class TestStrrayHooksTool(unittest.TestCase):
|
|
|
977
977
|
with patch.object(tools_mod, "_bridge_call", return_value={
|
|
978
978
|
"status": "ok", "action": "uninstall", "removed": ["pre-commit"], "restored": [],
|
|
979
979
|
}) as m:
|
|
980
|
-
r = json.loads(tools_mod.
|
|
980
|
+
r = json.loads(tools_mod.xray_hooks({"action": "uninstall"}))
|
|
981
981
|
self.assertEqual(r["via"], "bridge")
|
|
982
982
|
|
|
983
983
|
def test_bridge_error_fallback(self):
|
|
984
984
|
"""Falls back to file-based when bridge errors."""
|
|
985
985
|
with patch.object(tools_mod, "_bridge_call", return_value={"error": "node not found"}):
|
|
986
986
|
# Without a real git repo, should return error
|
|
987
|
-
r = json.loads(tools_mod.
|
|
987
|
+
r = json.loads(tools_mod.xray_hooks({"action": "list"}))
|
|
988
988
|
self.assertIn("via", r)
|
|
989
989
|
|
|
990
990
|
def test_specific_hooks(self):
|
|
@@ -993,7 +993,7 @@ class TestStrrayHooksTool(unittest.TestCase):
|
|
|
993
993
|
"status": "ok", "action": "list",
|
|
994
994
|
"hooks": {"managed": [], "missing": ["pre-commit"], "external": [], "stale": []},
|
|
995
995
|
}) as m:
|
|
996
|
-
tools_mod.
|
|
996
|
+
tools_mod.xray_hooks({"action": "list", "hooks": ["pre-commit"]})
|
|
997
997
|
call_cmd = m.call_args[0][0]
|
|
998
998
|
self.assertEqual(call_cmd["hooks"], ["pre-commit"])
|
|
999
999
|
|
|
@@ -1003,7 +1003,7 @@ class TestStrrayHooksTool(unittest.TestCase):
|
|
|
1003
1003
|
"status": "ok", "action": "status",
|
|
1004
1004
|
"hooks": {"managed": [], "missing": [], "external": [], "stale": []},
|
|
1005
1005
|
}) as m:
|
|
1006
|
-
r = json.loads(tools_mod.
|
|
1006
|
+
r = json.loads(tools_mod.xray_hooks({"action": "status"}))
|
|
1007
1007
|
self.assertEqual(r["status"], "ok")
|
|
1008
1008
|
m.assert_called_once()
|
|
1009
1009
|
|
|
@@ -1013,23 +1013,23 @@ class TestStrrayHooksTool(unittest.TestCase):
|
|
|
1013
1013
|
"status": "ok", "action": "list",
|
|
1014
1014
|
"hooks": {"managed": [], "missing": [], "external": [], "stale": []},
|
|
1015
1015
|
}) as m:
|
|
1016
|
-
tools_mod.
|
|
1016
|
+
tools_mod.xray_hooks({})
|
|
1017
1017
|
call_cmd = m.call_args[0][0]
|
|
1018
1018
|
self.assertEqual(call_cmd["action"], "list")
|
|
1019
1019
|
|
|
1020
1020
|
|
|
1021
|
-
class
|
|
1022
|
-
"""Tests for the
|
|
1021
|
+
class TestXrayHooksSchema(unittest.TestCase):
|
|
1022
|
+
"""Tests for the XRAY_HOOKS schema."""
|
|
1023
1023
|
|
|
1024
1024
|
def test_schema_has_required_fields(self):
|
|
1025
|
-
s = schemas.
|
|
1026
|
-
self.assertEqual(s["name"], "
|
|
1025
|
+
s = schemas.XRAY_HOOKS
|
|
1026
|
+
self.assertEqual(s["name"], "xray_hooks")
|
|
1027
1027
|
self.assertIn("action", s["parameters"]["properties"])
|
|
1028
1028
|
self.assertIn("hooks", s["parameters"]["properties"])
|
|
1029
1029
|
self.assertIn("action", s["parameters"]["required"])
|
|
1030
1030
|
|
|
1031
1031
|
def test_action_enum(self):
|
|
1032
|
-
s = schemas.
|
|
1032
|
+
s = schemas.XRAY_HOOKS
|
|
1033
1033
|
action = s["parameters"]["properties"]["action"]
|
|
1034
1034
|
self.assertIn("install", action["enum"])
|
|
1035
1035
|
self.assertIn("uninstall", action["enum"])
|
|
@@ -1037,7 +1037,7 @@ class TestStrrayHooksSchema(unittest.TestCase):
|
|
|
1037
1037
|
self.assertIn("status", action["enum"])
|
|
1038
1038
|
|
|
1039
1039
|
def test_hooks_enum(self):
|
|
1040
|
-
s = schemas.
|
|
1040
|
+
s = schemas.XRAY_HOOKS
|
|
1041
1041
|
hooks = s["parameters"]["properties"]["hooks"]
|
|
1042
1042
|
self.assertIn("pre-commit", hooks["items"]["enum"])
|
|
1043
1043
|
self.assertIn("post-commit", hooks["items"]["enum"])
|
|
@@ -1045,7 +1045,7 @@ class TestStrrayHooksSchema(unittest.TestCase):
|
|
|
1045
1045
|
self.assertIn("post-push", hooks["items"]["enum"])
|
|
1046
1046
|
|
|
1047
1047
|
def test_description_mentions_hooks(self):
|
|
1048
|
-
s = schemas.
|
|
1048
|
+
s = schemas.XRAY_HOOKS
|
|
1049
1049
|
self.assertIn("git hooks", s["description"])
|
|
1050
1050
|
|
|
1051
1051
|
|
|
@@ -1057,21 +1057,21 @@ class TestRegisterIntegrationV2_1(unittest.TestCase):
|
|
|
1057
1057
|
pi.register(ctx)
|
|
1058
1058
|
names = [c[1]["name"] for c in ctx.register_tool.call_args_list]
|
|
1059
1059
|
self.assertEqual(set(names), {
|
|
1060
|
-
"
|
|
1061
|
-
"
|
|
1060
|
+
"xray_validate", "xray_codex_check",
|
|
1061
|
+
"xray_health", "xray_hooks",
|
|
1062
1062
|
})
|
|
1063
1063
|
|
|
1064
|
-
def
|
|
1064
|
+
def test_xray_hooks_schema_wired(self):
|
|
1065
1065
|
ctx = MagicMock()
|
|
1066
1066
|
pi.register(ctx)
|
|
1067
1067
|
sm = {c[1]["name"]: c[1]["schema"] for c in ctx.register_tool.call_args_list}
|
|
1068
|
-
self.assertIs(sm["
|
|
1068
|
+
self.assertIs(sm["xray_hooks"], schemas.XRAY_HOOKS)
|
|
1069
1069
|
|
|
1070
|
-
def
|
|
1070
|
+
def test_xray_hooks_handler_wired(self):
|
|
1071
1071
|
ctx = MagicMock()
|
|
1072
1072
|
pi.register(ctx)
|
|
1073
1073
|
hm = {c[1]["name"]: c[1]["handler"] for c in ctx.register_tool.call_args_list}
|
|
1074
|
-
self.assertIs(hm["
|
|
1074
|
+
self.assertIs(hm["xray_hooks"], tools_mod.xray_hooks)
|
|
1075
1075
|
|
|
1076
1076
|
def test_registers_two_hooks(self):
|
|
1077
1077
|
ctx = MagicMock()
|
|
@@ -1090,7 +1090,7 @@ class TestRegisterIntegrationV2_1(unittest.TestCase):
|
|
|
1090
1090
|
|
|
1091
1091
|
def test_v2_2_log_message(self):
|
|
1092
1092
|
ctx = MagicMock()
|
|
1093
|
-
with self.assertLogs("
|
|
1093
|
+
with self.assertLogs("xray-hermes", level="INFO") as cm:
|
|
1094
1094
|
pi.register(ctx)
|
|
1095
1095
|
self.assertTrue(any("v2.2" in m for m in cm.output))
|
|
1096
1096
|
self.assertTrue(any("4 tools" in m for m in cm.output))
|