feat(tasks): the adapter reads a project tree (#1555, row 62)

With tracker.descendants on, the tracker project is the root of a tree.
Each reconcile reads the sync bot's project list (archived projects and
negative pseudo-project ids left out) and walks it by parent_project_id.
A child that passes the install checks is read through its board, and
one that doesn't is watch-only (bucket 0) and never refuses the
business. A tick reads each project in the tree. A child answering 403
or 404 leaves the tree only if GET /projects/<id> then answers 403 or
404 too, and its open tasks get the one-by-one read. Task refs carry
the project, so a task moved inside the tree shows under its new ref
and the old ref gets its tombstone. tracker.descendants reaches the adapter
through the business var and packages/cli/src/config.mjs (lead decision
87). Off, the adapter reads as v0 did.

Darkwing built it (agents/darkwing/work/queue-62), in three rounds.
Round 3 was approved by Filbert (27317) and Dewey (27338) on manifest
8b81e836, 13 paths. Their non-blocking notes and Darkwing's follow-ups
(two surviving mutants in test 18, the fake's view-404 code) go to a
follow-up row.

Sage's gate on 918a4a19, sequential and teed: node suites tasks 71,
business 60, bus 80, cli 110 and queue 150, 0 failed; webui 39 of 40,
then 40 of 40 on three reruns (row 62 touches no webui file; a timing
test under load 17 with Dewey's gate running beside it); test-auth 15,
test-conductor 17, test-config 24, test-discord 66,
test-extension-package 18, test-foundation 44, test-queue 27,
test-release 14, test-task 98, all 0 failed (Docker on). edc5de71..HEAD
touches none of the 13 paths.

Live read, 2026-10-11T21:28:03Z, tasks.woltje.com v2.7.0, root #19,
descendants on, sync key only, GETs only (the startup scope probes not
sent): reconcile 15 GETs, 42 snapshots; tick 8 GETs, 0 snapshots.
Children 18, 20 and 21 are watch-only. A missing view id on #18 answers
404 (code 3014) while #18 reads 200, as the README says. Record:
agents/sage/work/vikunja-setup/2026-10-11_tick-62.json.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-11 16:29:13 -05:00
co-authored by Claude Opus 5.5
parent 45fe40f759
commit 390d437ba2
13 changed files with 864 additions and 73 deletions
+6 -2
View File
@@ -23,7 +23,11 @@ test("trackers come from the tracker.* variables of the one project that names a
const f = fixture(root);
writeJson(join(root, "project", ".mosaic", "project.json"), { projectVersion: 1, id: "stack", vars: { "tracker.project": 12 } });
const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env });
assert.deepEqual(boot.trackers, { acme: { baseUrl: "http://127.0.0.1:3456", project: 12, pollSeconds: 60, reconcileMinutes: 60 } });
assert.deepEqual(boot.trackers, { acme: { baseUrl: "http://127.0.0.1:3456", project: 12, pollSeconds: 60, reconcileMinutes: 60, descendants: false } });
// Decision 87: the project's tracker.descendants reaches the adapter's boot entry.
writeJson(join(root, "project", ".mosaic", "project.json"), { projectVersion: 1, id: "stack", vars: { "tracker.project": 12, "tracker.descendants": true } });
const tree = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env });
assert.equal(tree.trackers.acme.descendants, true);
});
test("with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict", (t) => {
@@ -35,7 +39,7 @@ test("with two projects, the one that sets tracker.project supplies the tracker
writeJson(join(root, "project", ".mosaic", "project.json"), { projectVersion: 1, id: "stack", vars: { "tracker.project": 12 } });
writeJson(join(root, "web", ".mosaic", "project.json"), { projectVersion: 1, id: "web", vars: {} });
const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env });
assert.deepEqual(boot.trackers, { acme: { baseUrl: "http://127.0.0.1:3456", project: 12, pollSeconds: 60, reconcileMinutes: 60 } });
assert.deepEqual(boot.trackers, { acme: { baseUrl: "http://127.0.0.1:3456", project: 12, pollSeconds: 60, reconcileMinutes: 60, descendants: false } });
});
test("two projects that each name a tracker project refuse, since the boot shape holds one", (t) => {
+1 -1
View File
@@ -41,7 +41,7 @@ test("bootConfig trackers reach the S3 adapter in the real broker child, which g
mkdirSync(f.dataRoot, { recursive: true, mode: 0o700 });
const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env });
assert.deepEqual(boot.trackers, { acme: { baseUrl: srv.url, project, pollSeconds: 60, reconcileMinutes: 60 } });
assert.deepEqual(boot.trackers, { acme: { baseUrl: srv.url, project, pollSeconds: 60, reconcileMinutes: 60, descendants: false } });
const host = await startHost({ boot, business: "acme", log: () => {} });
t.after(() => host.close(0));