row 62 (#1555) round 3 packet: confirm read before a child drops, candidate 8b81e836

R2/B1: after a forbidden or not-found refusal on a child, the tick reads
GET /projects/<id> and drops the child only on 403 or 404; a deleted
kanban view (project read 200) fails the tick as in round 1. Tests 18
and 19, 23/23 mutants killed, gate on ae428a54 green (14 suites).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-11 15:54:19 -05:00
co-authored by Claude Opus 5.5
parent 8584e84cfd
commit 90e568624f
22 changed files with 1167 additions and 25 deletions
+179
View File
@@ -413,3 +413,182 @@ A `gate_default` Docker network appeared during the run with no
containers, and I removed it.
Candidate manifest sha256: `55316d885d51e0d089173476dbccacfdcf254e51c37aecff0f556ae80ca99179`.
## Round 3
Round 2 drew changes requested from both reviewers on candidate
`55316d88`: Filbert's comment 27296 (queue rev 426) and Dewey's comment
27297 (rev 427). Both found the same blocking defect, Filbert's R2 and
Dewey's B1. Both confirm R1 is fixed, Dewey on the poll timer path too.
`build.patch` and `candidate-manifest.sha256` now hold the round 3
candidate; the round 2 versions are in this packet's history at
`620c0e89`. `files.txt` is unchanged: 13
files, +864/−73 against `edc5de71`. Round 3 changed four of them:
`sync.mjs`, `fake.mjs`, `tree.test.mjs` and the tasks `README.md`. The
gate applied the patch to `ae428a54`; no candidate file changed between
`edc5de71` and `ae428a54`.
### Blocking
**R2 (Filbert) and B1 (Dewey): a deleted kanban view looked like a
deleted project.** `outcome()` in `vikunja.mjs` maps every 404 except
code 4002 to `missing`, which is `tracker-not-found`. A board read on a
child whose kanban view was deleted answers 404 with code 13001, so the
round 2 tick dropped a child the sync bot still reads. `missing()` then
wrote `no-access` tombstones for its open tasks, and the next reconcile
read them back watch-only. Round 1 failed that tick. Round 2 wrote false
evidence to the ledger, which is worse.
I took Filbert's first fix. `leftTree()` is now async. After a
`tracker-forbidden` or `tracker-not-found` refusal on a child, the tick
reads `GET /projects/<id>` with the sync token and drops the child only
if that read answers 403 or 404. Any other answer rethrows the board or
walk refusal, so the tick fails as in round 1. Dewey's comment gives the
real instance's answers: 200 for a deleted view, 403 for a hidden
project, 404 for a deleted one, 200 for an archived one. The sync token
already has `projects read_one`, so nothing changes in the scopes.
I chose the confirm read over Filbert's second option, dropping only on
403 and on 404 code 3001. That option depends on which code each route
returns, and I haven't probed which one the board route gives for a
deleted project. The confirm read asks the question the drop depends
on. It costs one GET, and only on a tick where
a child refused.
The tradeoff: a child with a deleted kanban view fails every tick until
the next reconcile, as in round 1, and the poll timer's `notify` skips
while it does. The reconcile's `inspect()` finds no kanban view and
reads the child watch-only, or reads the new view if someone installed
one. I didn't make the tick read it watch-only on the spot. That would
put the reconcile's install check into the tick, and a deleted view is
an operator change the reconcile already handles.
`fake.mjs` gained `deleteView()`. It drops the view, its buckets and the
tasks' places in them. The project still reads, and a read of the view
answers 404 with code 13001, the code the fake's view routes already
return.
New test 18, "a child whose kanban view is deleted still refuses the
tick, and the reconcile reads it watch-only":
- An installed child's kanban view is deleted, and the root has an edit.
The tick reads `GET /projects/<child>`, fails with
`tracker-not-found`, and writes no event or snapshot, the root's edit
included. The business stays ready.
- A 403 on the board of a child the bot still reads fails the tick with
`tracker-forbidden`.
- The reconcile records nothing missing, gives the child's task bucket 0
and records the root's edit.
- Dewey's 401 case: a 401 on the child's task list after the root read
succeeded fails the tick with `tracker-unauthorized`, and the tick
makes no project read. A 404 queued on the project read stays unused,
so a 401 can't drop a child whatever that read would say.
- A 404 on the task list with a 503 on the project read fails the tick
with `tracker-not-found`, and the child stays. The next tick records
an edit on it.
Dewey asked that a root edit in the same tick be recorded. On the drop
path, test 10 already does that: a deleted child and a hidden one drop,
and the root's and a sibling's edits from that tick are recorded. On
the deleted-view path the tick fails closed, so it records nothing, and
test 18 shows the reconcile recording the root's edit instead. I didn't
take the watch-only path, so Dewey's second ask doesn't apply.
The README paragraph now says a child leaves the tree "only if `GET
/projects/<id>` then answers 403 or 404 too", that a deleted kanban view
keeps the child and fails the tick until the next reconcile reads it
again ("watch-only if it has no installed kanban view"), and that a
dropped child the sync bot can read again comes back at the next
reconcile.
### Non-blocking
**Filbert's N1: a dropped child comes back after a reconcile.** New
test 19. A child moved under a project the bot can't read drops at the
tick with `no-access`. Moved back under the root, the next tick still
sends no request under it, and the reconcile records the task again
with the child's installed bucket. The task's snapshots read bucket,
`no-access`, bucket. A tick after that records an edit. Filbert's probe
used a one-time fault on the board; with the confirm read a fault alone
no longer drops a child, so the test moves the project instead. The
test passes on round 2's code as well. It pins the return, it doesn't
fix anything.
**Dewey's `r2-unauthorized-drops`.** Killed now, by test 18's 401 case.
With the confirm read the mutant only matters when the project read
answers 403 or 404 after a 401, which is the case the test sets up.
**Unchanged survivors.** Filbert's round 1 `keep-negative`,
`no-parent-shape` and `no-sort`, and the two equivalent mutants, still
survive. They were non-blocking in round 1 and round 2, and I didn't
take them this round.
### Tests, round 3
`packages/tasks/tests/tree.test.mjs` has 19 tests (17 in round 2). Tests
18 and 19 are new and sit at the end, so round 2's numbers hold. Test 18
fails on round 2's `sync.mjs`; test 19 passes on it, as said above.
### Mutants, round 3
`out/mutants-round3.sh` uses the same method: edit one source file, run
the test file meant to fail it, restore the file from the worktree
index, which holds the round 3 candidate. The round 2 lines that edited
`leftTree()` are retargeted at the new function. Output is
`out/mutants-round3.txt`. All 23 fail at least one test:
| Mutant | Fails |
|---|---|
| round 1's five and round 2's seven non-R1 mutants | as in round 2; `no-watch-only-bucket` also fails 18 |
| `r2-child-refuses` | 9, 10, 18, 19 |
| `r2-skip-keeps-child` | 10, 19 |
| `r2-root-drops` | 10 |
| `r2-any-error-drops` | 18 |
| `r2-403-only` | 10, 18 |
| `r2-404-only` | 9, 10, 19 |
| `r3-no-confirm`: drop on the refusal alone, no project read | 18 |
| `r3-confirm-403-only`: drop only when the project read answers 403 | 10 |
| `r3-confirm-404-only`: drop only when it answers 404 | 9, 10, 19 |
| `r3-confirm-not-200`: drop on any answer but 200 | 18 |
| `r2-unauthorized-drops` (Dewey's name): a 401 drops a child too | 18 |
`r2-any-error-drops` failed only test 10 in round 2; now test 18 fails
it, because test 10's 503 sibling no longer reaches a drop: the project
read answers 200.
### Gate, round 3
The gate ran in a detached worktree of `ae428a54` with the round 3
`build.patch` applied and `node_modules` linked
(`out/gate-round3/gate.sh`). `out/gate-round3/apply.txt` has the
manifest check. The script now runs the webui suite too; round 2's
didn't. test-task ran with `DOCKER_HOST=unix:///nonexistent`, so its
Docker groups printed skip, as before. Each log is in
`out/gate-round3/`.
Every suite passed (`out/gate-round3/summary.txt`):
| Suite | rc | Result |
|---|---|---|
| `packages/tasks` | 0 | 71 passed, 0 failed (69 in round 2) |
| `packages/business` | 0 | 60 passed, 0 failed |
| `packages/bus` | 0 | 80 passed, 0 failed |
| `packages/cli` | 0 | 83 passed, 0 failed |
| `packages/webui` | 0 | 40 passed, 0 failed |
| test-auth | 0 | 15 passed, 0 failed |
| test-conductor | 0 | 17 passed, 0 failed |
| test-config | 0 | 24 passed, 0 failed |
| test-discord | 0 | 66 passed, 0 failed |
| test-extension-package | 0 | 18 passed, 0 failed |
| test-foundation | 0 | 44 passed, 0 failed |
| test-queue | 0 | 27 passed, 0 failed |
| test-release | 0 | 14 passed, 0 failed |
| test-task | 0 | 26 passed, 0 failed |
The bus count went from 74 to 80 because rows landed on the base
between the two gates, not because of this candidate. A `gate_default`
Docker network with no containers was left after the run, and I removed
it.
Candidate manifest sha256: `8b81e836c9d1363383dac8900dd881056d27e663a9c4aa32d2d1ee7639cf18ca`.
+107 -21
View File
@@ -107,7 +107,7 @@ index b7710013..d7cef73a 100644
t.after(() => host.close(0));
diff --git a/packages/tasks/README.md b/packages/tasks/README.md
index 1b764e82..2417ac48 100644
index 1b764e82..61b36664 100644
--- a/packages/tasks/README.md
+++ b/packages/tasks/README.md
@@ -1,7 +1,7 @@
@@ -149,7 +149,7 @@ index 1b764e82..2417ac48 100644
A startup refused with `tracker-unavailable`, `tracker-rate-limited` or
`service-failed` is tried again at the next poll. Any other refusal
@@ -239,6 +242,90 @@ mtime moved.
@@ -239,6 +242,95 @@ mtime moved.
`close()` stops the timers and waits for the queue. A verb still queued
at that point refuses with `tracker-closed`.
@@ -217,9 +217,14 @@ index 1b764e82..2417ac48 100644
+child, or one moved out with a share of its own, until that reconcile.
+A child that answers 403 or 404 in a tick (no longer shared, moved under
+a project the sync bot can't read, or deleted) leaves the tree at that
+tick, and its tasks get the same one-by-one read. A 403 or 404 on the
+root still fails the tick, and so does any other failure on a child. A
+new child joins at the next reconcile.
+tick only if `GET /projects/<id>` then answers 403 or 404 too, and its
+tasks get the same one-by-one read. A deleted kanban view answers 404
+while its project still reads 200, so the child stays and the tick fails
+until the next reconcile reads the child again, watch-only if it has no
+installed kanban view. A 403 or 404 on the root
+still fails the tick, and so does any other failure on a child. A new
+child joins at the next reconcile, and so does a dropped one the sync
+bot can read again.
+
+Each tick reads per project. An installed project costs the board and
+the cursor walk, a watch-only one its open task list and the cursor
@@ -240,7 +245,7 @@ index 1b764e82..2417ac48 100644
## Refusals
| Code | Meaning |
@@ -277,6 +364,15 @@ every user or task any recording held, so a key one route adds, like
@@ -277,6 +369,15 @@ every user or task any recording held, so a key one route adds, like
The shape test catches a fake that invents a field, not one that puts a
real field on the wrong route.
@@ -315,7 +320,7 @@ index d564c645..f0f8cacf 100644
sync: use('@sync'),
as: (role, method, path, opts) => use(role)(method, path, opts),
diff --git a/packages/tasks/src/fake.mjs b/packages/tasks/src/fake.mjs
index fc55529a..30d9179e 100644
index fc55529a..633224db 100644
--- a/packages/tasks/src/fake.mjs
+++ b/packages/tasks/src/fake.mjs
@@ -6,9 +6,13 @@ import { NULL_DATE } from './digest.mjs';
@@ -352,7 +357,7 @@ index fc55529a..30d9179e 100644
for (const [kind, position] of [
['list', 100],
['gantt', 200],
@@ -115,6 +120,27 @@ export class FakeVikunja {
@@ -115,6 +120,34 @@ export class FakeVikunja {
share(project, user, permission = 0) {
this.#projects.get(project).shares.set(user, permission);
}
@@ -373,6 +378,13 @@ index fc55529a..30d9179e 100644
+ for (const t of [...this.#tasks.values()]) if (gone.includes(t.project)) this.#tasks.delete(t.id);
+ for (const id of gone) this.#projects.delete(id);
+ }
+ // Drops a view, its buckets and the tasks' places in them. The project still reads, and a read
+ // of the view answers 404 with code 13001.
+ deleteView(view) {
+ for (const [id, b] of this.#buckets) if (b.view === view) this.#buckets.delete(id);
+ for (const t of this.#tasks.values()) t.buckets.delete(view);
+ this.#views.delete(view);
+ }
+ // A saved filter or Favorites: a negative id in the project list, as Project.ReadAll appends.
+ pseudoProject(user, id = -1, title = 'Favorites') {
+ this.#pseudo.push({ id, title, user });
@@ -380,7 +392,7 @@ index fc55529a..30d9179e 100644
bucket(view, title) {
const id = ++this.#seq.bucket;
this.#buckets.set(id, { id, title, view, position: id * 100 });
@@ -231,11 +257,39 @@ export class FakeVikunja {
@@ -231,11 +264,39 @@ export class FakeVikunja {
});
return id;
}
@@ -424,7 +436,7 @@ index fc55529a..30d9179e 100644
}
#userJson(id) {
const u = this.#users.get(id);
@@ -378,7 +432,15 @@ export class FakeVikunja {
@@ -378,7 +439,15 @@ export class FakeVikunja {
_project(user, [p]) {
const r = this.#projectFor(user, p);
if (r.error) return r.error;
@@ -516,7 +528,7 @@ index 9a2c5bc5..f3bdc3d5 100644
+ return tree;
+}
diff --git a/packages/tasks/src/sync.mjs b/packages/tasks/src/sync.mjs
index 811fb683..0afd1c49 100644
index 811fb683..852d2b2c 100644
--- a/packages/tasks/src/sync.mjs
+++ b/packages/tasks/src/sync.mjs
@@ -1,10 +1,12 @@
@@ -626,15 +638,20 @@ index 811fb683..0afd1c49 100644
else continue; // still open in the project: the board read raced a move, the next tick sees it
} else if (outcome(x) === 'not-found' || x.status === 403) {
updated = c.updated;
@@ -147,39 +165,58 @@ async function missing(ctx, current, refs, out) {
@@ -147,39 +165,63 @@ async function missing(ctx, current, refs, out) {
const record = (ctx, out) => {
if (out.snapshots.length || out.events.length) ctx.broker.recordTask({ business: ctx.business, ...out });
};
+// A child that answers 403 or 404 left the tree after the last reconcile: no longer shared, moved
+// under a project the bot can't read, or deleted. The tick drops it from ctx.tree, and missing()
+// reads its open tasks one by one, as after a reconcile. The root still refuses.
+const leftTree = (ctx, project, e) =>
+ project !== ctx.project && e instanceof BusError && (e.code === 'tracker-forbidden' || e.code === 'tracker-not-found');
+// A child that answers 403 or 404 may have left the tree after the last reconcile: no longer
+// shared, moved under a project the bot can't read, or deleted. A 404 can also be the child's kanban
+// view deleted while the project still reads, so the tick reads the project itself and drops the
+// child only on a 403 or 404 there. Then missing() reads its open tasks one by one, as after a
+// reconcile. Anything else fails the tick, and so does a 403 or 404 on the root.
+async function leftTree(ctx, project, e) {
+ if (project === ctx.project || !(e instanceof BusError) || (e.code !== 'tracker-forbidden' && e.code !== 'tracker-not-found')) return false;
+ const r = await ctx.sync('GET', `/projects/${project}`);
+ return r.status === 403 || r.status === 404;
+}
export async function tick(ctx) {
const startMs = ctx.clock();
const readAt = iso(startMs);
@@ -648,7 +665,7 @@ index 811fb683..0afd1c49 100644
+ const hits = new Map((await walk(ctx, `updated > ${floorSecond(sinceMs)}`, {}, project)).map((t) => [t.id, t]));
+ reads.push({ project, view, open, hits });
+ } catch (e) {
+ if (!leftTree(ctx, project, e)) throw e;
+ if (!(await leftTree(ctx, project, e))) throw e;
+ ctx.tree.delete(project);
+ }
+ }
@@ -708,7 +725,7 @@ index 811fb683..0afd1c49 100644
await missing(ctx, current, gone, out);
record(ctx, out);
ctx.lastStart = startMs;
@@ -187,27 +224,32 @@ export async function tick(ctx) {
@@ -187,27 +229,32 @@ export async function tick(ctx) {
}
// The hourly full read, and the first read at startup. comment_count catches comments on tasks the
// cursor window missed; a changed count is checked against the comment list before it is reported.
@@ -801,10 +818,10 @@ index 3859dd22..c16f1f44 100644
for (const [name, r] of Object.entries(recorded)) {
diff --git a/packages/tasks/tests/tree.test.mjs b/packages/tasks/tests/tree.test.mjs
new file mode 100644
index 00000000..f03de85b
index 00000000..e6851468
--- /dev/null
+++ b/packages/tasks/tests/tree.test.mjs
@@ -0,0 +1,424 @@
@@ -0,0 +1,493 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { readFileSync } from 'node:fs';
@@ -1229,3 +1246,72 @@ index 00000000..f03de85b
+ assert.ok(!/\.titles\b|\.ids\[/.test(source));
+ assert.ok(!/'(PUT|POST|PATCH|DELETE)'/.test(source));
+});
+test('a child whose kanban view is deleted still refuses the tick, and the reconcile reads it watch-only', async (t) => {
+ const w = await world(t, TREE);
+ const child = w.fake.project('Child', w.owner, { parent: w.project });
+ w.fake.install(child);
+ const task = await add(w, child, 'child');
+ const rootTask = await add(w, w.project, 'root');
+ await ready(w);
+ const rows = () => w.store.get('SELECT (SELECT count(*) FROM events) + (SELECT count(*) FROM task_snapshots) n').n;
+ // The board answers 404 with code 13001, and the project still reads, so the child hasn't left
+ // the tree. The tick reads the project, fails, and writes nothing, not even the root's edit.
+ w.fake.deleteView(w.fake.kanban(child).id);
+ await w.ui('PATCH', `/tasks/${rootTask}`, { title: 'root renamed' });
+ await sleep(20);
+ const n = rows();
+ const reads = await counted(w, () => assert.rejects(w.adapter.tick('demo'), { code: 'tracker-not-found' }));
+ assert.ok(reads.includes(`GET /projects/${child}`), reads.join('\n'));
+ assert.equal(rows(), n);
+ assert.equal(state(w).state, 'ready');
+ // A 403 on a child the bot still reads fails the tick too, and the child stays.
+ w.fake.fault('GET', new RegExp(`^/projects/${child}/views/\\d+/buckets/tasks$`), 403);
+ await assert.rejects(w.adapter.tick('demo'), { code: 'tracker-forbidden' });
+ // The reconcile finds no kanban view and reads the child watch-only. Nothing is missing.
+ await w.adapter.reconcile('demo');
+ assert.deepEqual(missing(w), []);
+ assert.equal(w.snapshots(ref(child, task)).at(-1).fields.bucket, WATCH_ONLY);
+ assert.equal(w.snapshots(ref(w.project, rootTask)).at(-1).fields.title, 'root renamed');
+ // A 401 on a child read after the root read succeeds fails the tick, whatever the project read says.
+ w.fake.fault('GET', new RegExp(`^/projects/${child}/tasks$`), 401);
+ w.fake.fault('GET', new RegExp(`^/projects/${child}$`), 404);
+ const unauthorized = await counted(w, () => assert.rejects(w.adapter.tick('demo'), { code: 'tracker-unauthorized' }));
+ assert.ok(!unauthorized.includes(`GET /projects/${child}`), unauthorized.join('\n'));
+ // The owner's read takes the 404 the tick didn't use.
+ assert.equal((await w.ui('GET', `/projects/${child}`)).status, 404);
+ // A 404 on its task list whose project read fails as well keeps the child: the tick refuses.
+ w.fake.fault('GET', new RegExp(`^/projects/${child}/tasks$`), 404);
+ w.fake.fault('GET', new RegExp(`^/projects/${child}$`), 503);
+ await assert.rejects(w.adapter.tick('demo'), { code: 'tracker-not-found' });
+ await w.ui('PATCH', `/tasks/${task}`, { title: 'child renamed' });
+ await sleep(20);
+ await w.adapter.tick('demo');
+ assert.equal(w.snapshots(ref(child, task)).at(-1).fields.title, 'child renamed');
+ assert.deepEqual(missing(w), []);
+});
+test('a child dropped at a tick comes back at the next reconcile once the bot reads it again', async (t) => {
+ const w = await world(t, TREE);
+ const child = w.fake.project('Child', w.owner, { parent: w.project });
+ w.fake.install(child);
+ const launchpad = w.fake.project('Launchpad', w.owner);
+ const task = await add(w, child, 'child');
+ await ready(w);
+ const bucket = w.snapshots(ref(child, task)).at(-1).fields.bucket;
+ w.fake.reparent(child, launchpad);
+ await w.adapter.tick('demo');
+ assert.deepEqual(missing(w), [[ref(child, task), { reason: 'no-access' }]]);
+ // Back under the root: the tick still leaves it out, and the reconcile finds it again.
+ w.fake.reparent(child, w.project);
+ const reads = await counted(w, () => w.adapter.tick('demo'));
+ assert.ok(!reads.some((r) => r.includes(`/projects/${child}`)), reads.join('\n'));
+ await w.adapter.reconcile('demo');
+ assert.deepEqual(
+ w.snapshots(ref(child, task)).map((x) => x.fields.gone ?? x.fields.bucket),
+ [bucket, 'no-access', bucket],
+ );
+ assert.ok(open(w).some((r) => r.task_ref === ref(child, task)));
+ await w.ui('PATCH', `/tasks/${task}`, { title: 'renamed' });
+ await sleep(20);
+ await w.adapter.tick('demo');
+ assert.equal(w.snapshots(ref(child, task)).at(-1).fields.title, 'renamed');
+});
@@ -4,10 +4,10 @@ fcc95afc418823416c87475de0f6fcad33d312588d22e9d6fd5784d9e2478f0e packages/cli/s
1737237bbf062f83eeffeb7fb1a604bc6676be9f9db1e6b91844587f77491a1c packages/cli/tests/config.test.mjs
4ada6f215c8fb42bc95d46121e7c61c2477f7b1b1d7120675443b91cc4574fdc packages/cli/tests/trackers-boot.test.mjs
aad24a5db15943eba6c0bf5fd93add3f4f7b265ff5868c46e405e976bc26ce22 packages/tasks/live/README.md
0046d1f28fad5405ad2e27e1e350e20b1ab7df0b2795b5efa7bf11bb856d525b packages/tasks/README.md
552fe0d4ca0307bfcfec7405a4d3c8cf92c41521b37487833d1e110a5ba47bd2 packages/tasks/README.md
02a802a9bb1306bf5f82b8e56ca2064c308f1b7574eebba0f47932cdea7f41d1 packages/tasks/src/adapter.mjs
7c10b7e24a34e04179465c372a2660141c2eeea8b5d954ea7f6372025f567166 packages/tasks/src/fake.mjs
f9e7a06e812b6c14091e37852721421a7bf908116403010bd6d0fdf9ab11da94 packages/tasks/src/fake.mjs
e0c754aacf7e82c2fe18fb0548674044ac949ffc94435b353e4bd7aebcf797c2 packages/tasks/src/startup.mjs
9da4ab6ff55912e9d42c589fcb970493af1f3ada3056235f1d8c72c1cc62d597 packages/tasks/src/sync.mjs
35e79dc2ac56af8047bef11a5aaf5f1d37ce3e11513f9968454053068f59ebcf packages/tasks/src/sync.mjs
e8798609ec1bf7d4901f7f94b6346b37c7ea4504ee2eb781b07c634001e3bf72 packages/tasks/tests/shapes.test.mjs
1ad138752c83159813eeb586af87d6ad0690922791d33baa28ca346538a24d85 packages/tasks/tests/tree.test.mjs
5ba9ca7229ef5e733953ce6c6ea7b49a2a2ec00bdb3b138e56686992b5944349 packages/tasks/tests/tree.test.mjs
@@ -0,0 +1,14 @@
build.patch applied to ae428a5409a9171e0879e3ed843319e68be20ff8 in a detached worktree; sha256sum -c candidate-manifest.sha256:
packages/business/src/vars.mjs: OK
packages/business/tests/vars.test.mjs: OK
packages/cli/src/config.mjs: OK
packages/cli/tests/config.test.mjs: OK
packages/cli/tests/trackers-boot.test.mjs: OK
packages/tasks/live/README.md: OK
packages/tasks/README.md: OK
packages/tasks/src/adapter.mjs: OK
packages/tasks/src/fake.mjs: OK
packages/tasks/src/startup.mjs: OK
packages/tasks/src/sync.mjs: OK
packages/tasks/tests/shapes.test.mjs: OK
packages/tasks/tests/tree.test.mjs: OK
@@ -0,0 +1,88 @@
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (139.40729ms)
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (257.236596ms)
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (259.553271ms)
✔ launch events require a human CLI capability; generic emit cannot forge authority events (159.510355ms)
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (167.654014ms)
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (153.859138ms)
✔ task action subjects and linked decision trail are complete and ordered (205.347146ms)
✔ launch binding is durable and reconnecting requires the identical trusted record (99.186941ms)
✔ business isolation includes inherited object names and cross-business message references (178.25468ms)
✔ authority never transfers between action, run, target, unresolved or replaced role holder (258.614296ms)
✔ task projection uses schema current view, skipping earlier and equal-start polls (125.996912ms)
✔ revocation permanently bars the old run from reclaiming first, including after broker restart (191.700143ms)
✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (112.281238ms)
✔ both arbiters require human resolution when their cross-role route is themselves (200.727146ms)
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (3.099106ms)
✔ only validated broker references load; returned data and exceptions cannot expose a known token (7.796972ms)
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (7.984597ms)
✔ expiry refuses use and env references never become client data (2.207694ms)
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (2.514422ms)
✔ opaque tokens shorter than 16 characters refuse before use (2.308541ms)
✔ roles lists open decisions routed to a role, and never the human inbox (212.056541ms)
✔ recent lists closed decisions of every route, newest close first, with how each closed (266.181765ms)
✔ recent stops at RECENT rows (761.397104ms)
✔ every row is what trail already gives the same session for that id (207.124514ms)
✔ a role session can't list decisions, so it can't read another role's open decisions (369.694952ms)
✔ decisions is a read: one business, strict arguments, nothing written (328.184919ms)
✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (125.858017ms)
✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (124.321937ms)
✔ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (162.843817ms)
✔ endLaunch leaves a claim another run took alone (164.340421ms)
✔ refuse records action.refused against the caller with the code only (118.31943ms)
✔ launches off refuses role.launch with launch-revoked until launches on (159.360604ms)
✔ broker process: launch ops authorize role.launch, record refusals and end runs (295.387514ms)
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (4.291798ms)
✔ process reader gets own kernel identity without exposing environment values (2.82281ms)
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (1.944285ms)
✔ real pid 1 remains inspectable when its environment is protected (0.594795ms)
✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (195.520236ms)
✔ missing and foreign-business citations refuse and roll back message and grant (198.11136ms)
✔ cross-role sends still need a matching resolved decision and consume it once (232.375194ms)
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (232.829569ms)
✔ startup token refusal returns safe code without value or partial listening broker (70.256924ms)
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (211.88266ms)
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (223.278283ms)
✔ trusted host registers later launches; socket clients never have a registration verb (207.416193ms)
✔ runtime excludes declared project roots even when host supplies no repoRoots (63.717325ms)
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (221.146677ms)
✔ v3b prototype refusals, views and append-only mutations (986.469947ms)
✔ gated approval authorizes once, survives store reopen, and fresh approval works (252.600614ms)
✔ another run cannot consume an approval; a failed check leaves it usable (223.984508ms)
✔ two scheduled callers have exactly one grant and one consumed refusal (149.756091ms)
✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (302.82843ms)
✔ class drift gated to cross-role refuses before consumption (197.265286ms)
✔ class drift cross-role to gated refuses before consumption (225.478891ms)
✔ class drift gated to within-role refuses before consumption (195.773781ms)
✔ class drift cross-role to within-role refuses before consumption (211.770686ms)
✔ class drift within-role to gated refuses before consumption (173.750894ms)
✔ class drift within-role to cross-role refuses before consumption (171.8992ms)
✔ message.send consumes approval and prevents a later send or authorize (184.513755ms)
✔ role.revoke consumes approval and prevents a later revoke or authorize (197.988584ms)
✔ creates private WAL store and excludes a second writer until explicit close (127.864596ms)
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (183.830121ms)
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (184.409704ms)
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (190.29294ms)
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (89.757262ms)
✔ async transactions refuse before invoking their function (91.361957ms)
✔ recordTask keeps sync reads and a role write apart (171.311345ms)
✔ read_at must be one canonical UTC format, so the projection compares strings safely (116.906377ms)
✔ a bad entry refuses the whole record (110.340684ms)
✔ taskView reads the projection for one business (126.802477ms)
✔ requestTask hands only a holder and a task verb to the handler, and records refusals (210.525822ms)
✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (409.434719ms)
✔ without an adapter the server refuses every task verb (211.148606ms)
✔ the runtime refuses an invalid adapter and closes a valid one (209.345395ms)
✔ the process loads the S3 adapter from plain-data trackers (380.301811ms)
✔ socket capability stamps launch identity; shared views use wire, no SQL client (147.944517ms)
✔ two wire claims serialize; a lost reply never automatically retries (180.449933ms)
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (101.205468ms)
✔ client preserves UTF-8 when a response divides a multibyte character (16.448807ms)
✔ committed mutation followed by dropped reply reports unknown and is never retried (136.81512ms)
ℹ tests 80
ℹ suites 0
ℹ pass 80
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2684.940204
@@ -0,0 +1,68 @@
✔ config directory and file path follow MOSAIC_CONFIG (1.892352ms)
✔ the fixture business validates and comes back frozen (7.448149ms)
✔ two instances may share a definition (2.714814ms)
✔ top-level refusals (6.648985ms)
✔ arbiters and projects (11.000594ms)
✔ role instances (4.785667ms)
✔ Vikunja bots (8.453633ms)
✔ a role without Vikunja takes no tracker block (2.884655ms)
✔ credential references match the definition's services (3.908075ms)
✔ launch (12.85075ms)
✔ loadBusiness: file checks (2.529339ms)
✔ loadBusiness: not a regular file (57.68597ms)
✔ loading writes nothing (1.332999ms)
✔ names that are Object.prototype properties don't count as declared (3.901705ms)
✔ the shipped example refuses as written and validates once filled in (1.018877ms)
✔ usage errors exit 4 (419.466235ms)
✔ validate: a good business exits 0 and prints instance digests (123.311686ms)
✔ validate: project files (516.701692ms)
✔ validate: missing files and a broken system config (326.179227ms)
✔ validate: credential reference problems exit 2 and name each one (105.211556ms)
✔ validate: a token file inside the repository is refused (109.626132ms)
✔ validate: role definitions come from MOSAIC_ROLES_DIR (297.557716ms)
✔ resolve: prints one instance's record (270.387498ms)
✔ resolve: refusals (574.42706ms)
✔ parse: exactly one of file or env, plus the service's date (4.515509ms)
✔ check: a good file has no problems (1.405764ms)
✔ check never opens the file: a write-only token passes (0.611857ms)
✔ check: file problems (1.543643ms)
✔ check: token files can't live in the repository or dataRoot, even through a linked directory (1.603361ms)
✔ check: dates and environment references (0.730713ms)
✔ path and load (3.520671ms)
✔ refusals (1.964417ms)
✔ systemVars flattens the validated config (3.083652ms)
✔ precedence: system, business, project, project role, agent (7.555264ms)
✔ limits narrow the definition and never widen it (3.744234ms)
✔ role.launch stays within-role only for the instance the launch block names (7.246078ms)
✔ limits.authority without role.launch leaves the launcher with no launch block (2.348618ms)
✔ limits.authority narrows cross-role actions too (1.800475ms)
✔ classify (1.565433ms)
✔ the record carries what the broker and launcher need (1.18933ms)
✔ digest: key order doesn't matter, any value change does (13.672733ms)
✔ refusals (3.986559ms)
✔ the four shipped version 2 roles load (5.292599ms)
✔ shipped role scopes match addendum B section 2 and the SR runbook (1.994334ms)
✔ shipped authority follows the note's table (0.957963ms)
✔ version 1 files keep loading with no authority (1.620226ms)
✔ the conductor policy isn't a role (0.416096ms)
✔ a missing role file is exit 4, a symbolic link too (0.718458ms)
✔ version 2 refusals (2.24815ms)
✔ authority: closed vocabulary, no gated-only action, no overlap (4.014697ms)
✔ credentials: Gitea scopes (1.628781ms)
✔ credentials: Vikunja scopes are a group-to-verbs map from the grantable list (2.002421ms)
✔ credentials: services (1.013075ms)
✔ contract: a non-empty regular Markdown file beside the role file (1.424078ms)
✔ every key names known layers and a merge rule (1.379188ms)
✔ unknown keys and wrong layers refuse (1.183017ms)
✔ types (2.803649ms)
✔ merge: defaults, then the most specific layer wins (0.397903ms)
✔ merge: limits only narrow, and provenance lists each source (0.54692ms)
✔ merge doesn't change its inputs (0.252618ms)
ℹ tests 60
ℹ suites 0
ℹ pass 60
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2858.004917
@@ -0,0 +1,93 @@
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (105.23188ms)
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (138.679551ms)
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (106.718911ms)
✔ decide prints a declining choice as declining (106.29729ms)
✔ an unknown outcome is reported once and never resent (85.171953ms)
✔ a decision closed before the answer arrives exits 2 and points at its trail (99.21976ms)
✔ a prefix that matches two open decisions exits 2 and resolves neither (77.14719ms)
✔ without --business a command uses the live host's business, and a stale host.json is not a host (43.297901ms)
✔ every human command refuses inside an agent run before it touches the bus (43.014633ms)
✔ usage errors exit 4; no business and no host is a usage error (56.33613ms)
✔ agents and tasks print through the broker (80.171197ms)
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.228006ms)
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (91.629133ms)
✔ trackers come from the tracker.* variables of the one project that names a tracker project (160.005116ms)
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (76.301882ms)
✔ two projects that each name a tracker project refuse, since the boot shape holds one (65.683286ms)
✔ a business without tracker.baseUrl gets no trackers entry (63.717581ms)
✔ an unknown business and a broken system config refuse with exit 3 (104.642087ms)
✔ empty views say so (1.475732ms)
✔ the trail keeps the broker's order and names a decision's task without its rows (1.687869ms)
✔ tasks print the tracker fields the snapshot carries (1.357916ms)
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (1136.072192ms)
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (307.657854ms)
✔ a second host for the same data root refuses with exit 3 while the first runs (224.128166ms)
✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1212.48658ms)
✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (358.777338ms)
✔ a broker reply with no request waiting breaks the channel and the host exits 1 (212.108397ms)
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (307.194785ms)
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (300.970521ms)
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (238.483988ms)
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (262.891905ms)
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (174.059272ms)
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (266.408419ms)
✔ watchChildren reports a child that died before it was called, and one that dies later (41.996156ms)
✔ bus stop refuses to signal a live pid that is not a bus host (204.42459ms)
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (401.968574ms)
✔ bus start refuses with exit 3 without a notifier config (141.31995ms)
✔ bus start runs until bus stop; status reports it while it runs (892.70028ms)
✔ bus-service.sh renders the unit and installs it into a given directory (59.396981ms)
✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1387.430243ms)
✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (718.61757ms)
✔ a runner that stops at once ends its launch with the runner's reason (257.791359ms)
✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (789.36371ms)
✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3886.419518ms)
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (147.872622ms)
✔ an over-long launch request is refused at once, not at the 10 s idle timeout (164.283308ms)
✔ a launch client that never closes its side doesn't hold the host's close (195.796303ms)
✔ a runner that ignores SIGTERM is killed when the host closes (1289.651366ms)
✔ zoned uses the IANA zone across DST (33.053783ms)
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (114.058988ms)
✔ two blocking decisions get two DMs with different nonces (122.612504ms)
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.390491ms)
✔ a failed DM is journaled, backs off, and is retried until it lands (118.753476ms)
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (107.860744ms)
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (86.126161ms)
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (99.58961ms)
✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (199.643425ms)
✔ a restart after the second refusal does not send before that refusal's 30 min are up (153.621998ms)
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (73.527967ms)
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (58.113685ms)
✔ an inbox read failure is logged and the next poll retries (1.33976ms)
✔ no Discord id reaches the journal or the log (78.571724ms)
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (12.552555ms)
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (29.96297ms)
✔ the journal: a whole file that is one torn line truncates to empty (13.909831ms)
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.510737ms)
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (1.275492ms)
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (3.921819ms)
✔ the journal: a symlinked directory refuses and says it is a link (0.589528ms)
✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.747061ms)
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.768514ms)
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.935517ms)
✔ digest content stays within Discord's 2000 characters (0.620344ms)
✔ runLoop never overlaps ticks and stops after the one in flight (111.933447ms)
task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200
task.close on a missing task answered: task-not-found; it made GET /tasks/999 404
✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (523.445472ms)
✔ the transport writes {business, verb, args} to the child and reads its JSON (75.74559ms)
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2294.649393ms)
✔ busExit and refuseInsideAgent (0.513841ms)
✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (213.092077ms)
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1131.559209ms)
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (80.809643ms)
✔ launches off and on go to the broker and change the business's launch state (115.814851ms)
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (96.429552ms)
ℹ tests 83
ℹ suites 0
ℹ pass 83
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 9070.683394
@@ -0,0 +1,14 @@
#!/bin/bash
export TMPDIR=~/darkwing-scratch/r62/tmp
G=~/darkwing-scratch/r62/gate; O=~/darkwing-scratch/r62/out3/gate; mkdir -p $O; rm -f $O/summary.txt; cd $G
# node_modules is a link to the main checkout's, as in the row 55 gate; test-discord needs node_modules/.bin/pi.
[ -x node_modules/.bin/pi ] || { echo "node_modules not linked" >> $O/summary.txt; exit 1; }
for s in tasks business bus cli webui; do
env -u NODE_TEST_CONTEXT node --test "packages/$s/tests/*.test.mjs" > $O/$s-tests.log 2>&1; echo "$s rc=$?" >> $O/summary.txt
done
for s in scripts/test-*.sh; do
n=$(basename $s .sh)
if [ $n = test-task ]; then DOCKER_HOST=unix:///nonexistent bash $s > $O/$n.log 2>&1; else bash $s > $O/$n.log 2>&1; fi
echo "$n rc=$?" >> $O/summary.txt
done
echo DONE >> $O/summary.txt
@@ -0,0 +1,15 @@
tasks rc=0
business rc=0
bus rc=0
cli rc=0
webui rc=0
test-auth rc=0
test-conductor rc=0
test-config rc=0
test-discord rc=0
test-extension-package rc=0
test-foundation rc=0
test-queue rc=0
test-release rc=0
test-task rc=0
DONE
@@ -0,0 +1,79 @@
✔ the boot config is checked before anything starts (107.638785ms)
✔ a business with no tracker entry refuses task verbs (129.877905ms)
✔ credential.expiring and .expired are recorded once per instance (260.790887ms)
✔ a token file that changes on disk records credential.changed (124.03339ms)
✔ autostart polls, reconciles and retries a startup the tracker was down for (143.403689ms)
✔ a refusal a restart must clear is not retried by the poll (120.177483ms)
✔ a poll that fires while two are queued is dropped (123.360842ms)
✔ close waits for a running verb and refuses one that has not started (224.36445ms)
✔ the bundled Vikunja is the pinned upstream image the runbook names (1.540847ms)
✔ every published port is on 127.0.0.1, and no secret is in the file (0.659894ms)
✔ the fake answers each route with the statuses and shapes Vikunja v2.7.0 sent (441.157449ms)
✔ the project list holds recorded project objects in the recorded list envelope (104.526563ms)
✔ the recorded task bodies pass the checks S3 applies to every read (1.199297ms)
✔ the client works against the fake over real HTTP with the platform fetch (289.248853ms)
✔ a correct install starts, and the first reconcile records tasks that already exist (111.834434ms)
✔ verbs refuse while a business is starting and after startup refused it (150.93965ms)
✔ startup refuses a token that can do more than its role needs (414.895639ms)
✔ startup refuses an unsupported version and flags an untested one (332.430222ms)
✔ startup refuses a board that the runbook did not install (396.676191ms)
✔ startup refuses a project the sync bot cannot read (80.484415ms)
✔ startup refuses a configured label the pm bot cannot see (130.745598ms)
✔ startup refuses an expired credential and a missing sync credential (413.505735ms)
✔ an unreachable tracker refuses with tracker-unavailable (85.340654ms)
✔ an edit in the UI is recorded once, with the fields that changed (218.54575ms)
✔ a move between open buckets is seen on the board, though updated does not change (203.197293ms)
✔ a person's comment is counted and a bot's is not (281.633404ms)
✔ the hourly reconcile catches a comment through comment_count (258.430407ms)
✔ a task closed in the UI leaves the open view with its done bucket (442.117501ms)
✔ a task that leaves the board is recorded as deleted, moved or out of reach (323.030271ms)
✔ a poll that read before a verb wrote does not overwrite the verb (387.863848ms)
✔ a tracker fault during a tick is reported and the next tick catches up (159.285291ms)
✔ a malformed answer refuses the tick with tracker-shape (130.38857ms)
✔ no token value reaches the database, the log or a refusal (242.730807ms)
✔ the first look at a task counts only comments inside the window (162.675373ms)
✔ with descendants off the adapter reads no project list and no child, as in v0 (152.002207ms)
✔ a three-level tree is read per project, and a project outside it is not (127.620195ms)
✔ a watch-only child keeps the business ready, and a failed child read refuses as transient (232.854612ms)
✔ a root without the installed buckets refuses with descendants on (110.877043ms)
✔ the project list refuses without read_all, and skips archived projects and saved filters (226.300936ms)
✔ descendants must be true or false (128.427631ms)
✔ a move inside the tree is a moved tombstone plus a snapshot under the new ref (181.310285ms)
✔ a child made after startup joins the tree at the next reconcile (177.017084ms)
✔ a child that leaves the tree has its tasks recorded missing, and the ledger keeps their history (187.508091ms)
✔ a child deleted or hidden between reconciles drops out at the next tick, and the root still refuses (365.990545ms)
✔ a reconcile that fails keeps the previous tree (182.161483ms)
✔ a closed task the cursor missed takes its own project's done bucket (126.88546ms)
✔ closed tasks read at startup take their own project's done bucket (96.751649ms)
✔ a closed task the cursor returns takes its own project's done bucket (113.591435ms)
✔ a task moved out of a child that leaves the tree at the same time is moved, not no-access (124.182045ms)
✔ a task in a bucket outside the five keeps its raw bucket id (113.907088ms)
✔ bucket 0 on a watch-only task never becomes a bucket or a state (218.661971ms)
✔ a child whose kanban view is deleted still refuses the tick, and the reconcile reads it watch-only (146.544965ms)
✔ a child dropped at a tick comes back at the next reconcile once the bot reads it again (122.13478ms)
✔ task.create needs a recorded human request and a requirement id (233.727216ms)
✔ only labels named in the business file can be written (236.25273ms)
✔ task.schedule sets and clears a due date and relations (309.068497ms)
✔ assign and reassign move the role bots and record task.assigned (348.704887ms)
✔ task.update.assigned is for the assignee and records task.state (325.161826ms)
✔ a wrong expected digest records task.conflict and writes nothing (243.155011ms)
✔ a cross-role verb needs a resolved decision, used once (466.988182ms)
✔ task.close needs a verdict; after it every verb refuses with task-done (221.357406ms)
✔ a lost answer is settled by a re-read and never retried (233.965044ms)
✔ a create whose answer is lost is reported uncertain, and the poll finds the task (183.016414ms)
✔ a task the sync bot cannot read refuses and records nothing (147.561766ms)
✔ verbs and polls for one business run one at a time (204.175318ms)
✔ a due date with milliseconds is written to the second (173.266885ms)
✔ every write landed and the final read failed: the verb succeeds and records what it wrote (129.437837ms)
✔ some writes landed and the final read failed: write-uncertain, and nothing is recorded (97.807732ms)
✔ a create whose final read fails succeeds and records task.created (105.580405ms)
✔ an edit between the last write and the final read shows as external on the next poll (114.363256ms)
✔ task.created is recorded when a later label write fails (77.997358ms)
ℹ tests 71
ℹ suites 0
ℹ pass 71
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 4020.829639
@@ -0,0 +1,17 @@
OK status with missing harness credential exits 3 and still lists accounts
OK status reports harness credential (read-only) + mosaic accounts
OK api key material never reaches output
OK oauth token material never reaches output
OK unparseable credential file exits 2
OK symlinked credential file exits 4
OK env-side credential names reported
OK env var values never reach output
OK accounts without an accounts dir reports none and creates nothing
OK accounts lists files and marks the active one
OK loose account perms flagged in listing
OK agent --auth with missing account file refuses (exit 4)
OK agent --auth with non-0600 account file refuses
OK agent --auth with invalid account name refuses
OK auth.sh without valid config refuses
selftest: 15 passed, 0 failed
@@ -0,0 +1,55 @@
Note: switching to 'ae428a5409a9171e0879e3ed843319e68be20ff8'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
Not currently on any branch.
nothing to commit, working tree clean
Note: switching to 'ae428a5409a9171e0879e3ed843319e68be20ff8'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
OK dry-run: allowed change, exit 0, nothing committed (exit 0)
OK dry-run committed nothing
OK apply: allowed change exits 0 (exit 0)
OK apply: attribution in commit subject
OK apply: target tree clean after commit
OK disallowed path refused (exit 1)
OK disallowed path: target untouched
OK syntax gate refused broken .mjs (exit 1)
OK syntax gate: target untouched
OK suite failure refused (exit 1)
OK suite failure: target reverted to clean
OK disabled policy refused (exit 2)
OK disabled policy: target untouched
OK failed run refused (exit 1)
OK failed run: target untouched
OK missing run exits 4 (exit 4)
OK invalid policy exits 2 (exit 2)
selftest: 17 passed, 0 failed
@@ -0,0 +1,26 @@
OK absent adapter defaults to pi
OK adapter mock validates (exit 0)
OK unsupported adapter exits 2 (exit 2)
OK env exports adapter
OK bootstrap creates default when absent (exit 0)
OK bootstrap wrote config file
OK bootstrap is idempotent on existing config (exit 0)
OK bootstrap did not rewrite existing config
OK validate missing config exits 3 (exit 3)
OK malformed JSON exits 2 (exit 2)
OK unsupported configVersion exits 2 (exit 2)
OK unknown top-level key exits 2 (exit 2)
OK unknown execution key exits 2 (exit 2)
OK unsupported backend exits 2 (exit 2)
OK unsupported environment exits 2 (exit 2)
OK relative dataRoot exits 2 (exit 2)
OK non-canonical dataRoot exits 2 (exit 2)
OK filesystem root dataRoot exits 2 (exit 2)
OK home directory dataRoot exits 2 (exit 2)
OK dataRoot containing config dir exits 2 (exit 2)
OK control character in provider exits 2 (exit 2)
OK symlinked config file exits 2 (exit 2)
OK env exports resolve correctly
OK failed validation modified nothing
selftest: 24 passed, 0 failed
@@ -0,0 +1,70 @@
toolchain: node v26.8.1
OK syntax: packages/discord/src/approvals.mjs
OK syntax: packages/discord/src/authorize.mjs
OK syntax: packages/discord/src/binding.mjs
OK syntax: packages/discord/src/cli.mjs
OK syntax: packages/discord/src/connector.mjs
OK syntax: packages/discord/src/context.mjs
OK syntax: packages/discord/src/engine-pi.mjs
OK syntax: packages/discord/src/errors.mjs
OK syntax: packages/discord/src/gateway.mjs
OK syntax: packages/discord/src/git.mjs
OK syntax: packages/discord/src/journal.mjs
OK syntax: packages/discord/src/notify.mjs
OK syntax: packages/discord/src/rest.mjs
OK syntax: packages/discord/src/setspark.mjs
OK syntax: packages/discord/src/tools.mjs
OK syntax: packages/discord/src/web.mjs
OK syntax: packages/discord/bin/git-credential.mjs
OK syntax: packages/discord/extension/tools.mjs
OK syntax: packages/discord/tests/approvals.test.mjs
OK syntax: packages/discord/tests/authorize.test.mjs
OK syntax: packages/discord/tests/binding.test.mjs
OK syntax: packages/discord/tests/connector.test.mjs
OK syntax: packages/discord/tests/context.test.mjs
OK syntax: packages/discord/tests/engine.test.mjs
OK syntax: packages/discord/tests/fake-pi.mjs
OK syntax: packages/discord/tests/gateway.test.mjs
OK syntax: packages/discord/tests/git.test.mjs
OK syntax: packages/discord/tests/helpers.mjs
OK syntax: packages/discord/tests/journal.test.mjs
OK syntax: packages/discord/tests/notify.test.mjs
OK syntax: packages/discord/tests/recover.test.mjs
OK syntax: packages/discord/tests/rest.test.mjs
OK syntax: packages/discord/tests/setspark.test.mjs
OK syntax: packages/discord/tests/tools.test.mjs
OK syntax: packages/discord/tests/web.test.mjs
OK syntax: packages/discord/fixtures/claim-worker.mjs
OK syntax: packages/discord/fixtures/legacy-owner-worker.mjs
OK syntax: scripts/discord.sh
OK syntax: scripts/discord-service.sh
OK packages/discord declares no dependencies
OK no bot-token-shaped string in packages/discord
OK fixture binding uses placeholder ids only
OK fixture binding validates
OK real pi with the extension exposes exactly list_dir, read_file, search and no built-in tool
OK real pi with a writable root exposes exactly the three reads plus write_file and edit_file, and writes nothing at start
OK real pi with a web key exposes the three reads plus web_fetch and web_search, and no write tool without a writable root
OK real pi with a git root exposes the reads, writes and the four git verbs, commits nothing at start, and never shows the token
OK real pi with protocol vault adds reserve_id to the git verbs
OK real pi with a setspark key exposes the reads and the eight record verbs, no counters, and never shows the key
OK real pi refuses a git key on a read-only root (fail closed)
OK real pi with the pilot flags (--no-tools) exposes no tool at all
OK real pi exits non-zero without MOSAIC_DISCORD_TOOLS: no session, no tools (fail closed)
OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
OK node --test packages/discord/tests/ (ℹ pass 178)
OK scripts/discord.sh --help exits 0
OK scripts/discord.sh check without a binding exits 4
OK scripts/discord.sh recover without a binding exits 4
OK scripts/discord.sh reload without a binding exits 4
OK scripts/discord-service.sh without a command exits 4
OK service unit renders with the repository path, a supervised run as the main process, exit 3 never retried, and reload as SIGHUP
OK service install writes the rendered unit (0644) and leaves no temp file
OK service install a second time reports unchanged
OK systemd-analyze verify accepts the rendered unit
OK service uninstall removes the unit file
OK service install with an unknown flag exits 4
OK service install with USER unset finishes and names the account for lingering
discord suite: 66 passed, 0 failed
@@ -0,0 +1,21 @@
OK initial ordinary-file install
OK installed tree matches canonical source
OK installed tree has no symlinks
OK check detects installation drift
OK sync refuses to overwrite installation drift
OK check detects an extra destination file
OK check detects an extra destination directory
OK check rejects a destination symlink
OK sync accepts a canonical source update
OK updated installation matches canonical source
scripts/test-extension-package.sh: line 14: 318682 Killed "$@" > /dev/null 2>&1
OK forced interruption kills the replacing process
OK next invocation recovers old consistent installation
OK interrupted replacement rolled back
OK sync succeeds after interruption recovery
OK unlocked stale lock file does not block
OK active lock refuses a concurrent sync
OK source symlink fails closed
OK nested second entrypoint fails closed
extension package selftest: 18 passed, 0 failed
@@ -0,0 +1,53 @@
toolchain: node v26.8.1, python 3.12.8, jsonschema 4.26.0
OK syntax: scripts/foundation-inspect.mjs
OK syntax: scripts/foundation/strict-json.mjs
OK syntax: scripts/foundation/canonical.mjs
OK syntax: scripts/foundation/resolve.mjs
OK syntax: scripts/foundation/validate-record.mjs
OK syntax: scripts/foundation/fixtures/build-fixtures.mjs
OK syntax: scripts/foundation/canonical.test.mjs
OK syntax: scripts/foundation/cli.test.mjs
OK syntax: scripts/foundation/fixtures.test.mjs
OK syntax: scripts/foundation/resolve.test.mjs
OK syntax: scripts/foundation/strict-json.test.mjs
OK syntax: scripts/foundation/verify-schema.py (ast only; no bytecode written)
OK fixture generator runs
OK checked-in fixtures/bundles equal a fresh generation
OK checked-in fixtures/raw equal a fresh generation
OK checked-in fixtures/index.json equal a fresh generation
OK checked-in demo bundles equal a fresh generation
OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
OK node --test scripts/foundation/ (ℹ pass 80)
OK differential schema oracle: PASS: differential schema oracle (finite corpus; compatibility evidence, not equivalence proof)
platform witness: strftime('%Y') for year 999 -> '999' (pinned checker refuses years 0001..0999)
node v26.8.1; corpus 1568 records (38 pinned fixtures, 478 unique bundle records, 1052 typeCase/mutation/lexical cases)
schema column: agree-valid 540, agree-invalid 991, DISAGREEMENTS 0; strict-only (parser-bound) cases: 27; unsupported-kind records not schema-assessed by the inspector: 10
profile column (schema-valid records only): profile-valid 510, profile-invalid 30
profile refusals asserted: 30 schema-agreed-valid records refused only by the strict typed-string profile (rule profile-pattern-mismatch), 12 declared by name; 73 named probes verified against declared schema/profile columns
OK oracle: zero schema-column disagreements with the pinned checker
OK oracle: strict-only profile refusals are counted and asserted
OK demo: permitted read preview exits 0 (exit 0)
OK demo: permitted file.change preview exits 0 (exit 0)
OK demo: assignment.change proposal is unresolved (exit 3) (exit 3)
OK demo: revoked registration is refused (exit 3) (exit 3)
OK demo: message is not authority (exit 3) (exit 3)
OK usage: no arguments exits 2 (exit 2)
OK io: missing file exits 4 (exit 4)
OK io: directory exits 4 (exit 4)
OK io: symlink exits 4 (O_NOFOLLOW) (exit 4)
OK bound: oversize fixture exits 2 (exit 2)
OK profile: one final LF in a typed selection id is refused before admission (exit 2) (exit 2)
OK profile: two final LFs fail the schema pattern itself (exit 2) (exit 2)
OK profile: escaped newlines in free-form text stay allowed (exit 0) (exit 0)
OK profile refusal is invalid-request/profile-pattern-mismatch with selection and operation withheld, value not echoed
OK text output starts with the disclaimer
OK json output is valid JSON with result allowed and exactly the charter §7 fields
OK json golden matches byte-for-byte
OK sandboxed bundle run (env -i, PATH=/nonexistent) produced the unresolved proposal
OK sandbox inventory (path/type/size/mode/uid/gid/inode/mtime/sha256) unchanged by runs
OK canary never printed (bundle run and credential-file run)
OK a non-bundle JSON file is refused at the shape gate, not read into output
OK no field of the non-bundle file is echoed
selftest: 44 passed, 0 failed
@@ -0,0 +1,35 @@
toolchain: node v26.8.1, git version 2.55.0
OK syntax: packages/queue/src/cli.mjs
OK syntax: packages/queue/src/errors.mjs
OK syntax: packages/queue/src/io.mjs
OK syntax: packages/queue/src/lock.mjs
OK syntax: packages/queue/src/queue.mjs
OK syntax: packages/queue/src/review.mjs
OK syntax: packages/queue/src/store.mjs
OK syntax: packages/queue/tests/commit.test.mjs
OK syntax: packages/queue/tests/data.test.mjs
OK syntax: packages/queue/tests/dispatch.test.mjs
OK syntax: packages/queue/tests/helpers.mjs
OK syntax: packages/queue/tests/lock.test.mjs
OK syntax: packages/queue/tests/migration.test.mjs
OK syntax: packages/queue/tests/review.test.mjs
OK syntax: packages/queue/tests/store.test.mjs
OK syntax: packages/queue/tests/write.test.mjs
OK syntax: packages/queue/tests/fixtures/fake-gitea.mjs
OK syntax: packages/queue/tests/fixtures/kill-at.mjs
OK syntax: packages/queue/tests/fixtures/lock-child.mjs
OK syntax: packages/queue/tests/fixtures/mosaic-pre-a2.sh
OK syntax: scripts/queue-commit.sh
OK syntax: scripts/git-hooks/pre-commit
OK syntax: scripts/mosaic
OK queue-commit.sh, the guard and scripts/mosaic are executable
OK packages/queue declares no dependencies
ℹ tests 149
ℹ pass 149
ℹ fail 0
OK node --test packages/queue/tests/
OK scripts/mosaic queue help
skip queue verify and render --check: this checkout (/home/jwoltje/darkwing-scratch/r62/gate) is not the queue's canonical root (/mnt/storage/src/mosaic-stack)
queue suite: 27 passed, 0 failed
@@ -0,0 +1,16 @@
OK valid RELEASE resolves (exit 0)
OK invalid RELEASE exits 1 (exit 1)
OK missing RELEASE exits 1 (exit 1)
OK valid RELEASE leaves image tag consistent with version
OK status safe on empty state (exit 0)
OK status created no pointer
OK fault-injected activation refuses (exit 1)
OK refused activation wrote no pointer
OK refusal logged exactly once with valid fields
OK healthy activation succeeds (exit 0)
OK pointer written with valid fields
OK repeat activation succeeds (log grows) (exit 0)
OK log is append-only across activations
OK rollback without previous refuses (exit 1)
selftest: 14 passed, 0 failed
@@ -0,0 +1,32 @@
OK valid task validates (exit 0)
OK unknown task key exits 2 (exit 2)
OK unsupported taskVersion exits 2 (exit 2)
OK invalid task id exits 2 (exit 2)
OK empty prompt exits 2 (exit 2)
OK NUL in expectExact exits 2 (exit 2)
OK out-of-range timeout exits 2 (exit 2)
OK missing mission file exits 4 (exit 4)
OK task with valid mission validates (exit 0)
OK invalid mission exits 2 (exit 2)
OK validate missing task exits 4 (exit 4)
OK validation does not modify the task file
OK prune dry-run exits 0 (exit 0)
OK dry-run deleted nothing
OK prune --keep=2 --yes removes oldest (exit 0)
OK kept exactly 2 newest runs
OK newest run kept, oldest pruned
OK append-only receipt written (3 entries)
OK sessions/workspaces untouched by prune
OK prune with invalid keep exits 4 (exit 4)
skip adapter seam cases (docker daemon unavailable)
skip workspace/capability cases (docker daemon unavailable)
skip live task cases (docker unavailable)
OK onboard without name exits 4 (non-interactive) (exit 4)
OK onboard --name renders profile (exit 0)
OK profile written
OK canon structure: required filled, optional placeholdered
OK canon sections present
skip live recall pair: user recall run, recalled user name (docker daemon unavailable)
OK no agent identity on headless run
selftest: 26 passed, 0 failed
@@ -0,0 +1,49 @@
✔ browser edge states: loading, empty, malformed, stale, hostile/long values, in-flight reply and appearance fallback (8275.060975ms)
Rendered contrast: {"failures":[],"count":330,"lowest":4.504658476260286}
✔ served Console browser: real board fixtures, keyboard, drafts, receipts, themes, 320px and failures (10138.963463ms)
✔ S5 pages in a browser: real broker rows, inert text, Copy, routes, failures, layout and no writes (10492.520877ms)
✔ bus routes serve the four reads from the reader view, unescaped JSON for the page to escape (301.215759ms)
✔ decisions route serves the roles and recent lists from the reader view (337.567878ms)
✔ bus routes are GET-only, have no write verb, and keep the same-origin checks (266.605643ms)
✔ bus refusals map to statuses and never carry a path or a stack (41.391506ms)
✔ humanCall speaks the human transport protocol without blocking the server (2527.712709ms)
✔ busReader takes --business, else the live bus host, else refuses (711.823402ms)
✔ humanCall kills a transport that runs past its timeout (628.419398ms)
✔ serve refuses a --business value that is not a business id (699.578113ms)
✔ conversation view: full history, collapsed tools, hidden thinking, inert hostile content, malformed and reconcile markers (8062.630495ms)
✔ conversation view: a fork keeps the open branch, says so, and opens the new one on request (3329.101737ms)
✔ conversation view: a newer session with no readable history keeps the marker (2589.475907ms)
✔ conversation view: seats without history say so and offer no reply (2747.770851ms)
✔ Discord row through real board/WebUI: independent brake/liveness, no Reply, literal content (7501.091686ms)
✔ return flow through the conversation view: send, tool call, delayed result, peer message, exact long answers, relaunch (58788.908195ms)
✔ redactor: config, dataRoot, any absolute path, Discord ids and the JSON parser quote (2.946649ms)
✔ credential state from the date alone, as the bus states it (1.367965ms)
✔ queue: rows, one row, ids and methods, notes; no path or id from a row (1685.791737ms)
✔ queue: a refused read fails closed with the store's text, redacted (1259.762915ms)
✔ queue: a store that fails to load, or a child that dies, gives a fixed message with no path or stack (5741.339963ms)
✔ business: names, vars on the allowlist, authority, credential metadata only (91.754024ms)
✔ business: missing or invalid file refuses with the module's text, redacted (80.669373ms)
✔ business without --business: the live bus host, else no-bus-host; no system config: not-configured (30.992184ms)
✔ settings: release, board, loopback address, notifier binding name only (914.092724ms)
✔ both presentations replace old activity with relaunch notice, label retained history, then resume after new activity (8066.767917ms)
✔ reported return flow and relative Age: reply sent from the inspector, then the new answer appears there without manual refresh (25757.838229ms)
✔ loopback host and board origin fail closed (11.156798ms)
✔ real board fixture passes through WebUI; assets and isolated seen/reply work (198.700559ms)
✔ proxy preserves exact request bytes, status and receipt, rejects forms and malformed JSON, never follows redirect (169.690806ms)
✔ unreachable board reports URL; CLI rejects unsupported options (1111.797977ms)
✔ tokens.css and icons.svg are byte copies of docs/design (7.039293ms)
✔ JetBrains Mono files match the digests in their sources file, with the OFL text (18.659402ms)
✔ shipped strings are unchanged, and the web keeps its write boundary (9.963916ms)
✔ shell in a browser: System default, seeded states, 400px, focus and the Ctrl+K palette (12660.849318ms)
✔ five states on Board, Inbox, Tasks, Agents and Trail (4189.444232ms)
✔ Queue, Business and Settings in their five states, with no move button (10288.798194ms)
✔ seeded secrets, Discord ids and paths reach no response and no page; business refusals in the module's words (3712.943696ms)
✔ the server log names no config path when the system config refuses (776.870785ms)
ℹ tests 40
ℹ suites 0
ℹ pass 40
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 59465.878891
@@ -0,0 +1,48 @@
#!/bin/bash
# Round 3. Same method as mutants-round2.sh: each mutant edits one source file in the build worktree,
# runs the tests, then restores the file from the worktree index (the round 3 candidate). Rounds 1
# and 2 run again first, with the round 2 lines retargeted at the new leftTree(). Each runs only the
# test file that should fail it, since another seat's suite shared the machine; the gate runs every
# suite on the candidate.
export TMPDIR=~/darkwing-scratch/r62/tmp
cd ~/darkwing-scratch/r62/build
S=packages/tasks/src/sync.mjs; U=packages/tasks/src/startup.mjs; T=packages/tasks/tests/tree.test.mjs
run() { # name file sed-expr test-file
sed -i "$3" "$2"
if git diff --quiet -- "$2"; then echo "$1: MUTATION DID NOT APPLY"; return; fi
env -u NODE_TEST_CONTEXT node --test --test-timeout=30000 "$4" > ~/darkwing-scratch/r62/tmp/mut3-$1.log 2>&1
echo "$1: rc=$? $(grep -E '^ℹ (pass|fail) ' ~/darkwing-scratch/r62/tmp/mut3-$1.log | tr '\n' ' ')"
grep -E '^✖ ' ~/darkwing-scratch/r62/tmp/mut3-$1.log | sed 's/ ([0-9.]*ms)$//; s/^/ /' | sort -u | head -6
git checkout -q -- "$2"
}
# Round 1.
run no-leave-tree-check $S 's/} else if (!ctx.tree.has(project)) {/} else if (false) {/' $T
run no-watch-only-bucket $S 's/^const fieldsFor = .*/const fieldsFor = (task, bucket) => taskFields(task, bucket);/' $T
run placed-by-id $S 's/placed.add(ref(project, id));/placed.add(id);/; s/.filter((r) => !placed.has(r));/.filter((r) => !placed.has(parseRef(r).task));/' $T
run keep-archived $U 's/if (p.is_archived || p.id === ctx.project) continue;/if (p.id === ctx.project) continue;/' $T
run config-drops-descendants packages/cli/src/config.mjs 's/ descendants: v\["tracker.descendants"\],/ descendants: false,/' packages/cli/tests/config.test.mjs
# Round 2: R1, retargeted at the async leftTree().
run r2-child-refuses $S 's/ if (!(await leftTree(ctx, project, e))) throw e;/ throw e;/' $T
run r2-skip-keeps-child $S 's/ ctx.tree.delete(project);//' $T
run r2-root-drops $S 's/ if (project === ctx.project || !(e instanceof BusError)/ if (!(e instanceof BusError)/' $T
run r2-any-error-drops $S "s/ || (e.code !== 'tracker-forbidden' \&\& e.code !== 'tracker-not-found')) return false;/) return false;/" $T
run r2-403-only $S "s/(e.code !== 'tracker-forbidden' \&\& e.code !== 'tracker-not-found')/e.code !== 'tracker-forbidden'/" $T
run r2-404-only $S "s/(e.code !== 'tracker-forbidden' \&\& e.code !== 'tracker-not-found')/e.code !== 'tracker-not-found'/" $T
# Round 2: Filbert's N1 to N3, under the names Filbert used. Filbert's edits may differ from these.
run tree-early $U 's/^ const tree = new Map(\[\[ctx.project, ctx.view\]\]);/ const tree = (ctx.tree = new Map([[ctx.project, ctx.view]]));/' $T
run missing-done-root-bucket $S 's/doneBucket(ctx.tree.get(project))/doneBucket(ctx.view)/' $T
run leave-tree-no-keep-updated $S '/} else if (!ctx.tree.has(project)) {/{n;s/ updated = c.updated;//}' $T
# Round 2: Dewey's three test gaps (comment 27280), under Dewey's names. Dewey's edits may differ.
# tick-done-root-bucket has two lines on the tick path: a closed task the cursor returns that the
# board no longer shows, and one the board still showed.
run reconcile-done-root-bucket $S 's/const bucket = t.done ? doneBucket(view) :/const bucket = t.done ? doneBucket(ctx.view) :/' $T
run tick-done-root-bucket $S "s/bucket: doneBucket(view), via: 'cursor'/bucket: doneBucket(ctx.view), via: 'cursor'/" $T
run tick-done-root-bucket-board $S 's/const b = use.done ? doneBucket(view) : bucket;/const b = use.done ? doneBucket(ctx.view) : bucket;/' $T
run tree-check-before-moved $S 's/ if (x.json.project_id !== project) {/ if (x.json.project_id !== project \&\& ctx.tree.has(project)) {/' $T
# Round 3: R2 (Filbert) and B1 (Dewey), the confirm read. Dewey's name for the 401 one.
run r3-no-confirm $S "s|^ const r = await ctx.sync('GET', \`/projects/\${project}\`);| const r = { status: 404 };|" $T
run r3-confirm-403-only $S 's/ return r.status === 403 || r.status === 404;/ return r.status === 403;/' $T
run r3-confirm-404-only $S 's/ return r.status === 403 || r.status === 404;/ return r.status === 404;/' $T
run r3-confirm-not-200 $S 's/ return r.status === 403 || r.status === 404;/ return r.status !== 200;/' $T
run r2-unauthorized-drops $S "s/ \&\& e.code !== 'tracker-not-found')) return false;/ \&\& e.code !== 'tracker-not-found' \&\& e.code !== 'tracker-unauthorized')) return false;/" $T
git diff --quiet -- $S $U packages/cli/src/config.mjs && echo "restored: sources match the candidate index"
@@ -0,0 +1,84 @@
no-leave-tree-check: rc=1 ℹ pass 18 ℹ fail 1
✖ a child that leaves the tree has its tasks recorded missing, and the ledger keeps their history
✖ failing tests:
no-watch-only-bucket: rc=1 ℹ pass 9 ℹ fail 10
✖ a child deleted or hidden between reconciles drops out at the next tick, and the root still refuses
✖ a child made after startup joins the tree at the next reconcile
✖ a child whose kanban view is deleted still refuses the tick, and the reconcile reads it watch-only
✖ a closed task the cursor missed takes its own project's done bucket
✖ a closed task the cursor returns takes its own project's done bucket
✖ a move inside the tree is a moved tombstone plus a snapshot under the new ref
placed-by-id: rc=1 ℹ pass 18 ℹ fail 1
✖ a move inside the tree is a moved tombstone plus a snapshot under the new ref
✖ failing tests:
keep-archived: rc=1 ℹ pass 18 ℹ fail 1
✖ failing tests:
✖ the project list refuses without read_all, and skips archived projects and saved filters
config-drops-descendants: rc=1 ℹ pass 5 ℹ fail 1
✖ failing tests:
✖ trackers come from the tracker.* variables of the one project that names a tracker project
r2-child-refuses: rc=1 ℹ pass 15 ℹ fail 4
✖ a child deleted or hidden between reconciles drops out at the next tick, and the root still refuses
✖ a child dropped at a tick comes back at the next reconcile once the bot reads it again
✖ a child that leaves the tree has its tasks recorded missing, and the ledger keeps their history
✖ a child whose kanban view is deleted still refuses the tick, and the reconcile reads it watch-only
✖ failing tests:
r2-skip-keeps-child: rc=1 ℹ pass 17 ℹ fail 2
✖ a child deleted or hidden between reconciles drops out at the next tick, and the root still refuses
✖ a child dropped at a tick comes back at the next reconcile once the bot reads it again
✖ failing tests:
r2-root-drops: rc=1 ℹ pass 18 ℹ fail 1
✖ a child deleted or hidden between reconciles drops out at the next tick, and the root still refuses
✖ failing tests:
r2-any-error-drops: rc=1 ℹ pass 18 ℹ fail 1
✖ a child whose kanban view is deleted still refuses the tick, and the reconcile reads it watch-only
✖ failing tests:
r2-403-only: rc=1 ℹ pass 17 ℹ fail 2
✖ a child deleted or hidden between reconciles drops out at the next tick, and the root still refuses
✖ a child whose kanban view is deleted still refuses the tick, and the reconcile reads it watch-only
✖ failing tests:
r2-404-only: rc=1 ℹ pass 16 ℹ fail 3
✖ a child deleted or hidden between reconciles drops out at the next tick, and the root still refuses
✖ a child dropped at a tick comes back at the next reconcile once the bot reads it again
✖ a child that leaves the tree has its tasks recorded missing, and the ledger keeps their history
✖ failing tests:
tree-early: rc=1 ℹ pass 18 ℹ fail 1
✖ a reconcile that fails keeps the previous tree
✖ failing tests:
missing-done-root-bucket: rc=1 ℹ pass 18 ℹ fail 1
✖ a closed task the cursor missed takes its own project's done bucket
✖ failing tests:
leave-tree-no-keep-updated: rc=1 ℹ pass 18 ℹ fail 1
✖ a child that leaves the tree has its tasks recorded missing, and the ledger keeps their history
✖ failing tests:
reconcile-done-root-bucket: rc=1 ℹ pass 18 ℹ fail 1
✖ closed tasks read at startup take their own project's done bucket
✖ failing tests:
tick-done-root-bucket: rc=1 ℹ pass 17 ℹ fail 2
✖ a closed task the cursor returns takes its own project's done bucket
✖ bucket 0 on a watch-only task never becomes a bucket or a state
✖ failing tests:
tick-done-root-bucket-board: rc=1 ℹ pass 18 ℹ fail 1
✖ a closed task the cursor returns takes its own project's done bucket
✖ failing tests:
tree-check-before-moved: rc=1 ℹ pass 18 ℹ fail 1
✖ a task moved out of a child that leaves the tree at the same time is moved, not no-access
✖ failing tests:
r3-no-confirm: rc=1 ℹ pass 18 ℹ fail 1
✖ a child whose kanban view is deleted still refuses the tick, and the reconcile reads it watch-only
✖ failing tests:
r3-confirm-403-only: rc=1 ℹ pass 18 ℹ fail 1
✖ a child deleted or hidden between reconciles drops out at the next tick, and the root still refuses
✖ failing tests:
r3-confirm-404-only: rc=1 ℹ pass 16 ℹ fail 3
✖ a child deleted or hidden between reconciles drops out at the next tick, and the root still refuses
✖ a child dropped at a tick comes back at the next reconcile once the bot reads it again
✖ a child that leaves the tree has its tasks recorded missing, and the ledger keeps their history
✖ failing tests:
r3-confirm-not-200: rc=1 ℹ pass 18 ℹ fail 1
✖ a child whose kanban view is deleted still refuses the tick, and the reconcile reads it watch-only
✖ failing tests:
r2-unauthorized-drops: rc=1 ℹ pass 18 ℹ fail 1
✖ a child whose kanban view is deleted still refuses the tick, and the reconcile reads it watch-only
✖ failing tests:
restored: sources match the candidate index