Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 5 additions & 9 deletions src/agent_comms/todos.py
Original file line number Diff line number Diff line change
Expand Up @@ -1471,10 +1471,11 @@ def changed(name: str) -> bool:
if (named := self.NAMED_FILE.findall(edit)) and not any(map(changed, named)))

def copy_as[Kind: ProblemOrder](self, kind: type[Kind], **evidence) -> Kind:
return kind(**{item.name: getattr(self, item.name) for item in fields(self)}, **evidence)
return kind(**{item.name: getattr(self, item.name) for item in fields(kind)
if item.name not in evidence}, **evidence)

def merged(self, sha: str) -> ProblemOrder:
return MergedProblemOrder(original=self, sha=sha)
return self.copy_as(MergedProblemOrder, sha=sha)

@property
def pull_request_reference(self) -> str:
Expand Down Expand Up @@ -1549,16 +1550,11 @@ def waiting_on_install(self, todo: Todo, read: dict[str, OrderProgress]) -> tupl


@dataclass(frozen=True, kw_only=True)
class MergedProblemOrder(ProblemOrder):
"""An installed merge, retaining its original order without inventing a check or app proof."""
original: CheckedProblemOrder
class MergedProblemOrder(CheckedProblemOrder):
"""The checked order installed by a merge, with its actual merge identity."""
sha: str
line: ClassVar[str] = "Merged and installed"

@property
def id(self) -> str:
return self.original.id

def require_close(self) -> None:
pass

Expand Down
34 changes: 29 additions & 5 deletions tests/test_installed_merge.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
"""Merge observation closes the real stored order, without a completion request."""
from dataclasses import replace
from dataclasses import fields, replace
import json
import subprocess

import pytest

from agent_comms.repo import Repo
from agent_comms.field_codec import FieldCodec
from agent_comms.todos import (DoneTodoState, MergedProblemOrder, OperationDelivery,
from agent_comms.todos import (CheckedProblemOrder, DoneTodoState, MergedProblemOrder, OperationDelivery,
MergedPullRequest, PullRequests, UnorderedProblemOrder)
from agent_comms.tools import CommsOrderDoneTool
from agent_comms.recorded_time import RecordedEventTime
from cutover.carry_merged_orders import MergedOrders
from test_commander_problem_orders import working
from test_order_progress import checkout, commit, deadline_lines, git


@pytest.mark.parametrize('delivered', (False, True))
@pytest.mark.parametrize('legacy_depth', (0, 1, 2))
def test_installed_merge_closes_pending_or_delivered_order_once(
commander, commander_script, checkout, tmp_path, monkeypatch, capsys, delivered):
commander, commander_script, checkout, tmp_path, monkeypatch, capsys,
delivered, legacy_depth):
comms, caller = commander
script, _ = commander_script
todo, unit = checkout
if delivered:
CommsOrderDoneTool(problem=todo.id, order_id=todo.order.id,
delivery=OperationDelivery(), claims=working(comms)).apply(comms, caller)
delivery=OperationDelivery(at=RecordedEventTime(500)),
claims=working(comms)).apply(comms, caller)
todo = comms.goals.todos.get(todo.id)
original = todo.order
commit(unit)
Expand Down Expand Up @@ -59,11 +65,29 @@ def test_installed_merge_closes_pending_or_delivered_order_once(
assert closed.state is DoneTodoState and closed.assignment is None
assert closed.revision == todo.revision + 1 and not closed.pending
assert isinstance(closed.order, MergedProblemOrder)
assert closed.order.sha == sha and closed.order.original == original
assert closed.order.sha == sha
assert all(getattr(closed.order, item.name) == getattr(original, item.name)
for item in fields(CheckedProblemOrder))
assert closed.order.id == original.id
assert "original" not in FieldCodec.encode(closed.order)
assert closed.strikes == todo.strikes
assert FieldCodec.decode(type(closed), FieldCodec.encode(closed)) == closed

if legacy_depth:
legacy = {'kind': 'merged', 'original': FieldCodec.encode(original), 'sha': sha}
if legacy_depth == 2:
closed = replace(closed, order=closed.order.folded('P2', 'Same root cause.'))
legacy = {**FieldCodec.encode(closed.order), 'original': legacy}
encoded = json.dumps(legacy)
with comms.goals.todos.writing() as db:
db.execute('UPDATE todo SET "order"=? WHERE id=?', (encoded, todo.id))
assert MergedOrders.carry(comms.root, apply=False).changed == 1
with comms.goals.todos.writing() as db:
assert db.execute('SELECT "order" FROM todo WHERE id=?', (todo.id,)).fetchone()[0] == encoded
assert MergedOrders.carry(comms.root, apply=True).changed == 1
assert MergedOrders.carry(comms.root, apply=True).changed == 0
assert comms.goals.todos.get(todo.id) == closed

def no_child(*args, **kwargs):
pytest.fail('A repeated closed-order read launched a subprocess')

Expand Down
67 changes: 67 additions & 0 deletions tools/cutover/carry_merged_orders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""Carry retired merged wrappers through the checked order's own transition."""

from __future__ import annotations

import json
from contextlib import closing
from dataclasses import dataclass
from pathlib import Path

from agent_comms.coordination_database import CoordinationStore
from agent_comms.field_codec import FieldCodec
from agent_comms.todos import CheckedProblemOrder, MergedProblemOrder, Todo, TodoStore
from agent_comms.typed_table import TypedRow

from .carry import Carried, StoreCarry


@dataclass(frozen=True)
class SavedMergedOrder(TypedRow):
"""The old wrapper projected and decoded once at the SQLite boundary."""

id: str
path: str
checked: CheckedProblemOrder
sha: str


class MergedOrders(StoreCarry):
store = TodoStore

@classmethod
def carry(cls, root: Path, *, apply: bool) -> Carried:
path = cls.store.path_in(root)
if not path.exists():
return Carried(0)
parameters = (MergedProblemOrder.declared_name,)
with closing(CoordinationStore.writer(path)) as db:
db.execute("BEGIN IMMEDIATE")
# Folded provenance can contain a merged order too. The transition
# belongs to CheckedProblemOrder, not a second field list here.
saved = SavedMergedOrder.read(
db.execute(
f"SELECT t.id, n.path AS path,"
f" json_extract(t.\"order\", n.path || '.original') AS checked,"
f" json_extract(t.\"order\", n.path || '.sha') AS sha"
f' FROM "{Todo.declared_name}" t, json_tree(t."order") n'
" WHERE n.key='kind' AND n.value=?"
" AND json_type(t.\"order\", n.path || '.original')='object'"
" ORDER BY length(n.path) DESC",
parameters,
)
)
for row in saved:
merged = row.checked.merged(row.sha)
db.execute(
f'UPDATE "{Todo.declared_name}"'
' SET "order"=json_set("order", ?, json(?)) WHERE id=?',
(row.path, json.dumps(FieldCodec.encode(merged)), row.id),
)
db.commit() if apply else db.rollback()
return Carried(
len(saved), {"merged_orders": len(saved)}, (path.name,) if apply and saved else ()
)


if __name__ == "__main__":
MergedOrders.main()
Loading