四块互相交织的 benchmark 覆盖增量,统一提交: 1) 通信遥测(#23):orchestrator 路由 peer 消息时按 correlation_id 计请求/应答到 SwarmRun.collaboration(内部状态,不进 Manager 事件流);collector 算 s_communication。 治理计数由 run.approvals 派生(合规/总数)→ s_governance。 2) Q_quality 掩码归一(v2.1 裁定):metrics.quality_score 改为对 present 输入加权归一, 非编码任务自动忽略 TestPassRate,全缺 → NaN(不伪造)。 3) 质量插桩 / Group B:新增 Pod 内代码测试沙箱(orchestrator/sandbox.py,环境清洗 + 超时强杀 + 资源限额 + 路径越界校验,门控 ENABLE_QUALITY_EVAL)与 held-out fixture (benchmark/fixtures/);run 完成时用留出测试评分得 TestPassRate → Q_quality → collector 合成 reward。安全边界见 docs/integration/security-boundary.md §8.1。 4) 决策引擎 / Group A(#10,Option A score-at-pull):新增 orchestrator/decision_engine.py —— 信息素 τ(Redis 持久、(role,agent) 键控、冷启动 0.5、ρ 蒸发、夹紧、学习常开)+ η 启发式评分 + ε-greedy 概率采样;每次 dispatch 产一条 DecisionTrace → SwarmRun.decisions;collector 算 tau/eta/p_decision。概率选择门控 ENABLE_ACO_DISPATCH (默认关,CI 用 ACO_SEED 固定)。 覆盖:单次 run 真实可算字段由 4 提升至最多 10/15(新增 communication/reward/tau/eta/ p_decision,外加 governance 有条件)。 测试:新增 test-sandbox / test-quality / test-decision-engine;扩充 collector/metrics 用例; CI 纳入全部 benchmark 套件 + flag-on 的 ACO e2e。本地 11 项 gate 全绿。 诚实边界(未越界声称): - Group A 为单边匹配(Option B 待 Group C);概率派发优于贪心未证;默认关闭。 - reward 的 CodeReview/UserAcceptance 未采集(掩码忽略);P_risk 为审批派生低估。 - s_gain/s_swarm/g_e/g_e_cost/benchmark 仍 NaN —— 需基线(#21/#13),本 PR 不动验收。 影响范围:Swarm(orchestrator + benchmark + docs + CI)。不改 Manager↔Swarm 事件契约 (遥测均为运行时内部状态);不影响 Client/计费/密钥/发布链路。新增 ENABLE_QUALITY_EVAL / ENABLE_ACO_DISPATCH 两个开关,默认关闭。 Closes #10 Closes #23 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
97 lines
3.5 KiB
Python
97 lines
3.5 KiB
Python
"""In-sandbox test harness — runs INSIDE the isolated workdir as a child process.
|
|
|
|
Stdlib only (no pytest dependency): collects both `unittest.TestCase` tests and bare
|
|
`test_*` functions from every `test_*.py` in the working directory, runs them, and writes a
|
|
machine-readable `_result.json` ({total, passed, failed, errored, details}). The parent
|
|
(orchestrator/sandbox.py) reads that file; it never trusts stdout for counts.
|
|
|
|
This file is copied into the ephemeral sandbox workdir at run time and executed there with the
|
|
workdir as CWD. It must stay self-contained and import nothing outside the stdlib.
|
|
"""
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import sys
|
|
import unittest
|
|
|
|
RESULT_FILE = "_result.json"
|
|
|
|
|
|
def _load_module(path: str):
|
|
name = "sbx_" + os.path.splitext(os.path.basename(path))[0]
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module) # may raise on import/collection error
|
|
return module
|
|
|
|
|
|
def main() -> None:
|
|
workdir = os.getcwd()
|
|
sys.path.insert(0, workdir)
|
|
test_files = sorted(
|
|
f for f in os.listdir(workdir) if f.startswith("test_") and f.endswith(".py")
|
|
)
|
|
|
|
total = passed = failed = errored = 0
|
|
details = []
|
|
suite = unittest.TestSuite()
|
|
bare_funcs = [] # (label, callable)
|
|
|
|
for tf in test_files:
|
|
try:
|
|
module = _load_module(os.path.join(workdir, tf))
|
|
except Exception as exc: # import-time failure counts as one errored test
|
|
total += 1
|
|
errored += 1
|
|
failed += 1
|
|
details.append({"test": tf, "status": "error", "error": repr(exc)})
|
|
continue
|
|
# unittest.TestCase-style tests
|
|
suite.addTests(unittest.defaultTestLoader.loadTestsFromModule(module))
|
|
# bare pytest-style functions defined in this module
|
|
for attr in dir(module):
|
|
if not attr.startswith("test_"):
|
|
continue
|
|
obj = getattr(module, attr)
|
|
if callable(obj) and not isinstance(obj, type) and getattr(obj, "__module__", None) == module.__name__:
|
|
bare_funcs.append((f"{tf}::{attr}", obj))
|
|
|
|
# Run the unittest suite (TestCase subclasses).
|
|
ut_result = unittest.TestResult()
|
|
suite.run(ut_result)
|
|
ut_total = ut_result.testsRun
|
|
ut_failed = len(ut_result.failures) + len(ut_result.errors)
|
|
total += ut_total
|
|
failed += ut_failed
|
|
errored += len(ut_result.errors)
|
|
passed += ut_total - ut_failed
|
|
|
|
# Run bare test_* functions.
|
|
for label, fn in bare_funcs:
|
|
total += 1
|
|
try:
|
|
fn()
|
|
passed += 1
|
|
details.append({"test": label, "status": "passed"})
|
|
except AssertionError as exc:
|
|
failed += 1
|
|
details.append({"test": label, "status": "failed", "error": str(exc)})
|
|
except Exception as exc:
|
|
failed += 1
|
|
errored += 1
|
|
details.append({"test": label, "status": "error", "error": repr(exc)})
|
|
|
|
with open(os.path.join(workdir, RESULT_FILE), "w", encoding="utf-8") as fh:
|
|
json.dump(
|
|
{"total": total, "passed": passed, "failed": failed, "errored": errored, "details": details},
|
|
fh,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except Exception as exc: # never leave the parent without a result file
|
|
with open(RESULT_FILE, "w", encoding="utf-8") as fh:
|
|
json.dump({"total": 0, "passed": 0, "failed": 0, "errored": 1, "fatal": repr(exc)}, fh)
|