Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a78da39e5 |
+81
-17
@@ -133,10 +133,15 @@ ROLE_ACTIONS: dict[str, tuple[tuple[str, ...], tuple[str, ...]]] = {
|
|||||||
|
|
||||||
|
|
||||||
# Body phrases that prove an issue is an implementation container, not a
|
# Body phrases that prove an issue is an implementation container, not a
|
||||||
# unit of direct author work (#844). Matched case-insensitively against the
|
# unit of direct author work (#844 / #854). Matched case-insensitively against
|
||||||
# issue body. Title alone is never sufficient (ordinary issues may mention
|
# the issue body. Title alone is never sufficient (ordinary issues may mention
|
||||||
# "epic" incidentally).
|
# "epic", "roadmap", "vision", or "umbrella" incidentally).
|
||||||
|
#
|
||||||
|
# #854 extends the #844 marker set so product-vision (#652), phased-roadmap
|
||||||
|
# (#653), and umbrella (#655) coordination records — which do not use the word
|
||||||
|
# "epic" — are classified with the same semantic exclusion as epic containers.
|
||||||
_CHILD_ONLY_BODY_MARKERS: tuple[str, ...] = (
|
_CHILD_ONLY_BODY_MARKERS: tuple[str, ...] = (
|
||||||
|
# Epic / child-only (#844, live #631)
|
||||||
"implementation is delivered via child issues only",
|
"implementation is delivered via child issues only",
|
||||||
"implementation is delivered through child issues only",
|
"implementation is delivered through child issues only",
|
||||||
"implementation is delivered via child issues",
|
"implementation is delivered via child issues",
|
||||||
@@ -150,9 +155,26 @@ _CHILD_ONLY_BODY_MARKERS: tuple[str, ...] = (
|
|||||||
"coordination container",
|
"coordination container",
|
||||||
"child-only container",
|
"child-only container",
|
||||||
"implementation is delegated to child",
|
"implementation is delegated to child",
|
||||||
|
# Vision / roadmap / umbrella coordination (#854, live #652/#653/#655).
|
||||||
|
# Prefer authoritative non-implementation / child-only scope language over
|
||||||
|
# bare words like "roadmap" so ordinary implementable issues that mention
|
||||||
|
# a parent vision or roadmap stay eligible.
|
||||||
|
"do not implement features on this issue",
|
||||||
|
"implementing features on this roadmap issue",
|
||||||
|
"implementation is via linked children only",
|
||||||
|
"no product feature claimed complete on this issue alone",
|
||||||
|
"phased delivery roadmap and epic sequencing",
|
||||||
|
"this issue is the enduring source of truth",
|
||||||
|
"enduring source of truth for the",
|
||||||
|
"canonical product vision — enduring source of truth",
|
||||||
|
"canonical product vision - enduring source of truth",
|
||||||
|
"state: vision-active",
|
||||||
|
"state: roadmap-active",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Explicit epic / umbrella labels (structured evidence preferred over title).
|
# Explicit epic / umbrella / vision / roadmap labels (structured evidence
|
||||||
|
# preferred over title). Tracker alone is *not* included — ordinary issues
|
||||||
|
# may carry a tracker label without being non-implementable containers.
|
||||||
_EPIC_LABELS: frozenset[str] = frozenset(
|
_EPIC_LABELS: frozenset[str] = frozenset(
|
||||||
{
|
{
|
||||||
"type:epic",
|
"type:epic",
|
||||||
@@ -161,6 +183,16 @@ _EPIC_LABELS: frozenset[str] = frozenset(
|
|||||||
"scope:epic",
|
"scope:epic",
|
||||||
"type:umbrella",
|
"type:umbrella",
|
||||||
"umbrella",
|
"umbrella",
|
||||||
|
"kind:umbrella",
|
||||||
|
"scope:umbrella",
|
||||||
|
"type:vision",
|
||||||
|
"vision",
|
||||||
|
"kind:vision",
|
||||||
|
"scope:vision",
|
||||||
|
"type:roadmap",
|
||||||
|
"roadmap",
|
||||||
|
"kind:roadmap",
|
||||||
|
"scope:roadmap",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -222,16 +254,45 @@ class WorkCandidate:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _title_container_prefix(title_l: str) -> str | None:
|
||||||
|
"""Return a coordination-title prefix token if *title_l* uses one (#854).
|
||||||
|
|
||||||
|
Title prefixes alone never exclude; they only corroborate body/label
|
||||||
|
evidence. Ordinary issues may say "roadmap" or "vision" mid-title.
|
||||||
|
"""
|
||||||
|
for prefix, token in (
|
||||||
|
("epic:", "title_epic_prefix"),
|
||||||
|
("epic ", "title_epic_prefix"),
|
||||||
|
("umbrella:", "title_umbrella_prefix"),
|
||||||
|
("umbrella ", "title_umbrella_prefix"),
|
||||||
|
("roadmap:", "title_roadmap_prefix"),
|
||||||
|
("roadmap ", "title_roadmap_prefix"),
|
||||||
|
("product vision:", "title_vision_prefix"),
|
||||||
|
("product vision ", "title_vision_prefix"),
|
||||||
|
("vision:", "title_vision_prefix"),
|
||||||
|
("vision ", "title_vision_prefix"),
|
||||||
|
):
|
||||||
|
if title_l.startswith(prefix):
|
||||||
|
return token
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def classify_epic_or_child_only_container(
|
def classify_epic_or_child_only_container(
|
||||||
c: WorkCandidate,
|
c: WorkCandidate,
|
||||||
) -> tuple[bool, str | None]:
|
) -> tuple[bool, str | None]:
|
||||||
"""Return whether *c* is an epic / child-only implementation container (#844).
|
"""Return whether *c* is a non-implementable coordination container (#844/#854).
|
||||||
|
|
||||||
Exclusion uses structured evidence first (labels, body scope language).
|
Exclusion uses structured evidence first (labels, body scope language).
|
||||||
A bare title containing the word "epic" is **not** enough — ordinary
|
A bare title containing the words "epic", "roadmap", "vision", or
|
||||||
implementable issues may mention epics incidentally. A title that is
|
"umbrella" is **not** enough — ordinary implementable issues may mention
|
||||||
explicitly prefixed ``Epic:`` only counts when the body also proves
|
those terms incidentally. Explicit title prefixes (``Epic:``, ``Roadmap:``,
|
||||||
child-only / no-direct-implementation scope (or an epic label is present).
|
``Product vision:``, ``Umbrella:``) only count when the body also proves
|
||||||
|
child-only / no-direct-implementation scope (or a container label is
|
||||||
|
present).
|
||||||
|
|
||||||
|
Covers epic, product-vision, phased-roadmap, umbrella, and child-only
|
||||||
|
records so the allocator never assigns coordination containers as direct
|
||||||
|
author work.
|
||||||
|
|
||||||
PRs are never classified as containers here (they already have a head).
|
PRs are never classified as containers here (they already have a head).
|
||||||
"""
|
"""
|
||||||
@@ -245,25 +306,28 @@ def classify_epic_or_child_only_container(
|
|||||||
title_l = title.lower()
|
title_l = title.lower()
|
||||||
|
|
||||||
body_hits = [m for m in _CHILD_ONLY_BODY_MARKERS if m in body_l]
|
body_hits = [m for m in _CHILD_ONLY_BODY_MARKERS if m in body_l]
|
||||||
title_epic_prefix = title_l.startswith("epic:") or title_l.startswith("epic ")
|
title_prefix = _title_container_prefix(title_l)
|
||||||
|
|
||||||
if epic_label:
|
if epic_label:
|
||||||
detail = f"label={epic_label[0]}"
|
detail = f"label={epic_label[0]}"
|
||||||
if body_hits:
|
if body_hits:
|
||||||
detail = f"{detail}; body_marker={body_hits[0]!r}"
|
detail = f"{detail}; body_marker={body_hits[0]!r}"
|
||||||
|
if title_prefix:
|
||||||
|
detail = f"{title_prefix}; {detail}"
|
||||||
return True, detail
|
return True, detail
|
||||||
|
|
||||||
if body_hits:
|
if body_hits:
|
||||||
# Body proves child-only / umbrella scope. Title "Epic:" is corroborating
|
# Body proves child-only / vision / roadmap / umbrella scope. Title
|
||||||
# but not required — containers without the word still exclude.
|
# prefixes are corroborating but not required — containers without the
|
||||||
|
# title word still exclude.
|
||||||
detail = f"body_marker={body_hits[0]!r}"
|
detail = f"body_marker={body_hits[0]!r}"
|
||||||
if title_epic_prefix:
|
if title_prefix:
|
||||||
detail = f"title_epic_prefix; {detail}"
|
detail = f"{title_prefix}; {detail}"
|
||||||
return True, detail
|
return True, detail
|
||||||
|
|
||||||
# Title-only "Epic:" without body scope evidence is insufficient (#844 AC:
|
# Title-only coordination prefix without body scope evidence is
|
||||||
# eligibility does not rely solely on the word "Epic" in a title).
|
# insufficient (#844/#854 AC: eligibility does not rely solely on a title
|
||||||
# Similarly, incidental "epic" mid-title without markers stays eligible.
|
# word). Incidental mid-title mentions without markers stay eligible.
|
||||||
return False, None
|
return False, None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ cryptography==49.0.0
|
|||||||
h11==0.16.0
|
h11==0.16.0
|
||||||
httpcore==1.0.9
|
httpcore==1.0.9
|
||||||
httpx==0.28.1
|
httpx==0.28.1
|
||||||
# Starlette 1.3.x TestClient prefers httpx2; plain httpx remains for MCP/runtime (#682).
|
|
||||||
httpx2==2.9.1
|
|
||||||
httpx-sse==0.4.3
|
httpx-sse==0.4.3
|
||||||
idna==3.18
|
idna==3.18
|
||||||
iniconfig==2.3.0
|
iniconfig==2.3.0
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
"""Starlette TestClient uses httpx2 without deprecation warning (#682)."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import importlib
|
|
||||||
import sys
|
|
||||||
import unittest
|
|
||||||
import warnings
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
||||||
|
|
||||||
|
|
||||||
class TestHttpx2Installed(unittest.TestCase):
|
|
||||||
def test_httpx2_importable(self) -> None:
|
|
||||||
httpx2 = importlib.import_module("httpx2")
|
|
||||||
self.assertTrue(hasattr(httpx2, "Client") or hasattr(httpx2, "AsyncClient"))
|
|
||||||
|
|
||||||
|
|
||||||
class TestNoStarletteTestClientDeprecation(unittest.TestCase):
|
|
||||||
def test_starlette_testclient_import_does_not_warn(self) -> None:
|
|
||||||
# Force a fresh import path so a previous httpx-fallback warning cannot
|
|
||||||
# hide a regression after httpx2 is present. Restore sys.modules afterwards.
|
|
||||||
saved = {
|
|
||||||
name: sys.modules[name]
|
|
||||||
for name in list(sys.modules)
|
|
||||||
if name == "starlette.testclient" or name.startswith("starlette.testclient.")
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
for name in list(saved):
|
|
||||||
del sys.modules[name]
|
|
||||||
|
|
||||||
with warnings.catch_warnings(record=True) as caught:
|
|
||||||
warnings.simplefilter("always")
|
|
||||||
import starlette.testclient as testclient # noqa: F401
|
|
||||||
|
|
||||||
dep = [
|
|
||||||
w
|
|
||||||
for w in caught
|
|
||||||
if "httpx" in str(w.message).lower()
|
|
||||||
and "deprecated" in str(w.message).lower()
|
|
||||||
]
|
|
||||||
self.assertEqual(
|
|
||||||
dep,
|
|
||||||
[],
|
|
||||||
f"expected no Starlette httpx deprecation warning; got: "
|
|
||||||
f"{[str(w.message) for w in dep]}",
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
sys.modules.update(saved)
|
|
||||||
|
|
||||||
def test_canonical_webui_testclient_import(self) -> None:
|
|
||||||
with warnings.catch_warnings(record=True) as caught:
|
|
||||||
warnings.simplefilter("always")
|
|
||||||
from tests.webui_testclient import TestClient
|
|
||||||
from webui.app import create_app
|
|
||||||
|
|
||||||
client = TestClient(create_app())
|
|
||||||
response = client.get("/")
|
|
||||||
self.assertIn(response.status_code, {200, 302, 404})
|
|
||||||
|
|
||||||
dep = [
|
|
||||||
w
|
|
||||||
for w in caught
|
|
||||||
if "httpx" in str(w.message).lower()
|
|
||||||
and "deprecated" in str(w.message).lower()
|
|
||||||
]
|
|
||||||
self.assertEqual(dep, [], [str(w.message) for w in dep])
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
"""Allocator semantic container exclusion for vision/roadmap/umbrella (#854).
|
||||||
|
|
||||||
|
#844 excluded epic / child-only containers (live #631) but product-vision
|
||||||
|
(#652), phased-roadmap (#653), and umbrella (#655) coordination records still
|
||||||
|
ranked as implementable work. This module is the live-equivalent canary:
|
||||||
|
|
||||||
|
* #631 / #652 / #653 / #655-shaped records are all excluded in one inventory.
|
||||||
|
* Independently executable children remain eligible and can be selected.
|
||||||
|
* Ordinary issues that merely mention vision / roadmap / umbrella stay eligible.
|
||||||
|
* Excluded containers never receive assignments or workflow leases.
|
||||||
|
* Structured skip reason ``epic_or_child_only_container`` is reported.
|
||||||
|
* Candidate-set fingerprint remains stable after exclusions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from allocator_service import (
|
||||||
|
OUTCOME_ASSIGNED,
|
||||||
|
OUTCOME_PREVIEW,
|
||||||
|
SKIP_EPIC_OR_CHILD_ONLY_CONTAINER,
|
||||||
|
WorkCandidate,
|
||||||
|
allocate_next_work,
|
||||||
|
candidate_set_fingerprint,
|
||||||
|
classify_epic_or_child_only_container,
|
||||||
|
)
|
||||||
|
from control_plane_db import ControlPlaneDB
|
||||||
|
|
||||||
|
REMOTE = "prgs"
|
||||||
|
ORG = "Scaled-Tech-Consulting"
|
||||||
|
REPO = "Gitea-Tools"
|
||||||
|
|
||||||
|
# Minimal bodies mirroring live coordination records (not full issue text).
|
||||||
|
_EPIC_631_BODY = """
|
||||||
|
## Scope (umbrella)
|
||||||
|
|
||||||
|
This epic owns the **product roadmap and linkage** for the Web Console.
|
||||||
|
Implementation is delivered via child issues only.
|
||||||
|
|
||||||
|
## Explicit non-goals
|
||||||
|
|
||||||
|
* Do not implement product features in this epic issue itself.
|
||||||
|
* No product feature implementation is claimed complete solely on this epic.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_VISION_652_BODY = """
|
||||||
|
## Canonical product vision — enduring source of truth
|
||||||
|
|
||||||
|
**This issue is the enduring source of truth for the MCP Control Plane Web Console product vision.**
|
||||||
|
|
||||||
|
## Implementation linkage
|
||||||
|
|
||||||
|
* **Do not implement features on this issue.**
|
||||||
|
* Sequencing: roadmap issue + #631 children.
|
||||||
|
|
||||||
|
## Canonical issue state
|
||||||
|
|
||||||
|
```text
|
||||||
|
STATE: vision-active
|
||||||
|
WHO_IS_NEXT: controller (triage/ordering) / author (implementation of linked children only)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
_ROADMAP_653_BODY = """
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This issue is the **phased delivery roadmap and epic sequencing** for the MCP Control Plane Web Console.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
* Implementing features on this roadmap issue.
|
||||||
|
* Deleting vision items by omitting them from phases without #652 change log.
|
||||||
|
|
||||||
|
## Canonical issue state
|
||||||
|
|
||||||
|
```text
|
||||||
|
STATE: roadmap-active
|
||||||
|
WHO_IS_NEXT: author
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
_UMBRELLA_655_BODY = """
|
||||||
|
## Scope (umbrella)
|
||||||
|
|
||||||
|
This issue owns the **canonical restart-governance program**. Implementation is via linked children only.
|
||||||
|
|
||||||
|
## Acceptance criteria (umbrella)
|
||||||
|
|
||||||
|
6. No product feature claimed complete on this issue alone.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_CHILD_BODY = """
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Operators need a workflow-event timeline model for Phase 1.
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- [ ] Timeline model API exists
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _issue(
|
||||||
|
number: int,
|
||||||
|
*,
|
||||||
|
title: str = "",
|
||||||
|
body: str = "",
|
||||||
|
labels: tuple[str, ...] = ("status:ready", "type:feature"),
|
||||||
|
priority: int = 20,
|
||||||
|
) -> WorkCandidate:
|
||||||
|
return WorkCandidate(
|
||||||
|
kind="issue",
|
||||||
|
number=number,
|
||||||
|
state="open",
|
||||||
|
labels=labels,
|
||||||
|
title=title or f"issue {number}",
|
||||||
|
body=body,
|
||||||
|
priority=priority,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _live_shaped_containers() -> list[WorkCandidate]:
|
||||||
|
return [
|
||||||
|
_issue(
|
||||||
|
631,
|
||||||
|
title="Epic: MCP Control Plane Web Console",
|
||||||
|
body=_EPIC_631_BODY,
|
||||||
|
),
|
||||||
|
_issue(
|
||||||
|
652,
|
||||||
|
title="Product vision: MCP Control Plane Web Console (canonical)",
|
||||||
|
body=_VISION_652_BODY,
|
||||||
|
),
|
||||||
|
_issue(
|
||||||
|
653,
|
||||||
|
title="Roadmap: MCP Control Plane Web Console (phased delivery)",
|
||||||
|
body=_ROADMAP_653_BODY,
|
||||||
|
),
|
||||||
|
_issue(
|
||||||
|
655,
|
||||||
|
title="Umbrella: Governed MCP restart coordination and zero-disruption recovery",
|
||||||
|
body=_UMBRELLA_655_BODY,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ClassifySemanticContainersTest(unittest.TestCase):
|
||||||
|
def test_652_vision_is_container(self) -> None:
|
||||||
|
c = _issue(
|
||||||
|
652,
|
||||||
|
title="Product vision: MCP Control Plane Web Console (canonical)",
|
||||||
|
body=_VISION_652_BODY,
|
||||||
|
)
|
||||||
|
is_c, detail = classify_epic_or_child_only_container(c)
|
||||||
|
self.assertTrue(is_c)
|
||||||
|
self.assertIsNotNone(detail)
|
||||||
|
self.assertIn("body_marker", detail or "")
|
||||||
|
|
||||||
|
def test_653_roadmap_is_container(self) -> None:
|
||||||
|
c = _issue(
|
||||||
|
653,
|
||||||
|
title="Roadmap: MCP Control Plane Web Console (phased delivery)",
|
||||||
|
body=_ROADMAP_653_BODY,
|
||||||
|
)
|
||||||
|
is_c, detail = classify_epic_or_child_only_container(c)
|
||||||
|
self.assertTrue(is_c)
|
||||||
|
self.assertIn("body_marker", detail or "")
|
||||||
|
|
||||||
|
def test_655_umbrella_is_container(self) -> None:
|
||||||
|
c = _issue(
|
||||||
|
655,
|
||||||
|
title="Umbrella: Governed MCP restart coordination and zero-disruption recovery",
|
||||||
|
body=_UMBRELLA_655_BODY,
|
||||||
|
)
|
||||||
|
is_c, detail = classify_epic_or_child_only_container(c)
|
||||||
|
self.assertTrue(is_c)
|
||||||
|
self.assertIn("body_marker", detail or "")
|
||||||
|
|
||||||
|
def test_631_still_container_after_854(self) -> None:
|
||||||
|
c = _issue(
|
||||||
|
631,
|
||||||
|
title="Epic: MCP Control Plane Web Console",
|
||||||
|
body=_EPIC_631_BODY,
|
||||||
|
)
|
||||||
|
is_c, detail = classify_epic_or_child_only_container(c)
|
||||||
|
self.assertTrue(is_c)
|
||||||
|
self.assertIn("body_marker", detail or "")
|
||||||
|
|
||||||
|
def test_incidental_vision_roadmap_umbrella_words_not_container(self) -> None:
|
||||||
|
cases = (
|
||||||
|
(
|
||||||
|
"Document vision handoff conventions",
|
||||||
|
"Update docs so implementable issues that mention a vision "
|
||||||
|
"remain independently executable.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Clarify roadmap sequencing notes",
|
||||||
|
"Write a short note about how the roadmap issue relates to children.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Umbrella recovery checklist for authors",
|
||||||
|
"Authors should still implement the concrete recovery fix here.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Product vision wording in the help text",
|
||||||
|
"Fix a typo in the operator-facing help string that says product vision.",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for title, body in cases:
|
||||||
|
with self.subTest(title=title):
|
||||||
|
c = _issue(900, title=title, body=body)
|
||||||
|
is_c, detail = classify_epic_or_child_only_container(c)
|
||||||
|
self.assertFalse(is_c)
|
||||||
|
self.assertIsNone(detail)
|
||||||
|
|
||||||
|
def test_title_prefix_alone_not_container(self) -> None:
|
||||||
|
for title in (
|
||||||
|
"Epic: something mentioned only in title",
|
||||||
|
"Roadmap: title only without body scope",
|
||||||
|
"Product vision: title only without body scope",
|
||||||
|
"Umbrella: title only without body scope",
|
||||||
|
):
|
||||||
|
with self.subTest(title=title):
|
||||||
|
c = _issue(
|
||||||
|
901,
|
||||||
|
title=title,
|
||||||
|
body="Implement a concrete fix for the allocator skip list.",
|
||||||
|
)
|
||||||
|
is_c, detail = classify_epic_or_child_only_container(c)
|
||||||
|
self.assertFalse(is_c)
|
||||||
|
self.assertIsNone(detail)
|
||||||
|
|
||||||
|
def test_roadmap_label_alone_is_container(self) -> None:
|
||||||
|
c = _issue(
|
||||||
|
902,
|
||||||
|
title="Console delivery sequencing",
|
||||||
|
body="Track phased delivery only.",
|
||||||
|
labels=("status:ready", "type:roadmap"),
|
||||||
|
)
|
||||||
|
is_c, detail = classify_epic_or_child_only_container(c)
|
||||||
|
self.assertTrue(is_c)
|
||||||
|
self.assertIn("type:roadmap", detail or "")
|
||||||
|
|
||||||
|
def test_child_referencing_parent_policy_stays_eligible(self) -> None:
|
||||||
|
"""Children may quote parent policy without becoming containers."""
|
||||||
|
c = _issue(
|
||||||
|
637,
|
||||||
|
title="Web Console: Workflow-event timeline model (Phase 1)",
|
||||||
|
body=(
|
||||||
|
_CHILD_BODY
|
||||||
|
+ "\n\nParent #652 says do not implement on the vision issue; "
|
||||||
|
"this child is the implementable unit."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
is_c, _ = classify_epic_or_child_only_container(c)
|
||||||
|
self.assertFalse(is_c)
|
||||||
|
|
||||||
|
|
||||||
|
class AllocateSemanticContainerExclusionTest(unittest.TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self._tmp = tempfile.TemporaryDirectory()
|
||||||
|
self.addCleanup(self._tmp.cleanup)
|
||||||
|
self.db = ControlPlaneDB(os.path.join(self._tmp.name, "cp.sqlite3"))
|
||||||
|
|
||||||
|
def _alloc(self, candidates, **kwargs):
|
||||||
|
defaults = dict(
|
||||||
|
session_id="sess-854",
|
||||||
|
role="author",
|
||||||
|
remote=REMOTE,
|
||||||
|
org=ORG,
|
||||||
|
repo=REPO,
|
||||||
|
profile_name="prgs-author",
|
||||||
|
username="jcwalker3",
|
||||||
|
claims={},
|
||||||
|
apply=False,
|
||||||
|
)
|
||||||
|
defaults.update(kwargs)
|
||||||
|
return allocate_next_work(self.db, candidates=candidates, **defaults)
|
||||||
|
|
||||||
|
def test_live_equivalent_canary_excludes_all_containers_selects_child(self) -> None:
|
||||||
|
containers = _live_shaped_containers()
|
||||||
|
child = _issue(
|
||||||
|
637,
|
||||||
|
title="Web Console: Workflow-event timeline model (Phase 1)",
|
||||||
|
body=_CHILD_BODY,
|
||||||
|
)
|
||||||
|
inventory = containers + [child]
|
||||||
|
res = self._alloc(inventory, apply=False)
|
||||||
|
self.assertTrue(res["success"], res)
|
||||||
|
self.assertEqual(res["outcome"], OUTCOME_PREVIEW)
|
||||||
|
self.assertEqual(res["selected"]["number"], 637)
|
||||||
|
|
||||||
|
skipped = {s["number"]: s for s in res["skipped"]}
|
||||||
|
for number in (631, 652, 653, 655):
|
||||||
|
self.assertIn(number, skipped, res["skipped"])
|
||||||
|
self.assertEqual(
|
||||||
|
skipped[number]["reason_code"],
|
||||||
|
SKIP_EPIC_OR_CHILD_ONLY_CONTAINER,
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
SKIP_EPIC_OR_CHILD_ONLY_CONTAINER, skipped[number]["reason"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_containers_cannot_receive_assignment_or_lease(self) -> None:
|
||||||
|
containers = _live_shaped_containers()
|
||||||
|
res = self._alloc(containers, apply=True)
|
||||||
|
self.assertTrue(res["success"], res)
|
||||||
|
self.assertNotEqual(res["outcome"], OUTCOME_ASSIGNED)
|
||||||
|
self.assertIsNone(res.get("assignment"))
|
||||||
|
self.assertIsNone(res.get("selected"))
|
||||||
|
skipped = {s["number"]: s for s in res["skipped"]}
|
||||||
|
for number in (631, 652, 653, 655):
|
||||||
|
self.assertEqual(
|
||||||
|
skipped[number]["reason_code"],
|
||||||
|
SKIP_EPIC_OR_CHILD_ONLY_CONTAINER,
|
||||||
|
)
|
||||||
|
|
||||||
|
leases = []
|
||||||
|
if hasattr(self.db, "list_active_leases"):
|
||||||
|
leases = self.db.list_active_leases(
|
||||||
|
remote=REMOTE, org=ORG, repo=REPO
|
||||||
|
)
|
||||||
|
if not leases and hasattr(self.db, "list_leases"):
|
||||||
|
leases = self.db.list_leases(remote=REMOTE, org=ORG, repo=REPO)
|
||||||
|
for lease in leases or []:
|
||||||
|
work_number = (
|
||||||
|
lease.get("work_number") if isinstance(lease, dict) else None
|
||||||
|
)
|
||||||
|
self.assertNotIn(work_number, {631, 652, 653, 655})
|
||||||
|
|
||||||
|
def test_apply_selects_child_not_container(self) -> None:
|
||||||
|
containers = _live_shaped_containers()
|
||||||
|
child = _issue(
|
||||||
|
637,
|
||||||
|
title="Web Console: Workflow-event timeline model (Phase 1)",
|
||||||
|
body=_CHILD_BODY,
|
||||||
|
)
|
||||||
|
res = self._alloc(containers + [child], apply=True)
|
||||||
|
self.assertTrue(res["success"], res)
|
||||||
|
self.assertEqual(res["outcome"], OUTCOME_ASSIGNED)
|
||||||
|
self.assertEqual(res["selected"]["number"], 637)
|
||||||
|
self.assertEqual(res["assignment"]["work_number"], 637)
|
||||||
|
|
||||||
|
def test_fingerprint_stable_with_containers_present(self) -> None:
|
||||||
|
containers = _live_shaped_containers()
|
||||||
|
child = _issue(
|
||||||
|
637,
|
||||||
|
title="Web Console: Workflow-event timeline model (Phase 1)",
|
||||||
|
body=_CHILD_BODY,
|
||||||
|
)
|
||||||
|
inventory = containers + [child]
|
||||||
|
fp_before = candidate_set_fingerprint(inventory)
|
||||||
|
res = self._alloc(inventory, apply=False)
|
||||||
|
self.assertTrue(res["success"], res)
|
||||||
|
self.assertEqual(res["selected"]["number"], 637)
|
||||||
|
# Allocator reports the same CAS fingerprint for the full candidate set.
|
||||||
|
reported = res.get("candidate_set_fingerprint")
|
||||||
|
self.assertEqual(reported, fp_before)
|
||||||
|
# Re-fingerprint of the same inventory is byte-stable.
|
||||||
|
self.assertEqual(candidate_set_fingerprint(inventory), fp_before)
|
||||||
|
|
||||||
|
def test_incidental_mentions_remain_eligible(self) -> None:
|
||||||
|
ordinary = _issue(
|
||||||
|
700,
|
||||||
|
title="Document roadmap handoff conventions",
|
||||||
|
body="Write runbook text about vision vs roadmap vs child issues.",
|
||||||
|
)
|
||||||
|
res = self._alloc([ordinary], apply=False)
|
||||||
|
self.assertTrue(res["success"], res)
|
||||||
|
self.assertEqual(res["selected"]["number"], 700)
|
||||||
|
self.assertEqual(res["skipped"], [])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from tests.webui_testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
import control_plane_db
|
import control_plane_db
|
||||||
from webui.analytics_loader import (
|
from webui.analytics_loader import (
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
from tests.webui_testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
from webui.app import create_app
|
from webui.app import create_app
|
||||||
from webui.audit_validator import audit_report, infer_task_kind
|
from webui.audit_validator import audit_report, infer_task_kind
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from tests.webui_testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
from tests.webui_testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
from webui.app import create_app
|
from webui.app import create_app
|
||||||
from webui.deployment_boundary import (
|
from webui.deployment_boundary import (
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from unittest.mock import patch
|
|||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
from tests.webui_testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
from task_capability_map import TASK_CAPABILITY_MAP
|
from task_capability_map import TASK_CAPABILITY_MAP
|
||||||
from webui.app import create_app
|
from webui.app import create_app
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
from tests.webui_testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
import control_plane_db
|
import control_plane_db
|
||||||
from webui.app import create_app
|
from webui.app import create_app
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
from tests.webui_testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
from webui.app import create_app
|
from webui.app import create_app
|
||||||
from webui.lease_loader import (
|
from webui.lease_loader import (
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
from tests.webui_testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
from starlette.routing import Route
|
from starlette.routing import Route
|
||||||
|
|
||||||
from webui.app import create_app
|
from webui.app import create_app
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
from tests.webui_testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
from webui.app import create_app
|
from webui.app import create_app
|
||||||
from webui.project_registry import (
|
from webui.project_registry import (
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
from tests.webui_testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
from webui.app import create_app
|
from webui.app import create_app
|
||||||
from webui.prompt_library import find_prompt, library_to_dict, load_prompt_library, prompt_to_dict
|
from webui.prompt_library import find_prompt, library_to_dict, load_prompt_library, prompt_to_dict
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from unittest import mock
|
|||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
from tests.webui_testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
from webui.app import create_app
|
from webui.app import create_app
|
||||||
from webui.queue_loader import (
|
from webui.queue_loader import (
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from unittest import mock
|
|||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
from tests.webui_testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
from webui.app import create_app
|
from webui.app import create_app
|
||||||
from webui.runtime_health import _role_kind, load_runtime_snapshot, snapshot_to_dict
|
from webui.runtime_health import _role_kind, load_runtime_snapshot, snapshot_to_dict
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
|||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
from starlette.routing import Route
|
from starlette.routing import Route
|
||||||
from tests.webui_testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
from webui import layout
|
from webui import layout
|
||||||
from webui.app import create_app
|
from webui.app import create_app
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
from tests.webui_testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
from webui.app import create_app
|
from webui.app import create_app
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from unittest import mock
|
|||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
from tests.webui_testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
import control_plane_db
|
import control_plane_db
|
||||||
from webui.app import create_app
|
from webui.app import create_app
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
from tests.webui_testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
from webui.app import create_app
|
from webui.app import create_app
|
||||||
from webui.deployment_boundary import scan_text_for_client_secrets
|
from webui.deployment_boundary import scan_text_for_client_secrets
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
from tests.webui_testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
import control_plane_db
|
import control_plane_db
|
||||||
from canonical_thread_handoff import (
|
from canonical_thread_handoff import (
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
from tests.webui_testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
from webui.app import create_app
|
from webui.app import create_app
|
||||||
from webui.worktree_scanner import (
|
from webui.worktree_scanner import (
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
"""Canonical Web UI ``TestClient`` import for the Gitea-Tools suite (#682).
|
|
||||||
|
|
||||||
Starlette 1.3+ builds ``starlette.testclient.TestClient`` on ``httpx2``.
|
|
||||||
Plain ``httpx`` still works but emits ``StarletteDeprecationWarning``.
|
|
||||||
|
|
||||||
All Web UI tests import ``TestClient`` from this module so:
|
|
||||||
|
|
||||||
* dependency intent is single-sourced (``httpx2`` in ``requirements.txt``);
|
|
||||||
* a future client-construction helper has one place to land;
|
|
||||||
* import-site audits do not need to re-scan every ``test_webui_*.py``.
|
|
||||||
|
|
||||||
Runtime MCP / FastMCP code continues to use ``httpx``; this module is
|
|
||||||
test-only and does not change production clients.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from starlette.testclient import TestClient
|
|
||||||
|
|
||||||
__all__ = ["TestClient"]
|
|
||||||
Reference in New Issue
Block a user