feat(tuning): release V0.8.2 Agent 界面重构
This commit is contained in:
@@ -12,6 +12,7 @@ from tuning.schema import ( # noqa: E402
|
||||
RewardConfigError,
|
||||
merge_proposal,
|
||||
validate_configuration,
|
||||
validate_constraints,
|
||||
validate_proposal,
|
||||
)
|
||||
from tuning.scoring import ( # noqa: E402
|
||||
@@ -19,7 +20,7 @@ from tuning.scoring import ( # noqa: E402
|
||||
EvaluationError,
|
||||
score_evaluation,
|
||||
)
|
||||
from tuning.storage import TuningStorage # noqa: E402
|
||||
from tuning.storage import StorageConflict, TuningStorage # noqa: E402
|
||||
|
||||
|
||||
class RewardSchemaTest(unittest.TestCase):
|
||||
@@ -69,6 +70,33 @@ class RewardSchemaTest(unittest.TestCase):
|
||||
BASE_REWARD_CONFIGURATION,
|
||||
)
|
||||
|
||||
def test_session_constraints_reject_unknown_out_of_range_and_fixed_changes(self):
|
||||
constraints = validate_constraints(
|
||||
{
|
||||
"weights.track_linear_velocity": {"kind": "fixed", "value": 1.0},
|
||||
"params.foot_gait.period": {"kind": "range", "min": 0.5, "max": 0.7},
|
||||
}
|
||||
)
|
||||
validate_proposal(
|
||||
{"params": {"foot_gait.period": 0.65}},
|
||||
BASE_REWARD_CONFIGURATION,
|
||||
constraints,
|
||||
)
|
||||
with self.assertRaisesRegex(RewardConfigError, "已固定"):
|
||||
validate_proposal(
|
||||
{"weights": {"track_linear_velocity": 1.1}},
|
||||
BASE_REWARD_CONFIGURATION,
|
||||
constraints,
|
||||
)
|
||||
with self.assertRaisesRegex(RewardConfigError, "工程锁定范围"):
|
||||
validate_proposal(
|
||||
{"params": {"foot_gait.period": 0.75}},
|
||||
BASE_REWARD_CONFIGURATION,
|
||||
constraints,
|
||||
)
|
||||
with self.assertRaisesRegex(RewardConfigError, "未知参数约束"):
|
||||
validate_constraints({"weights.not_allowed": {"kind": "fixed", "value": 1.0}})
|
||||
|
||||
|
||||
class ScoringTest(unittest.TestCase):
|
||||
baseline = {
|
||||
@@ -174,8 +202,24 @@ class StorageTest(unittest.TestCase):
|
||||
self.assertEqual(sampled[0]["step"], 0)
|
||||
self.assertEqual(sampled[-1]["step"], 99)
|
||||
self.assertIn(50.0, [point["value"] for point in sampled])
|
||||
control = self.storage.replace_constraints(
|
||||
session["id"],
|
||||
0,
|
||||
{"weights.pose": {"kind": "range", "min": 0.5, "max": 1.5}},
|
||||
)
|
||||
self.assertEqual(control["constraintsRevision"], 1)
|
||||
with self.assertRaises(StorageConflict):
|
||||
self.storage.replace_constraints(session["id"], 0, {})
|
||||
self.storage.grant_dispatch_token(session["id"])
|
||||
with self.assertRaises(StorageConflict):
|
||||
self.storage.grant_dispatch_token(session["id"])
|
||||
self.assertTrue(self.storage.use_dispatch_token(session["id"]))
|
||||
self.assertFalse(self.storage.use_dispatch_token(session["id"]))
|
||||
incremental = self.storage.metrics(trial["id"], max_points=100, after_step=90)[0]
|
||||
self.assertEqual(incremental["points"][0]["step"], 91)
|
||||
reopened = TuningStorage(self.storage.path)
|
||||
self.assertEqual(reopened.get_session(session["id"])["mode"], "approval")
|
||||
self.assertEqual(reopened.get_control(session["id"])["constraintsRevision"], 1)
|
||||
|
||||
def test_recovery_marks_inflight_records(self):
|
||||
session = self.storage.create_session(
|
||||
|
||||
@@ -148,6 +148,8 @@ class TuningManagerTest(unittest.TestCase):
|
||||
self.fail("session did not wait for approval")
|
||||
proposal = detail["proposals"][-1]
|
||||
patch = {"weights": {"pose": 1.1}, "params": {}}
|
||||
with self.assertRaisesRegex(Exception, "feedback"):
|
||||
self.manager.approve(session["id"], proposal["id"], {"feedback": {}})
|
||||
approved = self.manager.approve(
|
||||
session["id"], proposal["id"], {"feedback": "ok", "patch": patch}
|
||||
)
|
||||
@@ -155,6 +157,34 @@ class TuningManagerTest(unittest.TestCase):
|
||||
self.manager.cancel(session["id"])
|
||||
self.assertEqual(self.wait_terminal(session["id"])["state"], "cancelled")
|
||||
|
||||
def test_runtime_mode_switch_auto_approves_pending_proposal(self):
|
||||
session = self.manager.create(self.payload("approval"))
|
||||
deadline = time.monotonic() + 3
|
||||
while time.monotonic() < deadline:
|
||||
detail = self.manager.detail(session["id"])
|
||||
if detail["state"] == "awaiting_approval":
|
||||
break
|
||||
time.sleep(0.01)
|
||||
else:
|
||||
self.fail("session did not wait for approval")
|
||||
changed = self.manager.set_mode(session["id"], {"mode": "automatic"})
|
||||
self.assertEqual(changed["mode"], "automatic")
|
||||
self.assertEqual(changed["proposals"][-1]["state"], "approved")
|
||||
completed = self.wait_terminal(session["id"])
|
||||
self.assertEqual(completed["state"], "succeeded", completed["message"])
|
||||
|
||||
def test_trial_count_is_user_configurable(self):
|
||||
payload = self.payload()
|
||||
payload["trialCount"] = 1
|
||||
_, config, _, _ = self.manager.parse_create(payload)
|
||||
self.assertEqual(config["trialCount"], 1)
|
||||
payload["trialCount"] = 100
|
||||
_, config, _, _ = self.manager.parse_create(payload)
|
||||
self.assertEqual(config["trialCount"], 100)
|
||||
payload["trialCount"] = 101
|
||||
with self.assertRaisesRegex(Exception, "trialCount"):
|
||||
self.manager.parse_create(payload)
|
||||
|
||||
def test_resume_discards_only_interrupted_trial_and_continues(self):
|
||||
mode, config, objective, fallback = self.manager.parse_create(self.payload())
|
||||
session = self.manager.storage.create_session(mode, config, objective, fallback)
|
||||
@@ -178,8 +208,10 @@ class TuningManagerTest(unittest.TestCase):
|
||||
"trial-001-rung-0",
|
||||
)
|
||||
self.manager.storage.update_trial(interrupted["id"], state="interrupted")
|
||||
self.manager.storage.reset_step_gate(session["id"])
|
||||
self.manager.storage.update_session(session["id"], state="interrupted")
|
||||
self.manager.resume(session["id"])
|
||||
self.assertEqual(self.manager.storage.get_control(session["id"])["runPolicy"], "continuous")
|
||||
completed = self.wait_terminal(session["id"])
|
||||
self.assertEqual(completed["state"], "succeeded", completed["message"])
|
||||
self.assertNotIn(interrupted["id"], [trial["id"] for trial in completed["trials"]])
|
||||
@@ -190,6 +222,129 @@ class TuningManagerTest(unittest.TestCase):
|
||||
self.manager.parse_create({"taskId": "Other"})
|
||||
self.assertEqual(self.manager.test_agent()["model"], "fake")
|
||||
|
||||
def test_pause_closes_persistent_dispatch_gate_at_trial_boundary(self):
|
||||
mode, config, objective, fallback = self.manager.parse_create(self.payload())
|
||||
session = self.manager.storage.create_session(mode, config, objective, fallback)
|
||||
self.manager.storage.update_session(session["id"], state="running")
|
||||
self.manager.storage.grant_dispatch_token(session["id"])
|
||||
|
||||
paused = self.manager.pause(session["id"])
|
||||
|
||||
self.assertEqual(paused["state"], "paused")
|
||||
self.assertEqual(paused["control"]["runPolicy"], "step")
|
||||
self.assertEqual(paused["control"]["dispatchTokens"], 0)
|
||||
|
||||
def test_step_token_executes_exactly_one_trial_then_pauses(self):
|
||||
session = self.manager.create(self.payload("approval"))
|
||||
deadline = time.monotonic() + 3
|
||||
while time.monotonic() < deadline:
|
||||
detail = self.manager.detail(session["id"])
|
||||
if detail["state"] == "awaiting_approval":
|
||||
break
|
||||
time.sleep(0.01)
|
||||
else:
|
||||
self.fail("session did not wait for approval")
|
||||
baseline_count = len([trial for trial in detail["trials"] if trial["state"] == "completed"])
|
||||
stepped = self.manager.step(session["id"], {"count": 1})
|
||||
self.assertEqual(stepped["control"]["runPolicy"], "step")
|
||||
self.assertEqual(stepped["control"]["dispatchTokens"], 1)
|
||||
proposal = stepped["proposals"][-1]
|
||||
self.manager.approve(session["id"], proposal["id"], {})
|
||||
deadline = time.monotonic() + 3
|
||||
while time.monotonic() < deadline:
|
||||
detail = self.manager.detail(session["id"])
|
||||
completed_count = len(
|
||||
[trial for trial in detail["trials"] if trial["state"] == "completed"]
|
||||
)
|
||||
if detail["state"] == "paused" and completed_count == baseline_count + 1:
|
||||
break
|
||||
time.sleep(0.01)
|
||||
else:
|
||||
self.fail("single-step trial did not pause at the next boundary")
|
||||
time.sleep(0.05)
|
||||
self.assertEqual(
|
||||
len(
|
||||
[
|
||||
trial
|
||||
for trial in self.manager.detail(session["id"])["trials"]
|
||||
if trial["state"] == "completed"
|
||||
]
|
||||
),
|
||||
baseline_count + 1,
|
||||
)
|
||||
self.manager.cancel(session["id"])
|
||||
self.assertEqual(self.wait_terminal(session["id"])["state"], "cancelled")
|
||||
|
||||
def test_constraints_are_revisioned_and_enforced_during_approval(self):
|
||||
session = self.manager.create(self.payload("approval"))
|
||||
deadline = time.monotonic() + 3
|
||||
while time.monotonic() < deadline:
|
||||
detail = self.manager.detail(session["id"])
|
||||
if detail["state"] == "awaiting_approval":
|
||||
break
|
||||
time.sleep(0.01)
|
||||
else:
|
||||
self.fail("session did not wait for approval")
|
||||
proposal = detail["proposals"][-1]
|
||||
constrained = self.manager.set_constraints(
|
||||
session["id"],
|
||||
{
|
||||
"revision": 0,
|
||||
"constraints": {"weights.track_linear_velocity": {"kind": "fixed", "value": 1.0}},
|
||||
},
|
||||
)
|
||||
self.assertEqual(constrained["control"]["constraintsRevision"], 1)
|
||||
with self.assertRaisesRegex(Exception, "已固定"):
|
||||
self.manager.approve(
|
||||
session["id"],
|
||||
proposal["id"],
|
||||
{"patch": {"weights": {"track_linear_velocity": 1.1}, "params": {}}},
|
||||
)
|
||||
with self.assertRaisesRegex(Exception, "revision"):
|
||||
self.manager.set_constraints(session["id"], {"revision": 0, "constraints": {}})
|
||||
self.manager.approve(session["id"], proposal["id"], {})
|
||||
self.manager.cancel(session["id"])
|
||||
self.assertEqual(self.wait_terminal(session["id"])["state"], "cancelled")
|
||||
|
||||
def test_rollback_uses_safe_completed_trial_as_next_proposal_base(self):
|
||||
session = self.manager.create(self.payload("approval"))
|
||||
deadline = time.monotonic() + 3
|
||||
while time.monotonic() < deadline:
|
||||
detail = self.manager.detail(session["id"])
|
||||
if detail["state"] == "awaiting_approval":
|
||||
break
|
||||
time.sleep(0.01)
|
||||
else:
|
||||
self.fail("session did not wait for approval")
|
||||
baseline = detail["trials"][0]
|
||||
old_proposal = detail["proposals"][-1]
|
||||
rolled_back = self.manager.rollback(
|
||||
session["id"], {"trialId": baseline["id"], "checkpoint": True}
|
||||
)
|
||||
self.assertEqual(rolled_back["state"], "paused")
|
||||
self.assertEqual(rolled_back["control"]["activeBaseTrialId"], baseline["id"])
|
||||
self.assertEqual(
|
||||
next(item for item in rolled_back["proposals"] if item["id"] == old_proposal["id"])[
|
||||
"state"
|
||||
],
|
||||
"rejected",
|
||||
)
|
||||
self.manager.step(session["id"], {"count": 1})
|
||||
deadline = time.monotonic() + 3
|
||||
while time.monotonic() < deadline:
|
||||
detail = self.manager.detail(session["id"])
|
||||
if (
|
||||
detail["state"] == "awaiting_approval"
|
||||
and detail["proposals"][-1]["id"] != old_proposal["id"]
|
||||
):
|
||||
break
|
||||
time.sleep(0.01)
|
||||
else:
|
||||
self.fail("rollback base did not produce a replacement proposal")
|
||||
self.assertEqual(detail["proposals"][-1]["baseTrialId"], baseline["id"])
|
||||
self.manager.cancel(session["id"])
|
||||
self.assertEqual(self.wait_terminal(session["id"])["state"], "cancelled")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user