Files
jason.woltje e24a13ad87 sage: Vikunja scripts and read-back records 2026-10-13 to 10-20 (decision 110, mos-0882)
102 files under agents/sage/work/vikunja-setup/: the comment, close and file scripts Sage ran on P19 and their
output records, including P19 386 comment 350 for the mos-0928 Part A window (rows 177 and 180-185). Each script
refuses to write a record that holds a key; a scan for both key values found none.
2026-10-20 03:32:46 -05:00

64 lines
4.9 KiB
JavaScript

// Sage, 2026-10-19: row 172 landed (be10e0c83 on refactor, #1670). Comments on P19 366
// with the landing, then marks it done. Sage's own key (bot user 13). The update sends the task as the v1
// GET returned it with only done changed; the sync key reads title, description and done back. Rollback is
// marking it undone. Prints no secret.
// Usage: node close-366.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
import { readFileSync, writeFileSync, existsSync } from "node:fs";
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
const TASK = 366;
const SIG = "Created by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const p = (...xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(SIG)}</p>`;
const TEXT = [
"Fixed by row 172, landed as be10e0c83 on refactor and pushed (0ed9412fc..c24fe5d0f). Sage built it, Darkwing approved candidate manifest 278b0eff in Gitea 29207, and #1670 is closed (29211).",
"The root cause was in the product, not the test. The Board page's Seen click returned without a word while a Board read was in flight, so no /api/seen went out and no sign-out answer came back. The click now waits for the read, then posts from the fresh row; a second click while one waits or posts is dropped. No timeout changed (ruling mos-0858).",
"The same row fixed the row 165 test in instances-browser.test.mjs: it reloaded from an evaluate and its next wait raced the reload. It now navigates to the page with a new query, which waits for the load.",
"Acceptance: 10 webui runs in a row green on the candidate, 239 pass each, and the landing gate passed. Live state is unchanged. The live stack runs from a pinned copy, so this reaches it only through a later re-pin.",
];
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
async function api(method, path, body, auth) {
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
return { status: r.status, json };
}
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
const rec = { at: new Date().toISOString(), task: TASK, title: null, problems: [] };
const save = () => {
const out = JSON.stringify(rec, null, 1) + "\n";
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
writeFileSync(OUT, out);
};
const task = await api("GET", `/api/v2/tasks/${TASK}`, undefined, sync);
if (task.status !== 200 || task.json?.project_id !== 19 || !/board-held\.test\.mjs:163 sign-out/.test(task.json?.title ?? "")) throw new Error(`task ${TASK}: ${task.status}, project ${task.json?.project_id}, title doesn't name the board-held sign-out wait`);
if (task.json.done) throw new Error(`task ${TASK} is already done; refusing`);
const before = { title: task.json.title, description: task.json.description };
rec.title = task.json.title;
writeFileSync(OUT, "", { flag: "wx" });
const c = await api("PUT", `/api/v1/tasks/${TASK}/comments`, { comment: p(...TEXT) }, key);
rec.comment = { status: c.status, id: c.json?.id ?? null };
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) rec.problems.push(`comment: ${c.status} ${c.json?.message ?? ""}`);
else {
const back = await api("GET", `/api/v2/tasks/${TASK}/comments?per_page=50`, undefined, sync);
const found = list(back.json).find((x) => x.id === c.json.id);
Object.assign(rec.comment, { readBack: back.status, found: Boolean(found) });
if (back.status !== 200 || !found) rec.problems.push(`comment read-back: ${back.status}`);
}
save();
if (!rec.problems.length) {
const full = await api("GET", `/api/v1/tasks/${TASK}`, undefined, key);
if (full.status !== 200 || full.json?.id !== TASK) rec.problems.push(`v1 get: ${full.status}`);
else {
const u = await api("POST", `/api/v1/tasks/${TASK}`, { ...full.json, done: true }, key);
rec.update = { status: u.status, inverse: `mark undone: POST /api/v1/tasks/${TASK} done false (sage key)` };
const back = await api("GET", `/api/v2/tasks/${TASK}`, undefined, sync);
Object.assign(rec.update, { readBack: back.status, done: back.json?.done, titleSame: back.json?.title === before.title, descriptionSame: back.json?.description === before.description });
if (u.status >= 300 || back.status !== 200 || back.json?.done !== true || !rec.update.titleSame || !rec.update.descriptionSame) rec.problems.push("update or read-back differs");
}
}
save();
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
if (rec.problems.length) process.exit(2);