copilot: rename FixContext -> CopilotContext + additive Build fields

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Charles YaoandClaude Opus 4.8 committed 2026-08-22 21:24:48 -07:00
1 parent 32077736ba
commit 4be0e77ade
23 files changed
+195 -148

No files matched your search

+14 -14
View File
@@ -41,7 +41,7 @@ from core.workflow_copilot.models import (
ChangeSet,
ChecklistError,
ConversationItem,
FixContext,
CopilotContext,
NodeOutput,
Run,
Session,
@@ -76,7 +76,7 @@ __all__ = [
# ---- helpers ---------------------------------------------------------------
def append_item(fc: FixContext, kind: str, payload: dict[str, Any]) -> list[ConversationItem]:
def append_item(fc: CopilotContext, kind: str, payload: dict[str, Any]) -> list[ConversationItem]:
"""Append a conversation item stamped with the next sequence number.
``fc.next_seq`` is the seq authority (port of ``handlers_fix.go:9-13``):
@@ -90,7 +90,7 @@ def append_item(fc: FixContext, kind: str, payload: dict[str, Any]) -> list[Conv
return [item]
def append_card(fc: FixContext, card) -> list[ConversationItem]:
def append_card(fc: CopilotContext, card) -> list[ConversationItem]:
"""Stamp a typed card into the conversation at the next seq (mirrors
append_item's seq authority; at_version preserved as the prior code's 0).
"""
@@ -123,7 +123,7 @@ def _mode_or_default(mode: str) -> str:
# ---- handlers ---------------------------------------------------------------
def handle_diagnose(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResult:
def handle_diagnose(env: Env, turn: Turn, s: Session, fc: CopilotContext) -> StepResult:
"""(working) Read the failed run's node outputs, diagnose, capture the
pre-repair checkpoint. Port of ``handlers_fix.go:38``."""
failed = env.repo.get_run(fc.failed_run_id)
@@ -153,7 +153,7 @@ def handle_diagnose(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepRes
)
def handle_propose(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResult:
def handle_propose(env: Env, turn: Turn, s: Session, fc: CopilotContext) -> StepResult:
"""(working) Ask the agent for repair intents + risk, stage them, branch
on risk. Port of ``handlers_fix.go:97``. Also serves as
``checklist.propose`` (Task 7 reuses this handler, as Go does)."""
@@ -179,7 +179,7 @@ def handle_propose(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResu
return StepResult(next=next_state, context=fc, items=items)
def handle_await_approval(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResult:
def handle_await_approval(env: Env, turn: Turn, s: Session, fc: CopilotContext) -> StepResult:
"""(waiting) High-risk gate. Only an explicit ``approve_repair`` advances
to ``fix.apply``; any other/absent action kind is a no-op that stays at
the gate. Port of ``handlers_fix.go:123``."""
@@ -193,7 +193,7 @@ def handle_await_approval(env: Env, turn: Turn, s: Session, fc: FixContext) -> S
return StepResult(next=PcState.FIX_AWAIT_APPROVAL, context=fc)
def handle_apply(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResult:
def handle_apply(env: Env, turn: Turn, s: Session, fc: CopilotContext) -> StepResult:
"""(working) Apply the staged repair to the real draft. The next state
branches on ``fc.source``: the run-fix path always lands at
``fix.await_verify``; the checklist-fix path lands at
@@ -222,7 +222,7 @@ def handle_apply(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResult
return StepResult(next=next_state, context=fc, items=items)
def handle_await_verify(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResult:
def handle_await_verify(env: Env, turn: Turn, s: Session, fc: CopilotContext) -> StepResult:
"""(waiting) The user runs verification or undoes. Port of
``handlers_fix.go:163``."""
if turn.action is not None and turn.action.kind == "undo":
@@ -234,7 +234,7 @@ def handle_await_verify(env: Env, turn: Turn, s: Session, fc: FixContext) -> Ste
return StepResult(next=PcState.FIX_VERIFY, context=fc)
def handle_await_testdata(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResult:
def handle_await_testdata(env: Env, turn: Turn, s: Session, fc: CopilotContext) -> StepResult:
"""(waiting) Prepare inputs for the verify run. Port of
``handlers_fix.go:176``."""
mode, _ = action_string(turn, "mode")
@@ -254,7 +254,7 @@ def handle_await_testdata(env: Env, turn: Turn, s: Session, fc: FixContext) -> S
return StepResult(next=PcState.FIX_VERIFY, context=fc)
def handle_verify(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResult:
def handle_verify(env: Env, turn: Turn, s: Session, fc: CopilotContext) -> StepResult:
"""(working) Run the repaired draft, mint a NEW immutable Run. Advances
to ``fix.await_decision`` on both pass and fail; never touches the
original failed run. Port of ``handlers_fix.go:206``."""
@@ -304,7 +304,7 @@ def handle_verify(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResul
)
def handle_await_decision(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResult:
def handle_await_decision(env: Env, turn: Turn, s: Session, fc: CopilotContext) -> StepResult:
"""(waiting) Terminal choice. Port of ``handlers_fix.go:239``."""
kind = turn.action.kind if turn.action is not None else ""
if kind == "publish":
@@ -329,7 +329,7 @@ def handle_await_decision(env: Env, turn: Turn, s: Session, fc: FixContext) -> S
return StepResult(next=PcState.SUCCESS, context=fc, items=items)
def handle_publish(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResult:
def handle_publish(env: Env, turn: Turn, s: Session, fc: CopilotContext) -> StepResult:
"""(working) Publish the repaired workflow. Port of
``handlers_fix.go:316``."""
env.dify.publish(s.app_id, turn.actor)
@@ -340,7 +340,7 @@ def handle_publish(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResu
# ---- checklist handlers -----------------------------------------------------
def handle_checklist_diagnose(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResult:
def handle_checklist_diagnose(env: Env, turn: Turn, s: Session, fc: CopilotContext) -> StepResult:
"""(working) Mirrors ``handle_diagnose`` but diagnoses from the
pre-publish checklist's config errors instead of a failed run's node
outputs — there is no ``fc.failed_run_id`` on this path, so no run
@@ -370,7 +370,7 @@ def handle_checklist_diagnose(env: Env, turn: Turn, s: Session, fc: FixContext)
)
def handle_await_recheck(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResult:
def handle_await_recheck(env: Env, turn: Turn, s: Session, fc: CopilotContext) -> StepResult:
"""(waiting) The checklist-fix counterpart of ``handle_await_verify``:
instead of running the workflow, the caller re-runs the pre-publish
checklist and reports the result via a ``"recheck"`` action. On ``undo``
+19 -4
View File
@@ -14,13 +14,13 @@ Deltas from the Go source (per the P1 port plan's Global Constraints / ADR):
Go test suite constructs these structs: almost every struct literal in
``*_test.go`` sets only a handful of fields and relies on Go's implicit
zero-initialization for the rest (e.g. ``&Session{AppID: "app", ...}``,
``&FixContext{}``, ``&FixContext{FailedRunID: "TR-1"}``). The excepted
``&CopilotContext{}``, ``&CopilotContext{FailedRunID: "TR-1"}``). The excepted
fields have no meaningful "empty" value (an empty string is not a valid
``PcState``/``EntryMode`` member) and are always set explicitly in the Go
suite, so they stay required here.
- ``Diagnosis`` / ``Risk`` / ``MutationIntent`` / ``ChangeSet`` / ``NodeEvent``
/ ``ApplyResult`` (Go: ``ports.go``) live here rather than in ``ports.py``.
``FixContext`` embeds ``Diagnosis``/``Risk``/``ChangeSet``/
``CopilotContext`` embeds ``Diagnosis``/``Risk``/``ChangeSet``/
``MutationIntent`` directly, and defining them in ``ports.py`` (which
imports the rest of these models) would create a models<->ports import
cycle; ``ports.py`` imports them back out of this module instead.
@@ -205,9 +205,16 @@ class ApplyResult:
@dataclass(kw_only=True)
class FixContext:
"""Per-session working state persisted as the commit context."""
class CopilotContext:
"""Per-session working state persisted as the commit context.
One context type for all entry modes (Fix, checklist, Build, Edit).
Fix-specific and Build-specific fields coexist; each scenario touches
only its own, all default-valued (serialized as a JSON blob -- no
migration on rename or on adding fields).
"""
# -- Fix / checklist fields --
failed_run_id: str = ""
diagnosis: Diagnosis | None = None
staged_repair: list[MutationIntent] = field(default_factory=list)
@@ -222,6 +229,14 @@ class FixContext:
source: str = ""
checklist_errors: list[ChecklistError] = field(default_factory=list)
# -- Build fields (Slice 2, additive) --
goal_text: str = ""
requirements: dict[str, Any] = field(default_factory=dict)
plan_items: list[str] = field(default_factory=list)
plan_version_tag: str = ""
resource_selection: dict[str, Any] = field(default_factory=dict)
built_node_ids: list[str] = field(default_factory=list)
@dataclass(kw_only=True)
class Action:
+5 -5
View File
@@ -11,7 +11,7 @@ Deltas from the Go source (per the P1 port plan's Global Constraints / ADR):
- The value types Go defines in this file (``Diagnosis``, ``Risk``,
``MutationIntent``, ``ChangeSet``, ``NodeEvent``, ``ApplyResult``) live in
``models.py`` instead, alongside the rest of the domain dataclasses —
``FixContext`` embeds several of them directly, and defining them here
``CopilotContext`` embeds several of them directly, and defining them here
(where the models are imported) would create a models<->ports import
cycle. They are re-exported from here for convenience.
- ``interface`` becomes ``typing.Protocol``, marked ``@runtime_checkable`` so
@@ -29,8 +29,8 @@ from core.workflow_copilot.models import (
ChecklistError,
Checkpoint,
ConversationItem,
CopilotContext,
Diagnosis,
FixContext,
Graph,
Inputs,
MutationIntent,
@@ -109,16 +109,16 @@ class Repository(Protocol):
``base_version``, else raises ``ConflictError``.
"""
def create_session(self, session: Session, initial_fc: FixContext, items: list[ConversationItem]) -> None: ...
def create_session(self, session: Session, initial_fc: CopilotContext, items: list[ConversationItem]) -> None: ...
def get_session(self, id: str) -> tuple[Session, FixContext]: ...
def get_session(self, id: str) -> tuple[Session, CopilotContext]: ...
def compare_and_advance(
self,
session_id: str,
base_version: int,
next: PcState,
fc: FixContext,
fc: CopilotContext,
items: list[ConversationItem],
) -> int: ...
+3 -3
View File
@@ -27,7 +27,7 @@ from core.workflow_copilot.errors import ConflictError
from core.workflow_copilot.models import (
Checkpoint,
ConversationItem,
FixContext,
CopilotContext,
NodeEvent,
Run,
Session,
@@ -61,14 +61,14 @@ class Env:
@dataclass
class StepResult:
next: PcState
context: FixContext | None = None
context: CopilotContext | None = None
items: list[ConversationItem] = field(default_factory=list)
checkpoint: Snapshot | None = None # if set, persist a checkpoint from this snapshot before advancing
run: Run | None = None # if set, persist this run before advancing
run_id_sink: list[str] | None = None # internal: runner writes the saved run id to run_id_sink[0]
Handler = Callable[[Env, Turn, Session, FixContext], StepResult]
Handler = Callable[[Env, Turn, Session, CopilotContext], StepResult]
class Runner:
+1 -1
View File
@@ -46,7 +46,7 @@ class CopilotSessionCommit(Base):
Copilot Session Commit Model
One immutable row per session version, carrying the serialized
`FixContext` (`context`) as of that version. The latest commit by
`CopilotContext` (`context`) as of that version. The latest commit by
`version` for a given `session_id` is the session's current context.
"""
+8 -8
View File
@@ -30,8 +30,8 @@ from core.workflow_copilot.errors import ConflictError, NotFoundError
from core.workflow_copilot.models import (
Checkpoint,
ConversationItem,
CopilotContext,
EntryMode,
FixContext,
NodeOutput,
Run,
Snapshot,
@@ -50,7 +50,7 @@ from models.workflow_copilot import (
CopilotSnapshot,
CopilotTestInput,
)
from services.workflow_copilot.serde import fix_context_from_dict, fix_context_to_dict
from services.workflow_copilot.serde import context_from_dict, context_to_dict
__all__ = ["SqlCopilotRepository"]
@@ -63,7 +63,7 @@ class SqlCopilotRepository:
# -- sessions --
def create_session(self, session: DomainSession, initial_fc: FixContext, items: list[ConversationItem]) -> None:
def create_session(self, session: DomainSession, initial_fc: CopilotContext, items: list[ConversationItem]) -> None:
with self._session_factory() as db_session, db_session.begin():
row = CopilotSession(
app_id=session.app_id,
@@ -93,12 +93,12 @@ class SqlCopilotRepository:
session_id=row.id,
version=1,
state=str(session.current_state),
context=fix_context_to_dict(initial_fc),
context=context_to_dict(initial_fc),
actor="",
)
)
def get_session(self, id: str) -> tuple[DomainSession, FixContext]:
def get_session(self, id: str) -> tuple[DomainSession, CopilotContext]:
with self._session_factory() as db_session, db_session.begin():
row = db_session.get(CopilotSession, id)
if row is None:
@@ -114,7 +114,7 @@ class SqlCopilotRepository:
if commit is None:
raise NotFoundError(f"no commits found for session {id}")
fc = fix_context_from_dict(commit.context)
fc = context_from_dict(commit.context)
return self._to_domain_session(row), fc
def compare_and_advance(
@@ -122,7 +122,7 @@ class SqlCopilotRepository:
session_id: str,
base_version: int,
next: PcState,
fc: FixContext,
fc: CopilotContext,
items: list[ConversationItem],
) -> int:
new_version = base_version + 1
@@ -145,7 +145,7 @@ class SqlCopilotRepository:
session_id=session_id,
version=new_version,
state=str(next),
context=fix_context_to_dict(fc),
context=context_to_dict(fc),
actor="",
)
)
+1 -1
View File
@@ -47,7 +47,7 @@ def map_run_result(data: Mapping[str, Any], node_execs: Sequence[Any]) -> Run:
"""Map a blocking ``AppGenerateService.generate`` result into a ``Run``.
``Run.id`` is deliberately left ``""`` — the caller (``handle_verify``)
assigns the id and sets ``FixContext.verify_run_id``, not this function.
assigns the id and sets ``CopilotContext.verify_run_id``, not this function.
"""
if _is_paused_shape(data):
status = "failed"
+25 -20
View File
@@ -1,11 +1,13 @@
"""Pure JSON (de)serialization for the domain ``FixContext``.
"""Pure JSON (de)serialization for the domain ``CopilotContext``.
``FixContext`` (see ``core.workflow_copilot.models``) is the per-session
``CopilotContext`` (see ``core.workflow_copilot.models``) is the per-session
working state persisted, one row per version, as the ``context`` JSONB
column on ``workflow_copilot_session_commits`` (P3a Task 4). This module
has no I/O and no SQLAlchemy dependency -- it only converts between the
dataclass and a JSON-safe ``dict``, so the SQL repository can hand the dict
straight to an ``AdjustedJSON`` column and get it back unchanged.
column on ``workflow_copilot_session_commits``. This module has no I/O and
no SQLAlchemy dependency -- it only converts between the dataclass and a
JSON-safe ``dict``, so the SQL repository can hand the dict straight to an
``AdjustedJSON`` column and get it back unchanged. The on-disk key names are
unchanged from the pre-rename dataclass form so existing rows deserialize
as-is.
"""
from dataclasses import asdict
@@ -14,34 +16,31 @@ from typing import Any
from core.workflow_copilot.models import (
ChangeSet,
ChecklistError,
CopilotContext,
Diagnosis,
FixContext,
MutationIntent,
Risk,
)
def fix_context_to_dict(fc: FixContext) -> dict[str, Any]:
"""Serialize a ``FixContext`` to a JSON-safe dict.
def context_to_dict(fc: CopilotContext) -> dict[str, Any]:
"""Serialize a ``CopilotContext`` to a JSON-safe dict.
``dataclasses.asdict`` recurses into the nested dataclasses
(``Diagnosis``, ``MutationIntent``, ``Risk``, ``ChangeSet``,
``ChecklistError``), turning each into a plain dict, and every field on
``FixContext`` and its nested dataclasses is already a
``ChecklistError``), turning each into a plain dict; every field is a
str/int/bool/list/dict/None -- so the result is JSON-safe as-is.
"""
return asdict(fc)
def fix_context_from_dict(d: dict[str, Any]) -> FixContext:
"""Rebuild a ``FixContext`` from a dict produced by ``fix_context_to_dict``.
def context_from_dict(d: dict[str, Any]) -> CopilotContext:
"""Rebuild a ``CopilotContext`` from a dict produced by ``context_to_dict``.
Reconstructs the nested dataclasses explicitly rather than splatting
``d`` into ``FixContext(**d)`` -- the nested values are plain dicts (and
lists of dicts) after a JSON round-trip and must become the right
dataclass instances. Every lookup is defensive (``.get`` with a default
matching the ``FixContext``/nested-dataclass field default) so an
older or partial dict still yields a valid ``FixContext``.
Reconstructs the nested dataclasses explicitly (they are plain dicts
after a JSON round-trip). Every lookup is defensive (``.get`` with a
default matching the field default) so an older or partial dict still
yields a valid ``CopilotContext``.
"""
diagnosis = Diagnosis(**d["diagnosis"]) if d.get("diagnosis") else None
staged_repair = [MutationIntent(**m) for m in d.get("staged_repair", [])]
@@ -49,7 +48,7 @@ def fix_context_from_dict(d: dict[str, Any]) -> FixContext:
change_set = ChangeSet(**d["change_set"]) if d.get("change_set") else None
checklist_errors = [ChecklistError(**e) for e in d.get("checklist_errors", [])]
return FixContext(
return CopilotContext(
failed_run_id=d.get("failed_run_id", ""),
diagnosis=diagnosis,
staged_repair=staged_repair,
@@ -62,4 +61,10 @@ def fix_context_from_dict(d: dict[str, Any]) -> FixContext:
next_seq=d.get("next_seq", 0),
source=d.get("source", ""),
checklist_errors=checklist_errors,
goal_text=d.get("goal_text", ""),
requirements=dict(d.get("requirements") or {}),
plan_items=list(d.get("plan_items") or []),
plan_version_tag=d.get("plan_version_tag", ""),
resource_selection=dict(d.get("resource_selection") or {}),
built_node_ids=list(d.get("built_node_ids") or []),
)
+3 -3
View File
@@ -21,8 +21,8 @@ from core.workflow_copilot.models import (
Actor,
ChecklistError,
ConversationItem,
CopilotContext,
EntryMode,
FixContext,
Run,
Session,
)
@@ -223,7 +223,7 @@ class WorkflowCopilotService:
failed_run: Run | None = None
if checklist_errors:
entry_mode, state = EntryMode.FIX_CHECKLIST, PcState.CHECKLIST_DIAGNOSE
fc = FixContext(source="checklist", checklist_errors=checklist_errors)
fc = CopilotContext(source="checklist", checklist_errors=checklist_errors)
else:
entry_mode, state = EntryMode.FIX, PcState.FIX_DIAGNOSE
failed_run = Run(
@@ -233,7 +233,7 @@ class WorkflowCopilotService:
status="failed",
immutable=True,
)
fc = FixContext(failed_run_id=failed_run.id, source="run")
fc = CopilotContext(failed_run_id=failed_run.id, source="run")
s = Session(
app_id=app_id,
@@ -42,8 +42,8 @@ from core.workflow_copilot.models import (
ChecklistError,
Checkpoint,
ConversationItem,
CopilotContext,
Diagnosis,
FixContext,
Graph,
Inputs,
MutationIntent,
@@ -75,7 +75,7 @@ class InMemoryRepository:
def __init__(self) -> None:
self._lock = threading.Lock()
self._sessions: dict[str, Session] = {}
self._contexts: dict[str, FixContext] = {}
self._contexts: dict[str, CopilotContext] = {}
self._commits: dict[str, list[int]] = {}
self._checkpoints: dict[str, Checkpoint] = {}
self._snapshots: dict[str, Snapshot] = {}
@@ -86,7 +86,7 @@ class InMemoryRepository:
# -- sessions --
def create_session(self, session: Session, initial_fc: FixContext, items: list[ConversationItem]) -> None:
def create_session(self, session: Session, initial_fc: CopilotContext, items: list[ConversationItem]) -> None:
with self._lock:
if not session.id:
session.id = str(uuid.uuid4())
@@ -109,7 +109,7 @@ class InMemoryRepository:
self._items[session.id] = copy.deepcopy(items)
self._used_seqs[session.id] = seqs
def get_session(self, id: str) -> tuple[Session, FixContext]:
def get_session(self, id: str) -> tuple[Session, CopilotContext]:
with self._lock:
session = self._sessions.get(id)
if session is None:
@@ -121,7 +121,7 @@ class InMemoryRepository:
session_id: str,
base_version: int,
next: PcState,
fc: FixContext,
fc: CopilotContext,
items: list[ConversationItem],
) -> int:
with self._lock:
@@ -16,9 +16,9 @@ from core.workflow_copilot.models import (
Actor,
Checkpoint,
ConversationItem,
CopilotContext,
Diagnosis,
EntryMode,
FixContext,
MutationIntent,
Run,
Session,
@@ -62,7 +62,7 @@ def test_create_session_assigns_id_and_starts_at_version_1():
repo = InMemoryRepository()
s = _session()
repo.create_session(s, FixContext(), [])
repo.create_session(s, CopilotContext(), [])
assert s.id != ""
assert s.version == 1
@@ -75,7 +75,7 @@ def test_create_session_assigns_id_and_starts_at_version_1():
def test_create_session_sets_next_seq_to_seed_item_count():
repo = InMemoryRepository()
s = _session()
fc = FixContext()
fc = CopilotContext()
items = [
ConversationItem(seq=0, kind="run-context", at_version=1),
ConversationItem(seq=1, kind="user", at_version=1),
@@ -93,7 +93,7 @@ def test_create_session_sets_next_seq_to_seed_item_count():
def test_create_session_does_not_downgrade_an_already_larger_next_seq():
repo = InMemoryRepository()
s = _session()
fc = FixContext(next_seq=5)
fc = CopilotContext(next_seq=5)
repo.create_session(s, fc, [])
@@ -109,7 +109,7 @@ def test_create_session_rejects_duplicate_seq_within_seed_items():
]
with pytest.raises(ConflictError):
repo.create_session(s, FixContext(), items)
repo.create_session(s, CopilotContext(), items)
# ---- compare_and_advance: version CAS -------------------------------------
@@ -118,10 +118,10 @@ def test_create_session_rejects_duplicate_seq_within_seed_items():
def test_compare_and_advance_rejects_stale_base_version():
repo = InMemoryRepository()
s = _session()
repo.create_session(s, FixContext(), [])
repo.create_session(s, CopilotContext(), [])
with pytest.raises(ConflictError):
repo.compare_and_advance(s.id, 0, PcState.FIX_PROPOSE, FixContext(), [])
repo.compare_and_advance(s.id, 0, PcState.FIX_PROPOSE, CopilotContext(), [])
# Nothing applied.
stored, _ = repo.get_session(s.id)
@@ -132,13 +132,13 @@ def test_compare_and_advance_rejects_stale_base_version():
def test_compare_and_advance_accepts_current_version_and_bumps_it():
repo = InMemoryRepository()
s = _session()
repo.create_session(s, FixContext(), [])
repo.create_session(s, CopilotContext(), [])
new_version = repo.compare_and_advance(
s.id,
1,
PcState.FIX_PROPOSE,
FixContext(diagnosis=Diagnosis(culprit_node_id="n1")),
CopilotContext(diagnosis=Diagnosis(culprit_node_id="n1")),
[],
)
@@ -154,15 +154,15 @@ def test_compare_and_advance_on_unknown_session_raises_not_found():
repo = InMemoryRepository()
with pytest.raises(NotFoundError):
repo.compare_and_advance("nope", 1, PcState.FIX_PROPOSE, FixContext(), [])
repo.compare_and_advance("nope", 1, PcState.FIX_PROPOSE, CopilotContext(), [])
def test_compare_and_advance_deep_copies_the_stored_context():
repo = InMemoryRepository()
s = _session()
repo.create_session(s, FixContext(), [])
repo.create_session(s, CopilotContext(), [])
fc = FixContext(staged_repair=[MutationIntent(op="set_node_config")])
fc = CopilotContext(staged_repair=[MutationIntent(op="set_node_config")])
repo.compare_and_advance(s.id, 1, PcState.FIX_APPLY, fc, [])
# Mutating the caller's fc after the commit must not affect stored state.
@@ -179,7 +179,7 @@ def test_compare_and_advance_deep_copies_the_stored_context():
def test_compare_and_advance_rejects_duplicate_seq_within_one_batch():
repo = InMemoryRepository()
s = _session()
repo.create_session(s, FixContext(), [])
repo.create_session(s, CopilotContext(), [])
dup_items = [
ConversationItem(seq=0, kind="a", at_version=2),
@@ -187,7 +187,7 @@ def test_compare_and_advance_rejects_duplicate_seq_within_one_batch():
]
with pytest.raises(ConflictError):
repo.compare_and_advance(s.id, 1, PcState.FIX_PROPOSE, FixContext(), dup_items)
repo.compare_and_advance(s.id, 1, PcState.FIX_PROPOSE, CopilotContext(), dup_items)
# Nothing applied: neither version nor conversation items advanced.
stored, _ = repo.get_session(s.id)
@@ -198,14 +198,14 @@ def test_compare_and_advance_rejects_duplicate_seq_within_one_batch():
def test_compare_and_advance_rejects_seq_reused_from_a_previous_commit():
repo = InMemoryRepository()
s = _session()
repo.create_session(s, FixContext(), [ConversationItem(seq=0, kind="run-context", at_version=1)])
repo.create_session(s, CopilotContext(), [ConversationItem(seq=0, kind="run-context", at_version=1)])
with pytest.raises(ConflictError):
repo.compare_and_advance(
s.id,
1,
PcState.FIX_PROPOSE,
FixContext(),
CopilotContext(),
[ConversationItem(seq=0, kind="user", at_version=2)],
)
@@ -217,13 +217,13 @@ def test_compare_and_advance_rejects_seq_reused_from_a_previous_commit():
def test_compare_and_advance_accepts_the_next_seq_in_order():
repo = InMemoryRepository()
s = _session()
repo.create_session(s, FixContext(), [ConversationItem(seq=0, kind="run-context", at_version=1)])
repo.create_session(s, CopilotContext(), [ConversationItem(seq=0, kind="run-context", at_version=1)])
repo.compare_and_advance(
s.id,
1,
PcState.FIX_PROPOSE,
FixContext(),
CopilotContext(),
[ConversationItem(seq=1, kind="user", at_version=2)],
)
@@ -24,8 +24,8 @@ from core.workflow_copilot.models import (
Actor,
ChecklistError,
ConversationItem,
CopilotContext,
EntryMode,
FixContext,
Run,
Session,
Turn,
@@ -66,12 +66,12 @@ def _fix_session(**overrides) -> Session:
def _seed_fix_session(repo: InMemoryRepository) -> Session:
"""Seed a session at fix.diagnose with a run-context item and a
FixContext pointing at a pre-existing failed run. The Fix flow diagnoses
CopilotContext pointing at a pre-existing failed run. The Fix flow diagnoses
from this already-ingested "red" run; it never re-runs it."""
s = _fix_session()
repo.create_session(
s,
FixContext(failed_run_id="TR-1"),
CopilotContext(failed_run_id="TR-1"),
[ConversationItem(kind="run-context", seq=0, payload={"failed_run_id": "TR-1"})],
)
repo.save_run(s.id, Run(id="TR-1", kind="original-failed", status="failed", immutable=True))
@@ -219,7 +219,7 @@ def _seed_checklist_session(repo: InMemoryRepository, errors: list[ChecklistErro
s = _fix_session(entry_mode=EntryMode.FIX_CHECKLIST, current_state=PcState.CHECKLIST_DIAGNOSE)
repo.create_session(
s,
FixContext(source="checklist", checklist_errors=errors),
CopilotContext(source="checklist", checklist_errors=errors),
[ConversationItem(kind="run-context", seq=0)],
)
return s
@@ -24,8 +24,8 @@ from core.workflow_copilot.models import (
Actor,
ChecklistError,
ConversationItem,
CopilotContext,
EntryMode,
FixContext,
Run,
Session,
Turn,
@@ -81,7 +81,7 @@ def _run_fix_registry() -> dict[PcState, object]:
def _seed_diagnose_session(repo: InMemoryRepository) -> Session:
s = _session()
repo.create_session(s, FixContext(failed_run_id="TR-1"), [ConversationItem(kind="run-context", seq=0)])
repo.create_session(s, CopilotContext(failed_run_id="TR-1"), [ConversationItem(kind="run-context", seq=0)])
repo.save_run(s.id, Run(id="TR-1", kind="original-failed", status="failed", immutable=True))
return s
@@ -374,7 +374,7 @@ def _seed_checklist_session(repo: InMemoryRepository, errors: list[ChecklistErro
s = _session(entry_mode=EntryMode.FIX_CHECKLIST, current_state=PcState.CHECKLIST_DIAGNOSE)
repo.create_session(
s,
FixContext(source="checklist", checklist_errors=errors),
CopilotContext(source="checklist", checklist_errors=errors),
[ConversationItem(kind="run-context", seq=0)],
)
return s
@@ -6,7 +6,7 @@ these structs (see e.g. handlers_fix_test.go, runner_test.go): almost every
struct literal in that suite sets only a handful of fields and relies on
Go's implicit zero-initialization for the rest (``&Session{AppID: "app",
TenantID: "t", OwnerAccountID: "u", EntryMode: EntryModeFix, CurrentState:
FixDiagnose}``, ``&FixContext{}``, ``&FixContext{FailedRunID: "TR-1"}``).
FixDiagnose}``, ``&CopilotContext{}``, ``&CopilotContext{FailedRunID: "TR-1"}``).
These tests port that same terse-construction ergonomics and assert the
Python defaults match the Go zero values field-for-field.
"""
@@ -19,8 +19,8 @@ from core.workflow_copilot.models import (
ChecklistError,
Checkpoint,
ConversationItem,
CopilotContext,
EntryMode,
FixContext,
NodeOutput,
Run,
Session,
@@ -162,8 +162,8 @@ def test_conversation_item_defaults():
def test_fix_context_round_trip_defaults_empty():
# Mirrors runner_test.go: repo.CreateSession(..., &FixContext{}, nil)
fc = FixContext()
# Mirrors runner_test.go: repo.CreateSession(..., &CopilotContext{}, nil)
fc = CopilotContext()
assert fc.failed_run_id == ""
assert fc.diagnosis is None
assert fc.staged_repair == []
@@ -179,14 +179,14 @@ def test_fix_context_round_trip_defaults_empty():
def test_fix_context_partial_construction():
# Mirrors handlers_fix_test.go: &FixContext{FailedRunID: "TR-1"}
fc = FixContext(failed_run_id="TR-1")
# Mirrors handlers_fix_test.go: &CopilotContext{FailedRunID: "TR-1"}
fc = CopilotContext(failed_run_id="TR-1")
assert fc.failed_run_id == "TR-1"
assert fc.source == ""
# Mirrors handlers_fix_test.go: &FixContext{Source: "checklist", ChecklistErrors: errs}
# Mirrors handlers_fix_test.go: &CopilotContext{Source: "checklist", ChecklistErrors: errs}
errs = [ChecklistError(node_id="n1", unconnected=True)]
fc2 = FixContext(source="checklist", checklist_errors=errs)
fc2 = CopilotContext(source="checklist", checklist_errors=errs)
assert fc2.source == "checklist"
assert fc2.checklist_errors == errs
assert fc2.failed_run_id == ""
@@ -14,8 +14,8 @@ from core.workflow_copilot.models import (
Actor,
ApplyResult,
ChangeSet,
CopilotContext,
Diagnosis,
FixContext,
MutationIntent,
NodeEvent,
Risk,
@@ -86,7 +86,7 @@ def test_apply_result_defaults_and_changed_nodes_not_shared():
def test_fix_context_embeds_the_tightened_value_types():
fc = FixContext(
fc = CopilotContext(
diagnosis=Diagnosis(culprit_node_id="n1"),
staged_repair=[MutationIntent(op="set_node_config")],
risk=Risk(level="low"),
@@ -10,7 +10,7 @@ from datetime import datetime
import pytest
from core.workflow_copilot.errors import ConflictError
from core.workflow_copilot.models import Action, Actor, EntryMode, FixContext, Session, Turn
from core.workflow_copilot.models import Action, Actor, CopilotContext, EntryMode, Session, Turn
from core.workflow_copilot.runner import Env, Runner, StepResult
from core.workflow_copilot.state import PcState
from tests.unit_tests.core.workflow_copilot.fakes import FakeDifyPort, InMemoryRepository, StubAgent
@@ -36,7 +36,7 @@ def _toy_registry() -> dict[PcState, object]:
"""A working -> waiting toy machine: FIX_DIAGNOSE auto-advances to
FIX_AWAIT_APPROVAL (a waiting state), where the loop stops."""
def diagnose(_env: Env, _turn: Turn, _s: Session, fc: FixContext) -> StepResult:
def diagnose(_env: Env, _turn: Turn, _s: Session, fc: CopilotContext) -> StepResult:
return StepResult(next=PcState.FIX_AWAIT_APPROVAL, context=fc)
return {PcState.FIX_DIAGNOSE: diagnose}
@@ -56,7 +56,7 @@ def _new_env() -> tuple[Env, InMemoryRepository]:
def test_advance_commits_each_transition_and_stops_at_waiting():
env, repo = _new_env()
s = _session()
repo.create_session(s, FixContext(), [])
repo.create_session(s, CopilotContext(), [])
runner = Runner(env, _toy_registry())
turn = Turn(action=Action(kind="request_fix", base_version=1), actor=_actor())
@@ -73,7 +73,7 @@ def test_advance_commits_each_transition_and_stops_at_waiting():
def test_advance_stale_base_version_raises_conflict_with_nothing_applied():
env, repo = _new_env()
s = _session()
repo.create_session(s, FixContext(), [])
repo.create_session(s, CopilotContext(), [])
runner = Runner(env, _toy_registry())
turn = Turn(action=Action(kind="request_fix", base_version=99), actor=_actor())
@@ -90,7 +90,7 @@ def test_advance_stale_base_version_raises_conflict_with_nothing_applied():
def test_advance_missing_handler_raises():
env, repo = _new_env()
s = _session()
repo.create_session(s, FixContext(), [])
repo.create_session(s, CopilotContext(), [])
runner = Runner(env, {}) # empty registry: no handler for FIX_DIAGNOSE
turn = Turn(action=Action(kind="request_fix", base_version=1), actor=_actor())
@@ -102,7 +102,7 @@ def test_advance_missing_handler_raises():
def test_advance_missing_handler_error_message_names_the_state():
env, repo = _new_env()
s = _session()
repo.create_session(s, FixContext(), [])
repo.create_session(s, CopilotContext(), [])
runner = Runner(env, {})
turn = Turn(action=Action(kind="request_fix", base_version=1), actor=_actor())
@@ -114,7 +114,7 @@ def test_advance_missing_handler_error_message_names_the_state():
def test_advance_waiting_state_first_turn_no_action_returns_session_unchanged():
env, repo = _new_env()
s = _session(current_state=PcState.FIX_AWAIT_APPROVAL)
repo.create_session(s, FixContext(), [])
repo.create_session(s, CopilotContext(), [])
runner = Runner(env, {}) # never consulted: loop returns before handler lookup
turn = Turn(actor=_actor()) # no action
@@ -127,7 +127,7 @@ def test_advance_waiting_state_first_turn_no_action_returns_session_unchanged():
def test_advance_terminal_state_returns_session_unchanged():
env, repo = _new_env()
s = _session(current_state=PcState.SUCCESS)
repo.create_session(s, FixContext(), [])
repo.create_session(s, CopilotContext(), [])
runner = Runner(env, {}) # never consulted: terminal short-circuits first
turn = Turn(actor=_actor())
@@ -143,15 +143,15 @@ def test_advance_passes_full_turn_to_first_step_and_actor_only_turn_to_subsequen
handler must see a Turn with Action consumed (None) but the same Actor."""
env, repo = _new_env()
s = _session()
repo.create_session(s, FixContext(), [])
repo.create_session(s, CopilotContext(), [])
seen_turns: list[Turn] = []
def diagnose(_env: Env, turn: Turn, _s: Session, fc: FixContext) -> StepResult:
def diagnose(_env: Env, turn: Turn, _s: Session, fc: CopilotContext) -> StepResult:
seen_turns.append(turn)
return StepResult(next=PcState.FIX_PROPOSE, context=fc)
def propose(_env: Env, turn: Turn, _s: Session, fc: FixContext) -> StepResult:
def propose(_env: Env, turn: Turn, _s: Session, fc: CopilotContext) -> StepResult:
seen_turns.append(turn)
return StepResult(next=PcState.FIX_AWAIT_APPROVAL, context=fc)
@@ -21,7 +21,7 @@ from sqlalchemy.engine import Engine
from sqlalchemy.orm import sessionmaker
import tasks.workflow_copilot_advance_task as mod
from core.workflow_copilot.models import ConversationItem, EntryMode, FixContext, Run, Session
from core.workflow_copilot.models import ConversationItem, CopilotContext, EntryMode, Run, Session
from core.workflow_copilot.state import PcState
from models.base import Base
from services.workflow_copilot.repository import SqlCopilotRepository
@@ -55,7 +55,7 @@ def _seed_fix_session(repo: SqlCopilotRepository) -> Session:
)
repo.create_session(
s,
FixContext(failed_run_id="TR-1"),
CopilotContext(failed_run_id="TR-1"),
[ConversationItem(kind="run-context", seq=0)],
)
repo.save_run(s.id, Run(id="TR-1", kind="original-failed", status="failed", immutable=True))
@@ -29,7 +29,7 @@ from sqlalchemy.orm import sessionmaker
import tasks.workflow_copilot_advance_task as task_mod
from core.workflow_copilot.errors import BusyError
from core.workflow_copilot.models import Action, Actor, ConversationItem, EntryMode, FixContext, Run, Session
from core.workflow_copilot.models import Action, Actor, ConversationItem, CopilotContext, EntryMode, Run, Session
from core.workflow_copilot.state import PcState
from models.base import Base
from services.workflow_copilot import session_lock
@@ -170,7 +170,7 @@ def test_submit_action_while_lock_held_raises_busy_eager_async(
entry_mode=EntryMode.FIX,
current_state=PcState.FIX_DIAGNOSE,
)
repo.create_session(s, FixContext(failed_run_id="TR-1"), [ConversationItem(kind="run-context", seq=0)])
repo.create_session(s, CopilotContext(failed_run_id="TR-1"), [ConversationItem(kind="run-context", seq=0)])
# Manually hold the lock, simulating an in-flight advance on another
# worker.
@@ -28,8 +28,8 @@ from core.workflow_copilot.models import (
Actor,
ChecklistError,
ConversationItem,
CopilotContext,
EntryMode,
FixContext,
Run,
Session,
Turn,
@@ -87,13 +87,13 @@ def _fix_session(**overrides) -> Session:
def _seed_fix_session(repo: SqlCopilotRepository) -> Session:
"""Seed a session at fix.diagnose with a run-context item and a
FixContext pointing at a pre-existing failed run -- mirrors the P1
CopilotContext pointing at a pre-existing failed run -- mirrors the P1
full-flow acceptance's seed, persisted through the SQL repo instead of
the in-memory fake."""
s = _fix_session()
repo.create_session(
s,
FixContext(failed_run_id="TR-1"),
CopilotContext(failed_run_id="TR-1"),
[ConversationItem(kind="run-context", seq=0, payload={"failed_run_id": "TR-1"})],
)
repo.save_run(s.id, Run(id="TR-1", kind="original-failed", status="failed", immutable=True))
@@ -244,7 +244,7 @@ def _seed_checklist_session(repo: SqlCopilotRepository, errors: list[ChecklistEr
s = _fix_session(entry_mode=EntryMode.FIX_CHECKLIST, current_state=PcState.CHECKLIST_DIAGNOSE)
repo.create_session(
s,
FixContext(source="checklist", checklist_errors=errors),
CopilotContext(source="checklist", checklist_errors=errors),
[ConversationItem(kind="run-context", seq=0)],
)
return s
@@ -24,7 +24,7 @@ from sqlalchemy.orm import sessionmaker
import schedule.workflow_copilot_reconcile_task as mod
from configs import dify_config
from core.workflow_copilot.models import ConversationItem, EntryMode, FixContext, Session
from core.workflow_copilot.models import ConversationItem, CopilotContext, EntryMode, Session
from core.workflow_copilot.state import PcState
from libs.datetime_utils import naive_utc_now
from models.base import Base
@@ -63,7 +63,7 @@ def _seed(repo: SqlCopilotRepository, state: PcState) -> Session:
entry_mode=EntryMode.FIX,
current_state=state,
)
repo.create_session(s, FixContext(), [ConversationItem(kind="run-context", seq=0)])
repo.create_session(s, CopilotContext(), [ConversationItem(kind="run-context", seq=0)])
return s
@@ -19,8 +19,8 @@ from core.workflow_copilot.errors import ConflictError, NotFoundError
from core.workflow_copilot.models import (
Checkpoint,
ConversationItem,
CopilotContext,
EntryMode,
FixContext,
MutationIntent,
NodeOutput,
Run,
@@ -67,7 +67,7 @@ def _make_domain_session(**overrides) -> DomainSession:
def test_create_session_then_get_session_round_trips(repo: SqlCopilotRepository) -> None:
domain_session = _make_domain_session()
fc = FixContext(failed_run_id="TR-1")
fc = CopilotContext(failed_run_id="TR-1")
items = [
ConversationItem(seq=0, kind="run-context", payload={"a": 1}, at_version=1),
ConversationItem(seq=1, kind="assistant-turn", payload={"b": 2}, at_version=1),
@@ -90,7 +90,7 @@ def test_create_session_then_get_session_round_trips(repo: SqlCopilotRepository)
assert loaded_session.entry_mode == EntryMode.FIX
assert loaded_session.current_state == PcState.FIX_DIAGNOSE
assert loaded_session.version == 1
assert loaded_fc == FixContext(failed_run_id="TR-1", next_seq=2)
assert loaded_fc == CopilotContext(failed_run_id="TR-1", next_seq=2)
def test_get_session_missing_id_raises_not_found(repo: SqlCopilotRepository) -> None:
@@ -100,10 +100,10 @@ def test_get_session_missing_id_raises_not_found(repo: SqlCopilotRepository) ->
def test_compare_and_advance_bumps_version_and_appends_commit_and_items(repo: SqlCopilotRepository) -> None:
domain_session = _make_domain_session()
fc = FixContext(failed_run_id="TR-1")
fc = CopilotContext(failed_run_id="TR-1")
repo.create_session(domain_session, fc, [])
next_fc = FixContext(failed_run_id="TR-1", staged_repair=[MutationIntent(op="set_node_config")])
next_fc = CopilotContext(failed_run_id="TR-1", staged_repair=[MutationIntent(op="set_node_config")])
new_items = [ConversationItem(seq=0, kind="diagnosis", payload={"x": 1}, at_version=2)]
new_version = repo.compare_and_advance(
@@ -122,7 +122,7 @@ def test_compare_and_advance_stale_version_raises_conflict_and_leaves_row_unchan
repo: SqlCopilotRepository,
) -> None:
domain_session = _make_domain_session()
fc = FixContext(failed_run_id="TR-1")
fc = CopilotContext(failed_run_id="TR-1")
repo.create_session(domain_session, fc, [])
with pytest.raises(ConflictError):
@@ -130,7 +130,7 @@ def test_compare_and_advance_stale_version_raises_conflict_and_leaves_row_unchan
domain_session.id,
base_version=999,
next=PcState.FIX_PROPOSE,
fc=FixContext(),
fc=CopilotContext(),
items=[],
)
@@ -146,14 +146,14 @@ def test_compare_and_advance_missing_session_raises_not_found(repo: SqlCopilotRe
"does-not-exist",
base_version=1,
next=PcState.FIX_PROPOSE,
fc=FixContext(),
fc=CopilotContext(),
items=[],
)
def test_compare_and_advance_duplicate_seq_raises_conflict(repo: SqlCopilotRepository) -> None:
domain_session = _make_domain_session()
fc = FixContext(failed_run_id="TR-1")
fc = CopilotContext(failed_run_id="TR-1")
seed_items = [ConversationItem(seq=0, kind="run-context", payload={}, at_version=1)]
repo.create_session(domain_session, fc, seed_items)
@@ -162,7 +162,7 @@ def test_compare_and_advance_duplicate_seq_raises_conflict(repo: SqlCopilotRepos
domain_session.id,
base_version=1,
next=PcState.FIX_PROPOSE,
fc=FixContext(),
fc=CopilotContext(),
items=[ConversationItem(seq=0, kind="diagnosis", payload={}, at_version=2)],
)
@@ -177,7 +177,7 @@ def test_compare_and_advance_duplicate_seq_raises_conflict(repo: SqlCopilotRepos
def test_create_checkpoint_then_get_checkpoint_round_trips(repo: SqlCopilotRepository) -> None:
domain_session = _make_domain_session()
fc = FixContext(failed_run_id="TR-1")
fc = CopilotContext(failed_run_id="TR-1")
repo.create_session(domain_session, fc, [])
snap = Snapshot(session_id=domain_session.id, hash="h1", graph={"nodes": [1, 2, 3]})
@@ -290,7 +290,7 @@ def test_get_test_input_missing_id_raises_not_found(repo: SqlCopilotRepository)
def test_list_conversation_returns_items_ordered_by_seq(repo: SqlCopilotRepository) -> None:
domain_session = _make_domain_session()
fc = FixContext(failed_run_id="TR-1")
fc = CopilotContext(failed_run_id="TR-1")
items = [
ConversationItem(seq=0, kind="run-context", payload={"a": 1}, at_version=1),
ConversationItem(seq=1, kind="assistant-turn", payload={"b": 2}, at_version=1),
@@ -299,7 +299,7 @@ def test_list_conversation_returns_items_ordered_by_seq(repo: SqlCopilotReposito
more_items = [ConversationItem(seq=2, kind="diagnosis", payload={"c": 3}, at_version=2)]
repo.compare_and_advance(
domain_session.id, base_version=1, next=PcState.FIX_PROPOSE, fc=FixContext(), items=more_items
domain_session.id, base_version=1, next=PcState.FIX_PROPOSE, fc=CopilotContext(), items=more_items
)
conv = repo.list_conversation(domain_session.id)
@@ -1,6 +1,6 @@
"""Round-trip tests for the ``FixContext`` JSON (de)serializer.
"""Round-trip tests for the ``CopilotContext`` JSON (de)serializer.
``FixContext`` is the per-session working state persisted as the
``CopilotContext`` is the per-session working state persisted as the
``context`` JSONB column on ``workflow_copilot_session_commits`` (P3a Task 4).
This module verifies the pure (de)serializer is lossless and produces a
JSON-safe dict, independent of any database concern.
@@ -11,16 +11,16 @@ import json
from core.workflow_copilot.models import (
ChangeSet,
ChecklistError,
CopilotContext,
Diagnosis,
FixContext,
MutationIntent,
Risk,
)
from services.workflow_copilot.serde import fix_context_from_dict, fix_context_to_dict
from services.workflow_copilot.serde import context_from_dict, context_to_dict
def _full_fix_context() -> FixContext:
return FixContext(
def _full_fix_context() -> CopilotContext:
return CopilotContext(
failed_run_id="TR-1",
diagnosis=Diagnosis(
culprit_node_id="n1",
@@ -57,15 +57,15 @@ def _full_fix_context() -> FixContext:
def test_fully_populated_fix_context_round_trips():
fc = _full_fix_context()
result = fix_context_from_dict(fix_context_to_dict(fc))
result = context_from_dict(context_to_dict(fc))
assert result == fc
def test_empty_fix_context_round_trips():
fc = FixContext()
fc = CopilotContext()
result = fix_context_from_dict(fix_context_to_dict(fc))
result = context_from_dict(context_to_dict(fc))
assert result == fc
@@ -73,7 +73,7 @@ def test_empty_fix_context_round_trips():
def test_to_dict_output_is_json_serializable():
fc = _full_fix_context()
d = fix_context_to_dict(fc)
d = context_to_dict(fc)
# Must not raise -- this is what actually lands in the JSONB column.
json.dumps(d)
@@ -83,6 +83,33 @@ def test_from_dict_tolerates_missing_optional_keys():
# An older/partial persisted dict -- only a subset of keys present.
d = {"failed_run_id": "TR-9"}
fc = fix_context_from_dict(d)
fc = context_from_dict(d)
assert fc == FixContext(failed_run_id="TR-9")
assert fc == CopilotContext(failed_run_id="TR-9")
def test_context_round_trips_build_fields():
from core.workflow_copilot.models import CopilotContext
from services.workflow_copilot.serde import context_from_dict, context_to_dict
fc = CopilotContext(
goal_text="Build a quarterly report workflow",
requirements={"currency": "USD", "prefer_audited": True},
plan_items=["Retrieve", "Summarize", "Emit"],
plan_version_tag="v2",
resource_selection={"resource_ids": ["kb-company"], "conflict_policy": "audited"},
built_node_ids=["start", "llm", "end"],
)
out = context_from_dict(context_to_dict(fc))
assert out == fc
def test_context_from_dict_defaults_build_fields_when_absent():
from core.workflow_copilot.models import CopilotContext
from services.workflow_copilot.serde import context_from_dict
fc = context_from_dict({"failed_run_id": "TR-9"})
assert fc == CopilotContext(failed_run_id="TR-9")
assert fc.goal_text == ""
assert fc.requirements == {}
assert fc.plan_items == []
@@ -21,8 +21,8 @@ from core.workflow_copilot.models import (
Actor,
ChecklistError,
ConversationItem,
CopilotContext,
EntryMode,
FixContext,
Session,
)
from core.workflow_copilot.state import PcState
@@ -251,7 +251,7 @@ def _seed_free_session(repo: SqlCopilotRepository) -> Session:
entry_mode=EntryMode.FIX,
current_state=PcState.FIX_DIAGNOSE,
)
repo.create_session(s, FixContext(failed_run_id="TR-1"), [ConversationItem(kind="run-context", seq=0)])
repo.create_session(s, CopilotContext(failed_run_id="TR-1"), [ConversationItem(kind="run-context", seq=0)])
return s
@@ -266,7 +266,7 @@ def _seed_session_at(repo: SqlCopilotRepository, state: PcState) -> Session:
entry_mode=EntryMode.FIX,
current_state=state,
)
repo.create_session(s, FixContext(failed_run_id="TR-1"), [ConversationItem(kind="run-context", seq=0)])
repo.create_session(s, CopilotContext(failed_run_id="TR-1"), [ConversationItem(kind="run-context", seq=0)])
return s