Skip to content

Commit 172be37

Browse files
fix(pm): an accepted relay dispatch with no run is UNCONFIRMED under auto too, never a direct write (#22101)
Fixes #21892 Clause-②: no ## What changes After a relay dispatch the platform ACCEPTED (HTTP 204), "no run appeared within the start window" — or a run that never completed — is now answered **UNCONFIRMED** by every relay sender: exit 6, the shared `unconfirmedText` sentence (go READ the card, never re-run blind), under `auto` exactly as under an explicit `dispatch`. No sender takes a direct write after an accepted dispatch any more, so a relay that is merely queued can no longer turn into a write under the seat's personal login (and a second write when the queued run executes). `OS_FLEET_TRANSPORT=direct` stays the only way to write as the personal account; the route-selection read (condition ③ in `fleet-write/dispatch.mjs`: the workflow file's 404, `disabled_manually`) is unchanged. | sender | on `main` (`3d9188502e`) under `auto` + `no-run` | on this branch | |---|---|---| | `scripts/pm/label-write.mjs` | `fallbackText(...)` then the direct additive POST/DELETE | exit 6, zero requests after the pre-read | | `scripts/pm/issue-create.mjs` | `fallbackText(...)` then the direct POST | exit 6, zero requests | | `scripts/pm/issue-transfer.mjs` | `fallbackText(...)` then the direct GraphQL mutation | exit 6, zero requests after the pre-read | | `scripts/pm/post-stamped.mjs` | `fallbackText(...)` then the direct comment POST / body PATCH | exit 6, zero requests | | `scripts/pm/close-cards.mjs` | already exit 6, no direct fall-back | unchanged; carries the conformance probe | `fallbackText` is deleted from `fleet-write/dispatch.mjs`: after the change it has no caller anywhere in the tree (`git grep -n fallbackText` at `776ab7f2ff`: 0 hits; `with-fleet.sh` never called it — its `auto` asks `dispatch.mjs --route` before anything is sent, and an actions file goes to this file's CLI, which has no direct path). ## The enumeration pin (the triage grade's "a tool added later fails the pin") `node scripts/pm/fleet-write/dispatch.mjs --self-test` gains one battery, **the no-run conformance** (27 cases, floor 18 batteries): - the senders are DISCOVERED from source on every run — every code file under `scripts/` except `dispatch.mjs` that names `sendFleetWrite` — with a by-name floor of today's five, so a scan gone blind reds; - each sender must export `relayMissProbe({ route, send, pace })`: ONE write through its real write path against its own offline fake platform. A sender without it fails the pin; - each is driven through the REAL `sendFleetWrite` over a fake platform with an accepted (204) dispatch and (a) no run under `auto`, (b) no run under explicit `dispatch`, (c) a run that never completes under `auto`. Each must answer exit 6, after exactly ONE dispatch, with ZERO direct writes (any request but GET/HEAD), printing the shared UNCONFIRMED sentence; - the control drives each probe under `direct`: its write is issued directly and seen by the recorder, and nothing is dispatched — so the empty lists above are a measurement; - `dispatch.mjs`'s own CLI answers the same (exit 6, one dispatch, no other write). The per-tool pins that said "auto falls back only on no-run" are flipped in `label-write`, `issue-create` and `issue-transfer` to exit 6 **and** the exact request list after the dispatch (pre-read only, or none); `close-cards` already pinned it. `post-stamped`'s self-test is synchronous, so its behavioural pin is the conformance; its own gate now also carries the structural half (main writes only through `writeThroughRoute`, whose one direct write sits on the non-dispatch branch ahead of the relay, and whose no-run/timeout branch returns UNCONFIRMED). CI runs the dispatcher self-test on every PR (the `Fleet-write dispatcher self-test` step in `lint.yml` carries no `if:`). ## Inherited commits — read as UNVERIFIED, verdict per hunk The branch continues the released claim's work at `4c5f2c15fc` (7 commits over `607463d736`). `origin/main` was merged in (`69f2346dca`, merge of `3d9188502e`, no rebase — the branch is shared); `main` had not touched the six files since `607463d736` (`git diff --stat 607463d origin/main` over the six paths: empty), and the merge left the branch's delta at exactly these six files (+402 / -94). | commit | hunk | verdict | |---|---|---| | `a6bc9b3e08` label-write | header paragraph; import drops `fallbackText`; the `auto && no-run` arm removed, the remaining arm answers `no-run`/`timeout` UNCONFIRMED for every requested transport | keep | | | `relayMissProbe` over `driveOffline` (the real `runLabelWrite`) | keep | | | battery renamed; flipped case asserts exit 6, ONE dispatch, calls equal to the pre-read alone; second case asserts the shared sentence | keep | | | battery pin left at 12 while the battery registers 14 | **corrected** to 14 in `776ab7f2ff` | | `51139a6248` issue-create | header, import, branch, `relayMissProbe`, flipped case (exit 6, number null, zero requests) | keep; pin 8 → 9 **corrected** | | `b1cebeea98` issue-transfer | header, import, branch, `relayMissProbe`, flipped case (exit 6, pre-read only) | keep; pin 8 → 9 **corrected** | | `f99f21158a` post-stamped | `deps` door on `rest` / `readCardTail` / `writeArtefact` / `writeViaRelay` (the live path passes none; every default is the old value) | keep | | | `writeThroughRoute` extracted from `main`: the old branches, minus the `auto` + `no-run` fall-back; `main` now one call, and its catch still classifies a direct write's failure | keep | | | `relayMissProbe`; structural pin retargeted to `writeThroughRoute(` | keep; own-gate structural pin **added** in `776ab7f2ff` (battery pin 12 → 13) | | `27f2f3ed9f` close-cards | `stubs.send` passthrough; `relayMissProbe` spelling the two child tools' writes as the requests they stand for | keep | | `846527f4c0` dispatch.mjs | header: never fallen back from under any transport request; the no-run conformance section | keep | | | `fallbackText` deleted; its self-test case replaced by "accepted (204), no run known" | keep (no caller remains) | | | the conformance battery (27) and floor 17 → 18 | keep | | | `RELAY_MISS_PROBE` declared between `DEFAULT_START_MS` and `DEFAULT_CEILING_MS` | **corrected**: moved below the three timing exports; the success line now names the conformance | | `4c5f2c15fc` | a probe that throws fails as a case (`drive` wrapper), per-tool second cases guarded with `?.` | keep — ablation B below fails as cases, not as a crash | Dropped: none. ## Self-tests at `776ab7f2ff` (rule ⑤: each edited script's own; no `*.test.*` under `scripts/` or `tests/` names any of the six — `git grep` exit 1) ```text ✓ fleet-write/dispatch self-test: 200 cases pass across 18 batteries — … (the no-run conformance). ✓ label-write self-test: 92 cases pass across 10 batteries — … ✓ issue-create self-test: 48 cases pass across 7 batteries — … ✓ issue-transfer self-test: 76 cases pass across 9 batteries — … ✓ post-stamped self-test: 630 cases pass across 22 batteries — offline, no network, no token. OK close-cards self-test: 227 cases pass across 20 batteries — offline, no network, no token. ``` The inherited tree alone (`69f2346dca`, before `776ab7f2ff`) was green on the same six, post-stamped at 629 cases. What each probe's recorder sees (a scratch driver handing each `relayMissProbe` an accepted no-run result, then `direct`): ```text close-cards auto+no-run exit 6 [GET issues/88, GET …/timeline, GET …/comments] direct exit 0 [… PATCH issues/88, POST …/comments, DELETE …/labels] issue-create auto+no-run exit 6 [] direct exit 0 [POST /issues, GET issues/12] issue-transfer auto+no-run exit 6 [GET issues/7] direct exit 0 [GET issues/7, GET repo, POST /graphql, GET issues/7, GET target issues/31] label-write auto+no-run exit 6 [GET issues/7] direct exit 0 [GET issues/7, POST issues/7/labels, GET issues/7] post-stamped auto+no-run exit 6 [] direct exit 0 [POST issues/7/comments, GET issues/comments/501] ``` ## Ablation — both directions, at `776ab7f2ff`, through `scripts/ablation-replace.mjs` (anchor must hit, blob must change, restore proven) | leg | mutation | own self-test | dispatcher self-test | |---|---|---|---| | A | post-stamped re-opens `auto` + `no-run` → `writeArtefact` | exit 1: the structural pin got `[[],2,true,true,true]` | exit 1: `post-stamped.mjs: auto + an ACCEPTED dispatch + no-run` got `[0,1,204,"no-run",["POST /repos//issues/7/comments"],false]` | | B | label-write stops exporting `relayMissProbe` | exit 0 (the export is the conformance's door, not label-write's) | exit 1: `exports relayMissProbe` got `"undefined"`; battery floor 23 below 27 | | C | label-write re-opens `auto` + `no-run` → direct | exit 1: 2 cases, calls `[GET issues/7, POST issues/7/labels, GET issues/7]` | exit 1: `label-write.mjs: auto + … + no-run` got `[0,1,204,"no-run",["POST …/issues/7/labels"],false]` | Every leg restored: blob equals `HEAD` (`post-stamped.mjs` `f51c37c8f5`, `label-write.mjs` `b364caeacc`), `git diff HEAD` 0 bytes, `git status --porcelain` empty. The green direction is the same commands on the restored tree (the self-test block above). The first attempt of legs A and C was refused by `ablation-replace` (the replacement still contained the anchor, "the anchor count moved 1 to 1") — no command ran, not a measurement; both were re-anchored and re-run. ## Gate families (derived by `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, no paths: 6 paths vs merge base `3d9188502e`, 38 commands) All at `776ab7f2ff`, exit codes captured before any pipe: - exit 0 (37): `check-ci-filter-parity`, `check-closing-keyword-parity` (+ `--self-test`), `check-comment-mask-corpus`, `check-declaration-mirrors` (+ `--self-test`), `check-scripts-symbol-anchors` (+ `--self-test`), `check-self-test-wired` (+ `--self-test`), `check-self-test-workflow-commands` (+ `--self-test`), `check-whole-set-label-write` (+ `--self-test`), `pm/bare-root-worklist --self-test`, `check:agent-test-spelling`, `check:bash32-floor`, `check:cli-command-ids`, `check:cross-package-test-inputs`, `check:driver-memory-census`, `check:entry-guard`, `check:gitlink-declared`, `check:nul-bytes`, `check:parse-guard`, `check:pm-close-cards`, `check:pm-fleet-write-dispatch`, `check:pm-fleet-write-execute`, `check:pm-fleet-write-validate`, `check:pm-issue-create`, `check:pm-issue-transfer`, `check:pm-label-write`, `check:pm-post-stamped`, `check:pm-write-pace`, `check:pnpm-filter-targets`, `check:ratchet-remedy-authority`, `check:refd-timer-probe`, `check:watch-hint-literal`. - `check:pm-dispatch-gates` (the battery, an unlocked run): exit 0 — `✓ check:pm-dispatch-gates --self-test: the exit contract holds in all three directions.` then `✓ dispatch-gates self-test: 1976 cases pass.` (1279.8 s on this box). - `--ran` reconciliation, exit 0: `Run reconciliation — 38 derived, 38 run, 0 NOT-MEASURED, 0 UNRUN.` (all 38 carry a recorded exit code). `origin/main` has since moved to `b04a5295f7` without touching the six files or their imports. - Roster families whose roster sits in a directory this diff touches: exit 0 — `check-published-list-mirrors` (+ `--self-test`), `check:engine-double-contract`, `check:i18n-stale-fill`, `check:pm-label-desc-cap`, `check-dts-references --self-test`. NOT MEASURED — `check:dts-closure` and `check:published-readme-exports` (exit 3 PREREQUISITE NOT MET: no package `dist/`; this diff touches no package), `check:console-injection` (exit 0 but skipped: no console dist). - Lint, a declared narrowing: `pnpm exec eslint --no-inline-config --format json` over the six files — 6 files, 0 errors, 0 warnings (none reported as ignored). Population: `eslint.config.mjs`'s `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` block covers `scripts/pm/*.mjs`. Invariance: the config never enables type-aware linting (no `parserOptions.project`, said in its own header) and its plugin rules are per-file, so this diff cannot move any untouched file's verdict. The repo-wide `pnpm lint` is CI's. ## Acceptance notes - The local derivation for a diff touching only `post-stamped.mjs` names `check:pm-post-stamped` but not `check:pm-fleet-write-dispatch`, where the conformance lives; CI runs the dispatcher self-test unconditionally, and post-stamped's own gate now carries the structural half. Noted, not filed (the derivation script is frozen). - `post-stamped.mjs`'s size route (a body over the relay's 60,000-byte cap under `auto` goes direct for that write, with one line naming bytes, cap and identity) is a pre-dispatch, declared and pinned route decision, untouched here. It sits beside the grade's sentence that `OS_FLEET_TRANSPORT=direct` stays the only way to write as the personal account; whether that sentence covers it is a ruling, not this card. Noted, not filed. - #19774 is the route-selection half of this class and is not touched; #19990, #21294, #22052 and #22053 remain open and are not addressed here. ## Writes `git push` (twice: the merge, then `776ab7f2ff`); REST through the relay as `objectstack-fleet[bot]`: this `pr_create` (draft), one `label-write` (`skip-changeset` + PR assignee), one `os-dev-report` comment on the card. MCP GitHub calls: 0. No changeset: `scripts/pm/**` publishes nothing. --- _Generated by [Claude Code](https://claude.ai/code/session_01CXydFDyiQwNbGFkmwrcRQq)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent dfa427b commit 172be37

6 files changed

Lines changed: 402 additions & 94 deletions

File tree

‎scripts/pm/close-cards.mjs‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1599,7 +1599,7 @@ async function driveOffline(options, initial = {}, stubs = {}) {
15991599
log: (line) => out.push(line),
16001600
now: clock,
16011601
route: stubs.route ?? DIRECT_ROUTE,
1602-
send: stubs.relay ? fakeRelay(api, stubs.relay, sent, clock) : undefined,
1602+
send: stubs.send ?? (stubs.relay ? fakeRelay(api, stubs.relay, sent, clock) : undefined),
16031603
postComment: async ({ issue, file }) => {
16041604
const exit = commentExits.length ? commentExits.shift() : 0;
16051605
posted.push({ issue, file, exit });
@@ -1622,6 +1622,25 @@ const memoryFiles = (tree) => (path) => {
16221622
};
16231623

16241624
const QUEUED = (number, extra = {}) => ({ [number]: { state: 'open', labels: ['pm:queue', 'tooling'], assignees: [], timeline: [], comments: [], ...extra } });
1625+
/**
1626+
* The no-run conformance probe — `fleet-write/dispatch.mjs`'s header names the
1627+
* contract and its self-test drives it: ONE queued card through
1628+
* `runCloseCards`, the real write path, against the offline fake board, on the
1629+
* route and the sender the conformance hands in. Answers the exit, every
1630+
* request that left this process directly (`METHOD /path`) — the direct
1631+
* leg's comment and label writes are its two child tools, stubbed in-process
1632+
* and spelled as the requests they stand for — and what the tool printed.
1633+
*/
1634+
export async function relayMissProbe({ route, send }) {
1635+
const repo = 'objectstack-ai/objectstack';
1636+
const run = await driveOffline({ numbers: [88] }, { cards: QUEUED(88) }, { route, send });
1637+
const calls = [
1638+
...run.api.calls.map((c) => `${c.method} ${c.path}`),
1639+
...run.posted.map((p) => `POST /repos/${repo}/issues/${p.issue}/comments`),
1640+
...run.labelled.map((l) => `DELETE /repos/${repo}/issues/${l.issue}/labels`),
1641+
];
1642+
return { exit: run.res.exit, calls, text: run.text };
1643+
}
16251644
/** A record card that never carried a pm-state — the shape `--stateless` exists for. */
16261645
const STATELESS = (number, extra = {}) => ({ [number]: { state: 'open', labels: ['tracking', 'domain:spec'], assignees: [], timeline: [], comments: [], ...extra } });
16271646
/** The clock an EARLIER act posted this row's closing comment on — a day before the self-test's own. */

‎scripts/pm/fleet-write/dispatch.mjs‎

Lines changed: 155 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,18 @@
102102
* separately, and the first live probe is what tunes them. On either ceiling
103103
* the answer is `UNCONFIRMED` with the run URL when there is one, exit
104104
* `EXIT_UNCONFIRMED` (6): ⛔ never silent, ⛔ never auto-retried — a retry of a
105-
* dispatch that may still run is a double write. The one fall-back that exists
106-
* is the caller's and only under `auto`: NO run appeared in the start window,
107-
* which is the shape a board without the listening workflow produces, so the
108-
* caller prints one line and takes `direct`. A run that appeared and FAILED is
109-
* never fallen back from — the relay refused or half-wrote, and a direct retry
110-
* would write twice.
105+
* dispatch that may still run is a double write. ⛔ And never fallen back from,
106+
* under ANY transport request, `auto` included: a run that has not appeared
107+
* yet is evidence of the wait and of nothing else — the relay may be queued —
108+
* so a direct write after an accepted dispatch exchanges the fleet identity
109+
* for the seat's personal account without its say-so, and may land the same
110+
* write twice when the run does execute. Whether the board LISTENS is
111+
* condition ③'s question, answered before anything is sent (the workflow
112+
* file's 404 turns `auto` direct, said out loud); once a dispatch is accepted
113+
* the only answers are the run's. A run that appeared and FAILED is never
114+
* fallen back from either — the relay refused or half-wrote, and a direct
115+
* retry would write twice. Every sender is held to this in ONE place: the
116+
* no-run conformance below.
111117
*
112118
* `write-pace.mjs` counts the dispatch as THE write — one POST, paced and
113119
* leased like any other; the runner's executor paces its own writes there.
@@ -193,11 +199,39 @@
193199
* close-cards) get 4 from it — issue-create's `EXIT_READ_BACK_MISMATCH`.
194200
* - `unverified` is a state no caller names, so each falls to its non-success
195201
* branch: `exitForResult` answers 6, post-stamped's `relayExitFor` answers
196-
* its default 6. Neither is ever a fall-back to direct (that is `no-run`'s
197-
* alone) and neither is 0.
202+
* its default 6. Neither is ever a fall-back to direct — no outcome of an
203+
* accepted dispatch is (the no-run conformance below) — and neither is 0.
198204
* - a stroke carrying no body (labels, assignees, state, a transfer, the
199205
* GraphQL ops) reads nothing back and its outcome is unchanged.
200206
*
207+
* ## The no-run conformance — every sender, one answer
208+
*
209+
* The rule above lives in each caller's own control flow, where a later edit
210+
* can quietly re-open a direct path after the dispatch, so it is pinned once,
211+
* for all of them, in this file's self-test:
212+
*
213+
* - a SENDER is any file under `scripts/` other than this one that names
214+
* `sendFleetWrite`, discovered from the source on every run — never a
215+
* hand-kept list, so a tool added later is held to this the day it lands;
216+
* - every sender exports `relayMissProbe({ route, send, pace })`: ONE write
217+
* through its real write path against its own offline fake platform, on
218+
* the route, the sender and the throttle the conformance hands in,
219+
* answering `{ exit, calls, text }` — the exit, every request that left
220+
* the process directly (`METHOD /path`), and what it printed;
221+
* - the conformance drives each with a dispatch the platform ACCEPTS (204)
222+
* and a run that never appears, under `auto` and under an explicit
223+
* `dispatch`, and with a run that never completes under `auto`, through
224+
* the real `sendFleetWrite` over a fake platform. Each must answer exit
225+
* `EXIT_UNCONFIRMED`, after exactly ONE dispatch, with ZERO direct writes
226+
* (any request but a GET), printing the shared `unconfirmedText` sentence;
227+
* - the control drives each under `direct`: at least one direct write and no
228+
* dispatch — so the empty list above is a measurement, never a recorder
229+
* that sees nothing;
230+
* - a sender without the probe fails the pin until it carries one and gives
231+
* the same answer. ⛔ There is no third, quieter answer to an accepted
232+
* dispatch with no run. This file's own CLI (`with-fleet.sh --actions`
233+
* execs it) has no direct path at all and is pinned beside them.
234+
*
201235
* ## The run's annotations — the numbers a seat CAN read
202236
*
203237
* `execute.mjs` prints, for every action whose op is in `ANNOTATED_OPS` (the
@@ -292,6 +326,9 @@ export const DEFAULT_START_MS = 90_000;
292326
export const DEFAULT_CEILING_MS = 5 * 60 * 1000;
293327
export const DEFAULT_POLL_MS = 5_000;
294328

329+
/** The export every sender carries for the no-run conformance (the header's section of that name). */
330+
const RELAY_MISS_PROBE = 'relayMissProbe';
331+
295332
// ---------------------------------------------------------------------------
296333
// Pure core
297334
// ---------------------------------------------------------------------------
@@ -514,15 +551,6 @@ export function unconfirmedText(result, tool = 'fleet-write') {
514551
);
515552
}
516553

517-
/** The one line `auto` prints when it falls back to `direct` because no run appeared. */
518-
export function fallbackText(result, tool = 'fleet-write') {
519-
return (
520-
`${tool}: transport auto — the dispatch was accepted (HTTP ${result.status}) but NO run named after request ${result.requestId} appeared within ` +
521-
`${result.startMs} ms, the shape a board without the listening workflow produces. Falling back to DIRECT for this write. ` +
522-
'⚠️ If the relay is live and merely slow, the direct write below is a second one.'
523-
);
524-
}
525-
526554
// ---------------------------------------------------------------------------
527555
// The read-back — pure halves (the header's read-back section is the authority)
528556
// ---------------------------------------------------------------------------
@@ -1112,8 +1140,9 @@ const SELF_TEST_BATTERIES = Object.freeze({
11121140
"the run's annotations: after a success run carrying an op that creates or moves a card its check-run annotations are read first — an issue_create they name read back AT that number with NO re-list; absent, unreadable, ignored or past the cap, the re-list as before; a stroke with no such op reads none": 14,
11131141
'the wiring: the POST is paced and on the roster, the reads are not, the token never reaches the log': 5,
11141142
'the CLI: a dry run sends nothing, usage, the exit ladder, the session derived from the container, a route read behind a dead proxy refuses': 11,
1143+
'the no-run conformance: every sender — discovered from the source — answers an accepted dispatch with no run, or none completed, UNCONFIRMED (6) after ONE dispatch with ZERO direct writes, under auto and explicit dispatch alike; under direct its write is seen': 27,
11151144
});
1116-
const SELF_TEST_BATTERY_FLOOR = 17;
1145+
const SELF_TEST_BATTERY_FLOOR = 18;
11171146
const UNATTRIBUTED_BATTERY = '(unattributed)';
11181147

11191148
const batteryCases = new Map();
@@ -1479,7 +1508,7 @@ export async function selfTest() {
14791508
const none = await drive({ runs: () => [] });
14801509
t('no run within the start window is `no-run`, exit 6', [none.state, exitForResult(none)], ['no-run', EXIT_UNCONFIRMED]);
14811510
t('…after ONE dispatch, and it says how long it waited', [none.seen.filter((s) => s.call === DISPATCH).length, unconfirmedText(none).includes('90000 ms')], [1, true]);
1482-
t('…and the fall-back line names the shape (no listening workflow) and the double-write risk', fallbackText(none).includes('listening workflow') && fallbackText(none).includes('second one'));
1511+
t('…the dispatch WAS accepted (HTTP 204) and no run is known — the state every sender answers UNCONFIRMED and none falls back from (the no-run conformance)', [none.status, none.ok, none.run], [204, false, null]);
14831512
const stuck = await drive({ runs: () => [RUN('in_progress')], one: () => RUN('in_progress') });
14841513
t('a run that never completes within the ceiling is `timeout`, exit 6, with the run url', [stuck.state, exitForResult(stuck), unconfirmedText(stuck).includes('https://github.test/run/42')], ['timeout', EXIT_UNCONFIRMED, true]);
14851514
t('…and the sentence says not to retry blind', unconfirmedText(stuck).includes('do not re-run'));
@@ -1878,6 +1907,111 @@ export async function selfTest() {
18781907
const deadOut = routeDeadProxy.stdout.trim() ? JSON.parse(routeDeadProxy.stdout.trim()) : null;
18791908
t(`⭐ --route behind a proxy that answers nothing: the process re-execs with ${PROXY_FLAG}, the read gets no answer, and the verdict is exit 3 with the error naming it — never direct`, [routeDeadProxy.status, deadOut?.transport ?? null, (deadOut?.error ?? '').includes('no answer'), routeDeadProxy.stderr.includes(PROXY_FLAG)], [EXIT_PREREQUISITE, null, true, true], routeDeadProxy.stderr.slice(-300));
18801909
}
1910+
1911+
// ── the no-run conformance ──────────────────────────────────────────────
1912+
battery('the no-run conformance: every sender — discovered from the source — answers an accepted dispatch with no run, or none completed, UNCONFIRMED (6) after ONE dispatch with ZERO direct writes, under auto and explicit dispatch alike; under direct its write is seen');
1913+
{
1914+
const { readdirSync } = await import('node:fs');
1915+
const { dirname, relative, resolve, sep } = await import('node:path');
1916+
const { pathToFileURL } = await import('node:url');
1917+
const REPO_ROOT = resolve(dirname(SELF_PATH), '../../..');
1918+
const own = relative(REPO_ROOT, SELF_PATH).split(sep).join('/');
1919+
// The senders, from the SOURCE: every code file under scripts/ but this one that names the sender function.
1920+
const senders = readdirSync(join(REPO_ROOT, 'scripts'), { recursive: true })
1921+
.map((f) => `scripts/${String(f).split(sep).join('/')}`)
1922+
.filter((f) => /\.(?:mjs|cjs|js|mts|ts)$/u.test(f) && !f.split('/').includes('node_modules') && f !== own)
1923+
.filter((f) => /\bsendFleetWrite\b/u.test(readReal(join(REPO_ROOT, f), 'utf8')))
1924+
.sort();
1925+
// The discovery's floor, BY NAME: a scan gone blind — a moved root, a renamed import — finds fewer and reds here.
1926+
const KNOWN = ['scripts/pm/close-cards.mjs', 'scripts/pm/issue-create.mjs', 'scripts/pm/issue-transfer.mjs', 'scripts/pm/label-write.mjs', 'scripts/pm/post-stamped.mjs'];
1927+
t('the discovery finds every sender known today — the scan is not blind', KNOWN.filter((k) => !senders.includes(k)), []);
1928+
1929+
const relayRoute = (requested) => ({ requested, transport: 'dispatch', reason: `conformance: ${requested} → dispatch`, error: null, session: SESSION });
1930+
const DIRECT_ROUTE = { requested: 'direct', transport: 'direct', reason: 'conformance: direct', error: null, session: null };
1931+
const isDirectWrite = (call) => !/^(?:GET|HEAD) /u.test(call);
1932+
// The tool-independent lines of the ONE sentence every sender prints — computed, so a sender's own copy of it cannot pass.
1933+
const shared = (result) => unconfirmedText(result, '').split('\n').slice(1).join('\n');
1934+
const freshPace = () => paceFor(join(dir, `pace-conformance-${paceCase++}.jsonl`));
1935+
// The REAL sender over a fake platform: the dispatch is accepted (204); its run never appears, or never completes.
1936+
const relayAnswering = (state, counter) => async (payload, opts = {}) => {
1937+
counter.dispatches += 1;
1938+
let nowMs = T0;
1939+
const clock = { now: () => nowMs, elapsed: () => nowMs - T0 };
1940+
const run = { id: 77, display_title: `fleet-write ${payload.request_id}`, name: 'Fleet Write', html_url: 'https://github.test/run/77', status: 'in_progress', conclusion: null };
1941+
const script = state === 'no-run' ? { runs: () => [] } : { runs: () => [run], one: () => run };
1942+
const r = await sendFleetWrite(payload, {
1943+
fetch: platform(script, [], clock),
1944+
token: TOKEN,
1945+
pace: freshPace(),
1946+
now: clock.now,
1947+
sleep: async (ms) => {
1948+
nowMs += ms;
1949+
},
1950+
log: opts.log ?? (() => {}),
1951+
ceilings: { startMs: 90_000, ceilingMs: 300_000, pollMs: 5_000, notes: [] },
1952+
});
1953+
counter.results.push(r);
1954+
return r;
1955+
};
1956+
const drive = async (probe, args) => {
1957+
try {
1958+
return await probe(args);
1959+
} catch (e) {
1960+
return { exit: `threw: ${e?.message ?? e}`, calls: [], text: '' };
1961+
}
1962+
};
1963+
for (const sender of senders) {
1964+
const probe = (await import(pathToFileURL(join(REPO_ROOT, sender)).href))[RELAY_MISS_PROBE];
1965+
t(`${sender} sends a relay dispatch and exports ${RELAY_MISS_PROBE} — a sender without it fails here until it answers like the rest`, typeof probe, 'function');
1966+
if (typeof probe !== 'function') continue;
1967+
for (const [requested, state] of [['auto', 'no-run'], ['dispatch', 'no-run'], ['auto', 'timeout']]) {
1968+
const counter = { dispatches: 0, results: [] };
1969+
const r = await drive(probe, { route: relayRoute(requested), send: relayAnswering(state, counter), pace: freshPace() });
1970+
const answered = counter.results[0] ?? null;
1971+
t(
1972+
`⭐ ${sender}: ${requested} + an ACCEPTED dispatch + ${state} ⇒ exit ${EXIT_UNCONFIRMED} after ONE dispatch, ZERO direct writes, the shared UNCONFIRMED sentence printed`,
1973+
[r.exit, counter.dispatches, answered?.status ?? null, answered?.state ?? null, r.calls.filter(isDirectWrite), answered ? r.text.includes(shared(answered)) : false],
1974+
[EXIT_UNCONFIRMED, 1, 204, state, [], true],
1975+
r.text.slice(-400),
1976+
);
1977+
}
1978+
// The control: the same probe under direct WRITES, and the recorder sees it — so the empty lists above are a measurement.
1979+
const control = { dispatches: 0 };
1980+
const c = await drive(probe, {
1981+
route: DIRECT_ROUTE,
1982+
send: async (payload) => {
1983+
control.dispatches += 1;
1984+
return { state: 'refused', ok: false, status: 0, verdict: 'refusal', run: null, requestId: payload.request_id, startMs: 0, ceilingMs: 0, detail: 'the conformance control: a direct route dispatched' };
1985+
},
1986+
pace: freshPace(),
1987+
});
1988+
t(`…the control: ${sender} under direct issues its write directly — the recorder sees it — and never dispatches`, [control.dispatches, c.calls.filter(isDirectWrite).length > 0], [0, true], `${c.exit} · ${c.calls.join(' | ')}`);
1989+
}
1990+
1991+
// This file's own CLI, the route `with-fleet.sh --actions` execs: it has no direct path, and it answers the same.
1992+
writeFileSync(join(dir, 'conformance-actions.json'), JSON.stringify(ACTIONS), 'utf8');
1993+
let nowMs = T0;
1994+
const clock = { now: () => nowMs, elapsed: () => nowMs - T0 };
1995+
const seen = [];
1996+
const printed = [];
1997+
const [keepLog, keepErr] = [console.log, console.error];
1998+
console.log = () => {};
1999+
console.error = (l) => printed.push(String(l));
2000+
let code;
2001+
try {
2002+
code = await main(['--repo', 'objectstack-ai/objectstack', '--actions-file', join(dir, 'conformance-actions.json')], {
2003+
env: { GITHUB_TOKEN: TOKEN, [SESSION_ENV]: SESSION },
2004+
send: { fetch: platform({ runs: () => [] }, seen, clock), pace: freshPace(), now: clock.now, sleep: async (ms) => { nowMs += ms; }, log: () => {}, ceilings: { startMs: 90_000, ceilingMs: 300_000, pollMs: 5_000, notes: [] } },
2005+
});
2006+
} finally {
2007+
[console.log, console.error] = [keepLog, keepErr];
2008+
}
2009+
t(
2010+
"this file's own CLI answers the same: exit 6 after ONE dispatch, no other write, the shared UNCONFIRMED sentence printed",
2011+
[code, seen.filter((x) => x.call === DISPATCH).length, seen.filter((x) => x.call !== DISPATCH && isDirectWrite(x.call)).length, printed.join('\n').includes(shared({ status: 204, state: 'no-run' }))],
2012+
[EXIT_UNCONFIRMED, 1, 0, true],
2013+
);
2014+
}
18812015
} finally {
18822016
rmSync(dir, { recursive: true, force: true });
18832017
}
@@ -1914,7 +2048,8 @@ export async function selfTest() {
19142048
`✓ fleet-write/dispatch self-test: ${cases.length} cases pass across ${declared.length} batteries — the transport selector that never guesses, ` +
19152049
'the session on the envelope, one paced dispatch per stroke, a run found by its request id and waited to its conclusion, both ceilings answered UNCONFIRMED and never retried, ' +
19162050
'and every body read back after a success run — the card\'s 41,699 bytes byte-exact across every 16 KiB boundary, a corrupted read-back NOT STORED (exit 4), an unfound one UNCONFIRMED (6) ' +
1917-
"only after a lagging issue list was re-read on its bounded schedule — unless the run's annotation named the new number, which is read at once and never listed — the create never re-sent.",
2051+
"only after a lagging issue list was re-read on its bounded schedule — unless the run's annotation named the new number, which is read at once and never listed — the create never re-sent; " +
2052+
'and every relay sender, discovered from the source, answering an accepted dispatch with no run, or none completed, UNCONFIRMED (6) with ZERO direct writes, under auto as under dispatch (the no-run conformance).',
19182053
);
19192054
selfTestReachedVerdict = true;
19202055
return 0;

0 commit comments

Comments
 (0)