Ticket graphs

Every ticket as the root of its provenance graph — goal, patch, approval, runs and (as the ticket-class build lands) mandate, manifestations, guarantees, journey, artifacts and children. Rendered through the <<<TicketGraphList>>> tag.

← all ticket-graphs

DONE#377 forge-refactor

Rewrite code__code-invalid__coupler:coupler/tests/test_integration.py as behavior-preserving, installable, control-config'd Python [forge-rw:993656d6360bc88d]

goal

THIS UNIT SUPERSEDES #292 — it is the SAME module and the same work, re-filed to be SELF-CONTAINED. The superseded unit named its source only as a `git cat-file` command against the forge's bare repos, which the sandboxed pair_loop worker that executes these tickets cannot run, so it could not be done as filed (#346; #259 is the proof). A ticket body is permanent in SQL (tg_tickets_immutable_identity, migrations/0029_ticket_authorization .sql:111-117), so that body could not be repaired — this ticket carries the fix instead, and the original is halted by a stop ticket rather than erased. Do the work HERE. Rewrite ONE unique forge module as entirely new, behavior-preserving, installable, control-config'd Python, per the FORGE-wide refactor standard #200 (#std). This ticket covers ONE module — the one identified below — and nothing else. It was filed against the module's DEDUPLICATED identity, so if that module was copy-pasted across the forge, every copy is covered by this single ticket. MODULE KEY : forge-rw:993656d6360bc88d (the ticketer's idempotency marker — it lives in this ticket's TITLE. Do not edit the title, or the module will be re-filed as a duplicate.) IDENTITY : AST-NORMALIZED — codebean's `_ast_norm` (#183), i.e. ast.dump(ast.parse(ast.unparse(ast.parse(src)))). Copies of this module that differ only in comments, blank lines, quoting or spacing carry the SAME module-sha and are covered by THIS ticket. A differing docstring is real content, so a module whose docstring differs is a DIFFERENT module with its own ticket. SIZE : 4058 bytes EXTENSION : .py CANONICAL SOURCE (the location this ticket is named for): repo : code__code-invalid__coupler path : coupler/tests/test_integration.py ref : refs/heads/stable (as recorded when this module was first filed) blob : 200447c35c9708ee1659c72a6c168e944c3026a3 (git blob sha1) REFERENCE ONLY — you do NOT need this command. This module's bytes are embedded COMPLETE in THE ORIGINAL SOURCE block below, and that is what the rewrite is based on. For a reader who does have forge access: git --git-dir=/Users/stevenpeterson/code/installed/jazz-project/config/forge/remotes/code__code-invalid__coupler.git cat-file blob 200447c35c9708ee1659c72a6c168e944c3026a3 THE SAME MODULE ALSO LIVES AT THESE FORGE TIPS — 2 copy/copies. They SHARE THIS ticket: this module is rewritten ONCE, and every location below is satisfied by that one rewrite. Do not file a ticket per copy. coupler:coupler/tests/test_integration.py gitlab__testmonkeyalpha__coupler:coupler/tests/test_integration.py HISTORICAL OCCURRENCES of this exact blob — every OTHER DISTINCT {repo, path} the omni-git index (`occ`, joined on the git blob sha1) records this byte-identical file at, with the index's own `ts` for the first row of each. Any place this run enumerated LIVE is listed under the forge tips above, not here. Context for the rewrite, not extra work: testmonkeyalpha/coupler:coupler/tests/test_integration.py [occ.ts gitlab] THE ORIGINAL SOURCE, EMBEDDED — YOU NEED NO FORGE ACCESS TO DO THIS TICKET. The module's own bytes are reproduced below, inside this ticket. THEY are the authoritative copy and the behavior-preserving basis: rewrite from them. The `git --git-dir=... cat-file blob` command under CANONICAL SOURCE above is kept only as a REFERENCE for a reader who happens to have forge access — running it is NOT part of this ticket. (Why the bytes are here at all: the pair_loop worker that executes this ticket is sandboxed to the ticket's own congruency worktree and cannot read the forge's bare repos. Ticket #346 is the fix that put them on the ticket.) The block BEGINS at the line ===ORIGINAL SOURCE (verbatim, the behavior-preserving basis)=== and ENDS at the first line ===END SOURCE=== Everything strictly between those two lines is the module — 4058 byte(s), byte for byte, with no re-indentation, no re-wrapping and nothing elided. ===ORIGINAL SOURCE (verbatim, the behavior-preserving basis)=== """ Integration tests — real server, real FastMCP client. Spins up coupler/server.py on a random port, connects a FastMCP client, and exercises the actual tool round-trip. Requires the coupler package installed and a working Python, but no live jazz services. Marked with @pytest.mark.slow — skip with: pytest -m "not slow" """ import asyncio import json import os import socket import sys import time from pathlib import Path import pytest import pytest_asyncio from fastmcp import Client from fastmcp.client.transports import SSETransport def _free_port() -> int: with socket.socket() as s: s.bind(("127.0.0.1", 0)) return s.getsockname()[1] @pytest.fixture(scope="module") def server_port(tmp_path_factory): """Start coupler/server.py on a free port, yield port, then terminate.""" tmp = tmp_path_factory.mktemp("jazz") env_dir = tmp / "conf" / "env" env_dir.mkdir(parents=True) (env_dir / "build.json").write_text(json.dumps({ "version": "1.0.0", "envs": {}, })) port = _free_port() env = { **os.environ, "JAZZ_ROOT": str(tmp), "JAZZ_DEV": "/nonexistent", "COUPLER_PORT": str(port), "PYTHONPATH": str(Path(__file__).parent.parent.parent), } import subprocess proc = subprocess.Popen( [sys.executable, "-m", "coupler.server", "--port", str(port)], env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) # wait up to 10s for the server to accept connections deadline = time.time() + 10 while time.time() < deadline: try: with socket.create_connection(("127.0.0.1", port), timeout=0.5): break except OSError: time.sleep(0.2) else: proc.terminate() out, err = proc.communicate(timeout=5) pytest.fail(f"Server did not start in time.\nstdout: {out}\nstderr: {err}") yield port proc.terminate() try: proc.wait(timeout=5) except subprocess.TimeoutExpired: proc.kill() @pytest.mark.slow @pytest.mark.asyncio async def test_coupler_status_via_sse(server_port): """coupler_status returns valid JSON over a real SSE connection.""" async with Client(SSETransport(f"http://127.0.0.1:{server_port}/sse")) as c: tools = await c.list_tools() names = {t.name for t in tools} assert "coupler_status" in names result = await c.call_tool("coupler_status", {}) text = result.content[0].text data = json.loads(text) assert data["coupler"] == "ok" assert "jazz_version" in data # jazz services will be down (no real jazz install) — that's expected for svc in ("notes", "notes_db", "z3"): assert data[svc] in ("ok", "down", "degraded") @pytest.mark.slow @pytest.mark.asyncio async def test_ui_tool_available_via_sse(server_port): """UI tools are registered and callable (returns error when UI not running).""" async with Client(SSETransport(f"http://127.0.0.1:{server_port}/sse")) as c: tools = {t.name for t in await c.list_tools()} assert "ui_list_pages" in tools result = await c.call_tool("ui_list_pages", {}) text = result.content[0].text # either works or returns a clean error string — not an exception assert isinstance(text, str) assert len(text) > 0 @pytest.mark.slow @pytest.mark.asyncio async def test_why5_tool_available_via_sse(server_port): """why5 tools are present if jazz dev tree is available, absent otherwise.""" async with Client(SSETransport(f"http://127.0.0.1:{server_port}/sse")) as c: tools = {t.name for t in await c.list_tools()} # why5 tools are optional — just verify no crash either way if "why5_new" in tools: result = await c.call_tool("why5_new", { "session_id": "test-session", "trigger": "integration test" }) assert "test-session" in result.content[0].text ===END SOURCE=== THE CALLABLE SURFACE — every callable in the original, lowered with codebean's `lower_node` (#183). The rewrite must present THIS surface (same names, same parameters, same defaults) so callers of the original keep working: _free_port() -> int server_port(tmp_path_factory) async test_coupler_status_via_sse(server_port) async test_ui_tool_available_via_sse(server_port) async test_why5_tool_available_via_sse(server_port) CAPTURED BEHAVIORAL EXEMPLARS: none. no function in this module is minable — criteria_miner mines only self-contained pure functions (it refuses anything that mentions a world-touching or non-deterministic name, plus methods and no-arg functions), and nothing here qualified This does NOT weaken the ticket's bar — it MOVES the work: you must write the equivalence exemplars by hand from the original's behavior before rewriting, and they are what settles this ticket. THE STANDARD — #200's four requirements, which this ticket exists to enforce. All four are the standard's own words; none is optional: 1. BEHAVIOR-PRESERVING, VERIFIED NOT ASSERTED — the deliverable is ENTIRELY NEW source code (rewritten, not copied). It must reproduce the ORIGINAL's behavior, and the proof is the exemplars above (captured via codebean lower_* #183 + criteria_miner IO-exemplars, exactly as #200 specifies). 2. A PYTHON MODULE — regardless of the source language. This module's source is `.py`; the rewrite is Python either way. 3. INSTALLABLE — a proper package: pyproject.toml, `pip install -e .` works, console entry points where applicable. 4. CONTROL-CONFIG'D — NO hardcoded paths or params. A JSON/registry config object drives it (the install.json / registry.json idiom of this repo), and per note-for-claude a tool that cannot see its config THROWS rather than guessing a default. ACCEPTANCE (#200's, per rewrite ticket) — this ticket settles when ALL of: a. the new source reproduces every captured exemplar above, plus the edge cases you add for what sampling cannot reach (paste the runs as evidence); b. it presents the callable surface above, so existing callers keep working; c. `pip install -e .` installs it clean from its own pyproject.toml; d. it runs off a config object with ZERO hardcoded settings, and throws without one. Kind is `build` (a pair: coder + adversarial tester). This ticket hangs off #200 by a `spawned` edge, so a stop ticket aimed at #200 brakes every rewrite in this programme. Filed mechanically by checkouts/current/congruency/tools/forge_refactor_ticketer.py under ticket #201, from the forge bare repos at /Users/stevenpeterson/code/installed/jazz-project/config/forge/remotes. Its original source is embedded above (#346), so this ticket is complete on its own.

patch

none

approval

unapproved

runs

no runs recorded

mandate (clauses)

not yet recorded — lands with the ticket-class build

manifestations

not yet recorded — lands with the ticket-class build

guarantees

not yet recorded — lands with the ticket-class build

journey (blunders & successes)

not yet recorded — lands with the ticket-class build

artifacts (forge)

not yet recorded — lands with the ticket-class build

source (forge tree)

browse congruency source (forge-parked tree)

children

#292 Rewrite code__code-invalid__coupler:coupler/tests/test_integration.py as behavior-preserving, installable, control-config'd Python [forge-rw:993656d6360bc88d] (supersedes · REQUESTED)