Merge PR #9: harden company resume, external sessions, and Office UI i18n
Merges xiaodong-l's hardening work: company suspend-checkpoint resume reconciliation, external session token lifecycle fixes, Windows launch shims, codex shell_environment_policy forwarding, Office UI i18n (en/zh-CN), and two org presets. frontend_dist is rebuilt from the merged sources in this commit so the #11 runtime-status fixes and the i18n bundle coexist. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> # Conflicts: # opc/plugins/office_ui/frontend_dist/assets/index-BgyI65M_.js # opc/plugins/office_ui/frontend_dist/index.html
This commit is contained in:
@@ -920,6 +920,53 @@ class CompanyCollaborationTests(unittest.IsolatedAsyncioTestCase):
|
||||
any(item.startswith("[Workspace] Prepared workspace roots:") for item in task.metadata.get("progress_log", []))
|
||||
)
|
||||
|
||||
async def test_failed_work_item_transitions_out_of_running_phase(self) -> None:
|
||||
execute_task = AsyncMock(
|
||||
return_value=TaskResult(status=TaskStatus.FAILED, content="provider limit")
|
||||
)
|
||||
save_task = AsyncMock()
|
||||
executor = CompanyWorkItemExecutor(
|
||||
org_engine=DummyOrgEngine(),
|
||||
communication=SimpleNamespace(),
|
||||
approval_engine=SimpleNamespace(),
|
||||
memory=None,
|
||||
execute_task=execute_task,
|
||||
save_task=save_task,
|
||||
)
|
||||
task = Task(
|
||||
id="failed-work-item-task",
|
||||
title="Failed Work Item",
|
||||
assigned_to="executor",
|
||||
status=TaskStatus.PENDING,
|
||||
metadata={
|
||||
"work_item_projection_id": "failed_execution",
|
||||
"work_item_role_id": "executor",
|
||||
"work_item_turn_type": "execute",
|
||||
"work_item_execution_strategy": "native",
|
||||
"progress_log": [],
|
||||
},
|
||||
)
|
||||
|
||||
with patch(
|
||||
"opc.layer2_organization.company_mode.transition_work_item_from_task",
|
||||
new_callable=AsyncMock,
|
||||
) as transition:
|
||||
result = await executor._run_work_item(task, {task.id: task, "failed_execution": task})
|
||||
|
||||
self.assertEqual(result.status, TaskStatus.FAILED)
|
||||
self.assertTrue(
|
||||
any(
|
||||
call.kwargs.get("target_status_or_phase") == TaskStatus.FAILED
|
||||
and call.kwargs.get("reason") == "work_item_failed"
|
||||
for call in transition.await_args_list
|
||||
)
|
||||
)
|
||||
self.assertIn(
|
||||
"Work item ended with status failed.",
|
||||
task.metadata.get("progress_log", []),
|
||||
)
|
||||
save_task.assert_awaited()
|
||||
|
||||
async def test_workspace_bootstrap_work_item_records_manifest_and_reserved_layout(self) -> None:
|
||||
with _workspace_tempdir() as tmpdir:
|
||||
workspace = tmpdir / "runtime-root"
|
||||
|
||||
@@ -276,6 +276,154 @@ class CompanyRuntimeSuspendResumeTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(refreshed_item.metadata.get("dispatch_hold"), "")
|
||||
self.assertEqual(refreshed_item.claimed_by_role_runtime_session_id, "")
|
||||
|
||||
async def test_continue_merges_task_suspended_after_checkpoint_creation(self) -> None:
|
||||
store = await self._store()
|
||||
plan, task = await self._seed_runtime(store)
|
||||
engine = self._engine(store)
|
||||
await engine.suspend_company_runtime(
|
||||
origin_task_id=task.id,
|
||||
session_id="sess-parent",
|
||||
reason="startup_interrupted",
|
||||
)
|
||||
|
||||
review_item = DelegationWorkItem(
|
||||
work_item_id="review-item-v3",
|
||||
run_id="run-1",
|
||||
role_id="executor",
|
||||
seat_id="seat-1",
|
||||
title="Review v3",
|
||||
summary="Review the completed report.",
|
||||
kind="review",
|
||||
projection_id="review-v3",
|
||||
phase=Phase.RUNNING,
|
||||
metadata={
|
||||
"work_item_projection_id": "review-v3",
|
||||
"dispatch_hold": "company_runtime_suspended",
|
||||
"suspend_checkpoint_type": "company_runtime_interrupted",
|
||||
"suspended_phase": Phase.RUNNING.value,
|
||||
},
|
||||
)
|
||||
await store.save_delegation_work_item(review_item)
|
||||
review_task = Task(
|
||||
id="review-task-v3",
|
||||
title="Review v3",
|
||||
session_id="sess-review-v3",
|
||||
parent_session_id="sess-parent",
|
||||
status=TaskStatus.RUNNING,
|
||||
project_id="proj1",
|
||||
assigned_to="executor",
|
||||
assigned_external_agent="codex",
|
||||
metadata={
|
||||
"company_profile": "corporate",
|
||||
"execution_model": "multi_team_org",
|
||||
"runtime_model": "multi_team_org",
|
||||
"work_item_runtime": True,
|
||||
"work_item_projection_id": "review-v3",
|
||||
"delegation_run_id": "run-1",
|
||||
"delegation_role_session_id": "role-runtime-1",
|
||||
"company_work_item_plan": serialize_company_work_item_runtime_plan(plan),
|
||||
"company_runtime_stop_state": "suspended",
|
||||
"company_runtime_suspend_checkpoint_type": "company_runtime_interrupted",
|
||||
"company_runtime_suspended_at": datetime.now().isoformat(),
|
||||
"dispatch_hold": "company_runtime_suspended",
|
||||
"suspended_task_status": TaskStatus.RUNNING.value,
|
||||
},
|
||||
)
|
||||
set_linked_work_item_id(review_task, review_item.work_item_id)
|
||||
await store.save_task(review_task)
|
||||
await store.link_work_item_runtime_task(review_item.work_item_id, review_task.id)
|
||||
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
class DummyCompanyExecutor:
|
||||
async def execute(
|
||||
self,
|
||||
resumed_plan: CompanyWorkItemRuntimePlan,
|
||||
tasks: list[Task],
|
||||
) -> str:
|
||||
captured["plan"] = resumed_plan
|
||||
captured["tasks"] = tasks
|
||||
return "runtime resumed"
|
||||
|
||||
engine.company_executor = DummyCompanyExecutor()
|
||||
await engine._maybe_resume_checkpoint(
|
||||
"continue",
|
||||
"sess-parent",
|
||||
reply_metadata={"ui_force_resume": True},
|
||||
)
|
||||
|
||||
resumed_ids = {item.id for item in captured["tasks"]}
|
||||
refreshed_review_item = await store.get_delegation_work_item(
|
||||
review_item.work_item_id
|
||||
)
|
||||
resolved = await store.get_execution_checkpoints(
|
||||
project_id="proj1",
|
||||
session_id="sess-parent",
|
||||
checkpoint_types=["company_runtime_suspended"],
|
||||
statuses=["resolved"],
|
||||
)
|
||||
|
||||
self.assertEqual(resumed_ids, {task.id, review_task.id})
|
||||
assert refreshed_review_item is not None
|
||||
self.assertEqual(refreshed_review_item.metadata.get("dispatch_hold"), "")
|
||||
self.assertEqual(len(resolved), 1)
|
||||
self.assertEqual(
|
||||
set(resolved[0].payload.get("task_ids", [])),
|
||||
{task.id, review_task.id},
|
||||
)
|
||||
self.assertEqual(
|
||||
{
|
||||
snapshot.get("task_id")
|
||||
for snapshot in resolved[0].payload.get("task_snapshots", [])
|
||||
},
|
||||
{task.id, review_task.id},
|
||||
)
|
||||
|
||||
async def test_runtime_refreshes_canonical_adapter_state_before_persisting(self) -> None:
|
||||
store = await self._store()
|
||||
_, task = await self._seed_runtime(store)
|
||||
role_session_id = "role-runtime-1"
|
||||
stale_state = {
|
||||
"selected_execution_agent": "codex",
|
||||
"external_resume_session_id": "thread-terminal",
|
||||
"external_resume_agent_type": "codex",
|
||||
"codex": {
|
||||
"resume_session_id": "thread-terminal",
|
||||
"provider_session_id": "thread-terminal",
|
||||
},
|
||||
}
|
||||
role_session = await store.get_delegation_role_session(role_session_id)
|
||||
assert role_session is not None
|
||||
role_session.adapter_session_state = dict(stale_state)
|
||||
await store.save_delegation_role_session(role_session)
|
||||
|
||||
runtime = CompanyRuntime(org_engine=None, communication=None, store=store)
|
||||
runtime.role_sessions[role_session_id] = role_session
|
||||
member = CompanyMemberSession(
|
||||
member_session_id="member-operations",
|
||||
role_session_id=role_session_id,
|
||||
role_id="executor",
|
||||
employee_id="employee-1",
|
||||
adapter_session_state=dict(stale_state),
|
||||
)
|
||||
await store.update_role_session_adapter_state(
|
||||
role_session_id,
|
||||
"codex",
|
||||
None,
|
||||
)
|
||||
|
||||
await runtime._refresh_adapter_session_state_from_store(member)
|
||||
|
||||
self.assertNotIn("codex", member.adapter_session_state)
|
||||
self.assertNotIn(
|
||||
"external_resume_session_id",
|
||||
member.adapter_session_state,
|
||||
)
|
||||
self.assertEqual(
|
||||
runtime.role_sessions[role_session_id].adapter_session_state,
|
||||
member.adapter_session_state,
|
||||
)
|
||||
|
||||
async def test_second_stop_during_resumed_execution_restores_pending_checkpoint(self) -> None:
|
||||
store = await self._store()
|
||||
_, task = await self._seed_runtime(store)
|
||||
|
||||
@@ -2513,7 +2513,8 @@ class ExternalAgentMonitoringTests(unittest.IsolatedAsyncioTestCase):
|
||||
def test_codex_adapter_resolves_launch_binary_before_spawn(self) -> None:
|
||||
metadata: dict[str, object] = {}
|
||||
|
||||
with patch("opc.layer3_agent.adapters.base.shutil.which", return_value=r"C:\Tools\codex.cmd"):
|
||||
with patch("opc.layer3_agent.adapters.base.os.name", "posix"), \
|
||||
patch("opc.layer3_agent.adapters.base.shutil.which", return_value=r"C:\Tools\codex.cmd"):
|
||||
cmd = CodexAdapter._resolve_launch_command(
|
||||
["codex", "exec", "--json", "-"],
|
||||
launch_metadata=metadata,
|
||||
@@ -2523,6 +2524,39 @@ class ExternalAgentMonitoringTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(metadata["configured_binary"], "codex")
|
||||
self.assertEqual(metadata["resolved_binary"], r"C:\Tools\codex.cmd")
|
||||
|
||||
def test_codex_adapter_wraps_windows_cmd_shim_before_spawn(self) -> None:
|
||||
metadata: dict[str, object] = {}
|
||||
which_results = {
|
||||
"codex": r"D:\nodejs\codex",
|
||||
"cmd.exe": r"C:\Windows\System32\cmd.exe",
|
||||
}
|
||||
|
||||
with patch("opc.layer3_agent.adapters.base.os.name", "nt"), \
|
||||
patch("opc.layer3_agent.adapters.base.os.path.isfile", side_effect=lambda path: path == r"D:\nodejs\codex.cmd"), \
|
||||
patch("opc.layer3_agent.adapters.base.shutil.which", side_effect=lambda name, path=None: which_results.get(name)), \
|
||||
patch.dict(os.environ, {"COMSPEC": r"C:\Windows\System32\cmd.exe"}, clear=False):
|
||||
cmd = CodexAdapter._resolve_launch_command(
|
||||
["codex", "exec", "--json", "-"],
|
||||
launch_metadata=metadata,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
cmd,
|
||||
[
|
||||
r"C:\Windows\System32\cmd.exe",
|
||||
"/d",
|
||||
"/s",
|
||||
"/c",
|
||||
r"D:\nodejs\codex.cmd",
|
||||
"exec",
|
||||
"--json",
|
||||
"-",
|
||||
],
|
||||
)
|
||||
self.assertEqual(metadata["configured_binary"], "codex")
|
||||
self.assertEqual(metadata["resolved_binary"], r"D:\nodejs\codex.cmd")
|
||||
self.assertEqual(metadata["launch_wrapper"], r"C:\Windows\System32\cmd.exe")
|
||||
|
||||
def test_codex_adapter_resolves_launch_binary_with_extra_env_path(self) -> None:
|
||||
with patch("opc.layer3_agent.adapters.base.shutil.which", return_value=r"D:\bin\codex.cmd") as which_mock:
|
||||
CodexAdapter._resolve_launch_command(
|
||||
@@ -2532,6 +2566,40 @@ class ExternalAgentMonitoringTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
self.assertEqual(which_mock.call_args.kwargs["path"], r"D:\bin")
|
||||
|
||||
def test_codex_adapter_forwards_only_openopc_runtime_env_to_shell_policy(self) -> None:
|
||||
metadata: dict[str, object] = {}
|
||||
|
||||
cmd = CodexAdapter._inject_runtime_shell_environment(
|
||||
["codex", "exec", "--json", "-"],
|
||||
{
|
||||
"OPC_ALLOWED_COLLAB_TOOLS": json.dumps(["delegate_work", "inbox"]),
|
||||
"OPC_COLLAB_RPC_TOKEN": "rpc-token",
|
||||
"PYTHONPATH": r"D:\OpenOPC",
|
||||
"UNRELATED_SECRET": "do-not-forward",
|
||||
},
|
||||
launch_metadata=metadata,
|
||||
)
|
||||
|
||||
self.assertEqual(cmd[:2], ["codex", "exec"])
|
||||
config_values = [cmd[index + 1] for index, value in enumerate(cmd[:-1]) if value == "-c"]
|
||||
self.assertIn(
|
||||
"shell_environment_policy.set.OPC_ALLOWED_COLLAB_TOOLS='''delegate_work,inbox'''",
|
||||
config_values,
|
||||
)
|
||||
self.assertIn(
|
||||
"shell_environment_policy.set.OPC_COLLAB_RPC_TOKEN='''rpc-token'''",
|
||||
config_values,
|
||||
)
|
||||
self.assertIn(
|
||||
"shell_environment_policy.set.PYTHONPATH='''D:\\OpenOPC'''",
|
||||
config_values,
|
||||
)
|
||||
self.assertFalse(any("UNRELATED_SECRET" in value for value in config_values))
|
||||
self.assertEqual(
|
||||
metadata["runtime_shell_environment_keys"],
|
||||
["OPC_ALLOWED_COLLAB_TOOLS", "OPC_COLLAB_RPC_TOKEN", "PYTHONPATH"],
|
||||
)
|
||||
|
||||
def test_codex_adapter_full_auto_uses_dangerous_bypass_flag(self) -> None:
|
||||
adapter = CodexAdapter(
|
||||
config=ExternalAgentConfig(command="codex", approval_mode="full-auto")
|
||||
@@ -5242,10 +5310,11 @@ class ExternalAgentMonitoringTests(unittest.IsolatedAsyncioTestCase):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
store = _SessionStoreStub()
|
||||
adapter = _CollabSurfaceScriptAdapter("print('company-mode external run')\n")
|
||||
broker = ExternalAgentBroker(store, _ApprovalStub())
|
||||
from opc.layer2_organization.org_engine import OrgEngine
|
||||
from opc.core.config import OPCConfig
|
||||
broker.org_engine = OrgEngine(OPCConfig(), Path(tmpdir))
|
||||
org_engine = OrgEngine(OPCConfig(), Path(tmpdir))
|
||||
broker = ExternalAgentBroker(store, _ApprovalStub(), org_engine=org_engine)
|
||||
self.assertIs(broker.org_engine, org_engine)
|
||||
task = Task(
|
||||
id="collab-env-ceo",
|
||||
title="collab env ceo",
|
||||
|
||||
@@ -21,6 +21,7 @@ from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
@@ -37,6 +38,7 @@ from opc.core.models import (
|
||||
from opc.database.store import OPCStore
|
||||
from opc.layer3_agent.adapters.base import ExternalAgentAdapter
|
||||
from opc.layer3_agent.external_broker import ExternalAgentBroker
|
||||
from opc.layer3_agent.external_session_identity import select_best_external_resume_session
|
||||
|
||||
|
||||
# ── Store helper unit tests ──────────────────────────────────────────────
|
||||
@@ -97,9 +99,28 @@ class AdapterSessionStateHelperTests(unittest.IsolatedAsyncioTestCase):
|
||||
await self.store.update_role_session_adapter_state(
|
||||
self.sid, "codex", {"resume_session_id": "thread-abc"},
|
||||
)
|
||||
role_session = await self.store.get_delegation_role_session(self.sid)
|
||||
role_session.adapter_session_state.update({
|
||||
"selected_execution_agent": "codex",
|
||||
"external_resume_session_id": "thread-abc",
|
||||
"external_resume_session_scope_id": "sess-a",
|
||||
"external_resume_agent_type": "codex",
|
||||
"resume_source_session": "thread-abc",
|
||||
})
|
||||
await self.store.save_delegation_role_session(role_session)
|
||||
await self.store.update_role_session_adapter_state(self.sid, "codex", None)
|
||||
entry = await self.store.get_role_session_adapter_state(self.sid, "codex")
|
||||
refreshed = await self.store.get_delegation_role_session(self.sid)
|
||||
self.assertIsNone(entry)
|
||||
self.assertEqual(
|
||||
refreshed.adapter_session_state.get("selected_execution_agent"),
|
||||
"codex",
|
||||
)
|
||||
self.assertNotIn(
|
||||
"external_resume_session_id",
|
||||
refreshed.adapter_session_state,
|
||||
)
|
||||
self.assertNotIn("resume_source_session", refreshed.adapter_session_state)
|
||||
|
||||
async def test_missing_session_returns_false(self) -> None:
|
||||
ok = await self.store.update_role_session_adapter_state(
|
||||
@@ -245,6 +266,49 @@ class BrokerPersistWritesRoleStateTests(unittest.IsolatedAsyncioTestCase):
|
||||
)
|
||||
self.assertIsNone(entry)
|
||||
|
||||
async def test_failed_resume_clears_matching_older_role_token_and_task_metadata(self) -> None:
|
||||
await self._persist_run(
|
||||
task_id="task-old",
|
||||
agent_type="codex",
|
||||
resume_session_id="thread-full",
|
||||
)
|
||||
adapter = _MiniAdapter(agent_type="codex")
|
||||
adapter.config.session_mode = "resume"
|
||||
adapter.config.session_id = "thread-full"
|
||||
task = self._task(task_id="task-retry")
|
||||
task.metadata.update({
|
||||
"external_resume_session_id": "thread-full",
|
||||
"external_resume_session_scope_id": "sess-a",
|
||||
"external_resume_agent_type": "codex",
|
||||
})
|
||||
await self.broker._persist_session(
|
||||
adapter=adapter,
|
||||
task=task,
|
||||
workspace_path="/tmp/ws",
|
||||
metadata={
|
||||
"command": "codex exec resume",
|
||||
"model": "(cli default)",
|
||||
"resume_session_id": "thread-full",
|
||||
},
|
||||
result=TaskResult(
|
||||
status=TaskStatus.FAILED,
|
||||
content="context window full",
|
||||
artifacts={"resume_session_id": "thread-full"},
|
||||
),
|
||||
)
|
||||
|
||||
self.assertIsNone(
|
||||
await self.store.get_role_session_adapter_state(
|
||||
self.role_session_id,
|
||||
"codex",
|
||||
)
|
||||
)
|
||||
self.assertNotIn("external_resume_session_id", task.metadata)
|
||||
self.assertEqual(
|
||||
task.metadata.get("external_resume_fallback"),
|
||||
"provider_terminal_failure",
|
||||
)
|
||||
|
||||
async def test_consecutive_tasks_overwrite_with_latest_token(self) -> None:
|
||||
await self._persist_run(
|
||||
task_id="task-1", agent_type="codex", resume_session_id="thread-1",
|
||||
@@ -258,6 +322,34 @@ class BrokerPersistWritesRoleStateTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(entry["resume_session_id"], "thread-2")
|
||||
self.assertEqual(entry["last_task_id"], "task-2")
|
||||
|
||||
def test_newer_failed_row_vetoes_older_done_row_for_same_token(self) -> None:
|
||||
now = datetime.now()
|
||||
older = ExternalSession(
|
||||
agent_type="codex",
|
||||
project_id="proj1",
|
||||
session_id="thread-full",
|
||||
status="done",
|
||||
metadata={"resume_session_id": "thread-full"},
|
||||
updated_at=now - timedelta(minutes=1),
|
||||
)
|
||||
newer = ExternalSession(
|
||||
agent_type="codex",
|
||||
project_id="proj1",
|
||||
session_id="thread-full",
|
||||
status="failed",
|
||||
metadata={"resume_session_id": "thread-full"},
|
||||
updated_at=now,
|
||||
)
|
||||
|
||||
selected, token = select_best_external_resume_session(
|
||||
[older, newer],
|
||||
agent_type="codex",
|
||||
project_id="proj1",
|
||||
)
|
||||
|
||||
self.assertIsNone(selected)
|
||||
self.assertEqual(token, "")
|
||||
|
||||
async def test_per_agent_isolation_codex_claude_opencode(self) -> None:
|
||||
# All three external adapters land in the same broker path. Each
|
||||
# gets its own slot under adapter_session_state keyed by agent_type.
|
||||
@@ -430,6 +522,55 @@ class BrokerRestorePrefersRoleStateTests(unittest.IsolatedAsyncioTestCase):
|
||||
"codex",
|
||||
))
|
||||
|
||||
async def test_restore_rejects_terminal_normal_role_token_even_when_preconfigured(self) -> None:
|
||||
token = "thread-terminal"
|
||||
await self.store.update_role_session_adapter_state(
|
||||
self.role_session_id,
|
||||
"codex",
|
||||
{
|
||||
"resume_session_id": token,
|
||||
"provider_session_id": token,
|
||||
"last_task_id": "task-old",
|
||||
},
|
||||
)
|
||||
await self.store.save_external_session(ExternalSession(
|
||||
agent_type="codex",
|
||||
project_id="proj1",
|
||||
session_id=token,
|
||||
opc_session_id=self.role_session_id,
|
||||
task_id="task-failed",
|
||||
workspace_path="/tmp/ws",
|
||||
run_mode="exec",
|
||||
status="failed",
|
||||
metadata={
|
||||
"resume_session_id": token,
|
||||
"provider_session_id": token,
|
||||
},
|
||||
))
|
||||
adapter = _MiniAdapter(agent_type="codex", can_resume_blank=False)
|
||||
adapter.config.session_mode = "resume"
|
||||
adapter.config.session_id = token
|
||||
task = self._task()
|
||||
task.metadata.update({
|
||||
"external_resume_session_id": token,
|
||||
"external_resume_session_scope_id": "sess-a",
|
||||
"external_resume_agent_type": "codex",
|
||||
})
|
||||
|
||||
await self.broker._restore_session_resume_from_store(adapter, task)
|
||||
|
||||
self.assertEqual(adapter.config.session_mode, "new")
|
||||
self.assertEqual(adapter.config.session_id, "")
|
||||
self.assertIsNone(await self.store.get_role_session_adapter_state(
|
||||
self.role_session_id,
|
||||
"codex",
|
||||
))
|
||||
self.assertNotIn("external_resume_session_id", task.metadata)
|
||||
self.assertEqual(
|
||||
task.metadata.get("external_resume_fallback"),
|
||||
"provider_terminal_failure",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -164,6 +164,32 @@ def _make_review_task(
|
||||
|
||||
|
||||
class VerdictParseRetryTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_terminal_failed_review_card_can_record_parse_failure_outcome(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
root = Path(tmpdir)
|
||||
store = OPCStore(root / "tasks.db")
|
||||
await store.initialize()
|
||||
try:
|
||||
executor = _build_executor(store, _make_org_engine(root))
|
||||
review = _make_review_card(review_card_id="review-failed")
|
||||
review.phase = Phase.FAILED
|
||||
await store.save_delegation_work_item(review)
|
||||
|
||||
persisted = await executor._persist_terminal_review_card(
|
||||
"review-failed",
|
||||
phase=Phase.CANCELLED,
|
||||
outcome="verdict_parse_failed",
|
||||
)
|
||||
|
||||
self.assertIsNotNone(persisted)
|
||||
self.assertEqual(persisted.phase, Phase.FAILED)
|
||||
self.assertEqual(
|
||||
persisted.metadata.get("review_work_item_outcome"),
|
||||
"verdict_parse_failed",
|
||||
)
|
||||
finally:
|
||||
await store.close()
|
||||
|
||||
async def test_unparseable_verdict_spawns_retry_with_hint(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
root = Path(tmpdir)
|
||||
|
||||
Reference in New Issue
Block a user