vikunja: row 62 live tick on #19 with descendants on, GETs only (sage)
tick-62.mjs runs the candidate's discover(), reconcile() and tick() against tasks.woltje.com with the sync key, a real Store and Broker in a scratch root, and a fetch guard that refuses anything but GET. The startup scope probes are not sent. Reconcile 15 GETs, 42 snapshots; tick 8 GETs, 0 snapshots; 18, 20 and 21 watch-only. A missing view id on #18 answers 404 code 3014 while #18 reads 200 (the fake says 13001). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
{
|
||||
"at": "2026-10-11T21:28:03.309Z",
|
||||
"src": "/tmp/sage-r62g",
|
||||
"key": "sync (bot 10)",
|
||||
"root": 19,
|
||||
"hostTracker": {
|
||||
"project": 19,
|
||||
"descendants": false
|
||||
},
|
||||
"version": "v2.7.0",
|
||||
"rootView": {
|
||||
"id": 76,
|
||||
"buckets": {
|
||||
"todo": 159,
|
||||
"in-progress": 161,
|
||||
"in-review": 248,
|
||||
"blocked": 162,
|
||||
"done": 57
|
||||
}
|
||||
},
|
||||
"reconcile": {
|
||||
"result": {
|
||||
"snapshots": 42,
|
||||
"events": 42
|
||||
},
|
||||
"ms": 504,
|
||||
"gets": 15,
|
||||
"requests": [
|
||||
"200 GET /projects",
|
||||
"200 GET /projects/18/views",
|
||||
"200 GET /projects/18/views/72/buckets",
|
||||
"200 GET /projects/20/views",
|
||||
"200 GET /projects/20/views/80/buckets",
|
||||
"200 GET /projects/21/views",
|
||||
"200 GET /projects/21/views/84/buckets",
|
||||
"200 GET /projects/19/views/76/buckets/tasks",
|
||||
"200 GET /projects/19/tasks",
|
||||
"200 GET /projects/18/tasks",
|
||||
"200 GET /projects/18/tasks",
|
||||
"200 GET /projects/20/tasks",
|
||||
"200 GET /projects/20/tasks",
|
||||
"200 GET /projects/21/tasks",
|
||||
"200 GET /projects/21/tasks"
|
||||
]
|
||||
},
|
||||
"tree": [
|
||||
{
|
||||
"project": 19,
|
||||
"view": 76
|
||||
},
|
||||
{
|
||||
"project": 18,
|
||||
"view": "watch-only"
|
||||
},
|
||||
{
|
||||
"project": 20,
|
||||
"view": "watch-only"
|
||||
},
|
||||
{
|
||||
"project": 21,
|
||||
"view": "watch-only"
|
||||
}
|
||||
],
|
||||
"tick": {
|
||||
"result": {
|
||||
"snapshots": 0,
|
||||
"events": 0
|
||||
},
|
||||
"ms": 323,
|
||||
"gets": 8,
|
||||
"requests": [
|
||||
"200 GET /projects/19/views/76/buckets/tasks",
|
||||
"200 GET /projects/19/tasks",
|
||||
"200 GET /projects/18/tasks",
|
||||
"200 GET /projects/18/tasks",
|
||||
"200 GET /projects/20/tasks",
|
||||
"200 GET /projects/20/tasks",
|
||||
"200 GET /projects/21/tasks",
|
||||
"200 GET /projects/21/tasks"
|
||||
]
|
||||
},
|
||||
"current": {
|
||||
"18": {
|
||||
"tasks": 4,
|
||||
"watchOnly": 4,
|
||||
"done": 0
|
||||
},
|
||||
"19": {
|
||||
"tasks": 30,
|
||||
"watchOnly": 0,
|
||||
"done": 13
|
||||
},
|
||||
"20": {
|
||||
"tasks": 3,
|
||||
"watchOnly": 3,
|
||||
"done": 0
|
||||
},
|
||||
"21": {
|
||||
"tasks": 5,
|
||||
"watchOnly": 5,
|
||||
"done": 4
|
||||
}
|
||||
},
|
||||
"open": 25,
|
||||
"deletedViewProbe": {
|
||||
"child": 18,
|
||||
"view": 2147483647,
|
||||
"board": {
|
||||
"status": 404,
|
||||
"code": 3014
|
||||
},
|
||||
"project": {
|
||||
"status": 200
|
||||
}
|
||||
},
|
||||
"writes": 0
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Sage, 2026-10-11: row 62 gate, "one live read with descendants on, root #19, read-only". Runs the
|
||||
// candidate's own discover(), reconcile() and tick() against tasks.woltje.com with the sync key only,
|
||||
// a real Store and Broker in a scratch data root. startup() is not run: its scope probes send a PATCH,
|
||||
// a DELETE and a POST as role keys. This builds the root's view from the same two reads startup's
|
||||
// install check makes, and checks it the same way. The fetch guard refuses anything but GET, so no
|
||||
// write leaves this process. Also Darkwing's probe for leftTree(): a child's missing kanban view next
|
||||
// to the child project read. Records methods, paths, statuses, counts and ids; no titles, no token.
|
||||
// The business and tracker come from the candidate's bootConfig(), the host config's path to the bus.
|
||||
// Usage: node tick-62.mjs SRC_ROOT https://tasks.woltje.com BUSINESS_ID OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync, mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
const [SRC, ORIGIN, BIZ_ID, OUT] = process.argv.slice(2);
|
||||
if (ORIGIN !== 'https://tasks.woltje.com') throw new Error('tasks.woltje.com only');
|
||||
if (!OUT || existsSync(OUT)) throw new Error('OUT_JSON missing or exists; refusing');
|
||||
const load = (p) => import(join(SRC, p));
|
||||
const { Store } = await load('packages/bus/src/store.mjs');
|
||||
const { Broker } = await load('packages/bus/src/broker.mjs');
|
||||
const { Credentials } = await load('packages/bus/src/credentials.mjs');
|
||||
const { client, all } = await load('packages/tasks/src/vikunja.mjs');
|
||||
const { BUCKETS } = await load('packages/tasks/src/startup.mjs');
|
||||
const { tick, reconcile, WATCH_ONLY } = await load('packages/tasks/src/sync.mjs');
|
||||
const { loadSystem, bootConfig } = await load('packages/cli/src/config.mjs');
|
||||
|
||||
const ROOT = 19, MISSING = 2147483647;
|
||||
const boot = bootConfig({ system: loadSystem(), businessId: BIZ_ID });
|
||||
const name = BIZ_ID;
|
||||
const b = boot.businesses[name];
|
||||
const tracker = boot.trackers?.[name] ?? null;
|
||||
if (tracker && (tracker.baseUrl !== ORIGIN || tracker.project !== ROOT)) throw new Error('host tracker config is not #19 on ' + ORIGIN);
|
||||
const syncRef = b.tracker.sync.credentials.vikunja;
|
||||
const requests = [];
|
||||
const guarded = async (url, init = {}) => {
|
||||
const method = init.method ?? 'GET';
|
||||
const u = new URL(url);
|
||||
if (method !== 'GET' || u.origin !== ORIGIN) throw new Error(`refused ${method} ${u.origin}`);
|
||||
const r = await fetch(url, { ...init, redirect: 'error' });
|
||||
requests.push({ method, path: u.pathname.replace(/^\/api\/v2/, ''), status: r.status });
|
||||
return r;
|
||||
};
|
||||
const root = mkdtempSync(join(tmpdir(), 'tick-62-'));
|
||||
const credentials = new Credentials({ references: { [`${name}/@sync`]: { vikunja: syncRef } }, dataRoot: root, env: {} });
|
||||
const store = new Store(root);
|
||||
const broker = new Broker({ store, businesses: { [name]: b }, secretCheck: (v) => credentials.assertClean(v) });
|
||||
const call = client({ baseUrl: ORIGIN, fetch: guarded });
|
||||
const ctx = {
|
||||
business: name, project: ROOT, descendants: true, broker, credentials, clock: Date.now, call, per: 50,
|
||||
view: null, tree: null, bots: new Set([b.tracker.sync.botId, ...Object.values(b.roles).map((r) => r.tracker?.botId).filter(Boolean)]),
|
||||
sync: (method, path, opts) => credentials.use(`${name}/@sync`, 'vikunja', (token) => call(token, method, path, opts)),
|
||||
lastStart: null, lastReconcile: null, lastComment: new Map(), commentsAt: new Map(), commentCount: new Map(),
|
||||
};
|
||||
ctx.syncCall = (_t, method, path, opts) => ctx.sync(method, path, opts);
|
||||
const rec = { at: new Date().toISOString(), src: SRC, key: 'sync (bot 10)', root: ROOT, hostTracker: tracker && { project: tracker.project, descendants: tracker.descendants ?? null } };
|
||||
try {
|
||||
const info = await ctx.call(null, 'GET', '/info');
|
||||
rec.version = info.json?.version;
|
||||
// The root's install check, as startup.mjs inspect() makes it.
|
||||
const views = await all(ctx.syncCall, null, `/projects/${ROOT}/views`, {}, ctx.per);
|
||||
const kanban = views.items.filter((x) => x?.view_kind === 'kanban');
|
||||
if (kanban.length !== 1 || kanban[0].bucket_configuration_mode !== 'manual') throw new Error('root kanban check');
|
||||
const k = kanban[0];
|
||||
const buckets = await all(ctx.syncCall, null, `/projects/${ROOT}/views/${k.id}/buckets`, {}, ctx.per);
|
||||
const ids = {};
|
||||
for (const t of BUCKETS) {
|
||||
const hits = buckets.items.filter((x) => x?.title === t);
|
||||
if (hits.length !== 1) throw new Error('root bucket check ' + t);
|
||||
ids[t] = hits[0].id;
|
||||
}
|
||||
if (k.done_bucket_id !== ids.done || k.default_bucket_id !== ids.todo) throw new Error('root done/default check');
|
||||
ctx.view = { id: k.id, done: ids.done, todo: ids.todo, ids, titles: Object.fromEntries(Object.entries(ids).map(([t, id]) => [id, t])) };
|
||||
ctx.tree = new Map([[ROOT, ctx.view]]);
|
||||
rec.rootView = { id: k.id, buckets: ids };
|
||||
const step = async (label, fn) => {
|
||||
const from = requests.length;
|
||||
const t0 = Date.now();
|
||||
const result = await fn();
|
||||
rec[label] = { result, ms: Date.now() - t0, gets: requests.length - from, requests: requests.slice(from).map((r) => `${r.status} ${r.method} ${r.path}`) };
|
||||
};
|
||||
await step('reconcile', () => reconcile(ctx));
|
||||
rec.tree = [...ctx.tree].map(([p, v]) => ({ project: p, view: v ? v.id : 'watch-only' }));
|
||||
await step('tick', () => tick(ctx));
|
||||
const current = broker.taskView(name, 'current');
|
||||
const byProject = {};
|
||||
for (const x of current) {
|
||||
const p = x.task_ref.split(':')[1].split('/')[0];
|
||||
byProject[p] ??= { tasks: 0, watchOnly: 0, done: 0 };
|
||||
byProject[p].tasks++;
|
||||
if (x.fields.bucket === WATCH_ONLY) byProject[p].watchOnly++;
|
||||
if (x.fields.done) byProject[p].done++;
|
||||
}
|
||||
rec.current = byProject;
|
||||
rec.open = broker.taskView(name, 'open').length;
|
||||
// leftTree(): what a child's board read gets when its kanban view is gone, next to the project read.
|
||||
const child = 18;
|
||||
const gone = await ctx.sync('GET', `/projects/${child}/views/${MISSING}/buckets/tasks`, { query: { filter: 'done = false', per_page: 50, page: 1 } });
|
||||
const proj = await ctx.sync('GET', `/projects/${child}`);
|
||||
rec.deletedViewProbe = { child, view: MISSING, board: { status: gone.status, code: gone.json?.code ?? null }, project: { status: proj.status } };
|
||||
rec.writes = requests.filter((r) => r.method !== 'GET').length;
|
||||
} catch (e) {
|
||||
rec.error = String(e?.code ?? e?.message ?? e);
|
||||
} finally {
|
||||
store.close();
|
||||
credentials.close();
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
const out = JSON.stringify(rec, null, 1) + '\n';
|
||||
const token = readFileSync(syncRef.file, 'utf8').trim();
|
||||
if (!token || out.includes(token) || /bearer/i.test(out)) throw new Error('secret check failed; not written');
|
||||
writeFileSync(OUT, out, { flag: 'wx' });
|
||||
process.stdout.write(out);
|
||||
process.exitCode = rec.error ? 1 : 0;
|
||||
Reference in New Issue
Block a user