chore: auto release commit (2026-07-11 15:00:19)
All checks were successful
技能自动化发布 / release (push) Successful in 5s
All checks were successful
技能自动化发布 / release (push) Successful in 5s
This commit is contained in:
@@ -35,6 +35,11 @@ POLICY_IDS = (
|
||||
"POLICY-PACKAGING-001",
|
||||
"POLICY-PACKAGING-002",
|
||||
"POLICY-DOCS-001",
|
||||
"POLICY-LOGGING-001",
|
||||
"POLICY-LOGGING-002",
|
||||
"POLICY-LOGGING-003",
|
||||
"POLICY-LOGGING-004",
|
||||
"POLICY-LOGGING-005",
|
||||
)
|
||||
|
||||
STRUCTURE_PATHS = (
|
||||
@@ -104,6 +109,25 @@ RPA_002_FFMPEG_PATTERNS = (
|
||||
|
||||
RPA_002_EXEMPT_REL = "scripts/service/task_run_support.py"
|
||||
|
||||
LOGGING_001_SOURCE = "development/LOGGING.md; development/RUNTIME.md"
|
||||
LOGGING_002_SOURCE = "development/LOGGING.md; development/DEVELOPMENT.md"
|
||||
LOGGING_003_SOURCE = "development/LOGGING.md"
|
||||
LOGGING_004_SOURCE = "development/LOGGING.md; development/RPA.md"
|
||||
LOGGING_005_SOURCE = "development/LOGGING.md; development/CONFIG.md"
|
||||
|
||||
LOGGING_CALL_PATTERN = re.compile(
|
||||
r"\b(log|logger|get_skill_logger\s*\(\s*\))\s*\.\s*(info|warning|error|debug|exception|critical)\s*\(",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
LOGGING_CALL_GET_LOGGER_PATTERN = re.compile(
|
||||
r"get_skill_logger\s*\(\s*\)\s*\.\s*(info|warning|error|debug|exception|critical)\s*\(",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
SENSITIVE_LOG_ASSIGNMENT_PATTERN = re.compile(
|
||||
r"(password|token|cookie|secret|api_key|authorization)\s*=",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _policy_msg(policy_id: str, source: str, detail: str) -> str:
|
||||
return f"{policy_id} [{source}]: {detail}"
|
||||
@@ -446,6 +470,126 @@ class TestPolicyRpa002(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
def _scan_sensitive_logging_assignments(skill_root: str) -> list[str]:
|
||||
offenders: list[str] = []
|
||||
for path in _walk_files(skill_root, "scripts", suffix=".py"):
|
||||
rel = _rel(skill_root, path)
|
||||
for lineno, line in enumerate(_read_text(path).splitlines(), 1):
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#"):
|
||||
continue
|
||||
if not LOGGING_CALL_PATTERN.search(line):
|
||||
continue
|
||||
if SENSITIVE_LOG_ASSIGNMENT_PATTERN.search(line):
|
||||
offenders.append(f"{rel}:{lineno}: {stripped}")
|
||||
return offenders
|
||||
|
||||
|
||||
class TestPolicyLogging001(unittest.TestCase):
|
||||
def test_cli_app_sets_up_unified_logging(self) -> None:
|
||||
skill_root = get_skill_root()
|
||||
path = os.path.join(skill_root, "scripts", "cli", "app.py")
|
||||
text = _read_text(path)
|
||||
rel = _rel(skill_root, path)
|
||||
self.assertIn(
|
||||
"setup_skill_logging",
|
||||
text,
|
||||
msg=_policy_msg(
|
||||
"POLICY-LOGGING-001",
|
||||
LOGGING_001_SOURCE,
|
||||
f"{rel} missing setup_skill_logging",
|
||||
),
|
||||
)
|
||||
has_logger_info = bool(
|
||||
LOGGING_CALL_GET_LOGGER_PATTERN.search(text)
|
||||
or re.search(r"\bget_skill_logger\s*\(\s*\)\s*\.\s*info\s*\(", text)
|
||||
)
|
||||
self.assertTrue(
|
||||
has_logger_info,
|
||||
msg=_policy_msg(
|
||||
"POLICY-LOGGING-001",
|
||||
LOGGING_001_SOURCE,
|
||||
f"{rel} missing get_skill_logger().info (or equivalent)",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class TestPolicyLogging002(unittest.TestCase):
|
||||
def test_task_service_uses_get_skill_logger(self) -> None:
|
||||
skill_root = get_skill_root()
|
||||
path = os.path.join(skill_root, "scripts", "service", "task_service.py")
|
||||
text = _read_text(path)
|
||||
rel = _rel(skill_root, path)
|
||||
self.assertRegex(
|
||||
text,
|
||||
r"get_skill_logger",
|
||||
msg=_policy_msg(
|
||||
"POLICY-LOGGING-002",
|
||||
LOGGING_002_SOURCE,
|
||||
f"{rel} must import/use get_skill_logger",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class TestPolicyLogging003(unittest.TestCase):
|
||||
def test_task_service_logs_start_failure_and_task_logs(self) -> None:
|
||||
skill_root = get_skill_root()
|
||||
path = os.path.join(skill_root, "scripts", "service", "task_service.py")
|
||||
text = _read_text(path)
|
||||
rel = _rel(skill_root, path)
|
||||
missing: list[str] = []
|
||||
if not re.search(r"\b(log|logger)\.info\s*\(", text):
|
||||
missing.append("log.info or logger.info")
|
||||
if not re.search(r"\b(log|logger)\.exception\s*\(", text):
|
||||
missing.append("log.exception or logger.exception")
|
||||
if "save_task_log" not in text:
|
||||
missing.append("save_task_log")
|
||||
self.assertEqual(
|
||||
missing,
|
||||
[],
|
||||
msg=_policy_msg(
|
||||
"POLICY-LOGGING-003",
|
||||
LOGGING_003_SOURCE,
|
||||
f"{rel} missing: " + ", ".join(missing),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class TestPolicyLogging004(unittest.TestCase):
|
||||
def test_task_service_emits_progress_for_long_tasks(self) -> None:
|
||||
skill_root = get_skill_root()
|
||||
path = os.path.join(skill_root, "scripts", "service", "task_service.py")
|
||||
text = _read_text(path)
|
||||
rel = _rel(skill_root, path)
|
||||
has_progress = bool(
|
||||
re.search(r"\bemit\s*\(", text)
|
||||
or re.search(r"\bactivity\.emit\s*\(", text)
|
||||
or re.search(r"\bvideo\.add_step\s*\(", text)
|
||||
)
|
||||
self.assertTrue(
|
||||
has_progress,
|
||||
msg=_policy_msg(
|
||||
"POLICY-LOGGING-004",
|
||||
LOGGING_004_SOURCE,
|
||||
f"{rel} must use emit( / activity.emit / video.add_step",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class TestPolicyLogging005(unittest.TestCase):
|
||||
def test_scripts_do_not_log_sensitive_assignments(self) -> None:
|
||||
offenders = _scan_sensitive_logging_assignments(get_skill_root())
|
||||
self.assertEqual(
|
||||
offenders,
|
||||
[],
|
||||
msg=_policy_msg(
|
||||
"POLICY-LOGGING-005",
|
||||
LOGGING_005_SOURCE,
|
||||
"\n".join(offenders),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class TestPolicyDocs001(unittest.TestCase):
|
||||
def test_policy_matrix_exists_and_lists_policy_ids(self) -> None:
|
||||
skill_root = get_skill_root()
|
||||
|
||||
@@ -157,5 +157,78 @@ class TestTemplateRunVideoSession(unittest.TestCase):
|
||||
self.assertIn("video_path", summary)
|
||||
|
||||
|
||||
class TestCmdRunTaskLogCoverage(unittest.TestCase):
|
||||
def test_cmd_run_entitlement_failure_writes_task_log(self) -> None:
|
||||
with IsolatedDataRoot(user_id="_entitlement_fail"):
|
||||
from service import task_service
|
||||
|
||||
with patch.object(task_service, "check_entitlement", return_value=(False, "mock denied")):
|
||||
buf = io.StringIO()
|
||||
with redirect_stdout(buf), redirect_stderr(io.StringIO()):
|
||||
rc = task_service.cmd_run(target="t1", input_id="99")
|
||||
|
||||
self.assertEqual(rc, 1)
|
||||
from db import task_logs_repository as tlr
|
||||
|
||||
rows = tlr.list_task_logs(1)
|
||||
self.assertTrue(rows)
|
||||
_rid, _ttype, tid, iid, _ititle, status, err, rsum, _cat, _uat = rows[0]
|
||||
self.assertEqual(status, "failed")
|
||||
self.assertEqual(tid, "t1")
|
||||
self.assertEqual(iid, "99")
|
||||
self.assertIn("mock denied", err or "")
|
||||
summary = json.loads(rsum or "{}")
|
||||
self.assertEqual(summary.get("stage"), "auth")
|
||||
|
||||
def test_cmd_run_exception_writes_task_log(self) -> None:
|
||||
with IsolatedDataRoot(user_id="_exception_run"):
|
||||
os.environ["OPENCLAW_RECORD_VIDEO"] = "0"
|
||||
config.reset_cache()
|
||||
|
||||
from service import task_service
|
||||
|
||||
async def _boom(*_args, **_kwargs):
|
||||
raise RuntimeError("boom")
|
||||
|
||||
with patch.object(task_service, "check_entitlement", return_value=(True, "")):
|
||||
with patch.object(task_service, "_run_template_demo", side_effect=_boom):
|
||||
buf = io.StringIO()
|
||||
with redirect_stdout(buf), redirect_stderr(io.StringIO()):
|
||||
rc = task_service.cmd_run(target="t2", input_id="88")
|
||||
|
||||
self.assertEqual(rc, 1)
|
||||
from db import task_logs_repository as tlr
|
||||
|
||||
rows = tlr.list_task_logs(1)
|
||||
self.assertTrue(rows)
|
||||
_rid, _ttype, tid, iid, _ititle, status, err, rsum, _cat, _uat = rows[0]
|
||||
self.assertEqual(status, "failed")
|
||||
self.assertEqual(tid, "t2")
|
||||
self.assertEqual(iid, "88")
|
||||
self.assertIn("任务执行异常", err or "")
|
||||
summary = json.loads(rsum or "{}")
|
||||
self.assertEqual(summary.get("stage"), "run")
|
||||
self.assertEqual(summary.get("error"), "unexpected_exception")
|
||||
|
||||
def test_get_task_logger_fallback_when_not_initialized(self) -> None:
|
||||
from service import task_service
|
||||
|
||||
mock_logger = MagicMock()
|
||||
responses = [RuntimeError("logging not initialized"), mock_logger]
|
||||
|
||||
def _fake_get_skill_logger():
|
||||
item = responses.pop(0)
|
||||
if isinstance(item, Exception):
|
||||
raise item
|
||||
return item
|
||||
|
||||
with patch.object(task_service, "get_skill_logger", side_effect=_fake_get_skill_logger):
|
||||
with patch.object(task_service, "setup_skill_logging") as mock_setup:
|
||||
logger = task_service._get_task_logger()
|
||||
|
||||
mock_setup.assert_called_once_with(task_service.SKILL_SLUG, task_service.LOG_LOGGER_NAME)
|
||||
self.assertIs(logger, mock_logger)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user