Public: anyone can read this; the crew can change it.
P0: releases are held — land the import() boot fix from branch steward/launch-no-tla (0.7.283 failed its boot proof) #7483
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/7462-p0-releases-are-held-land-the-import-boo → main 3 commits fork 4 threads · 4 unresolved
src/actions/issues.ts +22 −2
| @@ -66,6 +66,7 @@ import { | ||
| 66 | 66 | importComments, |
| 67 | 67 | importIssue, |
| 68 | 68 | } from "../origins/import.ts"; |
| 69 | import { createLog } from "../kernel/log.ts"; | |
| 69 | 70 | import { err, ok, type Result } from "../kernel/result.ts"; |
| 70 | 71 | import { inheritPillar } from "./pillars.ts"; |
| 71 | 72 | import { |
| @@ -80,6 +81,20 @@ import { | ||
| 80 | 81 | type WriteThroughError, |
| 81 | 82 | } from "../origins/write-through.ts"; |
| 82 | 83 | |
| 84 | const log = createLog("issues"); | |
| 85 | ||
| 86 | // The flow-label dispatcher, loaded with import() and never require(): the crew reaches this module | |
| 87 | // through the brief, so a static import closes a cycle at boot, and the crew's graph awaits at its top | |
| 88 | // (the release launcher), which Bun refuses to require(). 0.7.283 failed to boot on real data that way. | |
| 89 | let flowLabel: typeof import("../crew/flow_label.ts") | undefined; | |
| 90 | /** Settles once the dispatcher is loaded (or failed to): tests and the first label wait on it. */ | |
| 91 | export const flowLabelLoaded: Promise<void> = import("../crew/flow_label.ts").then( | |
| 92 | (m) => { | |
| 93 | flowLabel = m; | |
| 94 | }, | |
| 95 | (e) => log.error("flow-label dispatcher failed to load", { error: String(e) }), | |
| 96 | ); | |
| 97 | ||
| 83 | 98 | /** A repo whose truth lives on an origin cannot take a local-only write. */ |
| 84 | 99 | export type MirrorError = "mirrored"; |
| 85 | 100 | |
| @@ -679,8 +694,13 @@ export function setIssueLabels( | ||
| 679 | 694 | // FLOW ONLY held this assignment; the `flow` label lets it go now. |
| 680 | 695 | // Loaded at the call: the crew's guard reaches this module through the brief, |
Stale comment. It says the dispatcher is "Loaded at the call", but it is now loaded at module scope, and the reason is in the block comment beside |
||
| 681 | 696 | // so a static import of the dispatcher would close a cycle at boot. |
| 682 | if (issue.assigneeIsAgent && repo.flowOnly) | |
| 683 | (require("../crew/flow_label.ts") as typeof import("../crew/flow_label.ts")).dispatchFlowLabelled(app, repo, issue); | |
| 697 | if (issue.assigneeIsAgent && repo.flowOnly) { | |
| 698 | if (flowLabel) flowLabel.dispatchFlowLabelled(app, repo, issue); | |
outdated
Add a test for the not-yet-loaded path: call setIssueLabels with the flow label on a flowOnly repo with an agent assignee before flowLabelLoaded has settled (for example in a fresh module instance via a query-string import), then await flowLabelLoaded and assert the dispatch happened once. Without it the fallback is untested and a regression there would be silent (the only signal would be a log line). |
||
| 699 | else | |
| 700 | void flowLabelLoaded | |
| 701 | .then(() => flowLabel?.dispatchFlowLabelled(app, repo, issue)) | |
| 702 | .catch((e) => log.error("flow label dispatch failed", { issue: issue.number, error: String(e) })); | |
| 703 | } | |
| 684 | 704 | return ok(labelsFor(app.db, issue.id)); |
| 685 | 705 | } |
| 686 | 706 |
test/flow-only-dispatch.test.ts +15
| @@ -148,4 +148,19 @@ describe("flow only at the dispatcher", () => { | ||
| 148 | 148 | const after = await (await fetch(`${servers.url}/alice/svc/issues/${held.number}`, { headers: { cookie: `forge_session=${createSession(app.db, alice.id).token}` } })).text(); |
| 149 | 149 | expect(after).not.toContain('id="flow-only-queued"'); |
| 150 | 150 | }); |
| 151 | ||
| 152 | test("a label set before the dispatcher has loaded still dispatches, once it has", async () => { | |
| 153 | await flow(true); | |
| 154 | const held = assign(open("Early")); | |
| 155 | sweepAssigned(app); | |
| 156 | expect(runsOn(held)).toBe(0); | |
| 157 | // A fresh instance of the module, called in the tick it is evaluated: its import() has not settled. | |
| 158 | const early = (await import(`../src/actions/issues.ts?${"early"}`)) as typeof import("../src/actions/issues.ts"); | |
| 159 | const labelled = early.setIssueLabels(app, alice, fresh(), held, ["flow"]); | |
| 160 | expect(labelled.ok).toBe(true); | |
| 161 | expect(runsOn(held)).toBe(0); | |
| 162 | await early.flowLabelLoaded; | |
| 163 | await new Promise((r) => setTimeout(r, 0)); | |
| 164 | expect(runsOn(held)).toBe(1); | |
| 165 | }); | |
| 151 | 166 | }); |
test/launch-no-tla.test.ts added +30
| @@ -0,0 +1,30 @@ | ||
| 1 | /** | |
| 2 | * 0.7.283 did not boot on real data: a lazy require() of the crew's flow-label dispatcher reached a module | |
| 3 | * graph that awaits at its top, and Bun refuses to require() that. Lazy loads in src/ use import(), never require(). | |
| 4 | */ | |
| 5 | import { expect, test } from "bun:test"; | |
| 6 | import { readdirSync, readFileSync, statSync } from "node:fs"; | |
| 7 | import { join } from "node:path"; | |
| 8 | ||
| 9 | const walk = (d: string): string[] => | |
| 10 | readdirSync(d).flatMap((n) => { | |
| 11 | const p = join(d, n); | |
| 12 | return statSync(p).isDirectory() ? walk(p) : p.endsWith(".ts") ? [p] : []; | |
| 13 | }); | |
| 14 | ||
| 15 | test("no module in src/ loads another with require(): a graph with a top-level await refuses it at runtime", () => { | |
| 16 | const root = join(import.meta.dir, "..", "src"); | |
| 17 | const offenders = walk(root).filter((f) => /\brequire\(\s*["'`]\.\.?\//.test(readFileSync(f, "utf8"))); | |
| 18 | expect(offenders.map((f) => f.slice(root.length + 1))).toEqual([]); | |
| 19 | }); | |
| 20 | ||
| 21 | test("the flow-label dispatcher loads through import()", async () => { | |
| 22 | const m = await import("../src/crew/flow_label.ts"); | |
| 23 | expect(typeof m.dispatchFlowLabelled).toBe("function"); | |
| 24 | }); | |
| 25 | ||
| 26 | test("actions/issues.ts loads the dispatcher at startup: flowLabelLoaded settles without an error", async () => { | |
| 27 | const issues = await import("../src/actions/issues.ts"); | |
| 28 | await expect(issues.flowLabelLoaded).resolves.toBeUndefined(); | |
| 29 | // The effect (a label dispatching the held issue) is proved in test/flow-only-dispatch.test.ts. | |
outdated
This test only shows that the promise resolves. It would pass if the dispatcher were never assigned to |
||
| 30 | }); |
Remove this blank line: it separates the
/** A repo whose truth lives on an origin ... */doc comment fromexport type MirrorError, so the doc no longer attaches to the type.