diff --git a/backend/druks/contrib/ship/models.py b/backend/druks/contrib/ship/models.py
index bc7a8e2..1c3aa4e 100644
--- a/backend/druks/contrib/ship/models.py
+++ b/backend/druks/contrib/ship/models.py
@@ -176,10 +176,10 @@ class WorkItem(StoredSubject):
__tablename__ = "work_items"
__table_args__ = (
Index("work_items_repo_idx", "repo", "pr_number"),
- # One WorkItem per (source, remote_key) — one row per ticket in the remote
+ # One WorkItem per (source, ticket_key) — one row per ticket in the remote
# tracker. ``source`` is part of the key so Linear "ABC-1" and Jira "ABC-1"
# don't collide once we support multiple providers.
- Index("work_items_remote_unique", "source", "remote_key", unique=True),
+ Index("work_items_ticket_unique", "source", "ticket_key", unique=True),
Index("work_items_project_idx", "project_id"),
Index("work_items_status_idx", "status"),
)
@@ -189,15 +189,15 @@ class WorkItem(StoredSubject):
)
project: Mapped[Project] = relationship(lazy="joined")
# Which remote tracker the ticket lives in: ``linear`` / ``github`` /
- # future ``jira``. Combined with ``remote_key`` to uniquely identify
+ # future ``jira``. Combined with ``ticket_key`` to uniquely identify
# a ticket.
source: Mapped[str] = mapped_column(default="github")
title: Mapped[str] = mapped_column(default="")
# Human-readable issue key in the source: ``ACME-270`` / ``#42`` /
# ``JIRA-123``. Every item is born from a ticket, so every item has one;
# Linear's GraphQL accepts the identifier wherever it accepts the UUID.
- remote_key: Mapped[str]
- remote_url: Mapped[str | None]
+ ticket_key: Mapped[str]
+ ticket_url: Mapped[str | None]
# The PR-target repo. Still on WorkItem (not derived from project)
# because a Project can hold N repos but every WorkItem PRs into one.
repo: Mapped[str]
@@ -214,7 +214,7 @@ class WorkItem(StoredSubject):
updated_at: Mapped[datetime] = mapped_column(default=Base.utc_now)
def get_label(self) -> str:
- return self.remote_key
+ return self.ticket_key
@classmethod
def create(
@@ -223,8 +223,8 @@ def create(
project_id: int,
source: str = "github",
title: str,
- remote_key: str,
- remote_url: str | None = None,
+ ticket_key: str,
+ ticket_url: str | None = None,
repo: str,
) -> "WorkItem":
session = db_session()
@@ -232,8 +232,8 @@ def create(
project_id=project_id,
source=source,
title=title,
- remote_key=remote_key,
- remote_url=remote_url,
+ ticket_key=ticket_key,
+ ticket_url=ticket_url,
repo=repo,
)
session.add(item)
@@ -281,7 +281,7 @@ async def ship(self) -> None:
build = self.get_status(workflow=Build)
if build.is_parked:
await Build.cancel(self, failure="pr merged while parked")
- await self.set_remote_status(TicketStatus.DONE)
+ await self.set_ticket_status(TicketStatus.DONE)
async def close_external(self) -> None:
# The attempt was abandoned, not the ticket, so the ticket returns to the
@@ -297,20 +297,20 @@ async def close_external(self) -> None:
await get_github_client(load_settings()).delete_branch(self.repo, self.branch)
except Exception: # noqa: BLE001 — cleanup only
logger.warning("Skipped branch cleanup for %s.", self.repo, exc_info=True)
- await self.set_remote_status(TicketStatus.READY_FOR_AGENT)
+ await self.set_ticket_status(TicketStatus.READY_FOR_AGENT)
@classmethod
- def get_for_remote_key(
+ def get_for_ticket_key(
cls,
*,
source: str,
- remote_key: str,
+ ticket_key: str,
) -> "WorkItem | None":
- """Look up a WorkItem by its source + remote_key pair.
+ """Look up a WorkItem by its source + ticket_key pair.
- The (source, remote_key) unique constraint guarantees at most
+ The (source, ticket_key) unique constraint guarantees at most
one live row; we return that row or None if no match exists."""
- stmt = select(cls).where(cls.source == source, cls.remote_key == remote_key).limit(1)
+ stmt = select(cls).where(cls.source == source, cls.ticket_key == ticket_key).limit(1)
return db_session().scalars(stmt).first()
@classmethod
@@ -341,7 +341,7 @@ def set_status(
self.updated_at = Base.utc_now()
db_session().flush()
- async def set_remote_status(self, status: TicketStatus) -> None:
+ async def set_ticket_status(self, status: TicketStatus) -> None:
# No-op for sources without a configured tracker (github, absent creds).
if not is_tracker_source(self.source):
return
@@ -356,7 +356,7 @@ async def set_remote_status(self, status: TicketStatus) -> None:
except TrackerNotConfigured:
return
- ticket = Ticket.ref(self.source, self.remote_key)
+ ticket = Ticket.ref(self.source, self.ticket_key)
async with tracker:
try:
await tracker.set_status(ticket, status)
@@ -364,7 +364,7 @@ async def set_remote_status(self, status: TicketStatus) -> None:
logger.warning(
"Could not sync %s ticket %s to %s.",
self.source,
- self.remote_key,
+ self.ticket_key,
status.value,
exc_info=True,
)
@@ -373,7 +373,7 @@ def update(
self,
*,
title: str = _KEEP,
- remote_url: str | None = _KEEP,
+ ticket_url: str | None = _KEEP,
pr_number: int | None = _KEEP,
branch: str | None = _KEEP,
build_run_id: str | None = _KEEP,
@@ -381,8 +381,8 @@ def update(
) -> None:
if title is not _KEEP:
self.title = title
- if remote_url is not _KEEP:
- self.remote_url = remote_url
+ if ticket_url is not _KEEP:
+ self.ticket_url = ticket_url
if pr_number is not _KEEP:
self.pr_number = pr_number
if branch is not _KEEP:
diff --git a/backend/druks/contrib/ship/schemas.py b/backend/druks/contrib/ship/schemas.py
index 24d1c05..de57890 100644
--- a/backend/druks/contrib/ship/schemas.py
+++ b/backend/druks/contrib/ship/schemas.py
@@ -94,7 +94,7 @@ class Links(BaseResponse):
@classmethod
def from_work_item(cls, item: WorkItem) -> "Links":
pr = f"https://github.com/{item.repo}/pull/{item.pr_number}" if item.pr_number else None
- return cls(repo=f"https://github.com/{item.repo}", pr=pr, ticket=item.remote_url)
+ return cls(repo=f"https://github.com/{item.repo}", pr=pr, ticket=item.ticket_url)
class WorkItemSummary(SubjectSummary):
@@ -108,8 +108,8 @@ class WorkItemSummary(SubjectSummary):
# whose Linear project doesn't map to one.
project_name: str
title: str
- remote_key: str
- remote_url: str | None = None
+ ticket_key: str
+ ticket_url: str | None = None
pr_number: int | None = None
branch: str | None = None
created_at: datetime
@@ -124,8 +124,8 @@ def from_work_item(cls, item: WorkItem) -> "WorkItemSummary":
repo=item.repo,
project_name=item.project.name,
title=item.title,
- remote_key=item.remote_key,
- remote_url=item.remote_url,
+ ticket_key=item.ticket_key,
+ ticket_url=item.ticket_url,
pr_number=item.pr_number,
branch=item.branch,
created_at=item.created_at,
@@ -137,7 +137,7 @@ def from_work_item(cls, item: WorkItem) -> "WorkItemSummary":
class DashboardItem(BaseResponse):
key: str
source_id: int | str
- ticket_ref: str
+ ticket_key: str
title: str
repo: str | None = None
pr_number: int | None = None
@@ -154,7 +154,7 @@ def from_work_item(cls, item: WorkItem) -> "DashboardItem":
return cls(
key=f"code:{item.id}",
source_id=item.id,
- ticket_ref=item.remote_key,
+ ticket_key=item.ticket_key,
title=item.title,
repo=item.repo,
pr_number=item.pr_number,
diff --git a/backend/druks/contrib/ship/subscribers.py b/backend/druks/contrib/ship/subscribers.py
index 1076a5a..528eadf 100644
--- a/backend/druks/contrib/ship/subscribers.py
+++ b/backend/druks/contrib/ship/subscribers.py
@@ -28,12 +28,12 @@ async def pr_open_mirrors_onto_item(
async def build_start_marks_ticket_in_progress(*, subject: WorkItem, **_: object) -> None:
# Every (re)start and gate-resume of a build means the ticket is in progress —
# including the return from a rework loop that had parked it In Review.
- await subject.set_remote_status(TicketStatus.IN_PROGRESS)
+ await subject.set_ticket_status(TicketStatus.IN_PROGRESS)
@subscribe(WorkflowEvent.PARKED, workflow=Build, gate=ReviewWork, subject=WorkItem)
async def review_park_marks_ticket_in_review(*, subject: WorkItem, **_: object) -> None:
- await subject.set_remote_status(TicketStatus.IN_REVIEW)
+ await subject.set_ticket_status(TicketStatus.IN_REVIEW)
@subscribe(WorkflowEvent.FAILED, workflow=Build, subject=WorkItem)
diff --git a/backend/druks/contrib/ship/workflows.py b/backend/druks/contrib/ship/workflows.py
index 2591ceb..9b563ea 100644
--- a/backend/druks/contrib/ship/workflows.py
+++ b/backend/druks/contrib/ship/workflows.py
@@ -94,9 +94,9 @@ class Settings(BaseModel):
async def dispatch(cls, *, ticket: dict) -> str | None:
# The tracker funnel's entry: a ticket at the trigger status opens a build.
# Resolve-or-refresh the item, then start (start() dedups a live run).
- item = WorkItem.get_for_remote_key(source=ticket["source"], remote_key=ticket["identifier"])
+ item = WorkItem.get_for_ticket_key(source=ticket["source"], ticket_key=ticket["identifier"])
if item:
- item.update(title=ticket["title"], remote_url=ticket["url"])
+ item.update(title=ticket["title"], ticket_url=ticket["url"])
else:
repo = ProjectRepo.lookup(project_name=ticket["project_name"], labels=ticket["labels"])
if repo:
@@ -104,8 +104,8 @@ async def dispatch(cls, *, ticket: dict) -> str | None:
project_id=repo.project_id,
source=ticket["source"],
title=ticket["title"] or ticket["identifier"],
- remote_key=ticket["identifier"],
- remote_url=ticket["url"],
+ ticket_key=ticket["identifier"],
+ ticket_url=ticket["url"],
repo=repo.full_name,
)
else:
@@ -118,9 +118,9 @@ async def dispatch(cls, *, ticket: dict) -> str | None:
account_id=assignee.id if assignee else None,
repo=item.repo,
source=item.source,
- ticket_ref=item.remote_key,
- remote_title=item.title,
- remote_url=item.remote_url,
+ ticket_ref=item.ticket_key,
+ ticket_title=item.title,
+ ticket_url=item.ticket_url,
task_owner_email=email,
task_owner_name=ticket["assignee_name"],
)
@@ -139,13 +139,13 @@ async def run_multistep(
issue_number: int | None = None,
source: str = "github",
# Human-readable ticket reference ("ACME-270", "#42"). Same value the
- # WorkItem keeps as ``remote_key``; carried separately on the input
+ # WorkItem keeps as ``ticket_key``; carried separately on the input
# so the workflow can render it into prompts / PR titles without a
# WorkItem fetch.
ticket_ref: str | None = None,
# Ticket title as intake received it; the implementer uses it for the PR title.
- remote_title: str | None = None,
- remote_url: str | None = None,
+ ticket_title: str | None = None,
+ ticket_url: str | None = None,
task_owner_email: str | None = None,
task_owner_name: str | None = None,
) -> None:
diff --git a/backend/migrations/versions/f2c8b81a9d4e_rename_work_item_ticket_fields.py b/backend/migrations/versions/f2c8b81a9d4e_rename_work_item_ticket_fields.py
new file mode 100644
index 0000000..a18c812
--- /dev/null
+++ b/backend/migrations/versions/f2c8b81a9d4e_rename_work_item_ticket_fields.py
@@ -0,0 +1,66 @@
+"""rename work item ticket fields
+
+Revision ID: f2c8b81a9d4e
+Revises: d3a5c71f8e40
+Create Date: 2026-07-27 14:00:00.000000
+
+"""
+
+from collections.abc import Sequence
+
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision: str = "f2c8b81a9d4e"
+down_revision: str | Sequence[str] | None = "d3a5c71f8e40"
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
+
+
+def upgrade() -> None:
+ op.drop_index("work_items_remote_unique", table_name="work_items")
+ op.alter_column(
+ "work_items",
+ "remote_key",
+ existing_type=sa.String(),
+ existing_nullable=False,
+ new_column_name="ticket_key",
+ )
+ op.alter_column(
+ "work_items",
+ "remote_url",
+ existing_type=sa.String(),
+ existing_nullable=True,
+ new_column_name="ticket_url",
+ )
+ op.create_index(
+ "work_items_ticket_unique",
+ "work_items",
+ ["source", "ticket_key"],
+ unique=True,
+ )
+
+
+def downgrade() -> None:
+ op.drop_index("work_items_ticket_unique", table_name="work_items")
+ op.alter_column(
+ "work_items",
+ "ticket_key",
+ existing_type=sa.String(),
+ existing_nullable=False,
+ new_column_name="remote_key",
+ )
+ op.alter_column(
+ "work_items",
+ "ticket_url",
+ existing_type=sa.String(),
+ existing_nullable=True,
+ new_column_name="remote_url",
+ )
+ op.create_index(
+ "work_items_remote_unique",
+ "work_items",
+ ["source", "remote_key"],
+ unique=True,
+ )
diff --git a/backend/tests/ship/factories.py b/backend/tests/ship/factories.py
index c73fe98..d6a456e 100644
--- a/backend/tests/ship/factories.py
+++ b/backend/tests/ship/factories.py
@@ -11,7 +11,7 @@ def make_test_work_item(*, repo: str, **kwargs):
if not project:
project = Project.create(name=repo)
ProjectRepo.create(project_id=project.id, full_name=repo)
- kwargs.setdefault("remote_key", f"TEST-{uuid7()}")
+ kwargs.setdefault("ticket_key", f"TEST-{uuid7()}")
return WorkItem.create(project_id=project.id, repo=repo, **kwargs)
diff --git a/backend/tests/ship/test_api_work_items.py b/backend/tests/ship/test_api_work_items.py
index dc8f53b..f84cea3 100644
--- a/backend/tests/ship/test_api_work_items.py
+++ b/backend/tests/ship/test_api_work_items.py
@@ -86,7 +86,11 @@ def test_subject_list_shows_active_and_excludes_handed_off(client: TestClient, d
def test_subject_detail_composes_summary_status_and_timeline(client: TestClient, druks_db):
item = make_test_work_item(
- title="detail", repo="ClawHaven/acme-app", source="linear", remote_key="ACME-5"
+ title="detail",
+ repo="ClawHaven/acme-app",
+ source="linear",
+ ticket_key="ACME-5",
+ ticket_url="https://linear.app/acme/issue/ACME-5/detail",
)
WorkItem.get(item.id).update(pr_number=8)
run = seed_build_run(
@@ -99,9 +103,12 @@ def test_subject_detail_composes_summary_status_and_timeline(client: TestClient,
seed_call(druks_db, run, "generate_plan")
detail = client.get(f"/api/ship/work_item/{item.id}").json()
- assert detail["summary"]["id"] == str(item.id)
- assert detail["summary"]["remoteKey"] == "ACME-5"
- assert detail["summary"]["links"]["pr"] == "https://github.com/ClawHaven/acme-app/pull/8"
+ summary = detail["summary"]
+ assert summary["id"] == str(item.id)
+ assert summary["ticketKey"] == "ACME-5"
+ assert summary["ticketUrl"] == "https://linear.app/acme/issue/ACME-5/detail"
+ assert summary["links"]["ticket"] == "https://linear.app/acme/issue/ACME-5/detail"
+ assert summary["links"]["pr"] == "https://github.com/ClawHaven/acme-app/pull/8"
# Status is the platform's, aggregated from the item's runs — parked on a gate.
assert detail["status"]["state"] == "parked"
assert detail["status"]["gate"] == "review_plan"
@@ -229,7 +236,7 @@ def test_update_stamps_build_run_id(druks_db):
def test_timeline_shows_every_build_attempt(druks_db):
# Each build attempt is its own run; the timeline shows them all, with a
# failed attempt's failure carried on its run.
- item = make_test_work_item(repo="ClawHaven/acme-app", title="x", remote_key="ACME-1")
+ item = make_test_work_item(repo="ClawHaven/acme-app", title="x", ticket_key="ACME-1")
run1 = seed_build_run(druks_db, work_item_id=item.id, state="failed", failure="boom")
run2 = seed_build_run(druks_db, work_item_id=item.id, state="failed")
seed_call(druks_db, run1, "generate_plan", status="failed", last_error="boom")
diff --git a/backend/tests/ship/test_build_dispatch.py b/backend/tests/ship/test_build_dispatch.py
index 7138868..396f662 100644
--- a/backend/tests/ship/test_build_dispatch.py
+++ b/backend/tests/ship/test_build_dispatch.py
@@ -9,18 +9,20 @@ async def test_dispatch_pulls_cancelled_item_back_onto_the_board(druks_db, monke
"""A cancelled item rests in History; dispatching its build must clear the
handoff status so the active board shows the run (and its gates) instead
of a stale "Cancelled" row."""
- item = make_test_work_item(repo="o/r", title="t", remote_key="ACME-1")
+ item = make_test_work_item(repo="o/r", title="t", ticket_key="ACME-1")
item.set_status(HandoffStatus.CANCELLED)
seed_run(druks_db, kind=Build.kind, run_id="run-1")
+ started = {}
async def fake_start(cls, **kwargs):
+ started.update(kwargs)
return "run-1"
monkeypatch.setattr(Build, "start", classmethod(fake_start))
run_id = await Build.dispatch(
ticket={
"source": item.source,
- "identifier": item.remote_key,
+ "identifier": item.ticket_key,
"status": "Ready",
"title": item.title,
"url": "https://tracker.test/ACME-1",
@@ -34,6 +36,9 @@ async def fake_start(cls, **kwargs):
assert run_id == "run-1"
assert item.build_run_id == "run-1"
assert item.status is None
+ assert started["ticket_ref"] == "ACME-1"
+ assert started["ticket_title"] == "t"
+ assert started["ticket_url"] == "https://tracker.test/ACME-1"
async def test_redispatch_to_a_new_run_clears_prior_attempt_branch_and_pr(
@@ -44,7 +49,7 @@ async def test_redispatch_to_a_new_run_clears_prior_attempt_branch_and_pr(
resolve this item onto the new run."""
seed_run(druks_db, kind=Build.kind, run_id="run-old")
seed_run(druks_db, kind=Build.kind, run_id="run-new")
- item = make_test_work_item(repo="o/r", title="t", remote_key="ACME-2")
+ item = make_test_work_item(repo="o/r", title="t", ticket_key="ACME-2")
item.update(build_run_id="run-old", pr_number=7, branch="agent/old")
async def fake_start(cls, **kwargs):
@@ -54,7 +59,7 @@ async def fake_start(cls, **kwargs):
await Build.dispatch(
ticket={
"source": item.source,
- "identifier": item.remote_key,
+ "identifier": item.ticket_key,
"status": "Ready",
"title": item.title,
"url": "https://tracker.test/ACME-2",
@@ -74,7 +79,7 @@ async def test_duplicate_dispatch_keeps_the_live_attempt_routing(druks_db, monke
"""A duplicate dispatch dedups to the live run — start() hands back its id —
so the item's branch/PR must survive, or PR routing and board links break."""
seed_run(druks_db, kind=Build.kind, run_id="run-live")
- item = make_test_work_item(repo="o/r", title="t", remote_key="ACME-3")
+ item = make_test_work_item(repo="o/r", title="t", ticket_key="ACME-3")
item.update(build_run_id="run-live", pr_number=7, branch="agent/live")
async def dedup_start(cls, **kwargs):
@@ -84,7 +89,7 @@ async def dedup_start(cls, **kwargs):
await Build.dispatch(
ticket={
"source": item.source,
- "identifier": item.remote_key,
+ "identifier": item.ticket_key,
"status": "Ready",
"title": item.title,
"url": "https://tracker.test/ACME-3",
@@ -103,7 +108,7 @@ async def dedup_start(cls, **kwargs):
def test_update_clears_nullable_with_none_and_skips_omitted(druks_db) -> None:
"""update() tells a clear from a skip: pr_number=None clears the column,
while leaving branch out preserves it."""
- item = make_test_work_item(repo="o/r", title="t", remote_key="ACME-4")
+ item = make_test_work_item(repo="o/r", title="t", ticket_key="ACME-4")
item.update(pr_number=9, branch="agent/keep")
item.update(pr_number=None)
diff --git a/backend/tests/ship/test_build_durable.py b/backend/tests/ship/test_build_durable.py
index d331485..fb51ede 100644
--- a/backend/tests/ship/test_build_durable.py
+++ b/backend/tests/ship/test_build_durable.py
@@ -82,7 +82,7 @@ def _seed_work_item(engine, *, repo: str):
project = Project(name=name)
session.add(project)
session.flush()
- item = WorkItem(project_id=project.id, repo=repo, title="rt", remote_key=name)
+ item = WorkItem(project_id=project.id, repo=repo, title="rt", ticket_key=name)
session.add(item)
session.commit()
session.refresh(item)
diff --git a/backend/tests/ship/test_lane_reactions.py b/backend/tests/ship/test_lane_reactions.py
index 7c6964d..65445d8 100644
--- a/backend/tests/ship/test_lane_reactions.py
+++ b/backend/tests/ship/test_lane_reactions.py
@@ -17,7 +17,7 @@
async def test_run_running_puts_item_back_on_board(druks_db):
- item = make_test_work_item(repo="acme/widget", title="t", source="linear", remote_key="ACME-1")
+ item = make_test_work_item(repo="acme/widget", title="t", source="linear", ticket_key="ACME-1")
item.set_status(HandoffStatus.CANCELLED)
await publish(WorkflowEvent.RUNNING, subject=item.identity, kind=Build.kind)
@@ -31,8 +31,8 @@ async def test_build_lifecycle_reaches_the_tracker(druks_db, monkeypatch):
async def _push(self, status):
pushed.append(status)
- monkeypatch.setattr(WorkItem, "set_remote_status", _push)
- item = make_test_work_item(repo="acme/widget", title="t", source="linear", remote_key="ACME-7")
+ monkeypatch.setattr(WorkItem, "set_ticket_status", _push)
+ item = make_test_work_item(repo="acme/widget", title="t", source="linear", ticket_key="ACME-7")
subject = item.identity
await publish(WorkflowEvent.RUNNING, subject=subject, kind=Build.kind)
@@ -45,7 +45,7 @@ async def _push(self, status):
async def test_pr_review_answers_through_the_review_gate(druks_db, monkeypatch):
- item = make_test_work_item(repo="acme/widget", title="t", source="linear", remote_key="ACME-9")
+ item = make_test_work_item(repo="acme/widget", title="t", source="linear", ticket_key="ACME-9")
item.update(pr_number=12, branch="agent/acme-9")
run = Run(
id=str(uuid7()),
@@ -94,7 +94,7 @@ async def answer(subject, **reply):
async def test_pr_open_reaches_the_work_item(druks_db):
- item = make_test_work_item(repo="acme/widget", title="t", source="linear", remote_key="ACME-8")
+ item = make_test_work_item(repo="acme/widget", title="t", source="linear", ticket_key="ACME-8")
await publish(
"pr.opened",
diff --git a/backend/tests/ship/test_subject_lifecycle.py b/backend/tests/ship/test_subject_lifecycle.py
index bcf7cc0..ba1112d 100644
--- a/backend/tests/ship/test_subject_lifecycle.py
+++ b/backend/tests/ship/test_subject_lifecycle.py
@@ -44,7 +44,7 @@ async def test_gate_answer_resumes_only_a_run_parked_on_its_gate(druks_db, monke
# A subject can carry runs of several workflows at once; the gate names which one
# answers, so a newer run of another kind never hides the parked one. A timed-out
# run keeps its stale ``input_gate``, so parked-ness decides, not that column.
- subject = _work_item(remote_key="ENG-748-A")
+ subject = _work_item(ticket_key="ENG-748-A")
parked = _subject_run(
druks_db,
subject=subject,
@@ -63,7 +63,7 @@ async def resume(self, **reply):
await OperatorReply.answer(subject, action="approve")
assert resumed == [parked.id]
- timed_out = _work_item(remote_key="ENG-748-B")
+ timed_out = _work_item(ticket_key="ENG-748-B")
_subject_run(
druks_db,
subject=timed_out,
@@ -81,7 +81,7 @@ async def test_workflow_cancel_takes_its_own_kind_and_passes_over_idle_subjects(
):
# Webhooks redeliver, and a PR can close long after its build ended: cancelling what
# is already gone is the no-op the caller expects, not an error.
- subject = _work_item(remote_key="ENG-748-C")
+ subject = _work_item(ticket_key="ENG-748-C")
build = _subject_run(druks_db, subject=subject, kind=Build.kind, state="running")
_subject_run(druks_db, subject=subject, kind=Profile.kind, state="running", order=1)
cancelled = []
@@ -94,14 +94,14 @@ async def cancel(self, *, failure=None):
await Build.cancel(subject)
assert cancelled == [build.id]
- idle = _work_item(remote_key="ENG-748-D")
+ idle = _work_item(ticket_key="ENG-748-D")
_subject_run(druks_db, subject=idle, kind=Build.kind, state="finished")
await Build.cancel(idle)
assert cancelled == [build.id]
async def test_subject_phase_reads_the_driving_running_workflow(druks_db, monkeypatch):
- subject = _work_item(remote_key="ENG-748-E")
+ subject = _work_item(ticket_key="ENG-748-E")
_subject_run(
druks_db,
subject=subject,
diff --git a/backend/tests/ship/test_ticketing.py b/backend/tests/ship/test_ticketing.py
index 36a2714..1cc3b2b 100644
--- a/backend/tests/ship/test_ticketing.py
+++ b/backend/tests/ship/test_ticketing.py
@@ -163,7 +163,7 @@ def test_linear_declares_known_exceptions():
assert httpx.HTTPError in Linear.known_exceptions
-# --- WorkItem.set_remote_status: the status-push consumer -------------------
+# --- WorkItem.set_ticket_status: the status-push consumer -------------------
class _FakeTracker:
@@ -186,37 +186,37 @@ async def aclose(self):
@pytest.mark.asyncio
-async def test_remote_state_pushes_status(druks_db, monkeypatch):
+async def test_ticket_state_pushes_status(druks_db, monkeypatch):
from druks.contrib.ship import models
from ship.factories import make_test_work_item
- item = make_test_work_item(repo="acme/widget", source="linear", remote_key="ACME-1", title="t")
+ item = make_test_work_item(repo="acme/widget", source="linear", ticket_key="ACME-1", title="t")
fake = _FakeTracker()
monkeypatch.setattr(models, "get_tracker", lambda source, **_: fake)
- await item.set_remote_status(TicketStatus.DONE)
+ await item.set_ticket_status(TicketStatus.DONE)
assert fake.calls == [("linear", "ACME-1", TicketStatus.DONE), "aclose"]
@pytest.mark.asyncio
-async def test_remote_state_skips_non_tracker_source(druks_db):
+async def test_ticket_state_skips_non_tracker_source(druks_db):
from ship.factories import make_test_work_item
- item = make_test_work_item(repo="acme/widget", source="github", remote_key="#5", title="t")
+ item = make_test_work_item(repo="acme/widget", source="github", ticket_key="#5", title="t")
# github has no tracker — a no-op that must not raise.
- await item.set_remote_status(TicketStatus.DONE)
+ await item.set_ticket_status(TicketStatus.DONE)
@pytest.mark.asyncio
-async def test_remote_state_closes_on_failure(druks_db, monkeypatch):
+async def test_ticket_state_closes_on_failure(druks_db, monkeypatch):
from druks.contrib.ship import models
from druks.core.apis.linear import LinearAPIError
from ship.factories import make_test_work_item
- item = make_test_work_item(repo="acme/widget", source="linear", remote_key="ACME-2", title="t")
+ item = make_test_work_item(repo="acme/widget", source="linear", ticket_key="ACME-2", title="t")
class _Boom(_FakeTracker):
known_exceptions = (LinearAPIError,)
@@ -227,7 +227,7 @@ async def set_status(self, ticket, status):
boom = _Boom()
monkeypatch.setattr(models, "get_tracker", lambda source, **_: boom)
- await item.set_remote_status(TicketStatus.DONE)
+ await item.set_ticket_status(TicketStatus.DONE)
assert "aclose" in boom.calls # closed even on failure
diff --git a/backend/tests/ship/test_webhooks_jira.py b/backend/tests/ship/test_webhooks_jira.py
index 124ca03..0ce9635 100644
--- a/backend/tests/ship/test_webhooks_jira.py
+++ b/backend/tests/ship/test_webhooks_jira.py
@@ -182,7 +182,7 @@ async def fake_start(cls, **kwargs):
payload=_jira_payload(key="SHRP-1", status="Ready", project="Octo", labels=["Alfred"]),
)
- item = WorkItem.get_for_remote_key(source="jira", remote_key="SHRP-1")
+ item = WorkItem.get_for_ticket_key(source="jira", ticket_key="SHRP-1")
assert item.build_run_id == "run-new"
assert item.repo == "octo/alfred"
assert item.project_id == project.id
diff --git a/backend/tests/ship/test_webhooks_pull_request.py b/backend/tests/ship/test_webhooks_pull_request.py
index 39068ca..015cca2 100644
--- a/backend/tests/ship/test_webhooks_pull_request.py
+++ b/backend/tests/ship/test_webhooks_pull_request.py
@@ -210,7 +210,7 @@ async def test_external_close_returns_ticket_to_resting_pool(druks_db, tmp_path,
async def _record(self, status):
pushed.append((self.id, status))
- monkeypatch.setattr(WorkItem, "set_remote_status", _record)
+ monkeypatch.setattr(WorkItem, "set_ticket_status", _record)
repo, pr_number, branch = "ClawHaven/acme-app", 91, "agent/eng-20"
work_item_id, _ = _park_work_item(repo=repo, pr_number=pr_number, branch=branch)
@@ -233,7 +233,7 @@ async def test_external_merge_pushes_done(druks_db, tmp_path, monkeypatch):
async def _record(self, status):
pushed.append((self.id, status))
- monkeypatch.setattr(WorkItem, "set_remote_status", _record)
+ monkeypatch.setattr(WorkItem, "set_ticket_status", _record)
repo, pr_number, branch = "ClawHaven/acme-app", 92, "agent/eng-21"
work_item_id, _ = _park_work_item(repo=repo, pr_number=pr_number, branch=branch)
@@ -325,7 +325,7 @@ async def _delete(repo, branch):
async def _record(self, status):
pushed.append(status)
- monkeypatch.setattr(WorkItem, "set_remote_status", _record)
+ monkeypatch.setattr(WorkItem, "set_ticket_status", _record)
repo, pr_number, branch = "ClawHaven/acme-app", 95, "agent/eng-24"
work_item_id, _ = _park_work_item(repo=repo, pr_number=pr_number, branch=branch)
diff --git a/frontend/src/extensions/ship/AgentCallPage.tsx b/frontend/src/extensions/ship/AgentCallPage.tsx
index 424b329..855b20f 100644
--- a/frontend/src/extensions/ship/AgentCallPage.tsx
+++ b/frontend/src/extensions/ship/AgentCallPage.tsx
@@ -37,7 +37,7 @@ export function AgentCallPage({ workItemId, runId }: Props) {
query.data
? agentCallPath(
workItemId,
- query.data.summary.remoteKey,
+ query.data.summary.ticketKey,
query.data.summary.title,
query.data.call.id,
)
@@ -96,10 +96,10 @@ function RunView({
/
- {summary.remoteKey}
+ {summary.ticketKey}
/
#{runId.slice(0, 8)}
diff --git a/frontend/src/extensions/ship/HistoryPage.tsx b/frontend/src/extensions/ship/HistoryPage.tsx
index 30da633..ee69363 100644
--- a/frontend/src/extensions/ship/HistoryPage.tsx
+++ b/frontend/src/extensions/ship/HistoryPage.tsx
@@ -45,7 +45,7 @@ export function HistoryPage() {
if (statusFilter !== 'all' && item.status !== statusFilter) return false
if (query.trim()) {
const q = query.toLowerCase()
- if (!`${item.title} ${item.ticketRef ?? ''}`.toLowerCase().includes(q)) return false
+ if (!`${item.title} ${item.ticketKey}`.toLowerCase().includes(q)) return false
}
return true
})
@@ -125,7 +125,7 @@ export function HistoryPage() {
onClick={() => navigate(dashboardItemPath(item))}
>