chore: 更新公共镜像至 1.5.43

This commit is contained in:
chen qiang
2026-08-27 10:38:35 +08:00
parent e5fc7a33cc
commit dfc6a96e10
8 changed files with 306 additions and 3 deletions
+10
View File
@@ -1,5 +1,15 @@
# oracle-jump-query Skill Release Standard # oracle-jump-query Skill Release Standard
## Git 提交说明
- 提交信息和发布记录使用简洁、准确的中文。
- 按可独立验证的小功能节点分别提交,不混入无关修改。
- Bug 修复通常只说明修复的功能点和结果,不展开代码缺陷细节;排障或防复发所必需的信息除外。
Release 1.5.43: 统一 Git 提交与发布记录规范,明确使用简洁中文、按小功能节点提交,并精简 Bug 修复说明。提交 `176bd4594779a8489ad25ab828380b2b7ab3354b` 已于 2026-08-27 10:18 +08:00 推送到私有 Gitea,并同步安装目录;`capabilities --json` 验证版本为 `1.5.43`。
Release 1.5.42: added `log_info`, `log_tail`, `log_search`, `log_enable`, and `log_disable`; uses authenticated dedicated transit endpoints and preserves structured log errors. Commit `e90cbc9ce3525970b2f07885d52c4f0172bf9d2f` was pushed to private Gitea at 2026-08-27 09:51 +08:00, pulled into the installed Skill, and verified by `capabilities --json` as 1.5.42. Log content is not stored in Skill config, output archives, or release history.
A published `VERSION` is frozen. Increment it for every later change, update `CHANGELOG.md`, commit and push to Gitea. Skill updates use Git only: pull the commit into the Codex Skill directory with `git pull --ff-only origin main`, then run `capabilities --json`. Do not publish or update this Skill through OSS. A published `VERSION` is frozen. Increment it for every later change, update `CHANGELOG.md`, commit and push to Gitea. Skill updates use Git only: pull the commit into the Codex Skill directory with `git pull --ff-only origin main`, then run `capabilities --json`. Do not publish or update this Skill through OSS.
`inspection_report` and `inspection_latest --refresh` must run `sys_function_check` before Agent collection. Missing SYS/current-schema functions stop generation and return the complete SQL bundle. Historical report reads do not run this preflight. `inspection_report` and `inspection_latest --refresh` must run `sys_function_check` before Agent collection. Missing SYS/current-schema functions stop generation and return the complete SQL bundle. Historical report reads do not run this preflight.
+11
View File
@@ -1,3 +1,14 @@
## 1.5.43 (2026-08-27)
- 统一 Git 提交与发布记录规范:使用简洁、准确的中文,通常按小功能节点分别提交。
- Bug 修复通常只描述修复的功能点和结果,不展开代码缺陷细节。
## 1.5.42 (2026-08-27)
- Added authenticated `log_info`, `log_tail`, `log_search`, `log_enable`, and `log_disable` commands and capability definitions.
- Added dedicated transit endpoint calls, paths containing spaces, keyword/regex context search, recent/full scan selection, and structured error handling.
- Log excerpts remain read-only and are analyzed by the Skill/model; the Skill does not execute server commands or write customer files.
## 1.5.37 (2026-08-14) ## 1.5.37 (2026-08-14)
### Public mirror documentation ### Public mirror documentation
+4
View File
@@ -101,6 +101,10 @@ python oracle_skill.py perm 940 12983
| `inspection_latest [clientCode] [--refresh] [--json] [--html [path]] [--markdown [path]]` | 查看最近归档巡检报告并默认导出 HTML;加 `--refresh` 时重新生成 | `inspection_latest HENLO --refresh --markdown` | | `inspection_latest [clientCode] [--refresh] [--json] [--html [path]] [--markdown [path]]` | 查看最近归档巡检报告并默认导出 HTML;加 `--refresh` 时重新生成 | `inspection_latest HENLO --refresh --markdown` |
| `inspection_get <id> [--json] [--html [path]] [--markdown [path]]` | 按归档 ID 查看历史巡检报告并默认导出 HTML | `inspection_get 123 --markdown` | | `inspection_get <id> [--json] [--html [path]] [--markdown [path]]` | 按归档 ID 查看历史巡检报告并默认导出 HTML | `inspection_get 123 --markdown` |
| `agent_update [clientCode] [timeout]` | 触发客户 Agent 自动更新,升级包由 Agent 从 OSS 下载 | `agent_update HENLO 300` | | `agent_update [clientCode] [timeout]` | 触发客户 Agent 自动更新,升级包由 Agent 从 OSS 下载 | `agent_update HENLO 300` |
| `log_info <path> [--client code]` | 验证服务器绝对日志路径和文件元数据 | `log_info "D:\logs\app.log" --client AHMW --json` |
| `log_tail <path> [--client code]` | 有界读取日志末尾内容 | `log_tail "D:\logs\app.log" --client AHMW --lines 200` |
| `log_search <path> <pattern...>` | 在 Agent 本机按关键词或正则流式搜索日志 | `log_search "D:\logs\app.log" ERROR --client AHMW --json` |
| `log_enable [clientCode]` / `log_disable [clientCode]` | 持久化开启或关闭目标客户日志分析 | `log_enable AHMW` |
| `discover [schema] [filter]` | 发现核心业务表 | `discover bosnds3 M_%` | | `discover [schema] [filter]` | 发现核心业务表 | `discover bosnds3 M_%` |
| `nl2sql <schema> <question>` | 自然语言转 SQL | `nl2sql bosnds3 查询花都二店5月销售额` | | `nl2sql <schema> <question>` | 自然语言转 SQL | `nl2sql bosnds3 查询花都二店5月销售额` |
| `servers` | 列出在线 Agent | `servers` | | `servers` | 列出在线 Agent | `servers` |
+15 -1
View File
@@ -5,7 +5,7 @@ description: Oracle 跳板查询技能。通过中转服务查询远程 Oracle
# Oracle 跳板查询 # Oracle 跳板查询
> **版本:v1.5.37** · [更新日志](./CHANGELOG.md) > **版本:v1.5.43** · [更新日志](./CHANGELOG.md)
## 使用原则 ## 使用原则
@@ -78,6 +78,10 @@ python scripts/oracle_skill.py login <secretKey> [clientCode]
| `awr_status [clientCode] [--json]` | 查看 AWR 授权确认、权限、快照和最近生成状态,不触发生成 | | `awr_status [clientCode] [--json]` | 查看 AWR 授权确认、权限、快照和最近生成状态,不触发生成 |
| `awr_list [clientCode] [--json]` | 列出 Agent 已生成并保留的 AWR 报告 | | `awr_list [clientCode] [--json]` | 列出 Agent 已生成并保留的 AWR 报告 |
| `awr_download <clientCode> <yyyyMMdd> [--output path]` | 经中转机下载 Agent 原始 AWR HTML,不在本地渲染 | | `awr_download <clientCode> <yyyyMMdd> [--output path]` | 经中转机下载 Agent 原始 AWR HTML,不在本地渲染 |
| `log_info <path> [--client code] [--json]` | 验证服务器绝对日志路径并返回文件元数据 |
| `log_tail <path> [--client code] [--lines N] [--json]` | 有界读取服务器日志末尾内容 |
| `log_search <path> <pattern...> [--client code] [--regex] [--scope recent|full] [--json]` | 在 Agent 本机流式筛选日志并返回有限上下文 |
| `log_enable [clientCode]` / `log_disable [clientCode]` | 为有权限的客户持久化开启或关闭日志分析 |
| `agent_update [clientCode] [timeout]` | 触发客户 Agent 自动更新;由中转机下发更新动作,Agent 按 OSS latest.json 返回的包地址下载升级 | | `agent_update [clientCode] [timeout]` | 触发客户 Agent 自动更新;由中转机下发更新动作,Agent 按 OSS latest.json 返回的包地址下载升级 |
## BOS 分析优先路径 ## BOS 分析优先路径
@@ -162,8 +166,18 @@ python scripts/oracle_skill.py awr_list WEIRUI
python scripts/oracle_skill.py awr_download WEIRUI 20260720 python scripts/oracle_skill.py awr_download WEIRUI 20260720
``` ```
日志分析默认关闭。用户提供服务器上的单个绝对文本日志路径后,先执行 `log_info`,再按问题选择 `log_tail` 或 `log_search`;默认只扫描末尾 100 MB。日志正文原样返回给当前 Skill 会话但不写入中转审计库:
```powershell
python scripts/oracle_skill.py log_enable AHMW
python scripts/oracle_skill.py log_info "D:\logs\app.log" --client AHMW --json
python scripts/oracle_skill.py log_search "D:\logs\app.log" ERROR ORA- --client AHMW --before 3 --after 3 --json
```
## 内容维护 ## 内容维护
Git 提交和发布记录使用简洁、准确的中文描述,并按可独立验证的小功能节点分别提交。Bug 修复通常只说明修复的功能点和用户可见结果,不展开代码层缺陷细节;只有排障或防止复发确有需要时才补充技术原因。每次提交避免混入无关修改。
本 skill 的 Git 源地址分为私有主库和公开镜像: 本 skill 的 Git 源地址分为私有主库和公开镜像:
```text ```text
+1 -1
View File
@@ -1 +1 @@
1.5.41 1.5.43
+8
View File
@@ -73,6 +73,10 @@ python scripts/oracle_skill.py <command> [args]
| `awr_status [clientCode]` | 查看 AWR 授权和可用状态,不生成报告 | `python oracle_skill.py awr_status WEIRUI` | | `awr_status [clientCode]` | 查看 AWR 授权和可用状态,不生成报告 | `python oracle_skill.py awr_status WEIRUI` |
| `awr_list [clientCode]` | 列出 Agent 已生成的 AWR 报告 | `python oracle_skill.py awr_list WEIRUI` | | `awr_list [clientCode]` | 列出 Agent 已生成的 AWR 报告 | `python oracle_skill.py awr_list WEIRUI` |
| `awr_download <clientCode> <yyyyMMdd>` | 下载 Agent 原始 AWR HTML | `python oracle_skill.py awr_download WEIRUI 20260720` | | `awr_download <clientCode> <yyyyMMdd>` | 下载 Agent 原始 AWR HTML | `python oracle_skill.py awr_download WEIRUI 20260720` |
| `log_info <path> [--client code]` | 验证单个绝对日志路径和文件元数据 | `python oracle_skill.py log_info "D:\logs\app.log" --client AHMW --json` |
| `log_tail <path> [--client code]` | 有界读取日志末尾内容 | `python oracle_skill.py log_tail "D:\logs\app.log" --client AHMW --lines 200` |
| `log_search <path> <pattern...>` | 关键词/RE2 正则流式搜索并返回有限上下文 | `python oracle_skill.py log_search "D:\logs\app.log" ERROR --client AHMW --json` |
| `log_enable [clientCode]` / `log_disable [clientCode]` | 开启或关闭目标客户日志分析 | `python oracle_skill.py log_enable AHMW` |
| `servers` | 列出在线 Agent | `python oracle_skill.py servers` | | `servers` | 列出在线 Agent | `python oracle_skill.py servers` |
| `health` | 检查中转服务状态 | `python oracle_skill.py health` | | `health` | 检查中转服务状态 | `python oracle_skill.py health` |
| `tablespace / tablespaces` | 查询表空间使用情况 | `python oracle_skill.py tablespace` | | `tablespace / tablespaces` | 查询表空间使用情况 | `python oracle_skill.py tablespace` |
@@ -139,6 +143,10 @@ curl -X POST https://ts.henlo.net/api/query \
| `execute_query` | 执行 SELECT 查询(只读)—— 表空间查询也复用此 action | | `execute_query` | 执行 SELECT 查询(只读)—— 表空间查询也复用此 action |
| `schema_discovery` | 发现核心业务域 | | `schema_discovery` | 发现核心业务域 |
| `generate_nl2sql_schema` | 生成 NL2SQL Schema 字典 | | `generate_nl2sql_schema` | 生成 NL2SQL Schema 字典 |
| `log_info` | 本机日志文件元数据 |
| `log_tail` | 本机日志有界尾部读取 |
| `log_search` | 本机日志有界流式筛选 |
| `log_config_set` | 只修改日志分析启用开关 |
## 故障排查 ## 故障排查
+164 -1
View File
@@ -978,6 +978,83 @@ def cmd_awr_download(client_code: str, date: str, output_path: str = "", as_json
print(f"Size: {result['size']} bytes") print(f"Size: {result['size']} bytes")
def _log_client_code(client_code: str = "") -> str:
cfg = get_config() or {}
return (client_code or cfg.get("client_code") or cfg.get("server_id") or "").strip()
def _log_api(endpoint: str, payload: Dict[str, Any], timeout: int = 120) -> Dict[str, Any]:
cfg = get_config() or {}
if not ensure_logged_in(cfg):
return {"success": False, "error": "login required", "error_code": "login_required"}
client_code = _log_client_code(str(payload.get("client_code") or ""))
if not client_code:
return {"success": False, "error": "clientCode is required", "error_code": "invalid_request"}
body = dict(payload)
body["client_code"] = client_code
body["timeout"] = max(1, min(int(timeout or 120), 300))
try:
resp = requests.post(
f"{str(cfg.get('transit_url') or TRANSIT_URL).rstrip('/')}/api/log/{endpoint}",
json=body,
headers=make_headers(),
timeout=body["timeout"] + 5,
)
try:
result = resp.json()
except Exception:
result = {"success": False, "error": resp.text or f"HTTP {resp.status_code}"}
if not resp.ok:
result.setdefault("success", False)
result.setdefault("status_code", resp.status_code)
return result
except requests.exceptions.RequestException as e:
return {"success": False, "error": str(e), "error_code": "request_failed"}
def log_info(path: str, client_code: str = "", timeout: int = 120) -> Dict[str, Any]:
return _log_api("info", {"client_code": client_code, "path": path}, timeout)
def log_tail(path: str, client_code: str = "", lines: int = 200,
max_bytes: int = 256 * 1024, timeout: int = 120) -> Dict[str, Any]:
return _log_api("tail", {
"client_code": client_code, "path": path, "lines": lines, "max_bytes": max_bytes,
}, timeout)
def log_search(path: str, patterns: List[str], client_code: str = "", regex: bool = False,
case_sensitive: bool = False, before_lines: int = 3, after_lines: int = 3,
max_matches: int = 100, scan_scope: str = "recent",
recent_bytes: int = 100 * 1024 * 1024, timeout: int = 120) -> Dict[str, Any]:
return _log_api("search", {
"client_code": client_code, "path": path, "patterns": patterns, "regex": regex,
"case_sensitive": case_sensitive, "before_lines": before_lines, "after_lines": after_lines,
"max_matches": max_matches, "scan_scope": scan_scope, "recent_bytes": recent_bytes,
}, timeout)
def set_log_analysis_enabled(enabled: bool, client_code: str = "", timeout: int = 30) -> Dict[str, Any]:
return _log_api("config", {"client_code": client_code, "enabled": bool(enabled)}, timeout)
def _print_log_result(result: Dict[str, Any], as_json: bool = False):
if as_json or not result.get("success"):
print(json.dumps(result, ensure_ascii=False, indent=2))
return
data = result.get("data") or {}
if isinstance(data.get("lines"), list):
for entry in data["lines"]:
if isinstance(entry, dict):
marker = ">" if entry.get("match") else " "
print(f"{marker} {entry.get('scan_line', '-')}: {entry.get('text', '')}")
else:
print(entry)
print(f"matches={data.get('matches', '-')} complete={data.get('complete', True)} stop_reason={data.get('stop_reason', '')}")
else:
print(json.dumps(data, ensure_ascii=False, indent=2))
def _inspection_report_payload(result: Dict[str, Any]) -> Dict[str, Any]: def _inspection_report_payload(result: Dict[str, Any]) -> Dict[str, Any]:
data = result.get("data") or {} data = result.get("data") or {}
if isinstance(data, dict) and data.get("report_json"): if isinstance(data, dict) and data.get("report_json"):
@@ -3351,6 +3428,45 @@ def cmd_capabilities(args):
"required": [] "required": []
} }
}, },
{
"name": "log_info",
"description": "Validate one absolute server log path and return bounded file metadata without Oracle access.",
"parameters": {"type": "object", "properties": {
"path": {"type": "string", "description": "Absolute log file path on the target server."},
"clientCode": {"type": "string", "description": "Optional client/server code."}
}, "required": ["path"]}
},
{
"name": "log_tail",
"description": "Read a bounded tail from one server log file.",
"parameters": {"type": "object", "properties": {
"path": {"type": "string"}, "clientCode": {"type": "string"},
"lines": {"type": "integer", "description": "Default 200, maximum 2000."},
"maxBytes": {"type": "integer", "description": "Default 262144, maximum 1048576."}
}, "required": ["path"]}
},
{
"name": "log_search",
"description": "Stream-search a server log with keywords or RE2 regular expressions and bounded context.",
"parameters": {"type": "object", "properties": {
"path": {"type": "string"}, "patterns": {"type": "array", "items": {"type": "string"}},
"clientCode": {"type": "string"}, "regex": {"type": "boolean"},
"caseSensitive": {"type": "boolean"}, "beforeLines": {"type": "integer"},
"afterLines": {"type": "integer"}, "maxMatches": {"type": "integer"},
"scanScope": {"type": "string", "enum": ["recent", "full"]},
"recentBytes": {"type": "integer"}, "timeout": {"type": "integer"}
}, "required": ["path", "patterns"]}
},
{
"name": "log_enable",
"description": "Enable persisted log analysis for an authorized client.",
"parameters": {"type": "object", "properties": {"clientCode": {"type": "string"}}, "required": []}
},
{
"name": "log_disable",
"description": "Disable persisted log analysis for an authorized client.",
"parameters": {"type": "object", "properties": {"clientCode": {"type": "string"}}, "required": []}
},
{ {
"name": "awr_list", "name": "awr_list",
"description": "List AWR reports already generated and stored by the target Agent.", "description": "List AWR reports already generated and stored by the target Agent.",
@@ -3419,7 +3535,7 @@ def cmd_capabilities(args):
"analyze", "list", "source", "describe", "analyze", "list", "source", "describe",
"search", "query", "discover", "nl2sql", "search", "query", "discover", "nl2sql",
"perm", "qperm", "login", "logout", "perm", "qperm", "login", "logout",
"status", "switch", "clients", "device_register", "device_login", "ops", "ops_report", "inspection_report", "inspection_latest", "inspection_get", "awr_status", "awr_list", "awr_download", "agent_update", "capabilities" "status", "switch", "clients", "device_register", "device_login", "ops", "ops_report", "inspection_report", "inspection_latest", "inspection_get", "awr_status", "awr_list", "awr_download", "log_info", "log_tail", "log_search", "log_enable", "log_disable", "agent_update", "capabilities"
], ],
"python_version": "3.6+", "python_version": "3.6+",
"dependencies": ["requests"] "dependencies": ["requests"]
@@ -3612,6 +3728,53 @@ def main():
output_path = sys.argv[output_index + 1] output_path = sys.argv[output_index + 1]
cmd_awr_download(sys.argv[2], sys.argv[3], output_path=output_path, as_json="--json" in sys.argv[4:]) cmd_awr_download(sys.argv[2], sys.argv[3], output_path=output_path, as_json="--json" in sys.argv[4:])
elif cmd == "log_info":
parser = argparse.ArgumentParser(prog="oracle_skill.py log_info")
parser.add_argument("path")
parser.add_argument("--client", default="")
parser.add_argument("--timeout", type=int, default=120)
parser.add_argument("--json", action="store_true")
args = parser.parse_args(sys.argv[2:])
_print_log_result(log_info(args.path, args.client, args.timeout), args.json)
elif cmd == "log_tail":
parser = argparse.ArgumentParser(prog="oracle_skill.py log_tail")
parser.add_argument("path")
parser.add_argument("--client", default="")
parser.add_argument("--lines", type=int, default=200)
parser.add_argument("--max-bytes", type=int, default=256 * 1024)
parser.add_argument("--timeout", type=int, default=120)
parser.add_argument("--json", action="store_true")
args = parser.parse_args(sys.argv[2:])
_print_log_result(log_tail(args.path, args.client, args.lines, args.max_bytes, args.timeout), args.json)
elif cmd == "log_search":
parser = argparse.ArgumentParser(prog="oracle_skill.py log_search")
parser.add_argument("path")
parser.add_argument("patterns", nargs="+")
parser.add_argument("--client", default="")
parser.add_argument("--regex", action="store_true")
parser.add_argument("--case-sensitive", action="store_true")
parser.add_argument("--before", type=int, default=3)
parser.add_argument("--after", type=int, default=3)
parser.add_argument("--max-matches", type=int, default=100)
parser.add_argument("--scope", choices=("recent", "full"), default="recent")
parser.add_argument("--recent-bytes", type=int, default=100 * 1024 * 1024)
parser.add_argument("--timeout", type=int, default=120)
parser.add_argument("--json", action="store_true")
args = parser.parse_args(sys.argv[2:])
_print_log_result(log_search(
args.path, args.patterns, args.client, args.regex, args.case_sensitive,
args.before, args.after, args.max_matches, args.scope, args.recent_bytes, args.timeout,
), args.json)
elif cmd in ("log_enable", "log_disable"):
parser = argparse.ArgumentParser(prog=f"oracle_skill.py {cmd}")
parser.add_argument("client", nargs="?", default="")
parser.add_argument("--json", action="store_true")
args = parser.parse_args(sys.argv[2:])
_print_log_result(set_log_analysis_enabled(cmd == "log_enable", args.client), args.json)
elif cmd == "agent_update": elif cmd == "agent_update":
timeout = int(sys.argv[3]) if len(sys.argv) >= 4 and sys.argv[3].isdigit() else 300 timeout = int(sys.argv[3]) if len(sys.argv) >= 4 and sys.argv[3].isdigit() else 300
cmd_agent_update(sys.argv[2] if len(sys.argv) >= 3 else "", timeout) cmd_agent_update(sys.argv[2] if len(sys.argv) >= 3 else "", timeout)
+93
View File
@@ -0,0 +1,93 @@
import io
import json
import unittest
from contextlib import redirect_stdout
from unittest import mock
from scripts import oracle_skill as skill
class FakeResponse:
def __init__(self, payload, ok=True, status_code=200):
self._payload = payload
self.ok = ok
self.status_code = status_code
self.text = ""
def json(self):
return self._payload
class LogAnalysisSkillTests(unittest.TestCase):
def config(self):
return {
"access_token": "token",
"expires_at": "2999-01-01T00:00:00+08:00",
"transit_url": "http://transit.example",
"client_code": "AHMW",
}
def test_log_info_preserves_path_with_spaces(self):
response = FakeResponse({"success": True, "data": {"size": 12}})
path = r"D:\Program Files\BOS\logs\app log.txt"
with mock.patch.object(skill, "get_config", return_value=self.config()), mock.patch.object(
skill.requests, "post", return_value=response
) as post:
result = skill.log_info(path, "AHMW")
self.assertTrue(result["success"])
self.assertEqual(post.call_args.args[0], "http://transit.example/api/log/info")
self.assertEqual(post.call_args.kwargs["json"]["path"], path)
self.assertEqual(post.call_args.kwargs["json"]["client_code"], "AHMW")
self.assertEqual(post.call_args.kwargs["headers"]["Authorization"], "Bearer token")
def test_log_search_forwards_filters_and_full_scope(self):
response = FakeResponse({"success": True, "data": {"matches": 1, "lines": []}})
with mock.patch.object(skill, "get_config", return_value=self.config()), mock.patch.object(
skill.requests, "post", return_value=response
) as post:
result = skill.log_search(
r"D:\logs\app.log", ["ERROR", r"ORA-\d+"], "AHMW", regex=True,
case_sensitive=True, before_lines=5, after_lines=7, max_matches=20,
scan_scope="full", timeout=300,
)
self.assertTrue(result["success"])
payload = post.call_args.kwargs["json"]
self.assertEqual(payload["patterns"], ["ERROR", r"ORA-\d+"])
self.assertTrue(payload["regex"])
self.assertEqual(payload["scan_scope"], "full")
self.assertEqual(payload["timeout"], 300)
def test_log_enable_uses_authenticated_customer_endpoint(self):
response = FakeResponse({"success": True, "data": {"enabled": True, "persisted": True}})
with mock.patch.object(skill, "get_config", return_value=self.config()), mock.patch.object(
skill.requests, "post", return_value=response
) as post:
result = skill.set_log_analysis_enabled(True, "AHMW")
self.assertTrue(result["success"])
self.assertEqual(post.call_args.args[0], "http://transit.example/api/log/config")
self.assertEqual(post.call_args.kwargs["json"]["enabled"], True)
def test_log_error_code_is_preserved(self):
response = FakeResponse(
{"success": False, "error": "log file not found", "error_code": "file_not_found"},
ok=False, status_code=404,
)
with mock.patch.object(skill, "get_config", return_value=self.config()), mock.patch.object(
skill.requests, "post", return_value=response
):
result = skill.log_info(r"D:\missing.log", "AHMW")
self.assertFalse(result["success"])
self.assertEqual(result["error_code"], "file_not_found")
self.assertEqual(result["status_code"], 404)
def test_capabilities_lists_all_log_commands(self):
output = io.StringIO()
with redirect_stdout(output):
skill.cmd_capabilities(type("Args", (), {"json": True})())
payload = json.loads(output.getvalue())
names = {item["name"] for item in payload["commands"]}
self.assertTrue({"log_info", "log_tail", "log_search", "log_enable", "log_disable"}.issubset(names))
if __name__ == "__main__":
unittest.main()