import json import os import sys import unittest from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from wflib import lanes as L from wflib import tasks as T from test_cli import Cli ITEMS = """\ ## Awaiting your decision ## Pending - **t-a** [P1] (1h): A. - **t-b** [P1] (1h): B. - After: [[t-s]] - **t-s** [P1] (1h): S. Model: sonnet - **t-s2** [P2] (1h): S2. Model: sonnet - After: [[t-x]] - **t-c** [P2] (1h): C. - After: [[t-s2]] ## Needs human ## Deferred """ SOCK = "/run/user/1000/cc-socks/1.sock" class LanesTest(unittest.TestCase): def setUp(self): self.doc = T.parse(ITEMS) def test_counts(self): self.assertEqual(L.counts(self.doc, set(), L.DEFAULT_LANES, "1h"), {"fast": (0, 0, 0), "slow": (2, 3, 0)}) def test_counts_held(self): self.assertEqual(L.counts(self.doc, set(), L.DEFAULT_LANES, "1h", {"t-a": "x"}), {"fast": (0, 0, 0), "slow": (1, 4, 0)}) def test_counts_slice_jobs(self): self.assertEqual(L.counts(T.parse(PICK), {"t-old1"}, L.DEFAULT_LANES, "1h"), {"fast": (3, 2, 1), "slow": (1, 1, 0)}) def test_lanes_block(self): counts = {"fast": (3, 1, 1), "slow": (0, 2, 0)} sessions = {"slow": {"socket": SOCK, "alive": True, "model": "opus"}} self.assertEqual(L.lanes_block(counts, sessions, "fast"), [ "fast (you): 3 pickable (1 slice) · 1 waiting", f"slow: 0 pickable · 2 waiting · session uds:{SOCK} (alive, opus)", ]) def test_lanes_block_no_session(self): self.assertEqual(L.lanes_block({"fast": (2, 0, 2), "slow": (0, 0, 0)}, {}, None), [ "fast: 2 pickable (2 slices) · 0 waiting · no session → orchestrator or owner: start one (wf next --lane fast)", "slow: 0 pickable · 0 waiting · no session", ]) def test_cross_waits_by_lane(self): doc = T.parse(PICK) self.assertEqual([(m.id, b.id) for m, b in L.cross_waits(doc, {"t-old1"}, "slow", L.DEFAULT_LANES, "1h")], [("t-w1", "t-blocker")]) self.assertEqual(L.cross_waits(doc, {"t-old1"}, "fast", L.DEFAULT_LANES, "1h"), []) def test_waiting_block(self): doc = T.parse(PICK) waits = L.cross_waits(doc, {"t-old1"}, "slow", L.DEFAULT_LANES, "1h") self.assertEqual(L.waiting_block(waits, {"fast": {"socket": SOCK, "alive": True}}, L.DEFAULT_LANES, "1h"), [f'- t-w1 waits on t-blocker (fast lane) → message uds:{SOCK}: ' '"t-blocker blocks my t-w1, please take it"']) self.assertEqual(L.waiting_block(waits, {}, L.DEFAULT_LANES, "1h"), ["- t-w1 waits on t-blocker (fast lane) → no fast session: tell the owner"]) def test_notify_block_by_lane(self): doc = T.parse(PICK) freed = [doc.item("t-w1"), doc.item("t-w2")] self.assertEqual(L.notify_block(freed, {"slow": {"socket": SOCK, "alive": True}}, L.DEFAULT_LANES, "1h"), [ "fast work now pickable: t-w2 (no session: tell the owner)", f"notify slow uds:{SOCK}: now pickable t-w1", ]) def test_solo_done_block_any_session_name(self): sessions = {"fast": {"socket": SOCK, "alive": True, "pid": 1}, "all": {"socket": "/b", "alive": True, "pid": 2}, "slow": {"socket": "/c", "alive": False, "pid": 3}} self.assertEqual(L.solo_done_block(["t-s"], sessions, "2"), [f"notify fast uds:{SOCK}: solo t-s done, run wf next"]) PICK = """\ ## Awaiting your decision ## Pending - **t-big** [P2] (5h): Big unsliced. - **t-p0** [P0] (<1h): Small P0. - **t-blocker** [P3] (<1h): Small, two wait on it. - **t-w1** [P2] (1h): Waits. - After: [[t-blocker]] - **t-w2** [P3] (<1h): Waits too. - After: [[t-blocker]] Model: sonnet - **t-mid** [P1] (1h): Mid. Model: sonnet - **t-done-parent** [P1] (10h): Sliced, slices archived. - Slices: [[t-old1]] ## Needs human ## Deferred """ class PickTest(unittest.TestCase): def setUp(self): self.doc = T.parse(PICK) def pick(self, lane=None, model=None, archived=frozenset({"t-old1"})): item, _ = L.pick(self.doc, set(archived), L.DEFAULT_LANES, "1h", lane, model) return item.id if item else None def test_lane_of(self): got = {i.id: L.lane_of(i, L.DEFAULT_LANES, "1h") for i in self.doc.section("pending").items} self.assertEqual(got, {"t-big": "fast", "t-p0": "fast", "t-blocker": "fast", "t-w1": "slow", "t-w2": "fast", "t-mid": "slow", "t-done-parent": "fast"}) def test_no_effort_is_fast(self): item = T.Item(id="t-x", prio=1) self.assertEqual(L.lane_of(item, L.DEFAULT_LANES, "1h"), "fast") def test_fast_unblock_order(self): # unblock: waited-on first (t-blocker: 2 waiters), then the slice job t-big (0 waiters) vs t-p0 by prio self.assertEqual([i.id for i in L.ranked(self.doc, {"t-old1"}, L.DEFAULT_LANES, "1h", "fast")], ["t-blocker", "t-p0", "t-big"]) def test_slow_priority_order_then_fallback(self): self.assertEqual(self.pick("slow"), "t-mid") self.assertEqual([i.id for i in L.ranked(self.doc, {"t-old1"}, L.DEFAULT_LANES, "1h", "slow")], ["t-mid", "t-blocker", "t-p0", "t-big"]) def test_fallback_when_lane_empty(self): doc = T.parse(PICK.replace("(1h): Mid.", "(<1h): Mid.")) # slow has only t-w1 (blocked) item, _ = L.pick(doc, {"t-old1"}, L.DEFAULT_LANES, "1h", "slow") self.assertEqual(item.id, "t-blocker") def test_all_lanes_priority_order(self): self.assertEqual(self.pick(None), "t-p0") def test_model_ceiling(self): # haiku session: no task has Model haiku; opus (default) and sonnet are above it self.assertIsNone(self.pick("fast", "haiku")) self.assertEqual(self.pick("slow", "sonnet"), "t-mid") self.assertEqual(self.pick("fast", "sonnet"), None) # t-w2 is blocked; others are opus self.assertEqual(self.pick("fast", "opus"), "t-blocker") def test_slice_job(self): big = self.doc.item("t-big") self.assertTrue(L.is_slice_job(big, "1h")) self.assertFalse(L.is_slice_job(self.doc.item("t-p0"), "1h")) def test_sliced_parent_not_slice_job(self): parent = self.doc.item("t-done-parent") self.assertFalse(L.is_slice_job(parent, "1h")) self.assertTrue(L.sliced_out(parent, "1h")) # skipped lists items ranked before the pick: drop the others so the parent is first doc = T.parse(PICK[:PICK.index("- **t-big**")] + PICK[PICK.index("- **t-done-parent**"):]) _, skipped = L.pick(doc, {"t-old1"}, L.DEFAULT_LANES, "1h", "fast") self.assertIn(("t-done-parent", "sliced: all slices done → wf done t-done-parent or add slices"), [(i.id, why) for i, why in skipped]) PICK_DONE = PICK.replace("Small P0.\n", "Small P0.\n Done: x\n").replace( "two wait on it.\n", "two wait on it.\n Done: x\n").replace("Big unsliced.\n", "Big unsliced.\n Done: x\n") class LanesCliTest(Cli): tasks_text = "# Tasks — demo\n\n" + PICK_DONE def env(self, name="me"): sock = self.root / f"{name}.sock" sock.write_text("") return {"CLAUDE_CODE_MESSAGING_SOCKET": str(sock), "CLAUDE_PID": str(os.getpid()), "CLAUDE_CODE_SESSION_ID": name} def session(self, name): return json.loads((self.root / ".wf" / "sessions" / f"{name}.json").read_text()) def test_next_lane_registers_lane(self): out = self.ok("next", "--lane", "slow", "--as", "opus", env=self.env()) self.assertIn("===== Next task =====\n- **t-mid**", out) s = self.session("slow") self.assertEqual((s["lane"], s["model"], s["socket"]), ("slow", "opus", str(self.root / "me.sock"))) self.assertIn("===== Lanes =====\nfast: 3 pickable (1 slice) · 2 waiting · no session → " "orchestrator or owner: start one (wf next --lane fast)\n" "slow (you): 1 pickable · 1 waiting\n\n" "===== Waiting on other lanes =====\n" "- t-w1 waits on t-blocker (fast lane) → no fast session: tell the owner\n", out) def test_next_without_lane_registers_all(self): out = self.ok("next", "--as", "opus", env=self.env()) self.assertIn("===== Next task =====\n- **t-p0**", out) self.assertEqual(self.session("all")["lane"], "all") self.assertNotIn("Waiting on other lanes", out) def test_slice_job_output(self): (self.root / "TASKS.md").write_text("# Tasks — demo\n\n## Awaiting your decision\n\n## Pending\n\n" "- **t-big** [P2] (5h): Big unsliced.\n\n## Needs human\n\n## Deferred\n") out = self.ok("next", "--lane", "fast", "--as", "opus", "--brief") self.assertEqual(out, "- **t-big** [P2] (5h): Big unsliced.\n\n" "===== Slice job (no code) =====\n" "effort 5h > slice_above 1h: split it, don't implement.\n" ' wf add "