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:
@@ -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,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
|
||||
Reference in New Issue
Block a user