@yeyuan98/opencode-bioresearcher-plugin 1.5.0 → 1.5.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/README.md +50 -50
- package/dist/index.js +6 -6
- package/dist/skills/bioresearcher-tests/README.md +90 -0
- package/dist/skills/bioresearcher-tests/SKILL.md +255 -0
- package/dist/skills/bioresearcher-tests/pyproject.toml +6 -0
- package/dist/skills/bioresearcher-tests/resources/json_samples/in_markdown.md.gz +0 -0
- package/dist/skills/bioresearcher-tests/resources/json_samples/nested_object.json.gz +0 -0
- package/dist/skills/bioresearcher-tests/resources/json_samples/schema_draft7.json.gz +0 -0
- package/dist/skills/bioresearcher-tests/resources/json_samples/simple_array.json.gz +0 -0
- package/dist/skills/bioresearcher-tests/resources/json_samples/simple_object.json.gz +0 -0
- package/dist/skills/bioresearcher-tests/resources/obo_sample.obo.gz +0 -0
- package/dist/skills/bioresearcher-tests/resources/pubmed_sample.xml.gz +0 -0
- package/dist/skills/bioresearcher-tests/resources/table_sample.xlsx.gz +0 -0
- package/dist/skills/bioresearcher-tests/test_cases/json_tests.md +137 -0
- package/dist/skills/bioresearcher-tests/test_cases/misc_tests.md +141 -0
- package/dist/skills/bioresearcher-tests/test_cases/parser_tests.md +80 -0
- package/dist/skills/bioresearcher-tests/test_cases/skill_tests.md +59 -0
- package/dist/skills/bioresearcher-tests/test_cases/table_tests.md +194 -0
- package/dist/skills/bioresearcher-tests/test_runner.py +607 -0
- package/dist/skills/env-jsonc-setup/SKILL.md +206 -206
- package/dist/skills/long-table-summary/SKILL.md +224 -153
- package/dist/skills/long-table-summary/combine_outputs.py +55 -9
- package/dist/skills/long-table-summary/generate_prompts.py +9 -0
- package/dist/skills/pubmed-weekly/pubmed_weekly.py +130 -29
- package/dist/{db-tools → tools/db}/backends/mysql/translator.js +23 -23
- package/dist/{db-tools → tools/db}/tools.js +34 -34
- package/dist/{misc-tools → tools/misc}/json-validate.js +4 -5
- package/dist/{skill-tools → tools/skill}/registry.js +1 -1
- package/package.json +1 -1
- package/dist/db-tools/executor.d.ts +0 -13
- package/dist/db-tools/executor.js +0 -54
- package/dist/db-tools/pool.d.ts +0 -8
- package/dist/db-tools/pool.js +0 -49
- package/dist/db-tools/tools/index.d.ts +0 -27
- package/dist/db-tools/tools/index.js +0 -191
- package/dist/db-tools/types.d.ts +0 -94
- package/dist/db-tools/types.js +0 -40
- package/dist/misc-tools/json-tools.d.ts +0 -33
- package/dist/misc-tools/json-tools.js +0 -187
- package/dist/skill/frontmatter.d.ts +0 -2
- package/dist/skill/frontmatter.js +0 -65
- package/dist/skill/index.d.ts +0 -3
- package/dist/skill/index.js +0 -2
- package/dist/skill/registry.d.ts +0 -11
- package/dist/skill/registry.js +0 -64
- package/dist/skill/tool.d.ts +0 -9
- package/dist/skill/tool.js +0 -115
- package/dist/skill/types.d.ts +0 -22
- package/dist/skill/types.js +0 -7
- /package/dist/{db-tools → tools/db}/backends/index.d.ts +0 -0
- /package/dist/{db-tools → tools/db}/backends/index.js +0 -0
- /package/dist/{db-tools → tools/db}/backends/mongodb/backend.d.ts +0 -0
- /package/dist/{db-tools → tools/db}/backends/mongodb/backend.js +0 -0
- /package/dist/{db-tools → tools/db}/backends/mongodb/connection.d.ts +0 -0
- /package/dist/{db-tools → tools/db}/backends/mongodb/connection.js +0 -0
- /package/dist/{db-tools → tools/db}/backends/mongodb/index.d.ts +0 -0
- /package/dist/{db-tools → tools/db}/backends/mongodb/index.js +0 -0
- /package/dist/{db-tools → tools/db}/backends/mongodb/translator.d.ts +0 -0
- /package/dist/{db-tools → tools/db}/backends/mongodb/translator.js +0 -0
- /package/dist/{db-tools → tools/db}/backends/mysql/backend.d.ts +0 -0
- /package/dist/{db-tools → tools/db}/backends/mysql/backend.js +0 -0
- /package/dist/{db-tools → tools/db}/backends/mysql/connection.d.ts +0 -0
- /package/dist/{db-tools → tools/db}/backends/mysql/connection.js +0 -0
- /package/dist/{db-tools → tools/db}/backends/mysql/index.d.ts +0 -0
- /package/dist/{db-tools → tools/db}/backends/mysql/index.js +0 -0
- /package/dist/{db-tools → tools/db}/backends/mysql/translator.d.ts +0 -0
- /package/dist/{db-tools → tools/db}/core/base.d.ts +0 -0
- /package/dist/{db-tools → tools/db}/core/base.js +0 -0
- /package/dist/{db-tools → tools/db}/core/config-loader.d.ts +0 -0
- /package/dist/{db-tools → tools/db}/core/config-loader.js +0 -0
- /package/dist/{db-tools → tools/db}/core/index.d.ts +0 -0
- /package/dist/{db-tools → tools/db}/core/index.js +0 -0
- /package/dist/{db-tools → tools/db}/core/jsonc-parser.d.ts +0 -0
- /package/dist/{db-tools → tools/db}/core/jsonc-parser.js +0 -0
- /package/dist/{db-tools → tools/db}/core/validator.d.ts +0 -0
- /package/dist/{db-tools → tools/db}/core/validator.js +0 -0
- /package/dist/{db-tools → tools/db}/index.d.ts +0 -0
- /package/dist/{db-tools → tools/db}/index.js +0 -0
- /package/dist/{db-tools → tools/db}/interface/backend.d.ts +0 -0
- /package/dist/{db-tools → tools/db}/interface/backend.js +0 -0
- /package/dist/{db-tools → tools/db}/interface/connection.d.ts +0 -0
- /package/dist/{db-tools → tools/db}/interface/connection.js +0 -0
- /package/dist/{db-tools → tools/db}/interface/index.d.ts +0 -0
- /package/dist/{db-tools → tools/db}/interface/index.js +0 -0
- /package/dist/{db-tools → tools/db}/interface/query.d.ts +0 -0
- /package/dist/{db-tools → tools/db}/interface/query.js +0 -0
- /package/dist/{db-tools → tools/db}/interface/schema.d.ts +0 -0
- /package/dist/{db-tools → tools/db}/interface/schema.js +0 -0
- /package/dist/{db-tools → tools/db}/tools.d.ts +0 -0
- /package/dist/{db-tools → tools/db}/utils.d.ts +0 -0
- /package/dist/{db-tools → tools/db}/utils.js +0 -0
- /package/dist/{misc-tools → tools/misc}/calculator.d.ts +0 -0
- /package/dist/{misc-tools → tools/misc}/calculator.js +0 -0
- /package/dist/{misc-tools → tools/misc}/index.d.ts +0 -0
- /package/dist/{misc-tools → tools/misc}/index.js +0 -0
- /package/dist/{misc-tools → tools/misc}/json-extract.d.ts +0 -0
- /package/dist/{misc-tools → tools/misc}/json-extract.js +0 -0
- /package/dist/{misc-tools → tools/misc}/json-infer.d.ts +0 -0
- /package/dist/{misc-tools → tools/misc}/json-infer.js +0 -0
- /package/dist/{misc-tools → tools/misc}/json-validate.d.ts +0 -0
- /package/dist/{misc-tools → tools/misc}/timer.d.ts +0 -0
- /package/dist/{misc-tools → tools/misc}/timer.js +0 -0
- /package/dist/{parser-tools → tools/parser}/obo/index.d.ts +0 -0
- /package/dist/{parser-tools → tools/parser}/obo/index.js +0 -0
- /package/dist/{parser-tools → tools/parser}/obo/obo.d.ts +0 -0
- /package/dist/{parser-tools → tools/parser}/obo/obo.js +0 -0
- /package/dist/{parser-tools → tools/parser}/obo/types.d.ts +0 -0
- /package/dist/{parser-tools → tools/parser}/obo/types.js +0 -0
- /package/dist/{parser-tools → tools/parser}/obo/utils.d.ts +0 -0
- /package/dist/{parser-tools → tools/parser}/obo/utils.js +0 -0
- /package/dist/{parser-tools → tools/parser}/pubmed/index.d.ts +0 -0
- /package/dist/{parser-tools → tools/parser}/pubmed/index.js +0 -0
- /package/dist/{parser-tools → tools/parser}/pubmed/pubmed.d.ts +0 -0
- /package/dist/{parser-tools → tools/parser}/pubmed/pubmed.js +0 -0
- /package/dist/{parser-tools → tools/parser}/pubmed/types.d.ts +0 -0
- /package/dist/{parser-tools → tools/parser}/pubmed/types.js +0 -0
- /package/dist/{parser-tools → tools/parser}/pubmed/utils.d.ts +0 -0
- /package/dist/{parser-tools → tools/parser}/pubmed/utils.js +0 -0
- /package/dist/{skill-tools → tools/skill}/frontmatter.d.ts +0 -0
- /package/dist/{skill-tools → tools/skill}/frontmatter.js +0 -0
- /package/dist/{skill-tools → tools/skill}/index.d.ts +0 -0
- /package/dist/{skill-tools → tools/skill}/index.js +0 -0
- /package/dist/{skill-tools → tools/skill}/registry.d.ts +0 -0
- /package/dist/{skill-tools → tools/skill}/tool.d.ts +0 -0
- /package/dist/{skill-tools → tools/skill}/tool.js +0 -0
- /package/dist/{skill-tools → tools/skill}/types.d.ts +0 -0
- /package/dist/{skill-tools → tools/skill}/types.js +0 -0
- /package/dist/{table-tools → tools/table}/index.d.ts +0 -0
- /package/dist/{table-tools → tools/table}/index.js +0 -0
- /package/dist/{table-tools → tools/table}/tools.d.ts +0 -0
- /package/dist/{table-tools → tools/table}/tools.js +0 -0
- /package/dist/{table-tools → tools/table}/utils.d.ts +0 -0
- /package/dist/{table-tools → tools/table}/utils.js +0 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BioResearcher Plugin Test Runner
|
|
3
|
+
|
|
4
|
+
Generates test structure and provides utilities for running tests.
|
|
5
|
+
Actual tests are executed by the agent using the skill.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
uv run python test_runner.py --help
|
|
9
|
+
uv run python test_runner.py --list
|
|
10
|
+
uv run python test_runner.py --category parser
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class TestCase:
|
|
26
|
+
"""Represents a single test case."""
|
|
27
|
+
|
|
28
|
+
name: str
|
|
29
|
+
tool: str
|
|
30
|
+
input: dict
|
|
31
|
+
validators: list[str]
|
|
32
|
+
expected: str
|
|
33
|
+
category: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class TestResult:
|
|
38
|
+
"""Result of a single test execution."""
|
|
39
|
+
|
|
40
|
+
test_name: str
|
|
41
|
+
tool: str
|
|
42
|
+
category: str
|
|
43
|
+
passed: bool
|
|
44
|
+
actual: Any
|
|
45
|
+
expected: str
|
|
46
|
+
validators: list[str]
|
|
47
|
+
validator_results: list[dict] = field(default_factory=list)
|
|
48
|
+
error: str | None = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def parse_test_cases(md_content: str, category: str) -> list[TestCase]:
|
|
52
|
+
"""
|
|
53
|
+
Parse test cases from markdown content.
|
|
54
|
+
|
|
55
|
+
Format:
|
|
56
|
+
## Test: <name>
|
|
57
|
+
- Tool: <tool>
|
|
58
|
+
- Input:
|
|
59
|
+
```json
|
|
60
|
+
{...}
|
|
61
|
+
```
|
|
62
|
+
- Validators:
|
|
63
|
+
- <validator>
|
|
64
|
+
- Expected: <description>
|
|
65
|
+
"""
|
|
66
|
+
tests = []
|
|
67
|
+
|
|
68
|
+
test_blocks = re.split(r"\n## Test: ", md_content)
|
|
69
|
+
|
|
70
|
+
for block in test_blocks[1:]:
|
|
71
|
+
if not block.strip():
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
name_match = re.match(r"(.+?)\n", block)
|
|
76
|
+
if not name_match:
|
|
77
|
+
continue
|
|
78
|
+
name = name_match.group(1).strip()
|
|
79
|
+
|
|
80
|
+
tool_match = re.search(r"- Tool:\s*(.+?)\n", block)
|
|
81
|
+
if not tool_match:
|
|
82
|
+
continue
|
|
83
|
+
tool = tool_match.group(1).strip()
|
|
84
|
+
|
|
85
|
+
input_match = re.search(r"- Input:\s*```json\n(.+?)```", block, re.DOTALL)
|
|
86
|
+
if not input_match:
|
|
87
|
+
continue
|
|
88
|
+
input_json = json.loads(input_match.group(1).strip())
|
|
89
|
+
|
|
90
|
+
validators_match = re.search(r"- Validators:\n((?: - .+\n)+)", block)
|
|
91
|
+
if validators_match:
|
|
92
|
+
validators = [
|
|
93
|
+
v.strip()[2:]
|
|
94
|
+
for v in validators_match.group(1).strip().split("\n")
|
|
95
|
+
if v.strip().startswith("- ")
|
|
96
|
+
]
|
|
97
|
+
else:
|
|
98
|
+
validators = []
|
|
99
|
+
|
|
100
|
+
expected_match = re.search(r"- Expected:\s*(.+?)(?:\n|$)", block)
|
|
101
|
+
expected = expected_match.group(1).strip() if expected_match else ""
|
|
102
|
+
|
|
103
|
+
tests.append(
|
|
104
|
+
TestCase(
|
|
105
|
+
name=name,
|
|
106
|
+
tool=tool,
|
|
107
|
+
input=input_json,
|
|
108
|
+
validators=validators,
|
|
109
|
+
expected=expected,
|
|
110
|
+
category=category,
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
except (json.JSONDecodeError, AttributeError) as e:
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
return tests
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def load_all_tests(test_cases_dir: str) -> dict[str, list[TestCase]]:
|
|
120
|
+
"""Load all test cases from test_cases directory."""
|
|
121
|
+
categories = {
|
|
122
|
+
"parser_tests.md": "parser",
|
|
123
|
+
"table_tests.md": "table",
|
|
124
|
+
"json_tests.md": "json",
|
|
125
|
+
"misc_tests.md": "misc",
|
|
126
|
+
"skill_tests.md": "skill",
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
all_tests = {}
|
|
130
|
+
|
|
131
|
+
for filename, category in categories.items():
|
|
132
|
+
filepath = os.path.join(test_cases_dir, filename)
|
|
133
|
+
if os.path.exists(filepath):
|
|
134
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
135
|
+
content = f.read()
|
|
136
|
+
tests = parse_test_cases(content, category)
|
|
137
|
+
all_tests[category] = tests
|
|
138
|
+
|
|
139
|
+
return all_tests
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def validate_result(result: Any, validators: list[str]) -> tuple[bool, list[dict]]:
|
|
143
|
+
"""
|
|
144
|
+
Run validators against result.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
(all_passed, validator_results)
|
|
148
|
+
"""
|
|
149
|
+
results = []
|
|
150
|
+
all_passed = True
|
|
151
|
+
|
|
152
|
+
result_str = json.dumps(result) if isinstance(result, (dict, list)) else str(result)
|
|
153
|
+
result_obj = result if isinstance(result, dict) else {}
|
|
154
|
+
|
|
155
|
+
for validator in validators:
|
|
156
|
+
passed = False
|
|
157
|
+
detail = ""
|
|
158
|
+
|
|
159
|
+
if validator == "success_is_true":
|
|
160
|
+
passed = result_obj.get("success") is True
|
|
161
|
+
detail = f"success={result_obj.get('success')}"
|
|
162
|
+
|
|
163
|
+
elif validator == "success_is_false":
|
|
164
|
+
passed = result_obj.get("success") is False
|
|
165
|
+
detail = f"success={result_obj.get('success')}"
|
|
166
|
+
|
|
167
|
+
elif validator == "file_exists":
|
|
168
|
+
path = result_obj.get("filePath") or result_obj.get("file_path")
|
|
169
|
+
passed = path and os.path.exists(path)
|
|
170
|
+
detail = f"path={path}, exists={passed}"
|
|
171
|
+
|
|
172
|
+
elif validator == "json_valid":
|
|
173
|
+
try:
|
|
174
|
+
if isinstance(result, str):
|
|
175
|
+
json.loads(result)
|
|
176
|
+
passed = True
|
|
177
|
+
except:
|
|
178
|
+
pass
|
|
179
|
+
detail = "parsed as JSON"
|
|
180
|
+
|
|
181
|
+
elif validator.startswith("stats.total"):
|
|
182
|
+
match = re.match(r"stats\.total\s*===\s*(\d+)", validator)
|
|
183
|
+
if match:
|
|
184
|
+
expected = int(match.group(1))
|
|
185
|
+
actual = result_obj.get("stats", {}).get("total", 0)
|
|
186
|
+
passed = actual == expected
|
|
187
|
+
detail = f"total={actual}, expected={expected}"
|
|
188
|
+
|
|
189
|
+
elif validator.startswith("stats.successful"):
|
|
190
|
+
match = re.match(r"stats\.successful\s*===\s*(\d+)", validator)
|
|
191
|
+
if match:
|
|
192
|
+
expected = int(match.group(1))
|
|
193
|
+
actual = result_obj.get("stats", {}).get("successful", 0)
|
|
194
|
+
passed = actual == expected
|
|
195
|
+
detail = f"successful={actual}, expected={expected}"
|
|
196
|
+
|
|
197
|
+
elif validator.startswith("stats.terms"):
|
|
198
|
+
match = re.match(r"stats\.terms\s*>=\s*(\d+)", validator)
|
|
199
|
+
if match:
|
|
200
|
+
min_val = int(match.group(1))
|
|
201
|
+
actual = result_obj.get("stats", {}).get("terms", 0)
|
|
202
|
+
passed = actual >= min_val
|
|
203
|
+
detail = f"terms={actual}, min={min_val}"
|
|
204
|
+
|
|
205
|
+
elif validator.startswith("array_length"):
|
|
206
|
+
match = re.match(r"array_length\s*>=?\s*(\d+)", validator)
|
|
207
|
+
if match:
|
|
208
|
+
min_len = int(match.group(1))
|
|
209
|
+
arr = (
|
|
210
|
+
result_obj.get("sheets")
|
|
211
|
+
or result_obj.get("results")
|
|
212
|
+
or result_obj.get("matched_rows")
|
|
213
|
+
or result_obj.get("preview")
|
|
214
|
+
or result_obj.get("headers")
|
|
215
|
+
or []
|
|
216
|
+
)
|
|
217
|
+
passed = len(arr) >= min_len
|
|
218
|
+
detail = f"length={len(arr)}, min={min_len}"
|
|
219
|
+
|
|
220
|
+
elif validator.startswith("rows"):
|
|
221
|
+
match = re.match(r"rows\s*===\s*(\d+)", validator)
|
|
222
|
+
if match:
|
|
223
|
+
expected = int(match.group(1))
|
|
224
|
+
actual = result_obj.get("rows", 0)
|
|
225
|
+
passed = actual == expected
|
|
226
|
+
detail = f"rows={actual}, expected={expected}"
|
|
227
|
+
|
|
228
|
+
elif validator.startswith("columns"):
|
|
229
|
+
match = re.match(r"columns\s*===\s*(\d+)", validator)
|
|
230
|
+
if match:
|
|
231
|
+
expected = int(match.group(1))
|
|
232
|
+
actual = result_obj.get("columns", 0)
|
|
233
|
+
passed = actual == expected
|
|
234
|
+
detail = f"columns={actual}, expected={expected}"
|
|
235
|
+
|
|
236
|
+
elif validator.startswith("contains_string"):
|
|
237
|
+
match = re.search(r'"(.+?)"', validator)
|
|
238
|
+
if match:
|
|
239
|
+
substr = match.group(1)
|
|
240
|
+
passed = substr.lower() in result_str.lower()
|
|
241
|
+
detail = f"contains '{substr}': {passed}"
|
|
242
|
+
|
|
243
|
+
elif validator.startswith("error.code"):
|
|
244
|
+
match = re.match(r'error\.code\s*===\s*"(.+?)"', validator)
|
|
245
|
+
if match:
|
|
246
|
+
expected = match.group(1)
|
|
247
|
+
actual = result_obj.get("error", {}).get("code")
|
|
248
|
+
passed = actual == expected
|
|
249
|
+
detail = f"error.code={actual}, expected={expected}"
|
|
250
|
+
|
|
251
|
+
elif validator.startswith("result"):
|
|
252
|
+
match = re.match(r"result\s*===\s*(\d+\.?\d*)", validator)
|
|
253
|
+
if match:
|
|
254
|
+
expected = float(match.group(1))
|
|
255
|
+
actual = result_obj.get("result")
|
|
256
|
+
passed = actual is not None and abs(float(actual) - expected) < 0.001
|
|
257
|
+
detail = f"result={actual}, expected={expected}"
|
|
258
|
+
|
|
259
|
+
elif validator == "has result":
|
|
260
|
+
passed = "result" in result_obj
|
|
261
|
+
detail = f"has result: {passed}"
|
|
262
|
+
|
|
263
|
+
elif validator == "has data object":
|
|
264
|
+
passed = "data" in result_obj and isinstance(result_obj["data"], dict)
|
|
265
|
+
detail = f"has data: {passed}"
|
|
266
|
+
|
|
267
|
+
elif validator.startswith("data.name"):
|
|
268
|
+
match = re.match(r'data\.(\w+)\s*===\s*"(.+)"', validator)
|
|
269
|
+
if match:
|
|
270
|
+
key, expected = match.group(1), match.group(2)
|
|
271
|
+
actual = result_obj.get("data", {}).get(key)
|
|
272
|
+
passed = actual == expected
|
|
273
|
+
detail = f"data.{key}={actual}, expected={expected}"
|
|
274
|
+
|
|
275
|
+
elif validator.startswith("data.value"):
|
|
276
|
+
match = re.match(r"data\.(\w+)\s*===\s*(\d+)", validator)
|
|
277
|
+
if match:
|
|
278
|
+
key, expected = match.group(1), int(match.group(2))
|
|
279
|
+
actual = result_obj.get("data", {}).get(key)
|
|
280
|
+
passed = actual == expected
|
|
281
|
+
detail = f"data.{key}={actual}, expected={expected}"
|
|
282
|
+
|
|
283
|
+
elif validator.startswith("data.embedded"):
|
|
284
|
+
match = re.match(r'data\.(\w+)\s*===\s*"(.+)"', validator)
|
|
285
|
+
if match:
|
|
286
|
+
key, expected = match.group(1), match.group(2)
|
|
287
|
+
actual = str(result_obj.get("data", {}).get(key, ""))
|
|
288
|
+
passed = actual == expected
|
|
289
|
+
detail = f"data.{key}={actual}, expected={expected}"
|
|
290
|
+
|
|
291
|
+
elif validator == "isArray(data)":
|
|
292
|
+
passed = isinstance(result_obj.get("data"), list)
|
|
293
|
+
detail = f"isArray: {passed}"
|
|
294
|
+
|
|
295
|
+
elif validator == "has data.nested":
|
|
296
|
+
data = result_obj.get("data", {})
|
|
297
|
+
passed = "nested" in data if isinstance(data, dict) else False
|
|
298
|
+
detail = f"has nested: {passed}"
|
|
299
|
+
|
|
300
|
+
elif validator == "has count":
|
|
301
|
+
passed = "count" in result_obj or "count" in result_obj.get("metadata", {})
|
|
302
|
+
detail = f"has count: {passed}"
|
|
303
|
+
|
|
304
|
+
elif validator.startswith("inferredType"):
|
|
305
|
+
match = re.match(r'inferredType\s*===\s*"(.+?)"', validator)
|
|
306
|
+
if match:
|
|
307
|
+
expected = match.group(1)
|
|
308
|
+
actual = result_obj.get("metadata", {}).get("inferredType")
|
|
309
|
+
passed = actual == expected
|
|
310
|
+
detail = f"inferredType={actual}, expected={expected}"
|
|
311
|
+
|
|
312
|
+
elif validator.startswith("strictMode"):
|
|
313
|
+
match = re.match(r'strictMode\s*===\s*"?(true|false)"?', validator)
|
|
314
|
+
if match:
|
|
315
|
+
expected = match.group(1) == "true"
|
|
316
|
+
actual = result_obj.get("metadata", {}).get("strictMode")
|
|
317
|
+
passed = actual == expected
|
|
318
|
+
detail = f"strictMode={actual}, expected={expected}"
|
|
319
|
+
|
|
320
|
+
elif validator.startswith("method"):
|
|
321
|
+
match = re.match(r'method\s*===\s*"(.+?)"', validator)
|
|
322
|
+
if match:
|
|
323
|
+
expected = match.group(1)
|
|
324
|
+
actual = result_obj.get("metadata", {}).get("method")
|
|
325
|
+
passed = actual == expected
|
|
326
|
+
detail = f"method={actual}, expected={expected}"
|
|
327
|
+
|
|
328
|
+
elif validator == "has errors":
|
|
329
|
+
passed = "errors" in result_obj and len(result_obj.get("errors", [])) > 0
|
|
330
|
+
detail = f"has errors: {passed}"
|
|
331
|
+
|
|
332
|
+
elif validator == "valid === true OR has data":
|
|
333
|
+
passed = result_obj.get("valid") is True or "data" in result_obj
|
|
334
|
+
detail = f"valid={result_obj.get('valid')}, has_data={'data' in result_obj}"
|
|
335
|
+
|
|
336
|
+
elif validator.startswith("headers includes"):
|
|
337
|
+
match = re.search(r'"(.+?)"', validator)
|
|
338
|
+
if match:
|
|
339
|
+
expected = match.group(1)
|
|
340
|
+
headers = result_obj.get("headers", [])
|
|
341
|
+
passed = expected in headers or expected.lower() in [
|
|
342
|
+
h.lower() for h in headers
|
|
343
|
+
]
|
|
344
|
+
detail = f"headers={headers}"
|
|
345
|
+
|
|
346
|
+
elif validator == "has preview array":
|
|
347
|
+
passed = "preview" in result_obj and isinstance(result_obj["preview"], list)
|
|
348
|
+
detail = f"has preview: {passed}"
|
|
349
|
+
|
|
350
|
+
elif validator == "has total_rows":
|
|
351
|
+
passed = "total_rows" in result_obj
|
|
352
|
+
detail = f"has total_rows: {passed}"
|
|
353
|
+
|
|
354
|
+
elif validator == "has headers array":
|
|
355
|
+
passed = "headers" in result_obj and isinstance(result_obj["headers"], list)
|
|
356
|
+
detail = f"has headers: {passed}"
|
|
357
|
+
|
|
358
|
+
elif validator == "has sheets array":
|
|
359
|
+
passed = "sheets" in result_obj and isinstance(result_obj["sheets"], list)
|
|
360
|
+
detail = f"has sheets: {passed}"
|
|
361
|
+
|
|
362
|
+
elif validator == "has value":
|
|
363
|
+
passed = "value" in result_obj
|
|
364
|
+
detail = f"has value: {passed}"
|
|
365
|
+
|
|
366
|
+
elif validator == "has type":
|
|
367
|
+
passed = "type" in result_obj
|
|
368
|
+
detail = f"has type: {passed}"
|
|
369
|
+
|
|
370
|
+
elif validator == "has data array":
|
|
371
|
+
passed = "data" in result_obj and isinstance(result_obj["data"], list)
|
|
372
|
+
detail = f"has data array: {passed}"
|
|
373
|
+
|
|
374
|
+
elif validator == "has matched_rows":
|
|
375
|
+
passed = "matched_rows" in result_obj
|
|
376
|
+
detail = f"has matched_rows: {passed}"
|
|
377
|
+
|
|
378
|
+
elif validator == "has total_matches":
|
|
379
|
+
passed = "total_matches" in result_obj
|
|
380
|
+
detail = f"has total_matches: {passed}"
|
|
381
|
+
|
|
382
|
+
elif validator == "has results array":
|
|
383
|
+
passed = "results" in result_obj and isinstance(result_obj["results"], list)
|
|
384
|
+
detail = f"has results: {passed}"
|
|
385
|
+
|
|
386
|
+
elif validator == "has total_found":
|
|
387
|
+
passed = "total_found" in result_obj
|
|
388
|
+
detail = f"has total_found: {passed}"
|
|
389
|
+
|
|
390
|
+
elif validator == "has summaries object":
|
|
391
|
+
passed = "summaries" in result_obj and isinstance(
|
|
392
|
+
result_obj["summaries"], dict
|
|
393
|
+
)
|
|
394
|
+
detail = f"has summaries: {passed}"
|
|
395
|
+
|
|
396
|
+
elif validator == "has groups object":
|
|
397
|
+
passed = "groups" in result_obj and isinstance(result_obj["groups"], dict)
|
|
398
|
+
detail = f"has groups: {passed}"
|
|
399
|
+
|
|
400
|
+
elif validator == "has pivot object":
|
|
401
|
+
passed = "pivot" in result_obj and isinstance(result_obj["pivot"], dict)
|
|
402
|
+
detail = f"has pivot: {passed}"
|
|
403
|
+
|
|
404
|
+
elif validator == "has columns array":
|
|
405
|
+
passed = "columns" in result_obj and isinstance(result_obj["columns"], list)
|
|
406
|
+
detail = f"has columns: {passed}"
|
|
407
|
+
|
|
408
|
+
elif validator == "rows_appended === 1":
|
|
409
|
+
passed = result_obj.get("rows_appended") == 1
|
|
410
|
+
detail = f"rows_appended={result_obj.get('rows_appended')}"
|
|
411
|
+
|
|
412
|
+
elif validator == "rows_created === 2":
|
|
413
|
+
passed = result_obj.get("rows_created") == 2
|
|
414
|
+
detail = f"rows_created={result_obj.get('rows_created')}"
|
|
415
|
+
|
|
416
|
+
elif validator == "directory exists":
|
|
417
|
+
path = result_obj.get("filePath") or result_obj.get("file_path")
|
|
418
|
+
passed = path and os.path.isdir(path)
|
|
419
|
+
detail = f"path={path}, is_dir={passed}"
|
|
420
|
+
|
|
421
|
+
elif validator.startswith("file ends with"):
|
|
422
|
+
match = re.search(r"\.(.+)", validator)
|
|
423
|
+
if match:
|
|
424
|
+
ext = "." + match.group(1)
|
|
425
|
+
path = result_obj.get("filePath") or result_obj.get("file_path") or ""
|
|
426
|
+
passed = path.endswith(ext)
|
|
427
|
+
detail = f"path={path}, ends_with={ext}"
|
|
428
|
+
|
|
429
|
+
else:
|
|
430
|
+
passed = False
|
|
431
|
+
detail = f"validator not implemented: {validator}"
|
|
432
|
+
|
|
433
|
+
if not passed:
|
|
434
|
+
all_passed = False
|
|
435
|
+
|
|
436
|
+
results.append({"validator": validator, "passed": passed, "detail": detail})
|
|
437
|
+
|
|
438
|
+
return all_passed, results
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def generate_report(results: list[TestResult], output_path: str) -> str:
|
|
442
|
+
"""Generate Markdown test report."""
|
|
443
|
+
total = len(results)
|
|
444
|
+
passed = sum(1 for r in results if r.passed)
|
|
445
|
+
failed = total - passed
|
|
446
|
+
|
|
447
|
+
categories = {}
|
|
448
|
+
for r in results:
|
|
449
|
+
if r.category not in categories:
|
|
450
|
+
categories[r.category] = {"passed": 0, "failed": 0}
|
|
451
|
+
if r.passed:
|
|
452
|
+
categories[r.category]["passed"] += 1
|
|
453
|
+
else:
|
|
454
|
+
categories[r.category]["failed"] += 1
|
|
455
|
+
|
|
456
|
+
lines = [
|
|
457
|
+
"# BioResearcher Plugin Test Report",
|
|
458
|
+
"",
|
|
459
|
+
f"**Run Date:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
|
460
|
+
f"**Total Tests:** {total}",
|
|
461
|
+
f"**Passed:** {passed}",
|
|
462
|
+
f"**Failed:** {failed}",
|
|
463
|
+
"",
|
|
464
|
+
"## Summary by Category",
|
|
465
|
+
"",
|
|
466
|
+
"| Category | Passed | Failed |",
|
|
467
|
+
"|----------|--------|--------|",
|
|
468
|
+
]
|
|
469
|
+
|
|
470
|
+
for cat, stats in sorted(categories.items()):
|
|
471
|
+
total_cat = stats["passed"] + stats["failed"]
|
|
472
|
+
lines.append(
|
|
473
|
+
f"| {cat.title()} | {stats['passed']}/{total_cat} | {stats['failed']} |"
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
lines.extend(
|
|
477
|
+
[
|
|
478
|
+
"",
|
|
479
|
+
"## Skipped Tests",
|
|
480
|
+
"",
|
|
481
|
+
"### Database Tools (Not Tested)",
|
|
482
|
+
"- `dbQuery` - Requires live database connection",
|
|
483
|
+
"- `dbListTables` - Requires live database connection",
|
|
484
|
+
"- `dbDescribeTable` - Requires live database connection",
|
|
485
|
+
"",
|
|
486
|
+
"### Skills with User Interaction (Metadata Only)",
|
|
487
|
+
"- `python-setup-uv` - Uses Question tool",
|
|
488
|
+
"- `long-table-summary` - Uses Question tool",
|
|
489
|
+
"- `env-jsonc-setup` - Uses Question tool",
|
|
490
|
+
"- `pubmed-weekly` - Uses Question tool",
|
|
491
|
+
"",
|
|
492
|
+
]
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
if failed > 0:
|
|
496
|
+
lines.extend(
|
|
497
|
+
[
|
|
498
|
+
"## Failed Tests",
|
|
499
|
+
"",
|
|
500
|
+
]
|
|
501
|
+
)
|
|
502
|
+
for r in results:
|
|
503
|
+
if not r.passed:
|
|
504
|
+
lines.append(f"### {r.category.title()}: {r.test_name}")
|
|
505
|
+
lines.append(f"- **Tool:** `{r.tool}`")
|
|
506
|
+
lines.append(f"- **Expected:** {r.expected}")
|
|
507
|
+
if r.error:
|
|
508
|
+
lines.append(f"- **Error:** {r.error}")
|
|
509
|
+
for vr in r.validator_results:
|
|
510
|
+
if not vr["passed"]:
|
|
511
|
+
lines.append(
|
|
512
|
+
f" - Validator `{vr['validator']}`: {vr['detail']}"
|
|
513
|
+
)
|
|
514
|
+
lines.append("")
|
|
515
|
+
|
|
516
|
+
lines.extend(
|
|
517
|
+
[
|
|
518
|
+
"## Detailed Results",
|
|
519
|
+
"",
|
|
520
|
+
"| Test | Tool | Status |",
|
|
521
|
+
"|------|------|--------|",
|
|
522
|
+
]
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
for r in results:
|
|
526
|
+
status = "PASS" if r.passed else "FAIL"
|
|
527
|
+
lines.append(f"| {r.test_name} | `{r.tool}` | {status} |")
|
|
528
|
+
|
|
529
|
+
report = "\n".join(lines)
|
|
530
|
+
|
|
531
|
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
|
532
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
533
|
+
f.write(report)
|
|
534
|
+
|
|
535
|
+
return report
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def list_tests(tests_by_category: dict[str, list[TestCase]]) -> None:
|
|
539
|
+
"""Print all test cases."""
|
|
540
|
+
print("Available Test Cases")
|
|
541
|
+
print("=" * 60)
|
|
542
|
+
|
|
543
|
+
for category, tests in sorted(tests_by_category.items()):
|
|
544
|
+
print(f"\n[{category.upper()}] ({len(tests)} tests)")
|
|
545
|
+
for test in tests:
|
|
546
|
+
print(f" - {test.name} ({test.tool})")
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def main():
|
|
550
|
+
parser = argparse.ArgumentParser(
|
|
551
|
+
description="BioResearcher Plugin Test Runner",
|
|
552
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
553
|
+
epilog="""
|
|
554
|
+
Examples:
|
|
555
|
+
uv run python test_runner.py --list
|
|
556
|
+
uv run python test_runner.py --category parser
|
|
557
|
+
uv run python test_runner.py --verbose
|
|
558
|
+
""",
|
|
559
|
+
)
|
|
560
|
+
parser.add_argument("--list", "-l", action="store_true", help="List all test cases")
|
|
561
|
+
parser.add_argument(
|
|
562
|
+
"--category", "-c", help="Show tests for specific category only"
|
|
563
|
+
)
|
|
564
|
+
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
|
|
565
|
+
parser.add_argument(
|
|
566
|
+
"--output",
|
|
567
|
+
"-o",
|
|
568
|
+
default=".bioresearcher-tests/test_report.md",
|
|
569
|
+
help="Output report path",
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
args = parser.parse_args()
|
|
573
|
+
|
|
574
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
575
|
+
test_cases_dir = os.path.join(script_dir, "test_cases")
|
|
576
|
+
|
|
577
|
+
tests_by_category = load_all_tests(test_cases_dir)
|
|
578
|
+
|
|
579
|
+
if args.list:
|
|
580
|
+
if args.category:
|
|
581
|
+
if args.category in tests_by_category:
|
|
582
|
+
tests = {args.category: tests_by_category[args.category]}
|
|
583
|
+
list_tests(tests)
|
|
584
|
+
else:
|
|
585
|
+
print(f"Unknown category: {args.category}")
|
|
586
|
+
print(f"Available: {', '.join(tests_by_category.keys())}")
|
|
587
|
+
else:
|
|
588
|
+
list_tests(tests_by_category)
|
|
589
|
+
return
|
|
590
|
+
|
|
591
|
+
print("BioResearcher Plugin Test Runner")
|
|
592
|
+
print("=" * 40)
|
|
593
|
+
print()
|
|
594
|
+
print("This script provides test structure utilities.")
|
|
595
|
+
print()
|
|
596
|
+
print("To run actual tests:")
|
|
597
|
+
print(" 1. Load skill: skill bioresearcher-tests")
|
|
598
|
+
print(" 2. Extract skill_path from output")
|
|
599
|
+
print(" 3. Follow workflow steps in SKILL.md")
|
|
600
|
+
print()
|
|
601
|
+
print(f"Total test cases: {sum(len(t) for t in tests_by_category.values())}")
|
|
602
|
+
for cat, tests in sorted(tests_by_category.items()):
|
|
603
|
+
print(f" - {cat}: {len(tests)} tests")
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
if __name__ == "__main__":
|
|
607
|
+
main()
|