e2e: S9 governed boost scenario in the governance sim

S9: boost draft (providerMetadata.sourceRemoteRef) → immediate-rule approve
→ POST /publishing/drafts/:id/amplify { connectionId, kind: repost } →
verify the source status reblogged on Mastodon. ONLY=S9 runs it standalone.
Gap list updated: boost + Temporal-queue + status-read gaps closed
(auto-publish remains a governance-model decision, not a missing route).
This commit is contained in:
glass beadian 2026-10-13 19:40:01 +00:00
parent 7aa99681dd
commit a8c691924d
2 changed files with 179 additions and 4 deletions

View file

@ -403,6 +403,46 @@ async function runImmediate(sourceIdx) {
}
}
// S9 — the governed boost: approved draft + the amplify route = a real reblog.
async function runBoost(sourceIdx) {
section("S9 — governed boost: approved draft + amplify route executes a reblog", {
why: "POST /publishing/drafts/:draftId/amplify { connectionId, kind: 'repost' }: the governance gate is the draft state (approved only) and the target is providerMetadata.sourceRemoteRef recorded at creation — never a caller-supplied id.",
});
const source = ctx.sources[sourceIdx % ctx.sources.length];
const before = await masto(ctx.radar, `/api/v1/statuses/${source.id}`);
const reblogsBefore = before.reblogs_count ?? 0;
note(`source ${source.id} reblogs_before ${reblogsBefore}`);
// The immediate rule (re-created; runImmediate's finally removes it) makes
// approve fire E1 directly, so the draft reaches 'approved' without a poll.
const rules = (await api("GET", `/chatgov/rules?accountId=${ctx.connectionId}`, { token: ctx.tokens.owner, as: "owner" })).data;
let rule = (Array.isArray(rules) ? rules : []).find((r) => r.scope === "account" && !r.supersededBy);
if (!rule) rule = (await api("POST", "/chatgov/rules", { token: ctx.tokens.owner, as: "owner", body: { scope: "account", accountId: ctx.connectionId, policy: { mode: "immediate" } } })).data;
ctx.ruleId = rule.id;
const draft = await api("POST", "/publishing/drafts", {
token: ctx.tokens.owner, as: "owner",
body: { post: { text: `[e2e-gov-${RUN}-S9] Boost proposal for ${source.url} #${TAG}`,
mediaAssetIds: [], visibility: "provider_default",
providerMetadata: { connectionId: ctx.connectionId, sourceRemoteRef: source.id } } },
});
const draftId = draft.data.id;
note(`boost draft ${draftId}`);
await api("POST", `/publishing/drafts/${draftId}/submit`, { token: ctx.tokens.owner, as: "owner" });
const approve = await api("POST", `/publishing/drafts/${draftId}/approve`, { token: ctx.tokens.owner, as: "owner", ok: false });
check("approve succeeds (immediate rule)", true, approve.status < 300, approve.data);
const amp = await api("POST", `/publishing/drafts/${draftId}/amplify`, {
token: ctx.tokens.owner, as: "owner", body: { connectionId: ctx.connectionId, kind: "repost" },
});
check("amplify accepted (200)", 200, amp.status, amp.data);
const after = await masto(ctx.radar, `/api/v1/statuses/${source.id}`);
check("source reblogged", true, after.reblogged === true || (after.reblogs_count ?? 0) > reblogsBefore, after);
note(`reblogs ${reblogsBefore} → ${after.reblogs_count ?? "?"}`);
}
function writeReport(fatal) {
const lines = [];
const scen = sections.filter((s) => /^S\d/.test(s.name));
@ -439,10 +479,7 @@ function writeReport(fatal) {
log(`report written to ${REPORT}`);
}
gap("Governed Mastodon boost/reblog: drafts only dispatch via connector.publish (status, optional providerMetadata.inReplyToId). connector-mastodon has amplify(repost) but no API route reaches it for Mastodon (POST /archive/items/:id/amplify is Telegram-only). Scenarios therefore execute a reply to the source post.");
gap("No auto-dispatch on poll pass: E2 moves the draft to 'approved' only; execution needs a separate POST /publishing/drafts/:id/publish by a publication.publish holder.");
gap("LIVE DEPLOY: publish workflows are started on Temporal task queue 'tmbh-publishing' (current API default, TEMPORAL_TASK_QUEUE unset) but the deployed scheduler worker (image built 2026-10-10) polls 'superposter-publishing'. Workflows sit Running with an unclaimed first task and scheduled_publications stay 'pending' — approved actions never reach Mastodon. Fix: set TEMPORAL_TASK_QUEUE identically on api+scheduler, or redeploy the scheduler from current source.");
gap("No read route for scheduled_publications / publication receipts (remote status id); execution is verified on the Mastodon side via the radar token.");
gap("No auto-dispatch on poll pass: E2 moves the draft to 'approved' only; execution needs a separate POST /publishing/drafts/:id/publish (or /amplify) by a publication.schedule holder. Whether a passing poll should auto-execute is a governance-model decision (execute-on-vote vs editor-release), not a missing route.");
deviations.push("Connection created through the real OAuth authorization-code flow (radar web sign-in + consent), not with {instanceUrl, access_token} — no such route exists.");
deviations.push("Invited members join as 'Editor' (an existing role holding chatgov.vote per migration 024); u1–u3 are then PATCHed to 'Admin' via /team/members/:membershipId/role.");
deviations.push("Polls are opened explicitly (POST /chatgov/polls with draftId + supplied rule, kind yes_no) because the workspace has no posting_rules; with no live rules draftPollRule() accepts the supplied rule.");
@ -465,6 +502,8 @@ try {
}
if (!only || only.has("S7")) try { await runImmediate(i++); }
catch (e) { check("scenario completed without error", "ok", String(e.message), e.data); }
if (!only || only.has("S9")) try { await runBoost(i++); }
catch (e) { check("scenario completed without error", "ok", String(e.message), e.data); }
} catch (e) {
fatal = e.message;
console.error(e);