agentseed-mcp 0.3.0
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/CHANGELOG.md +178 -0
- package/LICENSE +202 -0
- package/README.ja.md +320 -0
- package/README.md +318 -0
- package/README.zh.md +306 -0
- package/bin/cli.js +37 -0
- package/mcp.json +12 -0
- package/package.json +30 -0
- package/plugin.json +22 -0
- package/server/.agentseed/verification-log.jsonl +2 -0
- package/server/__pycache__/guard_cli.cpython-313.pyc +0 -0
- package/server/__pycache__/guard_engine.cpython-313.pyc +0 -0
- package/server/__pycache__/test_cli.cpython-313-pytest-9.1.1.pyc +0 -0
- package/server/__pycache__/test_cli.cpython-313.pyc +0 -0
- package/server/__pycache__/test_features.cpython-313-pytest-9.1.1.pyc +0 -0
- package/server/__pycache__/test_features.cpython-313.pyc +0 -0
- package/server/__pycache__/test_guard.cpython-313-pytest-9.1.1.pyc +0 -0
- package/server/__pycache__/test_guard.cpython-313.pyc +0 -0
- package/server/__pycache__/test_hook.cpython-313-pytest-9.1.1.pyc +0 -0
- package/server/__pycache__/test_hook.cpython-313.pyc +0 -0
- package/server/__pycache__/test_manifests.cpython-313-pytest-9.1.1.pyc +0 -0
- package/server/__pycache__/test_manifests.cpython-313.pyc +0 -0
- package/server/__pycache__/test_server.cpython-313-pytest-9.1.1.pyc +0 -0
- package/server/__pycache__/test_server.cpython-313.pyc +0 -0
- package/server/engine/__init__.py +64 -0
- package/server/engine/__pycache__/__init__.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/audit.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/config.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/hallucination.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/imports.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/plugin.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/sandbox.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/schema.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/symbols.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/version.cpython-313.pyc +0 -0
- package/server/engine/audit.py +84 -0
- package/server/engine/config.py +131 -0
- package/server/engine/hallucination.py +254 -0
- package/server/engine/imports.py +136 -0
- package/server/engine/plugin.py +367 -0
- package/server/engine/sandbox.py +287 -0
- package/server/engine/schema.py +193 -0
- package/server/engine/symbols.py +984 -0
- package/server/engine/version.py +17 -0
- package/server/guard_cli.py +455 -0
- package/server/guard_engine.py +111 -0
- package/server/guard_hook.py +404 -0
- package/server/guard_server.py +472 -0
- package/server/requirements.txt +7 -0
- package/server/test_cli.py +132 -0
- package/server/test_features.py +426 -0
- package/server/test_guard.py +828 -0
- package/server/test_hook.py +331 -0
- package/server/test_manifests.py +70 -0
- package/server/test_server.py +247 -0
- package/skills/verify-before-code/SKILL.ja.md +116 -0
- package/skills/verify-before-code/SKILL.md +140 -0
- package/skills/verify-before-code/SKILL.zh.md +117 -0
- package/skills/verify-before-code/references/DEFAULT-NORMS.md +52 -0
- package/skills/verify-before-code/references/HALLUCINATION-PATTERNS.ja.md +121 -0
- package/skills/verify-before-code/references/HALLUCINATION-PATTERNS.md +166 -0
- package/skills/verify-before-code/references/HALLUCINATION-PATTERNS.zh.md +145 -0
- package/skills/verify-before-code/references/PROMPT-POOL.ja.md +248 -0
- package/skills/verify-before-code/references/PROMPT-POOL.md +282 -0
- package/skills/verify-before-code/references/PROMPT-POOL.zh.md +252 -0
- package/skills/verify-before-code/references/SDD-CONTRACT.ja.md +61 -0
- package/skills/verify-before-code/references/SDD-CONTRACT.md +66 -0
- package/skills/verify-before-code/references/SDD-CONTRACT.zh.md +58 -0
- package/skills/verify-before-code/references/VENDOR-SOLUTIONS.ja.md +62 -0
- package/skills/verify-before-code/references/VENDOR-SOLUTIONS.md +62 -0
- package/skills/verify-before-code/references/VENDOR-SOLUTIONS.zh.md +54 -0
- package/skills/verify-before-code/references/VERIFICATION-CHECKLIST.ja.md +68 -0
- package/skills/verify-before-code/references/VERIFICATION-CHECKLIST.md +73 -0
- package/skills/verify-before-code/references/VERIFICATION-CHECKLIST.zh.md +68 -0
- package/skills/verify-before-code/scripts/check.ps1 +52 -0
- package/skills/verify-before-code/scripts/check.sh +44 -0
|
@@ -0,0 +1,828 @@
|
|
|
1
|
+
"""AgentSeed guard engine unit tests (stdlib unittest, zero deps)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import unittest
|
|
7
|
+
|
|
8
|
+
import guard_engine as engine # noqa: E402
|
|
9
|
+
|
|
10
|
+
PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
11
|
+
# sys.executable works on every platform; the literal "python3" does not
|
|
12
|
+
# exist on many Windows installs (the WindowsApps alias is a Store stub).
|
|
13
|
+
PY = sys.executable
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestUndefinedSymbols(unittest.TestCase):
|
|
17
|
+
def test_catches_hallucinated_call(self):
|
|
18
|
+
r = engine.detect_undefined_symbols("def f():\n return magic_unknown()\n")
|
|
19
|
+
self.assertIn("magic_unknown", r["suspects"])
|
|
20
|
+
|
|
21
|
+
def test_clean_code_passes(self):
|
|
22
|
+
r = engine.detect_undefined_symbols("import math\nprint(math.sqrt(4))\n")
|
|
23
|
+
self.assertEqual(r["suspects"], [])
|
|
24
|
+
|
|
25
|
+
def test_syntax_error_reported(self):
|
|
26
|
+
r = engine.detect_undefined_symbols("def f(:\n")
|
|
27
|
+
self.assertEqual(r["suspects"], [])
|
|
28
|
+
self.assertIn("syntax", r["note"])
|
|
29
|
+
|
|
30
|
+
def test_ts_catches_hallucinated_call(self):
|
|
31
|
+
src = "import fs from 'fs';\nfunction read(path) { return fs.readFileSync(path); }\nreadFile('../x');\n"
|
|
32
|
+
r = engine.detect_undefined_symbols(src, "typescript")
|
|
33
|
+
self.assertIn("readFile", r["suspects"])
|
|
34
|
+
|
|
35
|
+
def test_ts_clean_imports(self):
|
|
36
|
+
src = "import { join } from 'path';\nimport fs from 'fs';\nconsole.log(join('a', 'b'));\nfs.readFileSync('x');\n"
|
|
37
|
+
r = engine.detect_undefined_symbols(src, "typescript")
|
|
38
|
+
self.assertEqual(r["suspects"], [])
|
|
39
|
+
|
|
40
|
+
def test_ts_keywords_not_flagged(self):
|
|
41
|
+
src = "function f(x: number) {\n if (x > 0) return x;\n return 0;\n}\n"
|
|
42
|
+
r = engine.detect_undefined_symbols(src, "typescript")
|
|
43
|
+
self.assertEqual(r["suspects"], [])
|
|
44
|
+
|
|
45
|
+
def test_ts_call_args_are_not_definitions(self):
|
|
46
|
+
# `wrap(helper)` must not define `helper`; `helper()` must be flagged
|
|
47
|
+
src = "helper();\nwrap(helper);\n"
|
|
48
|
+
r = engine.detect_undefined_symbols(src, "typescript")
|
|
49
|
+
self.assertIn("helper", r["suspects"])
|
|
50
|
+
self.assertIn("wrap", r["suspects"])
|
|
51
|
+
|
|
52
|
+
def test_ts_multi_declaration_collected(self):
|
|
53
|
+
src = "const a = 1, b = 2;\nconsole.log(a + b);\n"
|
|
54
|
+
r = engine.detect_undefined_symbols(src, "typescript")
|
|
55
|
+
self.assertEqual(r["suspects"], [])
|
|
56
|
+
|
|
57
|
+
def test_python_module_dunders_not_flagged(self):
|
|
58
|
+
src = 'if __name__ == "__main__":\n print(__file__, __doc__)\n'
|
|
59
|
+
r = engine.detect_undefined_symbols(src, "python")
|
|
60
|
+
self.assertEqual(r["suspects"], [])
|
|
61
|
+
|
|
62
|
+
def test_python_local_assignment_not_flagged(self):
|
|
63
|
+
src = "def f():\n total = len([1, 2])\n return total\n"
|
|
64
|
+
r = engine.detect_undefined_symbols(src, "python")
|
|
65
|
+
self.assertEqual(r["suspects"], [])
|
|
66
|
+
|
|
67
|
+
def test_python_for_with_except_targets_not_flagged(self):
|
|
68
|
+
src = (
|
|
69
|
+
"def f(items, path):\n"
|
|
70
|
+
" out = []\n"
|
|
71
|
+
" for it in items:\n"
|
|
72
|
+
" out.append(it)\n"
|
|
73
|
+
" try:\n"
|
|
74
|
+
" with open(path) as fh:\n"
|
|
75
|
+
" out.append(fh.read())\n"
|
|
76
|
+
" except OSError as exc:\n"
|
|
77
|
+
" out.append(str(exc))\n"
|
|
78
|
+
" return out\n"
|
|
79
|
+
)
|
|
80
|
+
r = engine.detect_undefined_symbols(src, "python")
|
|
81
|
+
self.assertEqual(r["suspects"], [])
|
|
82
|
+
|
|
83
|
+
def test_python_walrus_and_augassign_not_flagged(self):
|
|
84
|
+
src = "def f(n):\n count = 0\n count += n\n if (big := count * 2) > 10:\n return big\n return count\n"
|
|
85
|
+
r = engine.detect_undefined_symbols(src, "python")
|
|
86
|
+
self.assertEqual(r["suspects"], [])
|
|
87
|
+
|
|
88
|
+
def test_python_comprehension_and_global_not_flagged(self):
|
|
89
|
+
src = (
|
|
90
|
+
"counter = 0\n"
|
|
91
|
+
"def f(vals):\n"
|
|
92
|
+
" global counter\n"
|
|
93
|
+
" counter += 1\n"
|
|
94
|
+
" return [v * 2 for v in vals]\n"
|
|
95
|
+
)
|
|
96
|
+
r = engine.detect_undefined_symbols(src, "python")
|
|
97
|
+
self.assertEqual(r["suspects"], [])
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TestGenericLanguages(unittest.TestCase):
|
|
101
|
+
"""Config-driven generic lexical verify_code for the registered languages."""
|
|
102
|
+
|
|
103
|
+
def test_go_catches_hallucinated_call(self):
|
|
104
|
+
src = "package main\n\nfunc main() {\n ghost()\n}\n"
|
|
105
|
+
r = engine.detect_undefined_symbols(src, "go")
|
|
106
|
+
self.assertIn("ghost", r["suspects"])
|
|
107
|
+
|
|
108
|
+
def test_go_clean_defined_and_attribute_calls(self):
|
|
109
|
+
src = (
|
|
110
|
+
"package main\n"
|
|
111
|
+
'import "fmt"\n'
|
|
112
|
+
"func helper(x int) int { return x * 2 }\n"
|
|
113
|
+
"func main() {\n"
|
|
114
|
+
" fmt.Println(helper(21))\n"
|
|
115
|
+
"}\n"
|
|
116
|
+
)
|
|
117
|
+
r = engine.detect_undefined_symbols(src, "golang")
|
|
118
|
+
self.assertEqual(r["suspects"], [])
|
|
119
|
+
|
|
120
|
+
def test_go_short_declaration_not_flagged(self):
|
|
121
|
+
src = "func main() {\n total := 0\n total += 1\n}\n"
|
|
122
|
+
r = engine.detect_undefined_symbols(src, "go")
|
|
123
|
+
self.assertEqual(r["suspects"], [])
|
|
124
|
+
|
|
125
|
+
def test_rust_catches_hallucinated_call(self):
|
|
126
|
+
src = "fn main() {\n ghost();\n}\n"
|
|
127
|
+
r = engine.detect_undefined_symbols(src, "rust")
|
|
128
|
+
self.assertIn("ghost", r["suspects"])
|
|
129
|
+
|
|
130
|
+
def test_rust_clean_module_with_path_calls(self):
|
|
131
|
+
src = (
|
|
132
|
+
"use std::io;\n"
|
|
133
|
+
"fn helper(x: i32) -> i32 { x + 1 }\n"
|
|
134
|
+
"fn main() {\n"
|
|
135
|
+
" let y = helper(1);\n"
|
|
136
|
+
" println!(\"{}\", y);\n"
|
|
137
|
+
" io::stdout().flush();\n"
|
|
138
|
+
"}\n"
|
|
139
|
+
)
|
|
140
|
+
r = engine.detect_undefined_symbols(src, "rs")
|
|
141
|
+
self.assertEqual(r["suspects"], [])
|
|
142
|
+
|
|
143
|
+
def test_java_catches_hallucinated_call(self):
|
|
144
|
+
src = "class A {\n void run() { ghost(); }\n}\n"
|
|
145
|
+
r = engine.detect_undefined_symbols(src, "java")
|
|
146
|
+
self.assertIn("ghost", r["suspects"])
|
|
147
|
+
|
|
148
|
+
def test_java_clean_methods_fields_and_imports(self):
|
|
149
|
+
src = (
|
|
150
|
+
"import java.util.List;\n"
|
|
151
|
+
"class Calc {\n"
|
|
152
|
+
" private int count = 0;\n"
|
|
153
|
+
" int add(int a, int b) { return a + b; }\n"
|
|
154
|
+
" void run() { System.out.println(add(1, 2)); }\n"
|
|
155
|
+
"}\n"
|
|
156
|
+
)
|
|
157
|
+
r = engine.detect_undefined_symbols(src, "java")
|
|
158
|
+
self.assertEqual(r["suspects"], [])
|
|
159
|
+
|
|
160
|
+
def test_c_catches_hallucinated_call(self):
|
|
161
|
+
src = "int main(void) { ghost(); return 0; }\n"
|
|
162
|
+
r = engine.detect_undefined_symbols(src, "c")
|
|
163
|
+
self.assertIn("ghost", r["suspects"])
|
|
164
|
+
|
|
165
|
+
def test_c_clean_with_libc_and_defs(self):
|
|
166
|
+
src = (
|
|
167
|
+
"#include <stdio.h>\n"
|
|
168
|
+
"int add(int a, int b) { return a + b; }\n"
|
|
169
|
+
"int main(void) { printf(\"%d\", add(1, 2)); return 0; }\n"
|
|
170
|
+
)
|
|
171
|
+
r = engine.detect_undefined_symbols(src, "c")
|
|
172
|
+
self.assertEqual(r["suspects"], [])
|
|
173
|
+
|
|
174
|
+
def test_cpp_clean_class_vars_and_fields(self):
|
|
175
|
+
src = (
|
|
176
|
+
"#include <vector>\n"
|
|
177
|
+
"#include <string>\n"
|
|
178
|
+
"class Box {\n"
|
|
179
|
+
"public:\n"
|
|
180
|
+
" Box(int s) : size(s) {}\n"
|
|
181
|
+
" int get() const { return size; }\n"
|
|
182
|
+
"private:\n"
|
|
183
|
+
" int size;\n"
|
|
184
|
+
"};\n"
|
|
185
|
+
"int main() { Box b(3); std::string s = \"x\"; return b.get(); }\n"
|
|
186
|
+
)
|
|
187
|
+
r = engine.detect_undefined_symbols(src, "cpp")
|
|
188
|
+
self.assertEqual(r["suspects"], [])
|
|
189
|
+
|
|
190
|
+
def test_cpp_catches_hallucinated_call(self):
|
|
191
|
+
src = "int main() { ghost(); return 0; }\n"
|
|
192
|
+
r = engine.detect_undefined_symbols(src, "c++")
|
|
193
|
+
self.assertIn("ghost", r["suspects"])
|
|
194
|
+
|
|
195
|
+
def test_csharp_catches_hallucinated_call(self):
|
|
196
|
+
src = "using System;\nclass P { static void Main() { Ghost(); } }\n"
|
|
197
|
+
r = engine.detect_undefined_symbols(src, "csharp")
|
|
198
|
+
self.assertIn("Ghost", r["suspects"])
|
|
199
|
+
|
|
200
|
+
def test_csharp_clean_console_call(self):
|
|
201
|
+
src = 'using System;\nclass P { static void Main() { Console.WriteLine("hi"); } }\n'
|
|
202
|
+
r = engine.detect_undefined_symbols(src, "c#")
|
|
203
|
+
self.assertEqual(r["suspects"], [])
|
|
204
|
+
|
|
205
|
+
def test_php_catches_hallucinated_call(self):
|
|
206
|
+
src = "<?php\nfunction run() { ghost(); }\nrun();\n"
|
|
207
|
+
r = engine.detect_undefined_symbols(src, "php")
|
|
208
|
+
self.assertIn("ghost", r["suspects"])
|
|
209
|
+
|
|
210
|
+
def test_php_clean_vars_and_functions(self):
|
|
211
|
+
src = (
|
|
212
|
+
"<?php\n"
|
|
213
|
+
"function add($a, $b) { return $a + $b; }\n"
|
|
214
|
+
"$x = 1;\n"
|
|
215
|
+
"$y = add($x, 2);\n"
|
|
216
|
+
"echo $y;\n"
|
|
217
|
+
)
|
|
218
|
+
r = engine.detect_undefined_symbols(src, "php")
|
|
219
|
+
self.assertEqual(r["suspects"], [])
|
|
220
|
+
|
|
221
|
+
def test_ruby_catches_hallucinated_call(self):
|
|
222
|
+
src = "def run\n ghost\nend\nrun\n"
|
|
223
|
+
r = engine.detect_undefined_symbols(src, "ruby")
|
|
224
|
+
self.assertIn("ghost", r["suspects"])
|
|
225
|
+
|
|
226
|
+
def test_ruby_clean(self):
|
|
227
|
+
src = "def helper(x)\n x * 2\nend\nputs helper(21)\n"
|
|
228
|
+
r = engine.detect_undefined_symbols(src, "rb")
|
|
229
|
+
self.assertEqual(r["suspects"], [])
|
|
230
|
+
|
|
231
|
+
def test_kotlin_catches_hallucinated_call(self):
|
|
232
|
+
src = "fun main() {\n ghost()\n}\n"
|
|
233
|
+
r = engine.detect_undefined_symbols(src, "kotlin")
|
|
234
|
+
self.assertIn("ghost", r["suspects"])
|
|
235
|
+
|
|
236
|
+
def test_kotlin_clean_import_and_lambdas(self):
|
|
237
|
+
src = (
|
|
238
|
+
"import kotlin.math.sqrt\n"
|
|
239
|
+
"fun main() {\n"
|
|
240
|
+
" val x = 4\n"
|
|
241
|
+
" println(sqrt(x.toDouble()))\n"
|
|
242
|
+
"}\n"
|
|
243
|
+
)
|
|
244
|
+
r = engine.detect_undefined_symbols(src, "kt")
|
|
245
|
+
self.assertEqual(r["suspects"], [])
|
|
246
|
+
|
|
247
|
+
def test_swift_catches_hallucinated_call(self):
|
|
248
|
+
src = "func run() {\n ghost()\n}\n"
|
|
249
|
+
r = engine.detect_undefined_symbols(src, "swift")
|
|
250
|
+
self.assertIn("ghost", r["suspects"])
|
|
251
|
+
|
|
252
|
+
def test_swift_clean(self):
|
|
253
|
+
src = (
|
|
254
|
+
"import Foundation\n"
|
|
255
|
+
"func helper(_ x: Int) -> Int { x + 1 }\n"
|
|
256
|
+
"let y = helper(1)\n"
|
|
257
|
+
"print(y)\n"
|
|
258
|
+
)
|
|
259
|
+
r = engine.detect_undefined_symbols(src, "swift")
|
|
260
|
+
self.assertEqual(r["suspects"], [])
|
|
261
|
+
|
|
262
|
+
def test_unsupported_language_reports_note(self):
|
|
263
|
+
r = engine.detect_undefined_symbols("print('x')", "brainfuck")
|
|
264
|
+
self.assertEqual(r["suspects"], [])
|
|
265
|
+
self.assertIn("Unsupported", r["note"])
|
|
266
|
+
self.assertIn("go", r["note"])
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class TestStreamedSandbox(unittest.TestCase):
|
|
270
|
+
def test_large_output_kept_bounded_tail(self):
|
|
271
|
+
# 100k chars then a marker: memory must stay bounded AND the tail
|
|
272
|
+
# (last 8000 chars) must survive — the marker proves tail semantics.
|
|
273
|
+
src = "import sys\nprint('X' * 100000)\nprint('TAIL_MARKER')\n"
|
|
274
|
+
r = engine.sandbox_run([PY, "-c", src], 15)
|
|
275
|
+
self.assertTrue(r["stdout"].rstrip().endswith("TAIL_MARKER"), repr(r["stdout"][-80:]))
|
|
276
|
+
self.assertLessEqual(len(r["stdout"]), 8000)
|
|
277
|
+
|
|
278
|
+
def test_clean_output_unaffected(self):
|
|
279
|
+
r = engine.sandbox_run([PY, "-c", "print(6*7)"], 10)
|
|
280
|
+
self.assertIn("42", r["stdout"])
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class TestCheckContract(unittest.TestCase):
|
|
284
|
+
def test_requires_missing_fails(self):
|
|
285
|
+
r = engine.check_contract(
|
|
286
|
+
"def run():\n return 1\n", '{"requires": ["run", "ghost"]}'
|
|
287
|
+
)
|
|
288
|
+
self.assertFalse(r["contract_ok"])
|
|
289
|
+
self.assertEqual(r["missing"], ["ghost"])
|
|
290
|
+
|
|
291
|
+
def test_requires_satisfied_passes(self):
|
|
292
|
+
r = engine.check_contract(
|
|
293
|
+
"def run():\n return helper()\ndef helper():\n return 1\n",
|
|
294
|
+
'{"requires": ["run", "helper"]}',
|
|
295
|
+
)
|
|
296
|
+
self.assertTrue(r["contract_ok"])
|
|
297
|
+
self.assertEqual(r["missing"], [])
|
|
298
|
+
|
|
299
|
+
def test_prohibits_token_hit(self):
|
|
300
|
+
r = engine.check_contract(
|
|
301
|
+
"def run():\n return 1 # TODO\n", '{"prohibits": ["TODO"]}'
|
|
302
|
+
)
|
|
303
|
+
self.assertFalse(r["contract_ok"])
|
|
304
|
+
self.assertEqual(r["prohibited_hits"], ["TODO"])
|
|
305
|
+
|
|
306
|
+
def test_prohibits_clean(self):
|
|
307
|
+
r = engine.check_contract("def run():\n return 1\n", '{"prohibits": ["TODO"]}')
|
|
308
|
+
self.assertTrue(r["contract_ok"])
|
|
309
|
+
|
|
310
|
+
def test_invalid_contract_json(self):
|
|
311
|
+
r = engine.check_contract("x = 1\n", "not json")
|
|
312
|
+
self.assertFalse(r["contract_ok"])
|
|
313
|
+
self.assertIn("invalid", r["note"])
|
|
314
|
+
|
|
315
|
+
def test_go_contract_uses_registry_definitions(self):
|
|
316
|
+
r = engine.check_contract(
|
|
317
|
+
"package main\nfunc helper(x int) int { return x }\n",
|
|
318
|
+
'{"requires": ["helper"]}',
|
|
319
|
+
"go",
|
|
320
|
+
)
|
|
321
|
+
self.assertTrue(r["contract_ok"])
|
|
322
|
+
|
|
323
|
+
def test_defined_symbols_public_helper(self):
|
|
324
|
+
ds = engine.defined_symbols(
|
|
325
|
+
"package main\nfunc helper(x int) int { return x }\n", "go"
|
|
326
|
+
)
|
|
327
|
+
self.assertIn("helper", ds)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
class TestExportPromptPool(unittest.TestCase):
|
|
331
|
+
def test_parse_and_render_deterministic(self):
|
|
332
|
+
sys.path.insert(0, os.path.join(PLUGIN_ROOT, "scripts"))
|
|
333
|
+
import export_prompt_pool as xp
|
|
334
|
+
|
|
335
|
+
pool = os.path.join(
|
|
336
|
+
PLUGIN_ROOT, "skills", "verify-before-code", "references", "PROMPT-POOL.md"
|
|
337
|
+
)
|
|
338
|
+
entries = xp.parse_pool(pool)
|
|
339
|
+
self.assertGreaterEqual(len(entries), 20) # 23 entries today
|
|
340
|
+
for fmt in ("claude", "agents", "cursor"):
|
|
341
|
+
out = xp.render(fmt, entries)
|
|
342
|
+
self.assertIn("## A1", out)
|
|
343
|
+
self.assertIn("## J4", out)
|
|
344
|
+
self.assertEqual(out, xp.render(fmt, entries)) # deterministic
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class TestPerformanceBaseline(unittest.TestCase):
|
|
348
|
+
"""Regression gate: verify_code + scan_hallucination on a ~0.5 MB
|
|
349
|
+
synthetic corpus must stay under 10 s (real baseline ~0.4 s). Catches a
|
|
350
|
+
pathological regression without flaking on slow CI. Uses the same
|
|
351
|
+
generator as ``scripts/bench.py`` so the measurement is comparable."""
|
|
352
|
+
|
|
353
|
+
def test_half_megabyte_under_ten_seconds(self):
|
|
354
|
+
import time
|
|
355
|
+
|
|
356
|
+
scripts_dir = os.path.join(PLUGIN_ROOT, "scripts")
|
|
357
|
+
sys.path.insert(0, scripts_dir)
|
|
358
|
+
try:
|
|
359
|
+
import bench # noqa: PLC0415
|
|
360
|
+
src = bench.make_source(0.5)
|
|
361
|
+
t0 = time.perf_counter()
|
|
362
|
+
engine.detect_undefined_symbols(src)
|
|
363
|
+
engine.scan_hallucination_words(src)
|
|
364
|
+
elapsed = time.perf_counter() - t0
|
|
365
|
+
finally:
|
|
366
|
+
sys.path.remove(scripts_dir)
|
|
367
|
+
self.assertLess(
|
|
368
|
+
elapsed, 10.0,
|
|
369
|
+
f"hot path took {elapsed:.2f}s on ~0.5MB synthetic source",
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class TestCheckImports(unittest.TestCase):
|
|
374
|
+
"""Package-hallucination (slopsquatting) guard — USENIX Security 2025."""
|
|
375
|
+
|
|
376
|
+
def test_stdlib_and_common_known_pass(self):
|
|
377
|
+
src = "import os\nimport json\nimport requests\nimport numpy as np\nfrom pathlib import Path\n"
|
|
378
|
+
r = engine.check_imports(src)
|
|
379
|
+
self.assertTrue(r["imports_ok"])
|
|
380
|
+
self.assertEqual(r["suspicious"], [])
|
|
381
|
+
|
|
382
|
+
def test_unknown_package_flagged(self):
|
|
383
|
+
src = "import os\nimport slopsquat_utils\n"
|
|
384
|
+
r = engine.check_imports(src)
|
|
385
|
+
self.assertFalse(r["imports_ok"])
|
|
386
|
+
self.assertEqual([s["package"] for s in r["suspicious"]], ["slopsquat_utils"])
|
|
387
|
+
|
|
388
|
+
def test_known_packages_allowlist(self):
|
|
389
|
+
src = "import os\nimport mycompany_core\n"
|
|
390
|
+
r = engine.check_imports(src, known_packages=["mycompany_core"])
|
|
391
|
+
self.assertTrue(r["imports_ok"])
|
|
392
|
+
|
|
393
|
+
def test_relative_imports_skipped(self):
|
|
394
|
+
# from . import x / from ..pkg import y — package-local, never flagged
|
|
395
|
+
src = "from . import helpers\nfrom ..db import session\nimport os\n"
|
|
396
|
+
r = engine.check_imports(src)
|
|
397
|
+
self.assertTrue(r["imports_ok"])
|
|
398
|
+
|
|
399
|
+
def test_other_language_honest_empty(self):
|
|
400
|
+
r = engine.check_imports("package main\nimport \"fmt\"\n", "go")
|
|
401
|
+
self.assertTrue(r["imports_ok"])
|
|
402
|
+
self.assertIn("python", r["note"])
|
|
403
|
+
|
|
404
|
+
def test_new_research_tokens_flagged(self):
|
|
405
|
+
# tokens added from 2025 research (slopsquatting + overclaim/fabrication)
|
|
406
|
+
r = engine.scan_hallucination_words(
|
|
407
|
+
"This is bulletproof and cannot fail. The data is fictitious and non-existent."
|
|
408
|
+
)
|
|
409
|
+
groups = {h["group"] for h in r["hits"]}
|
|
410
|
+
self.assertIn("oversold", groups)
|
|
411
|
+
self.assertIn("fabricated", groups)
|
|
412
|
+
r2 = engine.scan_hallucination_words("这个包是凭空捏造的,子虚乌有,绝对可靠。")
|
|
413
|
+
self.assertFalse(r2["clean"])
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
class TestHallucinationScan(unittest.TestCase):
|
|
417
|
+
def test_stub_group(self):
|
|
418
|
+
r = engine.scan_hallucination_words("def run():\n return stub_result # TODO\n")
|
|
419
|
+
self.assertFalse(r["clean"])
|
|
420
|
+
groups = {h["group"] for h in r["hits"]}
|
|
421
|
+
self.assertIn("stub_code", groups)
|
|
422
|
+
|
|
423
|
+
def test_oversold_group(self):
|
|
424
|
+
r = engine.scan_hallucination_words("The feature is production ready, all tests pass.")
|
|
425
|
+
self.assertFalse(r["clean"])
|
|
426
|
+
groups = {h["group"] for h in r["hits"]}
|
|
427
|
+
self.assertIn("oversold", groups)
|
|
428
|
+
|
|
429
|
+
def test_fabricated_group(self):
|
|
430
|
+
r = engine.scan_hallucination_words("this is a simulated example")
|
|
431
|
+
self.assertFalse(r["clean"])
|
|
432
|
+
groups = {h["group"] for h in r["hits"]}
|
|
433
|
+
self.assertIn("fabricated", groups)
|
|
434
|
+
|
|
435
|
+
def test_clean(self):
|
|
436
|
+
r = engine.scan_hallucination_words("import os\nprint(os.getcwd())\n")
|
|
437
|
+
self.assertTrue(r["clean"])
|
|
438
|
+
|
|
439
|
+
def test_unittest_mock_not_flagged(self):
|
|
440
|
+
src = "from unittest.mock import Mock\nm = Mock()\nm.fake_method.return_value = 1\n"
|
|
441
|
+
r = engine.scan_hallucination_words(src)
|
|
442
|
+
self.assertTrue(r["clean"], r["hits"])
|
|
443
|
+
|
|
444
|
+
def test_dotted_path_not_flagged(self):
|
|
445
|
+
r = engine.scan_hallucination_words(
|
|
446
|
+
"import unittest.mock\nresult = unittest.mock.call(x)\n"
|
|
447
|
+
)
|
|
448
|
+
self.assertTrue(r["clean"], r["hits"])
|
|
449
|
+
|
|
450
|
+
def test_real_stub_still_flagged(self):
|
|
451
|
+
src = "def run():\n return stub_result # TODO: replace with real call\n"
|
|
452
|
+
r = engine.scan_hallucination_words(src)
|
|
453
|
+
self.assertFalse(r["clean"])
|
|
454
|
+
|
|
455
|
+
def test_allowlist_override(self):
|
|
456
|
+
src = "m = Mock()\nthis is a fake thing\n"
|
|
457
|
+
strict = engine.scan_hallucination_words(src, allowlist=[])
|
|
458
|
+
self.assertFalse(strict["clean"])
|
|
459
|
+
relaxed = engine.scan_hallucination_words(src)
|
|
460
|
+
self.assertTrue(any(h["word"] == "fake" for h in relaxed["hits"]))
|
|
461
|
+
self.assertTrue(all(h["word"] != "mock" for h in relaxed["hits"]))
|
|
462
|
+
|
|
463
|
+
def test_default_severities(self):
|
|
464
|
+
src = "# TODO: later\nall tests pass, guaranteed\ndefinitely simulated\n"
|
|
465
|
+
r = engine.scan_hallucination_words(src)
|
|
466
|
+
sev_by_group = {h["group"]: h["severity"] for h in r["hits"]}
|
|
467
|
+
self.assertEqual(sev_by_group["stub_code"], "warning")
|
|
468
|
+
self.assertEqual(sev_by_group["oversold"], "error")
|
|
469
|
+
self.assertEqual(sev_by_group["fabricated"], "error")
|
|
470
|
+
self.assertTrue(r["blocking"])
|
|
471
|
+
self.assertFalse(r["clean"])
|
|
472
|
+
|
|
473
|
+
def test_severity_override_downgrades_to_info(self):
|
|
474
|
+
src = "guaranteed to work\n"
|
|
475
|
+
r = engine.scan_hallucination_words(src, severities={"oversold": "info"})
|
|
476
|
+
self.assertEqual(r["hits"][0]["severity"], "info")
|
|
477
|
+
self.assertFalse(r["blocking"])
|
|
478
|
+
|
|
479
|
+
def test_warning_only_does_not_block(self):
|
|
480
|
+
src = "# TODO: finish this section\n"
|
|
481
|
+
r = engine.scan_hallucination_words(src)
|
|
482
|
+
self.assertFalse(r["clean"])
|
|
483
|
+
self.assertFalse(r["blocking"])
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
class TestConformance(unittest.TestCase):
|
|
487
|
+
def test_self_conformant(self):
|
|
488
|
+
r = engine.check_plugin_conformance(PLUGIN_ROOT)
|
|
489
|
+
self.assertTrue(r["ok"], r)
|
|
490
|
+
|
|
491
|
+
def test_frontmatter_with_dashes_in_body(self):
|
|
492
|
+
import tempfile
|
|
493
|
+
|
|
494
|
+
with tempfile.TemporaryDirectory() as d:
|
|
495
|
+
skill_dir = os.path.join(d, "skills", "demo-skill")
|
|
496
|
+
os.makedirs(skill_dir)
|
|
497
|
+
with open(os.path.join(skill_dir, "SKILL.md"), "w", encoding="utf-8") as fh:
|
|
498
|
+
fh.write("---\nname: demo-skill\ndescription: ok\n---\n\n---\nnot frontmatter\n")
|
|
499
|
+
# body containing a '---' line must not corrupt the parse
|
|
500
|
+
r = engine.check_plugin_conformance(d)
|
|
501
|
+
self.assertEqual([e for e in r["errors"] if "demo-skill" in e], [], r["errors"])
|
|
502
|
+
|
|
503
|
+
def test_rejects_bad_repository_type(self):
|
|
504
|
+
import tempfile
|
|
505
|
+
|
|
506
|
+
with tempfile.TemporaryDirectory() as d:
|
|
507
|
+
with open(os.path.join(d, "plugin.json"), "w", encoding="utf-8") as fh:
|
|
508
|
+
fh.write(
|
|
509
|
+
'{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",'
|
|
510
|
+
'"name":"badplugin","repository":{"type":"git","url":"x"}}'
|
|
511
|
+
)
|
|
512
|
+
r = engine.check_plugin_conformance(d)
|
|
513
|
+
self.assertFalse(r["ok"])
|
|
514
|
+
self.assertTrue(any("repository" in e for e in r["errors"]))
|
|
515
|
+
|
|
516
|
+
@staticmethod
|
|
517
|
+
def _write_plugin(tmp, plugin_json, mcp_json):
|
|
518
|
+
with open(os.path.join(tmp, "plugin.json"), "w", encoding="utf-8") as fh:
|
|
519
|
+
fh.write(plugin_json)
|
|
520
|
+
with open(os.path.join(tmp, "mcp.json"), "w", encoding="utf-8") as fh:
|
|
521
|
+
fh.write(mcp_json)
|
|
522
|
+
|
|
523
|
+
PJ = '{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"t"}'
|
|
524
|
+
MJ = (
|
|
525
|
+
'{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",'
|
|
526
|
+
'"mcpServers":{"s":%s}}'
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
def test_mcp_unknown_field_in_variant(self):
|
|
530
|
+
import tempfile
|
|
531
|
+
|
|
532
|
+
with tempfile.TemporaryDirectory() as d:
|
|
533
|
+
# 'url' belongs to the http variants, not stdio
|
|
534
|
+
self._write_plugin(
|
|
535
|
+
d, self.PJ, self.MJ % '{"type":"stdio","command":"srv","url":"https://x"}'
|
|
536
|
+
)
|
|
537
|
+
r = engine.check_plugin_conformance(d)
|
|
538
|
+
self.assertFalse(r["ok"])
|
|
539
|
+
self.assertTrue(any("unknown field 'url'" in e for e in r["errors"]))
|
|
540
|
+
|
|
541
|
+
def test_mcp_reserved_env_keys(self):
|
|
542
|
+
import tempfile
|
|
543
|
+
|
|
544
|
+
with tempfile.TemporaryDirectory() as d:
|
|
545
|
+
self._write_plugin(
|
|
546
|
+
d, self.PJ, self.MJ % '{"type":"stdio","command":"srv","env":{"PLUGIN_DATA":"/x"}}'
|
|
547
|
+
)
|
|
548
|
+
r = engine.check_plugin_conformance(d)
|
|
549
|
+
self.assertFalse(r["ok"])
|
|
550
|
+
self.assertTrue(any("reserved" in e for e in r["errors"]))
|
|
551
|
+
|
|
552
|
+
def test_mcp_http_non_loopback_rejected(self):
|
|
553
|
+
import tempfile
|
|
554
|
+
|
|
555
|
+
with tempfile.TemporaryDirectory() as d:
|
|
556
|
+
self._write_plugin(
|
|
557
|
+
d, self.PJ, self.MJ % '{"type":"streamable-http","url":"http://example.com/mcp"}'
|
|
558
|
+
)
|
|
559
|
+
r = engine.check_plugin_conformance(d)
|
|
560
|
+
self.assertFalse(r["ok"])
|
|
561
|
+
self.assertTrue(any("HTTPS" in e for e in r["errors"]))
|
|
562
|
+
|
|
563
|
+
def test_mcp_loopback_http_allowed_and_fragment_rejected(self):
|
|
564
|
+
import tempfile
|
|
565
|
+
|
|
566
|
+
with tempfile.TemporaryDirectory() as d:
|
|
567
|
+
self._write_plugin(
|
|
568
|
+
d, self.PJ, self.MJ % '{"type":"sse","url":"http://localhost:3000/sse#frag"}'
|
|
569
|
+
)
|
|
570
|
+
r = engine.check_plugin_conformance(d)
|
|
571
|
+
self.assertFalse(r["ok"])
|
|
572
|
+
self.assertFalse(any("HTTPS" in e for e in r["errors"]))
|
|
573
|
+
self.assertTrue(any("fragment" in e for e in r["errors"]))
|
|
574
|
+
|
|
575
|
+
def test_mcp_valid_remote_entry_passes(self):
|
|
576
|
+
import tempfile
|
|
577
|
+
|
|
578
|
+
with tempfile.TemporaryDirectory() as d:
|
|
579
|
+
self._write_plugin(
|
|
580
|
+
d,
|
|
581
|
+
self.PJ,
|
|
582
|
+
self.MJ % '{"type":"streamable-http","url":"https://api.example.com/mcp",'
|
|
583
|
+
'"headers":{"X-Tenant":"public"}}',
|
|
584
|
+
)
|
|
585
|
+
r = engine.check_plugin_conformance(d)
|
|
586
|
+
self.assertEqual([e for e in r["errors"] if "mcp.json" in e], [], r["errors"])
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
class TestSandboxRun(unittest.TestCase):
|
|
590
|
+
def test_runs_command(self):
|
|
591
|
+
r = engine.sandbox_run([PY, "-c", "print(6*7)"], 10)
|
|
592
|
+
self.assertEqual(r["exit_code"], 0)
|
|
593
|
+
self.assertIn("42", r["stdout"])
|
|
594
|
+
|
|
595
|
+
def test_timeout_safety(self):
|
|
596
|
+
r = engine.sandbox_run([PY, "-c", "import time; time.sleep(30)"], 1)
|
|
597
|
+
self.assertTrue(r["timed_out"])
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
class TestSchemaValidate(unittest.TestCase):
|
|
601
|
+
def test_valid(self):
|
|
602
|
+
schema = {
|
|
603
|
+
"type": "object",
|
|
604
|
+
"required": ["name"],
|
|
605
|
+
"properties": {"name": {"type": "string"}},
|
|
606
|
+
}
|
|
607
|
+
r = engine.schema_validate({"name": "agentseed"}, schema)
|
|
608
|
+
self.assertTrue(r["valid"])
|
|
609
|
+
|
|
610
|
+
def test_invalid(self):
|
|
611
|
+
schema = {
|
|
612
|
+
"type": "object",
|
|
613
|
+
"required": ["name"],
|
|
614
|
+
"properties": {"name": {"type": "string"}},
|
|
615
|
+
}
|
|
616
|
+
r = engine.schema_validate({"name": 123}, schema)
|
|
617
|
+
self.assertFalse(r["valid"])
|
|
618
|
+
self.assertTrue(any("type" in e for e in r["errors"]))
|
|
619
|
+
|
|
620
|
+
def test_required_without_properties(self):
|
|
621
|
+
schema = {"type": "object", "required": ["name"]}
|
|
622
|
+
r = engine.schema_validate({}, schema)
|
|
623
|
+
self.assertFalse(r["valid"])
|
|
624
|
+
self.assertTrue(any("name" in e for e in r["errors"]))
|
|
625
|
+
|
|
626
|
+
def test_oversized_pattern_rejected_both_paths(self):
|
|
627
|
+
from engine import schema as schema_mod
|
|
628
|
+
|
|
629
|
+
evil = "a" * 300
|
|
630
|
+
r = schema_mod.schema_validate("x", {"type": "string", "pattern": evil})
|
|
631
|
+
self.assertFalse(r["valid"])
|
|
632
|
+
self.assertIn("ReDoS", r["errors"][0])
|
|
633
|
+
# nested occurrence is caught too (jsonschema path compiles lazily)
|
|
634
|
+
r2 = schema_mod.schema_validate(
|
|
635
|
+
{"s": "x"}, {"type": "object", "properties": {"s": {"pattern": evil}}}
|
|
636
|
+
)
|
|
637
|
+
self.assertFalse(r2["valid"])
|
|
638
|
+
# boundary: 256 chars still validates normally
|
|
639
|
+
ok_pattern = "^" + "a" * 254 + "$"
|
|
640
|
+
r3 = schema_mod.schema_validate("a" * 254, {"type": "string", "pattern": ok_pattern})
|
|
641
|
+
self.assertTrue(r3["valid"], r3)
|
|
642
|
+
|
|
643
|
+
def test_const_null_validated(self):
|
|
644
|
+
r = engine.schema_validate(None, {"const": None})
|
|
645
|
+
self.assertTrue(r["valid"])
|
|
646
|
+
r = engine.schema_validate("x", {"const": None})
|
|
647
|
+
self.assertFalse(r["valid"])
|
|
648
|
+
|
|
649
|
+
def test_enum_bool_not_equal_int(self):
|
|
650
|
+
r = engine.schema_validate(False, {"enum": [0]})
|
|
651
|
+
self.assertFalse(r["valid"])
|
|
652
|
+
r = engine.schema_validate(True, {"enum": [1]})
|
|
653
|
+
self.assertFalse(r["valid"])
|
|
654
|
+
r = engine.schema_validate(0, {"enum": [0]})
|
|
655
|
+
self.assertTrue(r["valid"])
|
|
656
|
+
|
|
657
|
+
def test_type_array(self):
|
|
658
|
+
schema = {"type": ["string", "null"]}
|
|
659
|
+
self.assertTrue(engine.schema_validate(None, schema)["valid"])
|
|
660
|
+
self.assertTrue(engine.schema_validate("s", schema)["valid"])
|
|
661
|
+
r = engine.schema_validate(5, schema)
|
|
662
|
+
self.assertFalse(r["valid"])
|
|
663
|
+
self.assertTrue(any("type" in e for e in r["errors"]))
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
class TestConfig(unittest.TestCase):
|
|
667
|
+
def test_load_config_missing_returns_empty(self):
|
|
668
|
+
import tempfile
|
|
669
|
+
|
|
670
|
+
with tempfile.TemporaryDirectory() as d:
|
|
671
|
+
old = os.getcwd()
|
|
672
|
+
os.chdir(d)
|
|
673
|
+
try:
|
|
674
|
+
self.assertEqual(engine.load_config(), {})
|
|
675
|
+
finally:
|
|
676
|
+
os.chdir(old)
|
|
677
|
+
|
|
678
|
+
def test_load_config_plugin_data(self):
|
|
679
|
+
import tempfile
|
|
680
|
+
|
|
681
|
+
with tempfile.TemporaryDirectory() as d:
|
|
682
|
+
cfg = os.path.join(d, engine.CONFIG_FILENAME)
|
|
683
|
+
with open(cfg, "w", encoding="utf-8") as fh:
|
|
684
|
+
fh.write('{"severities": {"stub_code": "info"}, "timeout": 15}')
|
|
685
|
+
env = {k: v for k, v in os.environ.items() if k not in ("AGENTSEED_CONFIG",)}
|
|
686
|
+
env["PLUGIN_DATA"] = d
|
|
687
|
+
import subprocess
|
|
688
|
+
|
|
689
|
+
out = subprocess.run(
|
|
690
|
+
[
|
|
691
|
+
sys.executable,
|
|
692
|
+
"-c",
|
|
693
|
+
"import json, sys; sys.path.insert(0, r'%s');"
|
|
694
|
+
"import guard_engine as e; print(json.dumps(e.load_config()))"
|
|
695
|
+
% os.path.dirname(os.path.abspath(__file__)),
|
|
696
|
+
],
|
|
697
|
+
capture_output=True,
|
|
698
|
+
text=True,
|
|
699
|
+
encoding="utf-8",
|
|
700
|
+
errors="replace",
|
|
701
|
+
env=env,
|
|
702
|
+
check=True,
|
|
703
|
+
)
|
|
704
|
+
self.assertEqual(json.loads(out.stdout)["timeout"], 15)
|
|
705
|
+
|
|
706
|
+
def test_load_config_invalid_json_ignored(self):
|
|
707
|
+
import tempfile
|
|
708
|
+
|
|
709
|
+
with tempfile.TemporaryDirectory() as d:
|
|
710
|
+
bad = os.path.join(d, "bad.json")
|
|
711
|
+
with open(bad, "w", encoding="utf-8") as fh:
|
|
712
|
+
fh.write("{not json")
|
|
713
|
+
self.assertEqual(engine.load_config(bad), {})
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
class TestSchemaDraft07Tuples(unittest.TestCase):
|
|
717
|
+
"""draft-07 tuple 'items' must validate, not crash (regression)."""
|
|
718
|
+
|
|
719
|
+
def test_tuple_items_via_jsonschema_fallback(self):
|
|
720
|
+
from engine import schema as schema_mod
|
|
721
|
+
|
|
722
|
+
r = schema_mod.schema_validate(
|
|
723
|
+
[1, "a"], {"items": [{"type": "integer"}, {"type": "string"}]}
|
|
724
|
+
)
|
|
725
|
+
self.assertTrue(r["valid"], r)
|
|
726
|
+
self.assertNotIn("crashed", "".join(r["errors"]))
|
|
727
|
+
|
|
728
|
+
def test_bad_tuple_types_detected(self):
|
|
729
|
+
from engine import schema as schema_mod
|
|
730
|
+
|
|
731
|
+
r = schema_mod.schema_validate(
|
|
732
|
+
["x", 1], {"items": [{"type": "integer"}, {"type": "string"}]}
|
|
733
|
+
)
|
|
734
|
+
self.assertFalse(r["valid"])
|
|
735
|
+
self.assertEqual(len(r["errors"]), 2)
|
|
736
|
+
|
|
737
|
+
def test_additional_items_false(self):
|
|
738
|
+
from engine import schema as schema_mod
|
|
739
|
+
|
|
740
|
+
r = schema_mod.schema_validate(
|
|
741
|
+
[1, "a", True],
|
|
742
|
+
{"items": [{"type": "integer"}, {"type": "string"}], "additionalItems": False},
|
|
743
|
+
)
|
|
744
|
+
self.assertFalse(r["valid"])
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
class TestMatchGuardsForOldPythons(unittest.TestCase):
|
|
748
|
+
"""ast.Match* guards must keep 3.9 alive (no AttributeError)."""
|
|
749
|
+
|
|
750
|
+
def test_detection_runs_with_match_nodes_absent(self):
|
|
751
|
+
from engine import symbols as sym
|
|
752
|
+
|
|
753
|
+
saved = (sym._MATCH_AS, sym._MATCH_STAR, sym._MATCH_MAPPING)
|
|
754
|
+
sym._MATCH_AS = sym._MATCH_STAR = sym._MATCH_MAPPING = None
|
|
755
|
+
try:
|
|
756
|
+
r = sym.detect_undefined_symbols("match x:\n case [a]:\n f(a)\n")
|
|
757
|
+
finally:
|
|
758
|
+
sym._MATCH_AS, sym._MATCH_STAR, sym._MATCH_MAPPING = saved
|
|
759
|
+
if sys.version_info >= (3, 10):
|
|
760
|
+
# Nodes exist but were disabled: parse succeeds, guard degrades
|
|
761
|
+
# gracefully and the real suspect is still reported.
|
|
762
|
+
self.assertIn("f", r["suspects"])
|
|
763
|
+
else:
|
|
764
|
+
# Real pre-3.9 parser: match syntax is a SyntaxError; the engine
|
|
765
|
+
# must degrade to an empty (non-crashing) result.
|
|
766
|
+
self.assertEqual(r["suspects"], [])
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
class TestPyflakesMerge(unittest.TestCase):
|
|
770
|
+
"""Del-context undefined names are caught by the stdlib walk itself
|
|
771
|
+
(ast.Delete targets); when pyflakes is installed its findings merge in
|
|
772
|
+
as additional scope-aware coverage."""
|
|
773
|
+
|
|
774
|
+
def test_del_undefined_name_caught_regardless_of_pyflakes(self):
|
|
775
|
+
from engine import symbols as sym
|
|
776
|
+
|
|
777
|
+
r = sym.detect_undefined_symbols("def f():\n del ghost_var\n")
|
|
778
|
+
self.assertIn("ghost_var", r["suspects"], r)
|
|
779
|
+
if sym._HAS_PYFLAKES:
|
|
780
|
+
self.assertIn("pyflakes", r["note"])
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
class TestPluginVersionFallback(unittest.TestCase):
|
|
784
|
+
"""version.py must degrade to '0.0.0' when its root has no/broken
|
|
785
|
+
plugin.json. Executed from a COPY so the __file__-derived root is tmpdir."""
|
|
786
|
+
|
|
787
|
+
@staticmethod
|
|
788
|
+
def _run_copy(source_path: str, fake_file: str) -> str:
|
|
789
|
+
import subprocess
|
|
790
|
+
|
|
791
|
+
script = (
|
|
792
|
+
"ns = {'__file__': %r, '__name__': 'v_under_test'}\n"
|
|
793
|
+
"code = open(%r, encoding='utf-8').read()\n"
|
|
794
|
+
"exec(compile(code, %r, 'exec'), ns)\n"
|
|
795
|
+
"print(ns['plugin_version']())\n" % (fake_file, source_path, source_path)
|
|
796
|
+
)
|
|
797
|
+
out = subprocess.run(
|
|
798
|
+
[sys.executable, "-c", script],
|
|
799
|
+
capture_output=True,
|
|
800
|
+
text=True,
|
|
801
|
+
encoding="utf-8",
|
|
802
|
+
errors="replace",
|
|
803
|
+
timeout=60,
|
|
804
|
+
)
|
|
805
|
+
return out.stdout.strip()
|
|
806
|
+
|
|
807
|
+
def test_missing_and_broken_manifests_fall_back(self):
|
|
808
|
+
import tempfile
|
|
809
|
+
|
|
810
|
+
from engine import version as vmod
|
|
811
|
+
|
|
812
|
+
self.assertTrue(vmod.plugin_version().count(".") >= 1) # sanity: real path works
|
|
813
|
+
source_path = os.path.abspath(vmod.__file__)
|
|
814
|
+
|
|
815
|
+
with tempfile.TemporaryDirectory() as d:
|
|
816
|
+
fake_file = os.path.join(d, "engine", "version.py")
|
|
817
|
+
os.makedirs(os.path.dirname(fake_file))
|
|
818
|
+
# broken plugin.json -> fallback
|
|
819
|
+
with open(os.path.join(d, "plugin.json"), "w", encoding="utf-8") as fh:
|
|
820
|
+
fh.write("{not json")
|
|
821
|
+
self.assertEqual(self._run_copy(source_path, fake_file), "0.0.0")
|
|
822
|
+
# missing plugin.json -> fallback
|
|
823
|
+
os.remove(os.path.join(d, "plugin.json"))
|
|
824
|
+
self.assertEqual(self._run_copy(source_path, fake_file), "0.0.0")
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
if __name__ == "__main__":
|
|
828
|
+
unittest.main(verbosity=2)
|