From 84349a0f4098d1f4600013c72d1ac00d2b0ff384 Mon Sep 17 00:00:00 2001 From: "dilux-bot[bot]" <333187390+dilux-bot[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 22:11:01 +0000 Subject: [PATCH 1/9] chore(autopilot): start on #713 The draft pull request opens before any code: the Spec as its checklist. From 462f7bca37a8b2e814b5a924f1012019a0d84a42 Mon Sep 17 00:00:00 2001 From: "dilux-bot[bot]" <333187390+dilux-bot[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 22:24:38 +0000 Subject: [PATCH 2/9] fix(waiting): a cancelled check run is not a failed check (C1) The waiting view read only statusCheckRollup.state, which GitHub folds to FAILURE when a check run is cancelled, so a pull request whose only non-green run was the view's own job, superseded by its concurrency group, was told to fix the failed check. It now reads the head's contexts and counts only a run concluded FAILURE, TIMED_OUT or STARTUP_FAILURE, or a status FAILURE or ERROR, as failed; with none, the state is pending or green from the other contexts. Assisted-by: Claude Opus 5.5 (Anthropic) --- scripts/fixtures/waiting.json | 222 ++++++++++++++++++++++++++++++++++ scripts/waiting.py | 67 +++++++++- 2 files changed, 287 insertions(+), 2 deletions(-) diff --git a/scripts/fixtures/waiting.json b/scripts/fixtures/waiting.json index e0b0b6ac..dcde7177 100644 --- a/scripts/fixtures/waiting.json +++ b/scripts/fixtures/waiting.json @@ -567,6 +567,228 @@ "a person's merge", "the maintainer" ] + }, + { + "number": 12, + "title": "the autopilot's, only the view cancelled", + "url": "https://github.com/DiluxOne/diluxone-test/pull/12", + "createdAt": "2026-10-21T08:00:00Z", + "isDraft": false, + "author": { + "login": "dilux-bot[bot]" + }, + "repository": { + "name": "diluxone-test", + "isPrivate": false + }, + "labels": { + "nodes": [ + { + "name": "autopilot" + } + ] + }, + "reviewThreads": { + "nodes": [] + }, + "commits": { + "nodes": [ + { + "commit": { + "oid": "head12", + "statusCheckRollup": { + "state": "FAILURE", + "contexts": { + "pageInfo": { + "hasNextPage": false + }, + "nodes": [ + { + "__typename": "CheckRun", + "name": "gates / G1", + "status": "COMPLETED", + "conclusion": "SUCCESS" + }, + { + "__typename": "CheckRun", + "name": "review / Claude review", + "status": "COMPLETED", + "conclusion": "SUCCESS" + }, + { + "__typename": "CheckRun", + "name": "waiting / The \"What waits\" view", + "status": "COMPLETED", + "conclusion": "CANCELLED" + }, + { + "__typename": "StatusContext", + "context": "ci/legacy", + "state": "SUCCESS" + } + ] + } + } + } + } + ] + }, + "closingIssuesReferences": { + "nodes": [ + { + "number": 109, + "labels": { + "nodes": [ + { + "name": "accepted" + } + ] + } + } + ] + }, + "want": [ + "a person's merge", + "the maintainer" + ] + }, + { + "number": 13, + "title": "the view cancelled, a check still running", + "url": "https://github.com/DiluxOne/diluxone-test/pull/13", + "createdAt": "2026-10-21T08:00:00Z", + "isDraft": false, + "author": { + "login": "someone" + }, + "repository": { + "name": "diluxone-test", + "isPrivate": false + }, + "labels": { + "nodes": [] + }, + "reviewThreads": { + "nodes": [] + }, + "commits": { + "nodes": [ + { + "commit": { + "oid": "head13", + "statusCheckRollup": { + "state": "FAILURE", + "contexts": { + "pageInfo": { + "hasNextPage": false + }, + "nodes": [ + { + "__typename": "CheckRun", + "name": "waiting / The \"What waits\" view", + "status": "COMPLETED", + "conclusion": "CANCELLED" + }, + { + "__typename": "CheckRun", + "name": "gates / G2", + "status": "IN_PROGRESS", + "conclusion": null + } + ] + } + } + } + } + ] + }, + "closingIssuesReferences": { + "nodes": [ + { + "number": 109, + "labels": { + "nodes": [ + { + "name": "accepted" + } + ] + } + } + ] + }, + "want": [ + "checks running", + "the checks" + ] + }, + { + "number": 14, + "title": "the view cancelled and a check failed", + "url": "https://github.com/DiluxOne/diluxone-test/pull/14", + "createdAt": "2026-10-21T08:00:00Z", + "isDraft": false, + "author": { + "login": "someone" + }, + "repository": { + "name": "diluxone-test", + "isPrivate": false + }, + "labels": { + "nodes": [] + }, + "reviewThreads": { + "nodes": [] + }, + "commits": { + "nodes": [ + { + "commit": { + "oid": "head14", + "statusCheckRollup": { + "state": "FAILURE", + "contexts": { + "pageInfo": { + "hasNextPage": false + }, + "nodes": [ + { + "__typename": "CheckRun", + "name": "waiting / The \"What waits\" view", + "status": "COMPLETED", + "conclusion": "CANCELLED" + }, + { + "__typename": "CheckRun", + "name": "gates / G2", + "status": "COMPLETED", + "conclusion": "FAILURE" + } + ] + } + } + } + } + ] + }, + "closingIssuesReferences": { + "nodes": [ + { + "number": 109, + "labels": { + "nodes": [ + { + "name": "accepted" + } + ] + } + } + ] + }, + "want": [ + "a failed check", + "its author" + ] } ], "issues": [ diff --git a/scripts/waiting.py b/scripts/waiting.py index ebb27fb7..080fd91a 100644 --- a/scripts/waiting.py +++ b/scripts/waiting.py @@ -14,6 +14,9 @@ an acceptance an issue it closes is not `accepted` the maintainer a failed check a check of its head failed its author (the autopilot for its own) + (a cancelled check run is not a failed one, #713: + its concurrency group superseded it, or a person + stopped it; failed_checks) unresolved threads a review conversation is open its author the autopilot's work an autopilot draft the autopilot a draft someone else's draft its author @@ -81,7 +84,9 @@ repository { name nameWithOwner isPrivate } labels(first: 30) { nodes { name } } reviewThreads(first: 100) { nodes { isResolved } } - commits(last: 1) { nodes { commit { oid statusCheckRollup { state } } } } + commits(last: 1) { nodes { commit { oid statusCheckRollup { state contexts(first: 100) { + pageInfo { hasNextPage } + nodes { __typename ... on CheckRun { name conclusion status } ... on StatusContext { context state } } } } } } } closingIssuesReferences(first: 10) { nodes { number labels(first: 30) { nodes { name } } } } }""" QUERY = """query($q: String!, $after: String) { @@ -103,6 +108,51 @@ def is_pr(item): return item.get("__typename") == "PullRequest" or "isDraft" in item +# A check run that failed (scripts/needed-a-person.py reads them the same +# way): a CANCELLED one is none (#713), superseded by a newer run of its +# concurrency group (the "What waits" view's own, for one) or stopped by a +# person, though GitHub folds it into the rollup's FAILURE. +FAILED_RUN = {"FAILURE", "TIMED_OUT", "STARTUP_FAILURE"} +FAILED_STATUS = {"FAILURE", "ERROR"} + + +def failed_checks(rollup): + """The names of the head's checks that failed, from the rollup's + contexts: a check run concluded FAILURE, TIMED_OUT or STARTUP_FAILURE, + or a commit status FAILURE or ERROR; never a cancelled run. Pure.""" + out = [] + for c in ((rollup or {}).get("contexts") or {}).get("nodes") or []: + if not c: + continue + if c.get("__typename") == "StatusContext" or "context" in c: + if (c.get("state") or "").upper() in FAILED_STATUS: + out.append(c.get("context") or "") + elif (c.get("conclusion") or "").upper() in FAILED_RUN: + out.append(c.get("name") or "") + return out + + +def checks_state(rollup): + """The head's checks as one state, FAILURE only for a check that failed + (failed_checks): when GitHub's rollup says FAILURE or ERROR and no + context failed, it is PENDING while one still runs, else SUCCESS. A + rollup without its contexts, or with more than were read, keeps + GitHub's state. Pure.""" + rollup = rollup or {} + state = rollup.get("state") or "" + contexts = rollup.get("contexts") + if state not in ("FAILURE", "ERROR") or not isinstance(contexts, dict) or not contexts.get("nodes"): + return state + if failed_checks(rollup): + return "FAILURE" + if (contexts.get("pageInfo") or {}).get("hasNextPage"): + return state + running = [c for c in contexts["nodes"] if c and ( + (c.get("state") or "").upper() in ("PENDING", "EXPECTED") if (c.get("__typename") == "StatusContext" or "context" in c) + else (c.get("status") or "COMPLETED").upper() != "COMPLETED")] + return "PENDING" if running else "SUCCESS" + + def blocker(pr): """(what blocks it, on whom it waits) for one pull request. Pure.""" labels = names(pr.get("labels")) @@ -116,7 +166,7 @@ def blocker(pr): if any("accepted" not in names(i.get("labels")) for i in issues): return "an acceptance", MAINTAINER commits = (pr.get("commits") or {}).get("nodes") or [] - state = (((commits[-1] if commits else {}).get("commit") or {}).get("statusCheckRollup") or {}).get("state") or "" + state = checks_state(((commits[-1] if commits else {}).get("commit") or {}).get("statusCheckRollup")) if state in ("FAILURE", "ERROR"): return "a failed check", AUTOPILOT if autopilot else AUTHOR open_threads = sum(1 for t in ((pr.get("reviewThreads") or {}).get("nodes") or []) if not t.get("isResolved")) @@ -366,6 +416,19 @@ def check(name, got, want): items = fx["prs"] + fx["issues"] for p in fx["prs"]: check(f"C7 {p['title']}: {p['want'][0]}, waits on {p['want'][1]}", blocker(p), tuple(p["want"])) + # #713 C1: the view's own run, cancelled by its concurrency group, is no failed check. + lone = next(p for p in fx["prs"] if p["title"] == "the autopilot's, only the view cancelled") + roll = lone["commits"]["nodes"][0]["commit"]["statusCheckRollup"] + check("C1 (#713) a cancelled check run is not a failed one", (failed_checks(roll), checks_state(roll)), ([], "SUCCESS")) + check("C1 (#713) its board does not say fix the failed check", waits_of(lone, boss)["what"], "review it and merge it") + check("C1 (#713) a failed run, a timed-out one and an erroring status still fail", + failed_checks({"contexts": {"nodes": [{"__typename": "CheckRun", "name": "a", "conclusion": "FAILURE"}, {"__typename": "CheckRun", "name": "b", "conclusion": "TIMED_OUT"}, + {"__typename": "StatusContext", "context": "c", "state": "ERROR"}, {"__typename": "CheckRun", "name": "d", "conclusion": "CANCELLED"}]}}), + ["a", "b", "c"]) + check("C1 (#713) a rollup without its contexts, or with more than were read, keeps GitHub's state", + (checks_state({"state": "FAILURE"}), checks_state({"state": "FAILURE", "contexts": {"pageInfo": {"hasNextPage": True}, "nodes": [roll["contexts"]["nodes"][2]]}})), + ("FAILURE", "FAILURE")) + check("C1 (#713) the query reads the head's check runs and statuses", ("... on CheckRun { name conclusion status }" in FIELDS, "... on StatusContext { context state }" in FIELDS, FIELDS in ONE), (True, True, True)) for i in fx["issues"]: check(f"C3 issue {i['title']}: {i['want']}", list(issue_blocker(i) or []) or None, i["want"]) body = render(items, now, boss) From 42e45784be8ae0d5151d2f43fe1e83b6a469b161 Mon Sep 17 00:00:00 2001 From: "dilux-bot[bot]" <333187390+dilux-bot[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 22:24:41 +0000 Subject: [PATCH 3/9] fix(scenarios): wait for the autopilot's result after triage (C2) The scenarios that read the autopilot's result (its seal, the autopilot label, its pull request) opened their issue accepted, or accepted it before the triage answered, so the autopilot saw it unsealed or the expectation was read before it ran. Each now opens its issue plain, waits until awaiting-acceptance, accepts it, and waits until the seal before the autopilot's result, within 15 minutes; core-one-at-a-time does the same for its before issue, with steps that take `of: before`. A new autopilot_problems, run by coverage, names any scenario that reads that result too early, and the self-test plays accepted-spec against a fake repository that seals only after the triage. Assisted-by: Claude Opus 5.5 (Anthropic) --- scripts/release-scenarios.py | 144 +++++++++++++++++++++++++++++++++-- tests/release/scenarios.yml | 49 ++++++++---- 2 files changed, 174 insertions(+), 19 deletions(-) diff --git a/scripts/release-scenarios.py b/scripts/release-scenarios.py index fdada817..5e9a7364 100644 --- a/scripts/release-scenarios.py +++ b/scripts/release-scenarios.py @@ -27,6 +27,7 @@ `comment`, `label`, `unlabel`, `edit`, `push`, `dispatch`, and `until`, an `expect` polled every 15 seconds on its issue or pull request, or with `of: before` on its first `before` issue, never a fixed wait: #694; a +`comment` or a `label` with `of: before` acts on that issue too, #713; a file is written in a commit GitHub signs, `createCommitOnBranch` (#709); a `push: ` writes scenarios//.md on the head branch of the pull request it observes, the autopilot's for an issue, and `conflict` @@ -53,7 +54,11 @@ `expect` cannot hold against its own setup (`preconditions`: a dispatch of a workflow the test repository cannot run on demand among them; #707: a bug's verdict in a test repository with no way - to reproduce it, and the lane's `play` job on DX_RUNNER_HEAVY). + to reproduce it, and the lane's `play` job on DX_RUNNER_HEAVY; + #713: one that reads the autopilot's result on an issue it opens + accepted, accepts before the triage's verdict or waits for no + seal after it, + `autopilot_problems`). Every other pair a shipped caller wires is held to the engine statically, by scripts/caller-contract.py check; and what is wrong with the groups (`partition`): a scenario in no @@ -369,7 +374,8 @@ def preconditions(scenarios, bodies, pack, can_dispatch=(), passes=None, types=N out.append(f"scenario {sid}: expects its pull request to close an issue, but its body names no `{{issue}}` it opens") if waits_on_core(s): first = [b.get("issue") or {} for b in s.get("before") or []] - if not any("accepted" in (i.get("labels") or []) for i in first): + later = any(isinstance(st, dict) and st.get("of") == "before" and st.get("label") == "accepted" for st in s.get("steps") or []) + if not (later and first) and not any("accepted" in (i.get("labels") or []) for i in first): out.append(f"scenario {sid}: expects a wait on the core, but opens no accepted issue before it whose pull request stays open") fixed = str((pack.get("fixed-change") or {}).get("file") or "").replace("{n}", "1").replace("{id}", "c1").replace("{scenario}", sid) if not any(fnmatch.fnmatchcase(fixed, g) for g in pack.get("core") or []): @@ -924,11 +930,14 @@ def land(arg, path, content=None): number = url.rstrip("/").rsplit("/", 1)[-1] made.append((what, number)) for step in s.get("steps") or []: - (verb, arg), = step.items() + (verb, arg), = ((k, v) for k, v in step.items() if k != "of") + # #713: `of: before` takes a comment or a label on the first + # issue it opened before it (its acceptance after its triage). + kind, target = ("issue", befores[0]) if step.get("of") == "before" and befores else (what, number) if verb == "comment": - run(what, "comment", number, "--repo", repo, "--body", str(arg)) + run(kind, "comment", target, "--repo", repo, "--body", str(arg)) elif verb in ("label", "unlabel"): - run(what, "edit", number, "--repo", repo, f"--{'add' if verb == 'label' else 'remove'}-label", str(arg)) + run(kind, "edit", target, "--repo", repo, f"--{'add' if verb == 'label' else 'remove'}-label", str(arg)) elif verb == "edit": run(what, "edit", number, "--repo", repo, "--body-file", "-", input=marked(body_of(arg, bodies), sid)) elif verb == "dispatch": @@ -1336,6 +1345,57 @@ def waits_on_core(s): return CORE_LABEL in (expect.get("labels") or []) or CORE_MARK in (expect.get("comments") or []) +SEAL = "" + + +def needs_autopilot(want): + """Whether the expectation `want`, on an issue, needs the autopilot's + result (#713): its seal, the `autopilot` or core-wait label, its comment + on the core, or its pull request. Pure.""" + want = want or {} + return (any(m in (want.get("comments") or []) for m in (SEAL, CORE_MARK)) + or any(l in (want.get("labels") or []) for l in ("autopilot", CORE_LABEL)) or bool(want.get("pull"))) + + +def autopilot_problems(scenarios): + """Every scenario that reads the autopilot's result on an issue (its own, + or with `of: before` its first `before` one: `needs_autopilot`; a + release issue's pull request is the release's) too + early (#713): one that opens that issue `accepted` (the autopilot then + reads a body the triage had not answered, unsealed), takes no `label: + accepted` step, takes it before an `until` the triage's verdict + (`awaiting-acceptance`), or waits for no seal (`until` the + `dx-prd-seal` comment) after it, before its expectation. Pure.""" + out = [] + for s in scenarios: + sid, opens, steps = s.get("id") or "?", s.get("opens") or {}, [st for st in s.get("steps") or [] if isinstance(st, dict)] + if "issue" not in opens: + continue + first = ((s.get("before") or [{}])[0] or {}).get("issue") + for of, spec in (("own", opens["issue"]), ("before", first)): + # A release issue's pull request is the release's (issue-accepted.yml), never the autopilot's. + if spec is None or "lane:release" in (spec.get("labels") or []): + continue + on = [st for st in steps if ((st.get("until") or {}).get("of") if "until" in st else st.get("of")) == ("before" if of == "before" else None)] + wants = [st["until"] for st in on if "until" in st] + ([s.get("expect")] if of == "own" else []) + name = "its issue" if of == "own" else "its `before` issue" + accepted = next((i for i, st in enumerate(on) if st.get("label") == "accepted"), None) + if not any(needs_autopilot(w) for w in wants): + continue + if "accepted" in (spec.get("labels") or []): + out.append(f"scenario {sid}: opens {name} accepted, before the triage answered it, so the autopilot reads it unsealed; " + f"open it plain, wait `until` awaiting-acceptance, then take `label: accepted`") + continue + if accepted is None: + out.append(f"scenario {sid}: expects the autopilot's result on {name}, which it never accepts (`label: accepted`)") + continue + if not any("awaiting-acceptance" in ((st.get("until") or {}).get("labels") or []) for st in on[:accepted]): + out.append(f"scenario {sid}: accepts {name} before the triage's verdict (no `until` awaiting-acceptance before its `label: accepted`)") + if not any(SEAL in ((st.get("until") or {}).get("comments") or []) for st in on[accepted + 1:]): + out.append(f"scenario {sid}: reads the autopilot's result on {name} with no `until` its seal ({SEAL}) after its `label: accepted`") + return out + + def core_problems(scenarios, pack): """Every scenario that writes under the test repository's core (`pack`'s `core`) but expects no wait on it (#709): its fixed change would make its @@ -1684,6 +1744,7 @@ def problems(conf, root=ROOT): + lane_problems(load_yaml(os.path.join(root, LANE))) + partition(scenarios, groups(scenarios), pack) + timing(scenarios) + + autopilot_problems(scenarios) + fixture_problems(conf, engine_post(root).issue, engine_post(root).pull, engine_post(root).release)) @@ -2529,6 +2590,79 @@ def lay(files): rc = main(["merge", "--dir", lay(dict(every, b=None)), "--groups", json.dumps(mgs), "--job", "move=success", "--job", "play=failure", "--job", "back=success", "--run", lane_run, "--out", out]) check("C3: merge's command writes the table release-lane.py reads, and exits 1 on a failing row", (rc, [r["id"] for r in read_results(out) if not r["ok"]], lane.decide(plan, read_results(out))["scenario"]), (1, ["b", "play"], "b")) + # #713: the autopilot's result read once it is there. + def sealing(): + """A test repository whose triage answers issue 9 on its second view + (`awaiting-acceptance`), whose acceptance after that seals it a view + later and has the autopilot label it two views after the seal, and + an acceptance before the triage never seals (the autopilot calls it + unsealed); a `/claim` before the acceptance claims it, after it is + refused.""" + st = {"labels": [], "comments": [], "views": 0, "calls": [], "at": None} + + def fake(*a, input=None): + st["calls"].append(a) + if a[:2] == ("issue", "create"): + st["labels"] += [a[i + 1] for i, x in enumerate(a) if x == "--label"] + if "accepted" in st["labels"]: + st["at"] = "early" + return "https://github.com/o/t/issues/9\n" + if a[:2] == ("issue", "edit") and "--add-label" in a: + label = a[a.index("--add-label") + 1] + st["labels"].append(label) + if label == "accepted": + st["at"] = st["views"] if "awaiting-acceptance" in st["labels"] else "early" + return "" + if a[:2] == ("issue", "comment"): + text = a[a.index("--body") + 1] + if text == "/claim": + if "accepted" in st["labels"]: + st["comments"].append("the claim is refused: the autopilot is already on it") + else: + st["labels"].append("claimed") + elif text == "/unclaim" and "claimed" in st["labels"]: + st["labels"].remove("claimed") + return "" + if a[:2] == ("issue", "view"): + st["views"] += 1 + if st["views"] == 2 and "awaiting-acceptance" not in st["labels"]: + st["labels"].append("awaiting-acceptance") + if st["at"] == "early": + st["comments"].append("The autopilot did not run: the issue's PRD is not the one sealed when it was accepted (unsealed).") + if isinstance(st["at"], int) and st["views"] == st["at"] + 1: + st["comments"].append(f"{SEAL}\nsealed") + if isinstance(st["at"], int) and st["views"] == st["at"] + 3: + st["labels"].append("autopilot") + return json.dumps({"labels": [{"name": l} for l in st["labels"]], "comments": [{"body": c, "author": {"login": "dilux-bot"}} for c in st["comments"]]}) + return "[]" + return st, fake + by713 = {s["id"]: s for s in real["scenarios"]} + st, fake = sealing() + got = play(by713["accepted-spec"], "o/t", "v7-next", real["bodies"], fake, lambda s: None, 15, types=real["types"]) + check("#713 C2: the real accepted-spec, against a repository that seals only after the triage, passes once the autopilot labels it", + (got["ok"], got["why"], st["at"] >= 2, "autopilot" in st["labels"]), (True, [], True, True)) + st, fake = sealing() + early = dict(by713["accepted-spec"], steps=[{"label": "accepted"}], minutes=1) + got = play(early, "o/t", "v7-next", real["bodies"], fake, lambda s: None, 1, types=real["types"]) + check("#713 C2: accepted before the triage, as it was, it is never sealed and fails", (got["ok"], st["at"], any(SEAL in c for c in st["comments"])), (False, "early", False)) + check("#713 C2: and autopilot_problems names it, as every scenario that reads the autopilot's result too early", + [p.split(": ", 1)[1].split(" (")[0].split(",")[0] for p in autopilot_problems([ + early, + dict(by713["spec-to-draft"], opens={"issue": dict(by713["spec-to-draft"]["opens"]["issue"], labels=["accepted"])}), + dict(by713["ready-behind-main"], steps=[{"until": {"labels": ["awaiting-acceptance"]}}, {"label": "accepted"}, {"until": {"pull": {"closes": True}}}]), + dict(by713["core-one-at-a-time"], before=[{"issue": {"title": "t", "body": "complete-feature", "labels": ["accepted"]}}]), + dict(by713["stop-hint-continue"], steps=[{"until": {"pull": {"closes": True}}}])])], + ["accepts its issue before the triage's verdict", "reads the autopilot's result on its issue with no `until` its seal", + "opens its issue accepted", "reads the autopilot's result on its issue with no `until` its seal", + "opens its `before` issue accepted", "expects the autopilot's result on its issue"]) + reads = [s["id"] for s in real["scenarios"] if "issue" in (s.get("opens") or {}) and "lane:release" not in (s["opens"]["issue"].get("labels") or []) + and any(needs_autopilot(w) for w in [s.get("expect")] + [st_["until"] for st_ in s.get("steps") or [] if "until" in st_])] + check("#713 C2: the real file: every scenario that reads the autopilot's result waits for it, each within 15 minutes", + (autopilot_problems(real["scenarios"]), sorted(reads), {limit(by713[i]) for i in reads}), + ([], sorted(["accepted-spec", "spec-to-draft", "core-one-at-a-time", "prd-edited-after-acceptance", "question-answered", "stop-hint-continue", "draft-in-conflict", "ready-behind-main"]), {15})) + check("#713 C2: core-one-at-a-time accepts its `before` issue after its triage, and waits for its seal", + (by713["core-one-at-a-time"]["before"][0]["issue"].get("labels"), [p for p in autopilot_problems([by713["core-one-at-a-time"]])], + preconditions([by713["core-one-at-a-time"]], real["bodies"], rpack)), (None, [], [])) print("all tests passed" if not failed else "some tests failed") return 1 if failed else 0 diff --git a/tests/release/scenarios.yml b/tests/release/scenarios.yml index d78cc054..fb7b440c 100644 --- a/tests/release/scenarios.yml +++ b/tests/release/scenarios.yml @@ -31,6 +31,14 @@ # the first line of its first section, and an issue's title ends with # `[]` (but a release issue's, the version alone), so no two scenarios' # issues read as duplicates. +# +# A scenario that reads the autopilot's result on an issue (its seal, the +# `autopilot` or core-wait label, its pull request) opens that issue plain, +# waits `until` the triage's verdict (`awaiting-acceptance`), accepts it +# then, so the acceptance seals the triaged body as a maintainer's would, +# and waits `until` the seal before the autopilot's result (#713: +# release-scenarios.py autopilot_problems). A step with `of: before` +# acts on its first `before` issue. # The bodies the scenarios open and edit with, by name. bodies: @@ -221,29 +229,36 @@ scenarios: # Accepted, sealed, and the autopilot starting. - id: accepted-spec covers: [next_step:wait:accept, next_step:spec] + minutes: 15 opens: {issue: {title: "feat: a greeting", body: complete-feature}} - steps: [{label: accepted}] + steps: [{until: {labels: [awaiting-acceptance]}}, {label: accepted}, {until: {comments: [""]}}] expect: {labels: [accepted, autopilot], comments: [""]} - id: spec-to-draft covers: [next_step:draft, next_step:work, next_step:finish, pr-screenshots.yml:pull_request] + minutes: 15 opens: {issue: {title: "feat: a greeting, drafted", body: complete-feature}} - steps: [{label: accepted}] + steps: [{until: {labels: [awaiting-acceptance]}}, {label: accepted}, {until: {comments: [""]}}] expect: {pull: {closes: true, branch: "^feat/[0-9]+-", title: "^feat(\\(.+\\))?: "}, comments: [""]} # The first issue's draft (its fixed change under scenarios/, the test # repository's core: kinds/engine-test/pack.yml `core`) stays open while - # the second is accepted, so the second waits for it. + # the second is accepted, so the second waits for it. Each is accepted + # after its triage (#713). - id: core-one-at-a-time covers: [next_step:wait:core] - before: [{issue: {title: "feat: a greeting on the core, first", body: complete-feature, labels: [accepted]}}] + minutes: 15 + before: [{issue: {title: "feat: a greeting on the core, first", body: complete-feature}}] opens: {issue: {title: "feat: a greeting on the core, second", body: complete-feature}} # #694: until the first one's fixed change is on its pull request (its # box ticked), polled every 15 seconds. - steps: [{until: {of: before, pull: {closes: true, body: "\\[x\\] \\*\\*C1\\*\\*"}}}, {label: accepted}] + steps: [{until: {of: before, labels: [awaiting-acceptance]}}, {label: accepted, of: before}, {until: {of: before, comments: [""]}}, + {until: {of: before, pull: {closes: true, body: "\\[x\\] \\*\\*C1\\*\\*"}}}, + {until: {labels: [awaiting-acceptance]}}, {label: accepted}, {until: {comments: [""]}}] expect: {comments: [""], labels: [autopilot:core-wait]} - id: prd-edited-after-acceptance covers: [next_step:wait:answer] + minutes: 15 opens: {issue: {title: "feat: a greeting, then edited", body: complete-feature}} - steps: [{label: accepted}, {until: {comments: [""]}}, {edit: edited-feature}] + steps: [{until: {labels: [awaiting-acceptance]}}, {label: accepted}, {until: {comments: [""]}}, {edit: edited-feature}] expect: {absent: [accepted], waiting: true} - id: decision-sub-issues covers: [] @@ -286,13 +301,17 @@ scenarios: # The autopilot's questions, stops and hints. - id: question-answered covers: [] - opens: {issue: {title: "feat: a greeting, asked", body: complete-feature, labels: [accepted]}} - steps: [{until: {pull: {closes: true}}}, {comment: "Option 1."}] + minutes: 15 + opens: {issue: {title: "feat: a greeting, asked", body: complete-feature}} + steps: [{until: {labels: [awaiting-acceptance]}}, {label: accepted}, {until: {comments: [""]}}, + {until: {pull: {closes: true}}}, {comment: "Option 1."}] expect: {absent: [autopilot:waiting, autopilot:continue], pull: {closes: true}} - id: stop-hint-continue covers: [] - opens: {issue: {title: "feat: a greeting, stopped", body: complete-feature, labels: [accepted]}} - steps: [{until: {pull: {closes: true}}}, {comment: "Hint: write it in scenarios/."}, {label: autopilot:continue}] + minutes: 15 + opens: {issue: {title: "feat: a greeting, stopped", body: complete-feature}} + steps: [{until: {labels: [awaiting-acceptance]}}, {label: accepted}, {until: {comments: [""]}}, + {until: {pull: {closes: true}}}, {comment: "Hint: write it in scenarios/."}, {label: autopilot:continue}] expect: {absent: [needs-human], pull: {closes: true}} # The autopilot's pull request and main. @@ -303,14 +322,16 @@ scenarios: - id: draft-in-conflict covers: [next_step:merge-main, next_step:merge-main:conflicted, sweep_step:continue] minutes: 15 - opens: {issue: {title: "feat: a greeting, in conflict", body: complete-feature, labels: [accepted]}} - steps: [{until: {pull: {closes: true}}}, {push: conflict}] + opens: {issue: {title: "feat: a greeting, in conflict", body: complete-feature}} + steps: [{until: {labels: [awaiting-acceptance]}}, {label: accepted}, {until: {comments: [""]}}, + {until: {pull: {closes: true}}}, {push: conflict}] expect: {pull: {closes: true}, absent: [needs-human]} - id: ready-behind-main covers: [next_step:merge-main:clean, sweep_step:update] minutes: 15 - opens: {issue: {title: "feat: a greeting, behind", body: complete-feature, labels: [accepted]}} - steps: [{until: {pull: {closes: true, body: "\\[x\\] \\*\\*C1\\*\\*"}}}, {push: behind}] + opens: {issue: {title: "feat: a greeting, behind", body: complete-feature}} + steps: [{until: {labels: [awaiting-acceptance]}}, {label: accepted}, {until: {comments: [""]}}, + {until: {pull: {closes: true, body: "\\[x\\] \\*\\*C1\\*\\*"}}}, {push: behind}] expect: {pull: {closes: true}} # The review, the required checks and the merge queue. From 39667ed853df364205d3c8f9f3bcb77a8b20fc03 Mon Sep 17 00:00:00 2001 From: "dilux-bot[bot]" <333187390+dilux-bot[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 22:24:44 +0000 Subject: [PATCH 4/9] fix(scenarios): claim-and-release claims before acceptance (C3) claim-and-release opened its issue accepted and then commented /claim, which the claim refuses once the autopilot has started. It now opens the issue plain, claims it, waits until claimed, and only then accepts it before /unclaim. autopilot_problems refuses any scenario whose /claim comes after its issue's acceptance, and the self-test plays it against a fake that refuses a late claim. Assisted-by: Claude Opus 5.5 (Anthropic) --- scripts/release-scenarios.py | 25 +++++++++++++++++++++++-- tests/release/scenarios.yml | 9 +++++---- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/scripts/release-scenarios.py b/scripts/release-scenarios.py index 5e9a7364..bc9e2f6c 100644 --- a/scripts/release-scenarios.py +++ b/scripts/release-scenarios.py @@ -57,7 +57,7 @@ to reproduce it, and the lane's `play` job on DX_RUNNER_HEAVY; #713: one that reads the autopilot's result on an issue it opens accepted, accepts before the triage's verdict or waits for no - seal after it, + seal after it, or comments /claim after the acceptance, `autopilot_problems`). Every other pair a shipped caller wires is held to the engine statically, by scripts/caller-contract.py check; and @@ -1365,7 +1365,9 @@ def autopilot_problems(scenarios): reads a body the triage had not answered, unsealed), takes no `label: accepted` step, takes it before an `until` the triage's verdict (`awaiting-acceptance`), or waits for no seal (`until` the - `dx-prd-seal` comment) after it, before its expectation. Pure.""" + `dx-prd-seal` comment) after it, before its expectation. And every + scenario whose `/claim` comes after its issue's acceptance (the + autopilot has started then, and the claim is refused). Pure.""" out = [] for s in scenarios: sid, opens, steps = s.get("id") or "?", s.get("opens") or {}, [st for st in s.get("steps") or [] if isinstance(st, dict)] @@ -1380,6 +1382,9 @@ def autopilot_problems(scenarios): wants = [st["until"] for st in on if "until" in st] + ([s.get("expect")] if of == "own" else []) name = "its issue" if of == "own" else "its `before` issue" accepted = next((i for i, st in enumerate(on) if st.get("label") == "accepted"), None) + claim = next((i for i, st in enumerate(on) if str(st.get("comment") or "").startswith("/claim")), None) + if claim is not None and ("accepted" in (spec.get("labels") or []) or (accepted is not None and accepted < claim)): + out.append(f"scenario {sid}: comments /claim on {name} after it is accepted, when the autopilot has started and the claim is refused") if not any(needs_autopilot(w) for w in wants): continue if "accepted" in (spec.get("labels") or []): @@ -2663,6 +2668,22 @@ def fake(*a, input=None): check("#713 C2: core-one-at-a-time accepts its `before` issue after its triage, and waits for its seal", (by713["core-one-at-a-time"]["before"][0]["issue"].get("labels"), [p for p in autopilot_problems([by713["core-one-at-a-time"]])], preconditions([by713["core-one-at-a-time"]], real["bodies"], rpack)), (None, [], [])) + # #713 C3: the claim before the acceptance. + claim = by713["claim-and-release"] + order = [next(iter(k for k in st_ if k != "of")) + ":" + str(next(v for k, v in st_.items() if k != "of")) for st_ in claim["steps"]] + check("#713 C3: the real claim-and-release opens its issue plain and comments /claim before it is accepted, still covering the wait on a claim", + ("accepted" in (claim["opens"]["issue"].get("labels") or []), order.index("comment:/claim") < order.index("label:accepted"), "next_step:wait:claim" in claim["covers"]), + (False, True, True)) + st, fake = sealing() + got = play(claim, "o/t", "v7-next", real["bodies"], fake, lambda s: None, 15, types=real["types"]) + acts = [c[c.index("--body") + 1] if c[1] == "comment" else c[c.index("--add-label") + 1] for c in st["calls"] + if c[:2] == ("issue", "comment") or (c[:2] == ("issue", "edit") and "--add-label" in c)] + check("#713 C3: played, /claim goes before accepted, and the claim is not refused", (got["ok"], acts, any("refused" in c for c in st["comments"])), + (True, ["/claim", "accepted", "/unclaim"], False)) + check("#713 C3: autopilot_problems refuses a /claim after the acceptance", + [p.split(": ", 1)[1][:30] for p in autopilot_problems([dict(claim, steps=[{"label": "accepted"}, {"comment": "/claim"}]), + dict(claim, opens={"issue": dict(claim["opens"]["issue"], labels=["accepted"])})])], + ["comments /claim on its issue a"] * 2) print("all tests passed" if not failed else "some tests failed") return 1 if failed else 0 diff --git a/tests/release/scenarios.yml b/tests/release/scenarios.yml index fb7b440c..6b429340 100644 --- a/tests/release/scenarios.yml +++ b/tests/release/scenarios.yml @@ -37,8 +37,8 @@ # waits `until` the triage's verdict (`awaiting-acceptance`), accepts it # then, so the acceptance seals the triaged body as a maintainer's would, # and waits `until` the seal before the autopilot's result (#713: -# release-scenarios.py autopilot_problems). A step with `of: before` -# acts on its first `before` issue. +# release-scenarios.py autopilot_problems); a `/claim` comes before the +# acceptance. A step with `of: before` acts on its first `before` issue. # The bodies the scenarios open and edit with, by name. bodies: @@ -268,8 +268,9 @@ scenarios: # Claims. - id: claim-and-release covers: [issue-claim.yml:issue_comment, next_step:wait:claim] - opens: {issue: {title: "feat: a greeting, claimed", body: complete-feature, labels: [accepted]}} - steps: [{comment: "/claim"}, {until: {labels: [claimed]}}, {comment: "/unclaim"}] + # #713: claimed before it is accepted, so the autopilot steps aside. + opens: {issue: {title: "feat: a greeting, claimed", body: complete-feature}} + steps: [{comment: "/claim"}, {until: {labels: [claimed]}}, {label: accepted}, {comment: "/unclaim"}] expect: {absent: [claim:long]} - id: claim-pull-request covers: [issue-claim.yml:pull_request_target, waiting-update.yml:pull_request_target] From 17c9d48e35798cf1faeed3a3729f2454e8967fb3 Mon Sep 17 00:00:00 2001 From: "dilux-bot[bot]" <333187390+dilux-bot[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 22:24:47 +0000 Subject: [PATCH 5/9] fix(scenarios): release-issue expects a writer's refusal (C4) The test identity has the write role, and only a person with the admin or maintain role approves a release, so release-issue can never get a release pull request. It now expects issue-accepted.yml's "No release pull request:" refusal and no pull request (`pull: false`, which holds now reads); the self-test matches the expected words against the writer's refusal in issue-accepted.yml, so the two cannot drift apart. Assisted-by: Claude Opus 5.5 (Anthropic) --- scripts/release-scenarios.py | 13 ++++++++++++- tests/release/scenarios.yml | 5 ++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/scripts/release-scenarios.py b/scripts/release-scenarios.py index bc9e2f6c..520265c6 100644 --- a/scripts/release-scenarios.py +++ b/scripts/release-scenarios.py @@ -35,7 +35,7 @@ there, through a docs pull request of the scenario that merges: #698), `minutes` (its limit, 10 by default, 15 at most) and `expect` (`labels`, `absent`, `comments` (markers), `pull` (`closes`, -`branch`, `title`, patterns), `check` (name: conclusion, the name a +`branch`, `title`, patterns; `false` for none that closes it, #713), `check` (name: conclusion, the name a check run's caller's job or its own: `check_named`, #709), `merged`, `waiting`). Every scenario is a group of its own (`groups`, #694), and the lane plays the groups at once, one job each, on GitHub's runners, @@ -442,6 +442,8 @@ def holds(expect, seen): if marker not in text: out.append(f"no comment with {marker}") pull = seen.get("pull") or {} + if expect.get("pull") is False and pull: + out.append(f"a pull request closes it ({pull.get('number') or pull.get('branch') or '?'})") for key, pattern in (expect.get("pull") or {}).items(): if key == "closes": # The issue it should close: the one a pull scenario opened for @@ -2684,6 +2686,15 @@ def fake(*a, input=None): [p.split(": ", 1)[1][:30] for p in autopilot_problems([dict(claim, steps=[{"label": "accepted"}, {"comment": "/claim"}]), dict(claim, opens={"issue": dict(claim["opens"]["issue"], labels=["accepted"])})])], ["comments /claim on its issue a"] * 2) + # #713 C4: a writer's release is refused, in issue-accepted.yml's words. + rel = by713["release-issue"]["expect"] + with open(os.path.join(ROOT, ".github", "workflows", "issue-accepted.yml"), encoding="utf-8") as fh: + refusal = [l for l in fh if "only a person with the admin or maintain role approves a release" in l] + check("#713 C4: release-issue expects the refusal a writer gets, the very words issue-accepted.yml says, and no pull request", + (rel.get("pull"), [m for m in rel.get("comments") or [] if not (refusal and m in refusal[0])], len(refusal)), (False, [], 1)) + seen713 = {"labels": ["accepted", "lane:release"], "comments": ["**No release pull request:** x accepted this issue with the role write on the repository"]} + check("#713 C4: holds: the refusal and no pull request pass; a pull request that closes it fails, named", + (holds(rel, seen713), holds(rel, dict(seen713, pull={"number": 5, "title": "chore: release 0.0.1"}))[0]), ([], "a pull request closes it (5)")) print("all tests passed" if not failed else "some tests failed") return 1 if failed else 0 diff --git a/tests/release/scenarios.yml b/tests/release/scenarios.yml index 6b429340..269a37ee 100644 --- a/tests/release/scenarios.yml +++ b/tests/release/scenarios.yml @@ -371,7 +371,10 @@ scenarios: - id: release-issue covers: [] opens: {issue: {title: "Release 0.0.1", body: release, labels: [lane:release, accepted]}} - expect: {pull: {title: "^chore: release"}} + # #713: the test identity has the write role, and only admin or + # maintain approves a release: it gets the refusal + # (issue-accepted.yml's "No release pull request"), and no pull request. + expect: {comments: ["**No release pull request:**"], pull: false} - id: drift-and-adopt covers: [] opens: {issue: {title: "chore: the drift check and adopting", body: complete-feature}} From c712d8da77e3caad2064d490450acc9db68fd77c Mon Sep 17 00:00:00 2001 From: "dilux-bot[bot]" <333187390+dilux-bot[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 22:24:51 +0000 Subject: [PATCH 6/9] fix(scenarios): decision and bug bodies pass the schema and form (C5) The decision body had no PRD, so the triage left it incomplete, and bug-not-reproduced had no Version and no steps, so the triage asked for more. The decision is now a full PRD whose Repositories names only the test repository, opened plain and accepted after its triage; both bug bodies carry every field bug_report.yml requires. fixture_problems no longer exempts the decision, and checks each Bug body against the form's required fields, read from bug_report.yml. Assisted-by: Claude Opus 5.5 (Anthropic) --- scripts/release-scenarios.py | 65 +++++++++++++++++++++++++++++++----- tests/release/scenarios.yml | 46 ++++++++++++++++++++----- 2 files changed, 93 insertions(+), 18 deletions(-) diff --git a/scripts/release-scenarios.py b/scripts/release-scenarios.py index 520265c6..e523ed0c 100644 --- a/scripts/release-scenarios.py +++ b/scripts/release-scenarios.py @@ -58,7 +58,8 @@ #713: one that reads the autopilot's result on an issue it opens accepted, accepts before the triage's verdict or waits for no seal after it, or comments /claim after the acceptance, - `autopilot_problems`). + `autopilot_problems`; a body the schema refuses, a decision's + too, or a Bug's without a field bug_report.yml requires). Every other pair a shipped caller wires is held to the engine statically, by scripts/caller-contract.py check; and what is wrong with the groups (`partition`): a scenario in no @@ -1656,17 +1657,41 @@ def schema_passes(root=ROOT): return lambda body: bool(mod.check(body, "prd")["ok"]) -def fixture_problems(conf, issue_check, pull_check, release_check=None): +BUG_FORM = os.path.join(".github", "ISSUE_TEMPLATE", "bug_report.yml") + + +def form_required(form): + """The labels of the fields an issue form (`form`, read) requires: the + `### ` sections a body opened from it has. Pure.""" + return [str((f.get("attributes") or {}).get("label")) for f in (form or {}).get("body") or [] + if isinstance(f, dict) and f.get("type") != "markdown" and (f.get("validations") or {}).get("required")] + + +def missing_fields(body, fields): + """The `fields` (### headings) `body` leaves missing or empty. Pure.""" + found, on = {}, None + for line in str(body or "").split("\n"): + if line.startswith("### "): + on = line[4:].strip() + found.setdefault(on, "") + elif on is not None: + found[on] += line + return [f for f in fields if not re.sub(r"|\s", "", found.get(f, ""))] + + +def fixture_problems(conf, issue_check, pull_check, release_check=None, bug_fields=()): """What a person's check would find wrong with the scenarios' own fixtures (#694, scripts/engine-post.py): every issue body a scenario opens or edits to that should await acceptance, as G1 reads it - (`issue_check(body)`), and every pull request it opens, its title, its - branch and its `Closes #` as the conventions check reads them - (`pull_check(title, branch, body, closes)`), and a release issue's - title and label (`release_check(title, labels)`). Pure but for the - checks.""" + (`issue_check(body)`), a decision's too (#713), and every pull request + it opens, its title, its branch and its `Closes #` as the conventions + check reads them (`pull_check(title, branch, body, closes)`), a release + issue's title and label (`release_check(title, labels)`), and (#713) a + Bug's body that leaves out a field the bug report form requires + (`bug_fields`, `form_required` of bug_report.yml), which the triage + asks for (`needs-info`). Pure but for the checks.""" bodies, out = conf.get("bodies") or {}, [] - known = {b for b in bodies if b not in ("incomplete", "decision", "release")} + known = {b for b in bodies if b not in ("incomplete", "release")} for s in conf.get("scenarios") or []: sid, opens = s.get("id") or "?", s.get("opens") or {} specs = [b.get("issue") or {} for b in s.get("before") or []] + ([opens["issue"]] if "issue" in opens else []) @@ -1684,6 +1709,9 @@ def fixture_problems(conf, issue_check, pull_check, release_check=None): if name in known: for wrong in issue_check(marked(body_of(name, bodies), sid)): out.append(f"scenario {sid}: its body `{name}`: {wrong}") + if type_of(spec, conf.get("types") or {}) == "Bug": + for field in missing_fields(body_of(name, bodies), bug_fields): + out.append(f"scenario {sid}: its Bug body `{name}` has no `{field}`, which the bug report form requires") return out @@ -1752,7 +1780,8 @@ def problems(conf, root=ROOT): + partition(scenarios, groups(scenarios), pack) + timing(scenarios) + autopilot_problems(scenarios) - + fixture_problems(conf, engine_post(root).issue, engine_post(root).pull, engine_post(root).release)) + + fixture_problems(conf, engine_post(root).issue, engine_post(root).pull, engine_post(root).release, + form_required(load_yaml(os.path.join(root, BUG_FORM))))) def self_test(): @@ -2695,6 +2724,24 @@ def fake(*a, input=None): seen713 = {"labels": ["accepted", "lane:release"], "comments": ["**No release pull request:** x accepted this issue with the role write on the repository"]} check("#713 C4: holds: the refusal and no pull request pass; a pull request that closes it fails, named", (holds(rel, seen713), holds(rel, dict(seen713, pull={"number": 5, "title": "chore: release 0.0.1"}))[0]), ([], "a pull request closes it (5)")) + # #713 C5: the bodies the triage would refuse or ask about. + post = engine_post() + fields = form_required(load_yaml(os.path.join(ROOT, BUG_FORM))) + check("#713 C5: the bug report form's required fields, read from bug_report.yml", fields, ["What happened?", "Steps to reproduce", "Version"]) + check("#713 C5: the decision and bug bodies pass issue-schema.py, and each bug body has every field the form requires", + ({n: post.issue(marked(real["bodies"][n], n)) for n in ("decision", "bug", "bug-not-reproduced")}, + {n: missing_fields(real["bodies"][n], fields) for n in ("bug", "bug-not-reproduced")}), + ({n: [] for n in ("decision", "bug", "bug-not-reproduced")}, {n: [] for n in ("bug", "bug-not-reproduced")})) + check("#713 C5: the decision names only the test repository", "### Repositories\n\nname: diluxone-test\n" in real["bodies"]["decision"], True) + old = dict(real, bodies=dict(real["bodies"], decision="### Decision\n\nTwo.\n", **{"bug-not-reproduced": real["bodies"]["bug-not-reproduced"].split("### Version")[0] + "### Problem" + real["bodies"]["bug-not-reproduced"].split("### Problem", 1)[1]})) + got = fixture_problems(old, post.issue, post.pull, post.release, fields) + check("#713 C5: fixture_problems names a decision the schema refuses, and a Bug body without the form's Version", + (any(p.startswith("scenario decision-sub-issues: its body `decision`:") for p in got), + "scenario bug-not-reproduced: its Bug body `bug-not-reproduced` has no `Version`, which the bug report form requires" in got), + (True, True)) + dec = by713["decision-sub-issues"] + check("#713 C5: decision-sub-issues opens its decision plain and accepts it after its triage", + (dec["opens"]["issue"].get("labels"), dec["steps"][:2]), (None, [{"until": {"labels": ["awaiting-acceptance"]}}, {"label": "accepted"}])) print("all tests passed" if not failed else "some tests failed") return 1 if failed else 0 diff --git a/tests/release/scenarios.yml b/tests/release/scenarios.yml index 269a37ee..6b2169a1 100644 --- a/tests/release/scenarios.yml +++ b/tests/release/scenarios.yml @@ -97,6 +97,10 @@ bodies: Read `scenarios/bug-reproduced/greeting.md`. + ### Version + + The test repository's `main`. + ### Problem The greeting says "helo" instead of "hello". @@ -119,19 +123,24 @@ bodies: bug-not-reproduced: | ### What happened? - Something failed once, nobody knows where. + `scenarios/bug-not-reproduced/greeting.md` said "helo" once, in a run nobody kept. ### Steps to reproduce - Unknown. + 1. Read `scenarios/bug-not-reproduced/greeting.md` on `main`. + 2. See whether it says "helo" instead of "hello". + + ### Version + + The test repository's `main`. ### Problem - Something failed once. + The greeting said "helo" instead of "hello" once. ### Scope - - Whatever failed. + - The greeting's word. ### Out of scope @@ -139,20 +148,38 @@ bodies: ### Acceptance criteria - - C1: Given the repository, when it runs, then nothing fails. + - C1: Given the repository, when `scenarios/bug-not-reproduced/greeting.md` is read, then it says "hello". ### Criticality Normal + # A decision's PRD (#713: the schema's, with its Repositories), naming + # only the test repository, so it reaches no other. decision: | - ### Decision + ### Problem - The greeting and the farewell are two sub-issues of this one. + The test repository has neither a greeting nor a farewell. - ### Sub-issues + ### Scope - A greeting file. - A farewell file. + + ### Out of scope + + - Anything else. + + ### Acceptance criteria + + - C1: Given the repository, when it is read, then it has a greeting and a farewell. + + ### Criticality + + Normal + + ### Repositories + + name: diluxone-test # The Release form's (#694): its one section, and its label below. release: | ### Notes @@ -262,7 +289,8 @@ scenarios: expect: {absent: [accepted], waiting: true} - id: decision-sub-issues covers: [] - opens: {issue: {title: "Decision: a greeting and a farewell", body: decision, labels: [accepted]}} + opens: {issue: {title: "Decision: a greeting and a farewell", body: decision}} + steps: [{until: {labels: [awaiting-acceptance]}}, {label: accepted}] expect: {labels: [accepted], comments: ["sub-issue"]} # Claims. From bd64b6cd5bfde474c684c76c16511a2e832d3f8d Mon Sep 17 00:00:00 2001 From: "dilux-bot[bot]" <333187390+dilux-bot[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 00:09:08 +0000 Subject: [PATCH 7/9] docs(versioning): how the lane plays the autopilot's scenarios (C6) The release lane's run on v7.14.0's candidate with this fix is C6's proof, and it cannot run before the candidate is on v7-next. This brings docs/versioning.md in step with how the lane now plays a scenario that needs the autopilot's result (opened without accepted, accepted after its triage, then waiting until the seal and the result, with a /claim before accepted), and docs/workflows.md with the waiting view's rule that a cancelled check run is not a failed check. Assisted-by: Claude Opus 5.5 (Anthropic) --- docs/versioning.md | 2 +- docs/workflows.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/versioning.md b/docs/versioning.md index abd8521a..bcec9020 100644 --- a/docs/versioning.md +++ b/docs/versioning.md @@ -25,7 +25,7 @@ No person's step for a fix or a feature: the release lane ([`release-lane.yml`]( 1. **Plan.** When the pull request pipeline ends green on a push to `main` (or a maintainer dispatches the lane), `release-lane.py plan` reads what merged since the last `vX.Y.Z` (`scripts/next-version.py`, from the labels of what merged) and whether a commit since says `!`. Nothing releasable (docs, chores): the lane ends. 2. **Candidate.** `release-lane.py candidate` commits a `release-candidate` branch that is `main` with the reusable workflows' inner calls of the engine moved from `@v7` to `@v7-next` (GitHub reads them before any expression, so they would otherwise run `v7`'s), checks it differs from `main` in nothing else, and points the lightweight `v7-next` tag at it. Its token has `workflows`, since that commit rewrites workflow files, and the commit is made through the API with that token (`scripts/api-commit.py`), so GitHub signs it: the organisation's ruleset refuses an unsigned commit on any branch. -3. **Scenarios.** The test repository (org.yml `test-repo`, `-test` by default; kind [`engine-test`](../kinds/engine-test/), which calls no model and publishes nothing) has its callers at `@v7-next`: they move there, and back after the scenarios, through a pull request the bot opens in the test repository (with an App token scoped to it, `DX_CALLERS_TOKEN`, minted again for the way back by `release-scenarios.py back`, since one lasts an hour and the scenarios can take longer) and merges itself through the merge queue, since its rulesets refuse a write to `main`: the auto-merge decision never merges a change to `.github/workflows`, so the lane enables no auto-merge, reads from GitHub what the pull request really changes (every file, at its base and its head), refuses one that changes anything but the engine's ref in the callers it wrote (`@v7` ↔ `@v7-next`, and the pin's comment; `release-scenarios.py only_ref`), closing it, and once its required checks pass and every other check finished, queues it with `gh pr merge --squash --match-head-commit`; the first scenario plays only once it merged, and one that does not merge, or that does not put them back, is a failing `callers` row, so nothing is released; so is a record of what moved that `back` cannot read (`move`, like `run`, writes it once the callers moved, before the first scenario, so a run cancelled or killed while moving them leaves none, and that reads as "could not tell", never "nothing moved"). The lane plays there every scenario of [`tests/release/scenarios.yml`](../tests/release/scenarios.yml) in three jobs: `move` points the callers (`release-scenarios.py move`, which records what moved) and prints the groups (`release-scenarios.py groups`); `play` is a matrix with one job per group, all at once (`fail-fast: false`), on `ubuntu-latest`, never `DX_RUNNER_HEAVY`, where the test repository's own jobs run and would queue behind the play jobs that wait for them (#707; `release-scenarios.py coverage` refuses it), each playing its group (`release-scenarios.py play`) with the test identity's token alone (`DX_TEST_TOKEN`, in the `release` environment; no App token); and `back`, under `always()` once every group ended, puts the callers back. The groups are data: every scenario is a group of its own (#694), since each writes only under its own directory of the test repository, `scenarios//` (its fixed change writes `scenarios/{scenario}/{n}.md`, `{scenario}` the `dx-scenario` mark that opens every body a scenario writes), and the engine-test pack's `core` is `scenarios/core-one-at-a-time/**` alone (#709), so only that scenario's two issues wait on each other's change and no other scenario's Spec touches a core; every wait of a scenario polls its state every 15 seconds (an `until` step, never a fixed wait; an issue's pull request read from the issue's own references, never a search, whose API allows 30 a minute across every scenario at once, and a `gh` that fails for a while, a rate limit or a 5xx, retried after GitHub's back-off within the scenario's limit, #698), a `push` writes on the head branch of the pull request it observes (the autopilot's, for an issue) and lands on `main` (`conflict` the same file, otherwise; `behind` one of its own) through a docs pull request of the scenario that merges, never a write to `main`, a scenario stops as soon as its expectation can no longer hold (`hopeless`: merged when it should not be, closed unmerged, a check that ended otherwise, an `absent` label still there once nothing runs, an expected check whose run GitHub holds for a maintainer's approval, a fork's, #709), and each has a limit of 15 minutes or less (`release-scenarios.py plan`), under its play step's 20; `release-scenarios.py coverage` fails for a scenario in no group or in two, two scenarios that could write to the same part of the core, and a group id unfit for an artifact name. `act` merges what every job wrote into one table (`release-scenarios.py merge`, into the run's summary): each group's rows of its own scenarios, and a failing row, with the lane's run, for a group that left no results (its job failed, timed out or was cancelled), a scenario of a group with no row, and a `move`, `play` or `back` job that did not succeed. In the test repository the autopilot writes the fixed change (`autopilot.py fixed`: one file per criterion, committed and ticked) where it would call the model, and the reproduction copies its pack's fixed test (`repro-ways.py fixed`, #707) where the writer would run: it fails while the scenario's `scenarios//greeting.md` says "helo", which `bug-reproduced` lands on main first (a `before` with `lands`), so a bug there is confirmed or not with no model; `coverage` refuses a bug's verdict expected in a test repository with no way to reproduce it. A row that fails names what it saw: the issue's labels and the first line of the bot's newest comment. The scenarios are data: one per pair of a reusable workflow and an event the test repository's callers wire (its pack's `callers`, and org-pull-request.yml; a `schedule` aside, which no scenario can cause), and one per transition of the autopilot's state machine (but those the file's `statically:` names, each with the test that holds it instead, as no scenario can cause them there: `next_step:fix`, the review's fix round, since the test repository's review is a model's, #707), which `release-scenarios.py coverage` holds in the scripts job, with each scenario whose setup cannot make it hold (a dispatch of a workflow the test repository cannot run on demand, an issue body the schema refuses); [`scripts/caller-contract.py`](../scripts/caller-contract.py) checks before any run, for every shipped caller (the products' batteries and the nightly and security runs, which the test repository does not have, among them), that no reusable workflow asks its callers for a permission, an input or a secret they do not give (#585), and that the waiting job mints its App token for the item's repository alone, with no more than it needs (its `TOKENS`, #619). +3. **Scenarios.** The test repository (org.yml `test-repo`, `-test` by default; kind [`engine-test`](../kinds/engine-test/), which calls no model and publishes nothing) has its callers at `@v7-next`: they move there, and back after the scenarios, through a pull request the bot opens in the test repository (with an App token scoped to it, `DX_CALLERS_TOKEN`, minted again for the way back by `release-scenarios.py back`, since one lasts an hour and the scenarios can take longer) and merges itself through the merge queue, since its rulesets refuse a write to `main`: the auto-merge decision never merges a change to `.github/workflows`, so the lane enables no auto-merge, reads from GitHub what the pull request really changes (every file, at its base and its head), refuses one that changes anything but the engine's ref in the callers it wrote (`@v7` ↔ `@v7-next`, and the pin's comment; `release-scenarios.py only_ref`), closing it, and once its required checks pass and every other check finished, queues it with `gh pr merge --squash --match-head-commit`; the first scenario plays only once it merged, and one that does not merge, or that does not put them back, is a failing `callers` row, so nothing is released; so is a record of what moved that `back` cannot read (`move`, like `run`, writes it once the callers moved, before the first scenario, so a run cancelled or killed while moving them leaves none, and that reads as "could not tell", never "nothing moved"). The lane plays there every scenario of [`tests/release/scenarios.yml`](../tests/release/scenarios.yml) in three jobs: `move` points the callers (`release-scenarios.py move`, which records what moved) and prints the groups (`release-scenarios.py groups`); `play` is a matrix with one job per group, all at once (`fail-fast: false`), on `ubuntu-latest`, never `DX_RUNNER_HEAVY`, where the test repository's own jobs run and would queue behind the play jobs that wait for them (#707; `release-scenarios.py coverage` refuses it), each playing its group (`release-scenarios.py play`) with the test identity's token alone (`DX_TEST_TOKEN`, in the `release` environment; no App token); and `back`, under `always()` once every group ended, puts the callers back. The groups are data: every scenario is a group of its own (#694), since each writes only under its own directory of the test repository, `scenarios//` (its fixed change writes `scenarios/{scenario}/{n}.md`, `{scenario}` the `dx-scenario` mark that opens every body a scenario writes), and the engine-test pack's `core` is `scenarios/core-one-at-a-time/**` alone (#709), so only that scenario's two issues wait on each other's change and no other scenario's Spec touches a core; every wait of a scenario polls its state every 15 seconds (an `until` step, never a fixed wait; an issue's pull request read from the issue's own references, never a search, whose API allows 30 a minute across every scenario at once, and a `gh` that fails for a while, a rate limit or a 5xx, retried after GitHub's back-off within the scenario's limit, #698), a scenario that needs the autopilot's result (its seal, its label, its pull request) opens its issue without `accepted`, waits `until` the triage's `awaiting-acceptance`, accepts it as a maintainer would, and waits `until` the seal and then the result (`autopilot_problems`, #713), a `/claim` comes before `accepted`, a `push` writes on the head branch of the pull request it observes (the autopilot's, for an issue) and lands on `main` (`conflict` the same file, otherwise; `behind` one of its own) through a docs pull request of the scenario that merges, never a write to `main`, a scenario stops as soon as its expectation can no longer hold (`hopeless`: merged when it should not be, closed unmerged, a check that ended otherwise, an `absent` label still there once nothing runs, an expected check whose run GitHub holds for a maintainer's approval, a fork's, #709), and each has a limit of 15 minutes or less (`release-scenarios.py plan`), under its play step's 20; `release-scenarios.py coverage` fails for a scenario in no group or in two, two scenarios that could write to the same part of the core, and a group id unfit for an artifact name. `act` merges what every job wrote into one table (`release-scenarios.py merge`, into the run's summary): each group's rows of its own scenarios, and a failing row, with the lane's run, for a group that left no results (its job failed, timed out or was cancelled), a scenario of a group with no row, and a `move`, `play` or `back` job that did not succeed. In the test repository the autopilot writes the fixed change (`autopilot.py fixed`: one file per criterion, committed and ticked) where it would call the model, and the reproduction copies its pack's fixed test (`repro-ways.py fixed`, #707) where the writer would run: it fails while the scenario's `scenarios//greeting.md` says "helo", which `bug-reproduced` lands on main first (a `before` with `lands`), so a bug there is confirmed or not with no model; `coverage` refuses a bug's verdict expected in a test repository with no way to reproduce it. A row that fails names what it saw: the issue's labels and the first line of the bot's newest comment. The scenarios are data: one per pair of a reusable workflow and an event the test repository's callers wire (its pack's `callers`, and org-pull-request.yml; a `schedule` aside, which no scenario can cause), and one per transition of the autopilot's state machine (but those the file's `statically:` names, each with the test that holds it instead, as no scenario can cause them there: `next_step:fix`, the review's fix round, since the test repository's review is a model's, #707), which `release-scenarios.py coverage` holds in the scripts job, with each scenario whose setup cannot make it hold (a dispatch of a workflow the test repository cannot run on demand, an issue body the schema refuses); [`scripts/caller-contract.py`](../scripts/caller-contract.py) checks before any run, for every shipped caller (the products' batteries and the nightly and security runs, which the test repository does not have, among them), that no reusable workflow asks its callers for a permission, an input or a secret they do not give (#585), and that the waiting job mints its App token for the item's repository alone, with no more than it needs (its `TOKENS`, #619). 4. **Decide** (`release-lane.py decide`, then `act`): - every scenario passed and nothing is breaking: only while `main` is still the commit the plan named and the scenarios tried (else nothing, and the next green run plans again), the release pull request (`scripts/changelog.py build --next vX.Y.Z`) is opened from that commit and merged as dilux-bot, `vX.Y.Z` is tagged at its merge commit when its parent is that commit (else nothing is tagged and `v7` stays), `v7` moves to it (lightweight: `git tag -f` and a force push of that ref alone) and its GitHub Release is published, or edited when [`engine-release.yml`](../.github/workflows/engine-release.yml) made it first on the tag's push; a Release that fails after `v7` moved fails the job, and the watch still runs; - one scenario failed: nothing is tagged, `v7` stays, and a bug names the scenario and its run; a job of the scenarios that failed (a step that broke, its time run out) still reaches this decision, as a failing row of the merged table with the lane's run (the job's, a group that left no results, a scenario with no row), so it opens a bug and never a release (#644); so does a play job that ran out of time (#690): its play step stops at 20 minutes (a failure), the job at 30 (GitHub then cancels the job, not the run), and the callers still go back; only a run a person cancelled stays silent, releasing nothing and opening nothing; diff --git a/docs/workflows.md b/docs/workflows.md index a7959d29..f304c89e 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -22,7 +22,7 @@ scripts with nothing else to set. A workflow another one calls is named at | [`review-eval.yml`](../.github/workflows/review-eval.yml) | Not reusable: a change to the review (its prompts, profiles, brief, model, verification) is scored before it merges (#234). Every case of `evals/review/cases.json`, real pull requests of the public repositories (a bug the review found and was fixed, which it must find again; a clean one it must not block), is reviewed again with the pull request's rules through `scripts/local-review.sh` and scored against `evals/review/baseline.json`; worse by more than one case on either side fails. Paid, so only on a pull request that touches the review, from a branch of this repository, and by hand. | none | | [`nightly-review.yml`](../.github/workflows/nightly-review.yml) | Reusable, called only from a private repository (DiluxOne's is `DiluxOne/security`, every night and by hand from its default branch; it refuses to run from a public one): the review of existing code (#234). Each night one repository with code, in turn (`scripts/nightly-review.py pick`), is reviewed whole by an agent within a budget, reading only that repository's code and changing nothing; its findings join one list per repository, an issue there, "Nightly review: ", never one issue per finding there; a new blocker or major that is not about security is also opened as a Bug issue in the reviewed repository (every one, #247, #287), which the triage labels and the usual bug path reproduces and fixes; a new blocker or major about security follows a report's path (#287): a draft security advisory in a public repository (dilux-security's key, credited in its text to the review), which `security-fix.yml` fixes, or a `security` issue in a private one; what it opens carries `found-by:dilux-ai`, for the statistics. A maintainer picks what becomes work; an item ticked or struck out is not listed again. `DX_AUTOPILOT=off` stops it. | `central-ref`, `repository` | | [`autonomy-digest.yml`](../.github/workflows/autonomy-digest.yml) | Not reusable: every Monday and by hand, opens an issue of its own (`run:autonomy-digest`, [`scripts/run-issue.sh`](../scripts/run-issue.sh), #188) with what the autonomy policy decided instead of a person in the last seven days ([`scripts/policy-digest.sh`](../scripts/policy-digest.sh)): issues labelled `accepted-by:policy` opened in those days, pull requests labelled `merged-by:policy` merged in them. The same issue says what still needed a person ([`scripts/needed-a-person.py`](../scripts/needed-a-person.py), #143): every pull request a person merged that week, or that waits on one now, with the reasons its gates, labels and review recorded (high risk, a protected path, a Critical issue, a review blocker, autopilot rounds exhausted, a failed gate, the size cap, an outside author, over a risk cap, or "unknown"); the totals per reason, per week and per repository over the last four weeks, read again from the API on every run rather than kept anywhere, so a missed week or a fix to the classification leaves no wrong total; and a proposal, as text only, for each reason that stopped 5 or more pull requests whose review found no blocker or major. Changing a limit stays a pull request a person accepts and merges. The week's issue closes as completed when it proposes nothing, and stays open with `needs:maintainer` when it does. The issues are public, so a private repository's pull requests are counted, never named. | — | -| [`waiting-digest.yml`](../.github/workflows/waiting-digest.yml) | Not reusable: every day and by hand (#479, #564), the sweep of the "What waits" view that `waiting-update.yml` rewrites on every event: every open issue and pull request of the organisation that waits on someone (needs-human, the autopilot's question, an acceptance, an incomplete issue, a triage's question, a claim, a failed check, an unresolved thread, a draft, the checks, a person's merge), the action and its full URL, grouped by whom it waits on, in one pinned issue of this repository labelled `run:waiting` ([`scripts/waiting.py`](../scripts/waiting.py) `publish`); a private repository's items are counted, never named. | this repository | +| [`waiting-digest.yml`](../.github/workflows/waiting-digest.yml) | Not reusable: every day and by hand (#479, #564), the sweep of the "What waits" view that `waiting-update.yml` rewrites on every event: every open issue and pull request of the organisation that waits on someone (needs-human, the autopilot's question, an acceptance, an incomplete issue, a triage's question, a claim, a failed check (a cancelled check run is not one, #713), an unresolved thread, a draft, the checks, a person's merge), the action and its full URL, grouped by whom it waits on, in one pinned issue of this repository labelled `run:waiting` ([`scripts/waiting.py`](../scripts/waiting.py) `publish`); a private repository's items are counted, never named. | this repository | | [`waiting-update.yml`](../.github/workflows/waiting-update.yml) | On every event of an issue or a pull request (#564), from `issues.yml` (issues, comments, and a fork's pull request by its metadata only, through `pull_request_target`) and, after the review, from `pull-request.yml` and `org-pull-request.yml`: whom the item waits on now, on its board (the one bot comment of the issue or the pull request, `scripts/receipts.py waits`), with one mention of that person per waiting step, recorded in the board's hidden state so it is never repeated (a maintainer is never mentioned for a contributor's step, nor a contributor for a maintainer's), then the "What waits" view rewritten (`scripts/waiting.py update`, `publish`). The board is written with the App's token for the item's repository alone (`issues: write`, and the reads of the item, its checks, its commit statuses and its seals; #619, #641), the view with one for the organisation. It checks out only DiluxOne/.github's scripts; a bot's comment and the bot's `run:` issues start nothing. | every repository | | [`pr-screenshots.yml`](../.github/workflows/pr-screenshots.yml) | When an autopilot pull request whose head is a branch of the same repository (never a fork's) is marked ready (#564), from `pull-request.yml` and `org-pull-request.yml`: the before and after screenshots of the browser-layer specs it affects, in its "📸 Screenshots" section ([`scripts/screenshots.py`](../scripts/screenshots.py)). Which specs comes from the pack: the way whose layer is `browser` and `repro.affected` on the paths it changes; none, nothing runs. The base and the head each run in their own checkout with the way's `setup`, its `shots` command through a generated `playwright.dx-shots.config.ts` that imports the repository's configuration and turns `screenshot` on (the repository's own is not changed), and its `teardown`, on a fresh GitHub runner with a read-only token and no secret; a second job, which never checks the code out, commits the images to the orphan branch `dx-screenshots` (`pr-/{before,after}//.png`) and rewrites the section between its markers as a before/after table. A run that fails says so there and blocks nothing. | every repository with a browser way | | [`autopilot-sweep.yml`](../.github/workflows/autopilot-sweep.yml) | Reusable, and run here every hour (#561, #669): a merge into main brings every open autopilot pull request it leaves behind or in conflict back in step on its own, with no label a person adds. [`scripts/autopilot.py`](../scripts/autopilot.py) `sweep` (next-step's answer) says `update`, and the bot merges main into it (GitHub's update-branch), or `continue`, and dilux-bot adds `autopilot:continue` to its issue, whose fix round merges main and leaves the conflict to the agent (free: `autopilot:catchup` beside the continue labels it `autopilot:free-N`, since it only catches up with another merge, #563). A draft only behind main gets the same update-branch, with no model and no run (#563); one in conflict is left to its own run, which merges main first. An issue waiting for another autopilot pull request on the engine's core (`autopilot:core-wait`, org.yml `core`, #563) gets its `autopilot:continue` once no open one touches the core its Spec touches. Within minutes of a merge, in the repository it merged in: the `autopilot-sweep` job of its issues.yml ([`workflow-templates/issues.yml`](../workflow-templates/issues.yml), and its mirror here) calls it on a pull request merged into the default branch (`pull_request_target` `closed`, metadata only), with `repo` and an App token for that repository alone; every hour for all, which covers a repository whose callers are not in step yet; and by hand. It runs only the engine's scripts, cloned at `job.workflow_sha` into `.dx-central`, never the caller's code. | `repo` (one repository; empty, every one) | From 53fc3c2c1db20988b234fa4afe0fb583af6f9e42 Mon Sep 17 00:00:00 2001 From: "dilux-bot[bot]" <333187390+dilux-bot[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 00:09:11 +0000 Subject: [PATCH 8/9] fix(scenarios): decision-sub-issues expects accepted and its seal (C5) The maintainer's answer on #713 settles what a decision gets in the test repository, the only one its body names: accepted and its seal, waiting until the seal, with no sub-issue comment, and issue-accepted.yml stays as it is. The scenario now waits until the seal within 15 minutes and expects it, and release-scenarios.py --test holds both, with decision-sub-issues among the scenarios that read the autopilot's result. Assisted-by: Claude Opus 5.5 (Anthropic) --- scripts/release-scenarios.py | 4 +++- tests/release/scenarios.yml | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/scripts/release-scenarios.py b/scripts/release-scenarios.py index e523ed0c..222e090b 100644 --- a/scripts/release-scenarios.py +++ b/scripts/release-scenarios.py @@ -2695,7 +2695,7 @@ def fake(*a, input=None): and any(needs_autopilot(w) for w in [s.get("expect")] + [st_["until"] for st_ in s.get("steps") or [] if "until" in st_])] check("#713 C2: the real file: every scenario that reads the autopilot's result waits for it, each within 15 minutes", (autopilot_problems(real["scenarios"]), sorted(reads), {limit(by713[i]) for i in reads}), - ([], sorted(["accepted-spec", "spec-to-draft", "core-one-at-a-time", "prd-edited-after-acceptance", "question-answered", "stop-hint-continue", "draft-in-conflict", "ready-behind-main"]), {15})) + ([], sorted(["accepted-spec", "spec-to-draft", "core-one-at-a-time", "decision-sub-issues", "prd-edited-after-acceptance", "question-answered", "stop-hint-continue", "draft-in-conflict", "ready-behind-main"]), {15})) check("#713 C2: core-one-at-a-time accepts its `before` issue after its triage, and waits for its seal", (by713["core-one-at-a-time"]["before"][0]["issue"].get("labels"), [p for p in autopilot_problems([by713["core-one-at-a-time"]])], preconditions([by713["core-one-at-a-time"]], real["bodies"], rpack)), (None, [], [])) @@ -2742,6 +2742,8 @@ def fake(*a, input=None): dec = by713["decision-sub-issues"] check("#713 C5: decision-sub-issues opens its decision plain and accepts it after its triage", (dec["opens"]["issue"].get("labels"), dec["steps"][:2]), (None, [{"until": {"labels": ["awaiting-acceptance"]}}, {"label": "accepted"}])) + check("#713 C5: decision-sub-issues expects what a decision gets in the test repository, accepted and sealed, waiting until its seal", + (dec["steps"][2:], dec["expect"]), ([{"until": {"comments": [SEAL]}}], {"labels": ["accepted"], "comments": [SEAL]})) print("all tests passed" if not failed else "some tests failed") return 1 if failed else 0 diff --git a/tests/release/scenarios.yml b/tests/release/scenarios.yml index 6b2169a1..3f0bc1bd 100644 --- a/tests/release/scenarios.yml +++ b/tests/release/scenarios.yml @@ -287,11 +287,14 @@ scenarios: opens: {issue: {title: "feat: a greeting, then edited", body: complete-feature}} steps: [{until: {labels: [awaiting-acceptance]}}, {label: accepted}, {until: {comments: [""]}}, {edit: edited-feature}] expect: {absent: [accepted], waiting: true} + # #713: what a decision gets in the test repository, the only one it + # names: accepted and sealed, no sub-issue of another repository. - id: decision-sub-issues covers: [] + minutes: 15 opens: {issue: {title: "Decision: a greeting and a farewell", body: decision}} - steps: [{until: {labels: [awaiting-acceptance]}}, {label: accepted}] - expect: {labels: [accepted], comments: ["sub-issue"]} + steps: [{until: {labels: [awaiting-acceptance]}}, {label: accepted}, {until: {comments: [""]}}] + expect: {labels: [accepted], comments: [""]} # Claims. - id: claim-and-release From 6ed6600efe15756c4cb1e405796ecd198633f273 Mon Sep 17 00:00:00 2001 From: "dilux-bot[bot]" <333187390+dilux-bot[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 00:09:35 +0000 Subject: [PATCH 9/9] chore: ready for review