docs(queue-62): row 62 round 2 candidate packet, R1 and six test gaps (darkwing)
Filbert's R1 (27279) and Dewey's matching blocking item (27280): a child
that answers 403 or 404 in a tick leaves the tree, and the root still
refuses. Tests for Filbert's N1-N3 and Dewey's three gaps. 18 mutants
killed; all 13 suites green at 122b1223. Manifest sha256 55316d88.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# Row 62 (#1555): the adapter reads a project tree, candidate packet
|
# Row 62 (#1555): the adapter reads a project tree, candidate packet, rounds 1 and 2
|
||||||
|
|
||||||
Author: Darkwing. Reviewers: Dewey and Filbert. Brief:
|
Author: Darkwing. Reviewers: Dewey and Filbert. Brief:
|
||||||
`docs/plans/2026-10-11_multi-project-sync.md`, row A. Base: `edc5de71`
|
`docs/plans/2026-10-11_multi-project-sync.md`, row A. Base: `edc5de71`
|
||||||
@@ -223,3 +223,193 @@ probes.md "Gate read" has the exact requests: `GET /api/v2/projects` and
|
|||||||
`GET /api/v2/tasks` with a `project in 19, 18, 20, 21` filter, sync key 9,
|
`GET /api/v2/tasks` with a `project in 19, 18, 20, 21` filter, sync key 9,
|
||||||
GETs only. The adapter reads per project whatever read 2 answers; a 200
|
GETs only. The adapter reads per project whatever read 2 answers; a 200
|
||||||
there opens a follow-up row.
|
there opens a follow-up row.
|
||||||
|
|
||||||
|
## Round 2
|
||||||
|
|
||||||
|
Round 1 drew changes requested from both reviewers on candidate
|
||||||
|
`ec3f7a42`. Filbert's comment 27279 (queue rev 408) had one blocking item,
|
||||||
|
R1, and three test gaps, N1 to N3. Dewey's comment 27280 (rev 409) found
|
||||||
|
the same blocking defect on its own and named three more test gaps. This
|
||||||
|
round fixes R1 and adds a test for each of the six gaps. `build.patch` and
|
||||||
|
`candidate-manifest.sha256` now hold the round 2 candidate; the round 1
|
||||||
|
versions are in this packet's history at `6b83edc3`. `files.txt` is
|
||||||
|
unchanged, the same 13 files: 13 files, +778/−73 against `edc5de71`.
|
||||||
|
Round 2 changed four of them: `sync.mjs`, `fake.mjs`, `tree.test.mjs`
|
||||||
|
and the tasks `README.md`. The gate applied the patch to `122b1223`
|
||||||
|
(`out/gate-round2/apply.txt`); no candidate file changed between
|
||||||
|
`6ae8b844` and `122b1223`.
|
||||||
|
|
||||||
|
### Blocking
|
||||||
|
|
||||||
|
**R1 (Filbert, and Dewey's blocking item): a child that answered 403 or
|
||||||
|
404 failed every tick until the next reconcile.** Dewey added that
|
||||||
|
`notify(ctx)` runs after `tick(ctx)` in the adapter's timer, so the
|
||||||
|
credential notices stopped too. A tick that drops the child no longer
|
||||||
|
throws, so `notify` runs again. No test drives the timer path; the tests
|
||||||
|
call `tick` directly. The tick now drops it. In `tick()`, a
|
||||||
|
`tracker-forbidden` or `tracker-not-found` refusal on a project's board,
|
||||||
|
task list or cursor walk removes that project from `ctx.tree`, and the
|
||||||
|
tick goes on with the others. The rule is `leftTree()` in `sync.mjs`.
|
||||||
|
The dropped project's tasks are then on no board this tick, so
|
||||||
|
`missing()` reads each one, as it does after a reconcile: a 403 gives
|
||||||
|
`no-access`, a 404 gives `not-found`, a 200 in the same project gives
|
||||||
|
`no-access`. The root still fails the tick on a 403 or 404. Any other
|
||||||
|
failure on a child, a 503 for one, still fails the tick, and the child
|
||||||
|
stays in the tree. A dropped child that comes back is found by the next
|
||||||
|
reconcile's `discover()`.
|
||||||
|
|
||||||
|
I chose drop over skip. A skipped child stays in `ctx.tree`, so every
|
||||||
|
tick reads it again and gets the same 403 until the reconcile; dropping
|
||||||
|
it does what the reconcile would do, one reconcile early. The tradeoff:
|
||||||
|
a child that answers 403 for a short time is out of the tree until the
|
||||||
|
next reconcile, and its open tasks get `no-access` tombstones meanwhile.
|
||||||
|
The reconcile reads them again if the access is back. No test covers
|
||||||
|
that return.
|
||||||
|
|
||||||
|
`fake.mjs` gained `deleteProject()`. Like Vikunja it deletes the
|
||||||
|
project, its descendants and their tasks. After it a project read
|
||||||
|
answers 404 with code 3001 and a task read 404 with code 4002.
|
||||||
|
|
||||||
|
Filbert's list names three ways a child leaves: unshared, reparented
|
||||||
|
under a project the bot can't read, and deleted. A child under the root
|
||||||
|
inherits the root's share, and project_access.go has no grant that
|
||||||
|
takes access away, so a child can't stop being shared on its own while
|
||||||
|
the root stays shared. The fake models it the same way. The reparented
|
||||||
|
case is the 403 a child can get; tests 9 and 10 use it.
|
||||||
|
|
||||||
|
Test 9 now ticks between the change and the reconcile. The hidden child
|
||||||
|
answers 403 on its board, so the tick drops it and records its task
|
||||||
|
`no-access`. The archived child and the one reparented to the top with a
|
||||||
|
share of its own still read, so they wait for the reconcile.
|
||||||
|
|
||||||
|
New test 10, "a child deleted or hidden between reconciles drops out at
|
||||||
|
the next tick, and the root still refuses":
|
||||||
|
|
||||||
|
- An installed child is deleted (404 on its board), and a watch-only
|
||||||
|
child is moved under a project the bot can't read (403 on its task
|
||||||
|
list). The root and a sibling child have edits in the same tick.
|
||||||
|
- The tick leaves the business ready, records `not-found` and
|
||||||
|
`no-access` for the two tasks, and records both edits.
|
||||||
|
- The next tick sends no request under either project, and the
|
||||||
|
reconcile records nothing.
|
||||||
|
- A 503 on the sibling's task list fails the tick with
|
||||||
|
`tracker-unavailable`, and the tick after it still reads the sibling.
|
||||||
|
- The root unshared fails the tick with `tracker-forbidden`; shared
|
||||||
|
again, the next tick records a root edit. The root deleted fails the
|
||||||
|
tick with `tracker-not-found`.
|
||||||
|
|
||||||
|
The README sentence "Ticks before that reconcile keep reading the
|
||||||
|
project" is gone. The paragraph now says: "A tick doesn't read the
|
||||||
|
project list, so it keeps reading an archived 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." The paragraph above it adds "or deleted" and "or a tick
|
||||||
|
before that".
|
||||||
|
|
||||||
|
### Non-blocking
|
||||||
|
|
||||||
|
**N1: a failed reconcile keeps the previous tree.** New test 11. Two
|
||||||
|
children; at the reconcile the first one's install check gets a 503, so
|
||||||
|
the walk stops before the second. The reconcile fails with
|
||||||
|
`tracker-unavailable`, and the next tick still records an edit on the
|
||||||
|
second child and no `task.missing`.
|
||||||
|
|
||||||
|
**N2: a closed task found through `missing()` takes its own project's
|
||||||
|
done bucket.** New test 12. The fake runs an hour behind, so the cursor
|
||||||
|
never returns the close. An installed child's task and a watch-only
|
||||||
|
child's task are closed. The tick reads both through `missing()` (`via:
|
||||||
|
task`): the installed one gets the child's done bucket, which differs
|
||||||
|
from the root's, and the watch-only one gets bucket 0.
|
||||||
|
|
||||||
|
**N3: the leave-tree tombstone keeps the last recorded `updated`.** Test
|
||||||
|
9 now edits the reparented task after the tick and before the reconcile,
|
||||||
|
with the fake's clock two seconds ahead, so the task's `updated`
|
||||||
|
changes. The reconcile reads the edit through `missing()`, and the
|
||||||
|
tombstone still carries the `updated` of the task's first snapshot.
|
||||||
|
|
||||||
|
**Dewey's three gaps.** Each passed on the round 1 candidate, as Dewey
|
||||||
|
said; the mutants below show what each test now catches.
|
||||||
|
|
||||||
|
- `reconcile-done-root-bucket`: new test 13. A task in an installed child
|
||||||
|
and one in a watch-only child are closed before startup. The startup
|
||||||
|
reconcile records the child's done bucket and bucket 0.
|
||||||
|
- `tick-done-root-bucket`: new test 14, the same check on the tick path,
|
||||||
|
where the cursor returns the close. The tick has two such lines: a
|
||||||
|
closed task the open read left out, and one closed between the open
|
||||||
|
read and the cursor walk. The test covers both. A `fetch` wrapper
|
||||||
|
closes a second task in each project right after that project's first
|
||||||
|
request in the tick, which is its open read.
|
||||||
|
- `tree-check-before-moved`: new test 15. The child is archived and its
|
||||||
|
task moved to the root at the same time. The reconcile records `moved`
|
||||||
|
to the root, not `no-access`, and a snapshot under the new ref.
|
||||||
|
|
||||||
|
### Tests, round 2
|
||||||
|
|
||||||
|
`packages/tasks/tests/tree.test.mjs` has 17 tests (11 in round 1). Test
|
||||||
|
9 changed; tests 10 to 15 are new. Round 1's tests 10 and 11 are now 16
|
||||||
|
and 17.
|
||||||
|
|
||||||
|
### Mutants, round 2
|
||||||
|
|
||||||
|
`out/mutants-round2.sh` uses round 1's method: edit one source file,
|
||||||
|
run the tests, restore the file from the worktree index. Its output is
|
||||||
|
`out/mutants-round2.txt`. Each mutant ran only the test file meant to
|
||||||
|
fail it (`tree.test.mjs`, or `config.test.mjs` for the config one),
|
||||||
|
because the row 55 mutant run shared the machine. The gate below runs
|
||||||
|
every suite on the candidate. All 18 fail at least one test:
|
||||||
|
|
||||||
|
| Mutant | Fails |
|
||||||
|
|---|---|
|
||||||
|
| round 1's five | as in round 1, with round 1's test 11 now 17; `no-watch-only-bucket` also fails 10, 12, 13 and 14 |
|
||||||
|
| `r2-child-refuses`: a child's 403 or 404 fails the tick | 9, 10 |
|
||||||
|
| `r2-skip-keeps-child`: skip the child, keep it in the tree | 10 |
|
||||||
|
| `r2-root-drops`: the root is dropped too | 10 |
|
||||||
|
| `r2-any-error-drops`: any refusal drops a child | 10 |
|
||||||
|
| `r2-403-only`: a 404 still fails the tick | 10 |
|
||||||
|
| `r2-404-only`: a 403 still fails the tick | 9, 10 |
|
||||||
|
| `tree-early`: `discover()` sets `ctx.tree` before the walk | 11 |
|
||||||
|
| `missing-done-root-bucket`: `missing()` uses the root's done bucket | 12 |
|
||||||
|
| `leave-tree-no-keep-updated`: the leave-tree tombstone takes the read's `updated` | 9 |
|
||||||
|
| `reconcile-done-root-bucket`: the reconcile uses the root's done bucket | 13 |
|
||||||
|
| `tick-done-root-bucket`: a closed task only the cursor returned takes the root's done bucket | 14, 17 |
|
||||||
|
| `tick-done-root-bucket-board`: a task the open read showed and the cursor returned closed takes the root's done bucket | 14 |
|
||||||
|
| `tree-check-before-moved`: the tree check runs before the moved check | 15 |
|
||||||
|
|
||||||
|
`tree-early`, `missing-done-root-bucket` and `leave-tree-no-keep-updated`
|
||||||
|
use Filbert's names, and the last four use Dewey's, with
|
||||||
|
`tick-done-root-bucket` split in two for the tick's two lines. Their
|
||||||
|
edits may differ from mine.
|
||||||
|
|
||||||
|
### Gate, round 2
|
||||||
|
|
||||||
|
The gate ran in a detached worktree of `122b1223` with the round 2
|
||||||
|
`build.patch` applied and `node_modules` linked, using round 1's script
|
||||||
|
with a new output directory (`out/gate-round2/gate.sh`). Each log is in
|
||||||
|
`out/gate-round2/`. test-task ran with `DOCKER_HOST=unix:///nonexistent`,
|
||||||
|
so its Docker groups printed skip, as in round 1.
|
||||||
|
|
||||||
|
Every suite passed (`out/gate-round2/summary.txt`):
|
||||||
|
|
||||||
|
| Suite | rc | Result |
|
||||||
|
|---|---|---|
|
||||||
|
| `packages/tasks` | 0 | 69 passed, 0 failed (63 in round 1) |
|
||||||
|
| `packages/business` | 0 | 60 passed, 0 failed |
|
||||||
|
| `packages/bus` | 0 | 74 passed, 0 failed |
|
||||||
|
| `packages/cli` | 0 | 83 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 |
|
||||||
|
|
||||||
|
A `gate_default` Docker network appeared during the run with no
|
||||||
|
containers, and I removed it.
|
||||||
|
|
||||||
|
Candidate manifest sha256: `55316d885d51e0d089173476dbccacfdcf254e51c37aecff0f556ae80ca99179`.
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ index b7710013..d7cef73a 100644
|
|||||||
t.after(() => host.close(0));
|
t.after(() => host.close(0));
|
||||||
|
|
||||||
diff --git a/packages/tasks/README.md b/packages/tasks/README.md
|
diff --git a/packages/tasks/README.md b/packages/tasks/README.md
|
||||||
index 1b764e82..ae2789be 100644
|
index 1b764e82..2417ac48 100644
|
||||||
--- a/packages/tasks/README.md
|
--- a/packages/tasks/README.md
|
||||||
+++ b/packages/tasks/README.md
|
+++ b/packages/tasks/README.md
|
||||||
@@ -1,7 +1,7 @@
|
@@ -1,7 +1,7 @@
|
||||||
@@ -149,7 +149,7 @@ index 1b764e82..ae2789be 100644
|
|||||||
|
|
||||||
A startup refused with `tracker-unavailable`, `tracker-rate-limited` or
|
A startup refused with `tracker-unavailable`, `tracker-rate-limited` or
|
||||||
`service-failed` is tried again at the next poll. Any other refusal
|
`service-failed` is tried again at the next poll. Any other refusal
|
||||||
@@ -239,6 +242,84 @@ mtime moved.
|
@@ -239,6 +242,90 @@ mtime moved.
|
||||||
`close()` stops the timers and waits for the queue. A verb still queued
|
`close()` stops the timers and waits for the queue. A verb still queued
|
||||||
at that point refuses with `tracker-closed`.
|
at that point refuses with `tracker-closed`.
|
||||||
|
|
||||||
@@ -202,8 +202,9 @@ index 1b764e82..ae2789be 100644
|
|||||||
+tombstone and nothing after it.
|
+tombstone and nothing after it.
|
||||||
+
|
+
|
||||||
+A project leaves the tree when it is archived, moved under a parent
|
+A project leaves the tree when it is archived, moved under a parent
|
||||||
+outside the tree, or no longer shared with the sync bot. The next
|
+outside the tree, no longer shared with the sync bot, or deleted. The
|
||||||
+reconcile drops it, and each of its tasks the bus holds is read once:
|
+next reconcile drops it, or a tick before that (below), and each of its
|
||||||
|
+tasks the bus holds is read once:
|
||||||
+
|
+
|
||||||
+- The sync bot can still read the task, in that project: `task.missing`
|
+- The sync bot can still read the task, in that project: `task.missing`
|
||||||
+ with `no-access`. The business no longer reaches it.
|
+ with `no-access`. The business no longer reaches it.
|
||||||
@@ -212,8 +213,13 @@ index 1b764e82..ae2789be 100644
|
|||||||
+
|
+
|
||||||
+The tombstone keeps the task's last `updated`. Nothing in the ledger is
|
+The tombstone keeps the task's last `updated`. Nothing in the ledger is
|
||||||
+rewritten: the earlier snapshots stay and the tombstone is a new row.
|
+rewritten: the earlier snapshots stay and the tombstone is a new row.
|
||||||
+Ticks before that reconcile keep reading the project, and a new child
|
+A tick doesn't read the project list, so it keeps reading an archived
|
||||||
+joins at the next reconcile the same way.
|
+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.
|
||||||
+
|
+
|
||||||
+Each tick reads per project. An installed project costs the board and
|
+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
|
+the cursor walk, a watch-only one its open task list and the cursor
|
||||||
@@ -234,7 +240,7 @@ index 1b764e82..ae2789be 100644
|
|||||||
## Refusals
|
## Refusals
|
||||||
|
|
||||||
| Code | Meaning |
|
| Code | Meaning |
|
||||||
@@ -277,6 +358,15 @@ every user or task any recording held, so a key one route adds, like
|
@@ -277,6 +364,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
|
The shape test catches a fake that invents a field, not one that puts a
|
||||||
real field on the wrong route.
|
real field on the wrong route.
|
||||||
|
|
||||||
@@ -309,7 +315,7 @@ index d564c645..f0f8cacf 100644
|
|||||||
sync: use('@sync'),
|
sync: use('@sync'),
|
||||||
as: (role, method, path, opts) => use(role)(method, path, opts),
|
as: (role, method, path, opts) => use(role)(method, path, opts),
|
||||||
diff --git a/packages/tasks/src/fake.mjs b/packages/tasks/src/fake.mjs
|
diff --git a/packages/tasks/src/fake.mjs b/packages/tasks/src/fake.mjs
|
||||||
index fc55529a..2a9cfc14 100644
|
index fc55529a..30d9179e 100644
|
||||||
--- a/packages/tasks/src/fake.mjs
|
--- a/packages/tasks/src/fake.mjs
|
||||||
+++ b/packages/tasks/src/fake.mjs
|
+++ b/packages/tasks/src/fake.mjs
|
||||||
@@ -6,9 +6,13 @@ import { NULL_DATE } from './digest.mjs';
|
@@ -6,9 +6,13 @@ import { NULL_DATE } from './digest.mjs';
|
||||||
@@ -346,7 +352,7 @@ index fc55529a..2a9cfc14 100644
|
|||||||
for (const [kind, position] of [
|
for (const [kind, position] of [
|
||||||
['list', 100],
|
['list', 100],
|
||||||
['gantt', 200],
|
['gantt', 200],
|
||||||
@@ -115,6 +120,21 @@ export class FakeVikunja {
|
@@ -115,6 +120,27 @@ export class FakeVikunja {
|
||||||
share(project, user, permission = 0) {
|
share(project, user, permission = 0) {
|
||||||
this.#projects.get(project).shares.set(user, permission);
|
this.#projects.get(project).shares.set(user, permission);
|
||||||
}
|
}
|
||||||
@@ -361,6 +367,12 @@ index fc55529a..2a9cfc14 100644
|
|||||||
+ archive(project, archived = true) {
|
+ archive(project, archived = true) {
|
||||||
+ for (const p of this.#projects.values()) if (p.id === project || this.#ancestors(p.id).includes(project)) p.archived = archived;
|
+ for (const p of this.#projects.values()) if (p.id === project || this.#ancestors(p.id).includes(project)) p.archived = archived;
|
||||||
+ }
|
+ }
|
||||||
|
+ // Vikunja deletes a project's descendants and every task in them with it.
|
||||||
|
+ deleteProject(project) {
|
||||||
|
+ const gone = [...this.#projects.keys()].filter((id) => id === project || this.#ancestors(id).includes(project));
|
||||||
|
+ 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);
|
||||||
|
+ }
|
||||||
+ // A saved filter or Favorites: a negative id in the project list, as Project.ReadAll appends.
|
+ // A saved filter or Favorites: a negative id in the project list, as Project.ReadAll appends.
|
||||||
+ pseudoProject(user, id = -1, title = 'Favorites') {
|
+ pseudoProject(user, id = -1, title = 'Favorites') {
|
||||||
+ this.#pseudo.push({ id, title, user });
|
+ this.#pseudo.push({ id, title, user });
|
||||||
@@ -368,7 +380,7 @@ index fc55529a..2a9cfc14 100644
|
|||||||
bucket(view, title) {
|
bucket(view, title) {
|
||||||
const id = ++this.#seq.bucket;
|
const id = ++this.#seq.bucket;
|
||||||
this.#buckets.set(id, { id, title, view, position: id * 100 });
|
this.#buckets.set(id, { id, title, view, position: id * 100 });
|
||||||
@@ -231,11 +251,39 @@ export class FakeVikunja {
|
@@ -231,11 +257,39 @@ export class FakeVikunja {
|
||||||
});
|
});
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
@@ -412,7 +424,7 @@ index fc55529a..2a9cfc14 100644
|
|||||||
}
|
}
|
||||||
#userJson(id) {
|
#userJson(id) {
|
||||||
const u = this.#users.get(id);
|
const u = this.#users.get(id);
|
||||||
@@ -378,7 +426,15 @@ export class FakeVikunja {
|
@@ -378,7 +432,15 @@ export class FakeVikunja {
|
||||||
_project(user, [p]) {
|
_project(user, [p]) {
|
||||||
const r = this.#projectFor(user, p);
|
const r = this.#projectFor(user, p);
|
||||||
if (r.error) return r.error;
|
if (r.error) return r.error;
|
||||||
@@ -504,7 +516,7 @@ index 9a2c5bc5..f3bdc3d5 100644
|
|||||||
+ return tree;
|
+ return tree;
|
||||||
+}
|
+}
|
||||||
diff --git a/packages/tasks/src/sync.mjs b/packages/tasks/src/sync.mjs
|
diff --git a/packages/tasks/src/sync.mjs b/packages/tasks/src/sync.mjs
|
||||||
index 811fb683..118f348e 100644
|
index 811fb683..0afd1c49 100644
|
||||||
--- a/packages/tasks/src/sync.mjs
|
--- a/packages/tasks/src/sync.mjs
|
||||||
+++ b/packages/tasks/src/sync.mjs
|
+++ b/packages/tasks/src/sync.mjs
|
||||||
@@ -1,10 +1,12 @@
|
@@ -1,10 +1,12 @@
|
||||||
@@ -614,7 +626,16 @@ index 811fb683..118f348e 100644
|
|||||||
else continue; // still open in the project: the board read raced a move, the next tick sees it
|
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) {
|
} else if (outcome(x) === 'not-found' || x.status === 403) {
|
||||||
updated = c.updated;
|
updated = c.updated;
|
||||||
@@ -151,35 +169,44 @@ export async function tick(ctx) {
|
@@ -147,39 +165,58 @@ 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');
|
||||||
|
export async function tick(ctx) {
|
||||||
const startMs = ctx.clock();
|
const startMs = ctx.clock();
|
||||||
const readAt = iso(startMs);
|
const readAt = iso(startMs);
|
||||||
const sinceMs = Math.floor(((ctx.lastStart ?? startMs) - WINDOW_MS) / 1000) * 1000;
|
const sinceMs = Math.floor(((ctx.lastStart ?? startMs) - WINDOW_MS) / 1000) * 1000;
|
||||||
@@ -622,9 +643,14 @@ index 811fb683..118f348e 100644
|
|||||||
- const hits = new Map((await walk(ctx, `updated > ${floorSecond(sinceMs)}`)).map((t) => [t.id, t]));
|
- const hits = new Map((await walk(ctx, `updated > ${floorSecond(sinceMs)}`)).map((t) => [t.id, t]));
|
||||||
+ const reads = [];
|
+ const reads = [];
|
||||||
+ for (const [project, view] of ctx.tree) {
|
+ for (const [project, view] of ctx.tree) {
|
||||||
|
+ try {
|
||||||
+ const open = await openTasks(ctx, project, view);
|
+ const open = await openTasks(ctx, project, view);
|
||||||
+ const hits = new Map((await walk(ctx, `updated > ${floorSecond(sinceMs)}`, {}, project)).map((t) => [t.id, t]));
|
+ const hits = new Map((await walk(ctx, `updated > ${floorSecond(sinceMs)}`, {}, project)).map((t) => [t.id, t]));
|
||||||
+ reads.push({ project, view, open, hits });
|
+ reads.push({ project, view, open, hits });
|
||||||
|
+ } catch (e) {
|
||||||
|
+ if (!leftTree(ctx, project, e)) throw e;
|
||||||
|
+ ctx.tree.delete(project);
|
||||||
|
+ }
|
||||||
+ }
|
+ }
|
||||||
const current = new Map(ctx.broker.taskView(ctx.business, 'current').map((x) => [x.task_ref, x]));
|
const current = new Map(ctx.broker.taskView(ctx.business, 'current').map((x) => [x.task_ref, x]));
|
||||||
const out = { snapshots: [], events: [] };
|
const out = { snapshots: [], events: [] };
|
||||||
@@ -682,7 +708,7 @@ index 811fb683..118f348e 100644
|
|||||||
await missing(ctx, current, gone, out);
|
await missing(ctx, current, gone, out);
|
||||||
record(ctx, out);
|
record(ctx, out);
|
||||||
ctx.lastStart = startMs;
|
ctx.lastStart = startMs;
|
||||||
@@ -187,27 +214,32 @@ export async function tick(ctx) {
|
@@ -187,27 +224,32 @@ export async function tick(ctx) {
|
||||||
}
|
}
|
||||||
// The hourly full read, and the first read at startup. comment_count catches comments on tasks the
|
// 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.
|
// cursor window missed; a changed count is checked against the comment list before it is reported.
|
||||||
@@ -775,10 +801,10 @@ index 3859dd22..c16f1f44 100644
|
|||||||
for (const [name, r] of Object.entries(recorded)) {
|
for (const [name, r] of Object.entries(recorded)) {
|
||||||
diff --git a/packages/tasks/tests/tree.test.mjs b/packages/tasks/tests/tree.test.mjs
|
diff --git a/packages/tasks/tests/tree.test.mjs b/packages/tasks/tests/tree.test.mjs
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 00000000..9410779b
|
index 00000000..f03de85b
|
||||||
--- /dev/null
|
--- /dev/null
|
||||||
+++ b/packages/tasks/tests/tree.test.mjs
|
+++ b/packages/tasks/tests/tree.test.mjs
|
||||||
@@ -0,0 +1,276 @@
|
@@ -0,0 +1,424 @@
|
||||||
+import test from 'node:test';
|
+import test from 'node:test';
|
||||||
+import assert from 'node:assert/strict';
|
+import assert from 'node:assert/strict';
|
||||||
+import { readFileSync } from 'node:fs';
|
+import { readFileSync } from 'node:fs';
|
||||||
@@ -966,7 +992,9 @@ index 00000000..9410779b
|
|||||||
+ assert.equal(w.snapshots(ref(child, task)).at(-1).fields.title, 'renamed');
|
+ assert.equal(w.snapshots(ref(child, task)).at(-1).fields.title, 'renamed');
|
||||||
+});
|
+});
|
||||||
+test('a child that leaves the tree has its tasks recorded missing, and the ledger keeps their history', async (t) => {
|
+test('a child that leaves the tree has its tasks recorded missing, and the ledger keeps their history', async (t) => {
|
||||||
+ const w = await world(t, TREE);
|
+ // The fake's clock runs ahead on demand, so an edit gets a new `updated` second.
|
||||||
|
+ let ahead = 0;
|
||||||
|
+ const w = await world(t, { ...TREE, fake: { now: () => Date.now() + ahead } });
|
||||||
+ const children = {};
|
+ const children = {};
|
||||||
+ for (const name of ['archived', 'reparented', 'hidden']) {
|
+ for (const name of ['archived', 'reparented', 'hidden']) {
|
||||||
+ children[name] = w.fake.project(name, w.owner, { parent: w.project });
|
+ children[name] = w.fake.project(name, w.owner, { parent: w.project });
|
||||||
@@ -983,13 +1011,20 @@ index 00000000..9410779b
|
|||||||
+ w.fake.reparent(children.reparented, 0);
|
+ w.fake.reparent(children.reparented, 0);
|
||||||
+ w.fake.reparent(children.hidden, launchpad);
|
+ w.fake.reparent(children.hidden, launchpad);
|
||||||
+ await sleep(20);
|
+ await sleep(20);
|
||||||
+ // A tick reads the tree it has; the project list is read at reconcile.
|
|
||||||
+ assert.deepEqual(missing(w), []);
|
|
||||||
+ await w.adapter.reconcile('demo');
|
|
||||||
+ const refs = Object.keys(children).map((name) => ref(children[name], tasks[name]));
|
+ const refs = Object.keys(children).map((name) => ref(children[name], tasks[name]));
|
||||||
|
+ const [archived, reparented, hidden] = refs;
|
||||||
|
+ // A tick reads the tree it has, and the project list is read at reconcile. The hidden child's
|
||||||
|
+ // board answers 403, so the tick drops it at once; the other two still read until the reconcile.
|
||||||
|
+ await w.adapter.tick('demo');
|
||||||
|
+ assert.deepEqual(missing(w), [[hidden, { reason: 'no-access' }]]);
|
||||||
|
+ // An edit the reconcile reads after the project left keeps the tombstone's `updated` the last one
|
||||||
|
+ // the bus recorded.
|
||||||
|
+ ahead = 2000;
|
||||||
|
+ await w.ui('PATCH', `/tasks/${tasks.reparented}`, { title: 'edited outside' });
|
||||||
|
+ await w.adapter.reconcile('demo');
|
||||||
+ assert.deepEqual(
|
+ assert.deepEqual(
|
||||||
+ missing(w),
|
+ missing(w),
|
||||||
+ refs.map((r) => [r, { reason: 'no-access' }]),
|
+ [hidden, archived, reparented].map((r) => [r, { reason: 'no-access' }]),
|
||||||
+ );
|
+ );
|
||||||
+ for (const r of refs) {
|
+ for (const r of refs) {
|
||||||
+ const s = w.snapshots(r);
|
+ const s = w.snapshots(r);
|
||||||
@@ -1005,6 +1040,145 @@ index 00000000..9410779b
|
|||||||
+ assert.deepEqual(await w.adapter.tick('demo'), { snapshots: 0, events: 0 });
|
+ assert.deepEqual(await w.adapter.tick('demo'), { snapshots: 0, events: 0 });
|
||||||
+ assert.deepEqual(await w.adapter.reconcile('demo'), { snapshots: 0, events: 0 });
|
+ assert.deepEqual(await w.adapter.reconcile('demo'), { snapshots: 0, events: 0 });
|
||||||
+});
|
+});
|
||||||
|
+test('a child deleted or hidden between reconciles drops out at the next tick, and the root still refuses', async (t) => {
|
||||||
|
+ const w = await world(t, TREE);
|
||||||
|
+ const deleted = w.fake.project('Deleted', w.owner, { parent: w.project });
|
||||||
|
+ w.fake.install(deleted);
|
||||||
|
+ // Watch-only, so its tick read is the task list, not a board.
|
||||||
|
+ const hidden = w.fake.project('Hidden', w.owner, { parent: w.project });
|
||||||
|
+ const kept = w.fake.project('Kept', w.owner, { parent: w.project });
|
||||||
|
+ const launchpad = w.fake.project('Launchpad', w.owner);
|
||||||
|
+ const tasks = {};
|
||||||
|
+ for (const [name, p] of Object.entries({ root: w.project, deleted, hidden, kept })) tasks[name] = await add(w, p, name);
|
||||||
|
+ await ready(w);
|
||||||
|
+ w.fake.deleteProject(deleted);
|
||||||
|
+ w.fake.reparent(hidden, launchpad);
|
||||||
|
+ await w.ui('PATCH', `/tasks/${tasks.root}`, { title: 'root renamed' });
|
||||||
|
+ await w.ui('PATCH', `/tasks/${tasks.kept}`, { title: 'kept renamed' });
|
||||||
|
+ await sleep(20);
|
||||||
|
+ await w.adapter.tick('demo');
|
||||||
|
+ assert.equal(state(w).state, 'ready');
|
||||||
|
+ assert.deepEqual(missing(w).sort(), [
|
||||||
|
+ [ref(deleted, tasks.deleted), { reason: 'not-found' }],
|
||||||
|
+ [ref(hidden, tasks.hidden), { reason: 'no-access' }],
|
||||||
|
+ ]);
|
||||||
|
+ // The root and the sibling changed in the same tick, and both are recorded.
|
||||||
|
+ assert.equal(w.snapshots(ref(w.project, tasks.root)).at(-1).fields.title, 'root renamed');
|
||||||
|
+ assert.equal(w.snapshots(ref(kept, tasks.kept)).at(-1).fields.title, 'kept renamed');
|
||||||
|
+ // The next tick reads neither, and the reconcile finds neither.
|
||||||
|
+ const reads = await counted(w, () => w.adapter.tick('demo'));
|
||||||
|
+ assert.ok(!reads.some((r) => r.includes(`/projects/${deleted}/`) || r.includes(`/projects/${hidden}/`)), reads.join('\n'));
|
||||||
|
+ assert.deepEqual(await w.adapter.reconcile('demo'), { snapshots: 0, events: 0 });
|
||||||
|
+ // Any other failure on a child still refuses the tick, and the child stays.
|
||||||
|
+ w.fake.fault('GET', new RegExp(`^/projects/${kept}/tasks$`), 503);
|
||||||
|
+ await assert.rejects(w.adapter.tick('demo'), { code: 'tracker-unavailable' });
|
||||||
|
+ await w.ui('PATCH', `/tasks/${tasks.kept}`, { title: 'kept again' });
|
||||||
|
+ await sleep(20);
|
||||||
|
+ await w.adapter.tick('demo');
|
||||||
|
+ assert.equal(w.snapshots(ref(kept, tasks.kept)).at(-1).fields.title, 'kept again');
|
||||||
|
+ // The root answering 403 or 404 still refuses the tick, and stays in the tree.
|
||||||
|
+ w.fake.unshare(w.project, w.bots.sync);
|
||||||
|
+ await assert.rejects(w.adapter.tick('demo'), { code: 'tracker-forbidden' });
|
||||||
|
+ w.fake.share(w.project, w.bots.sync, 0);
|
||||||
|
+ await w.ui('PATCH', `/tasks/${tasks.root}`, { title: 'root again' });
|
||||||
|
+ await sleep(20);
|
||||||
|
+ await w.adapter.tick('demo');
|
||||||
|
+ assert.equal(w.snapshots(ref(w.project, tasks.root)).at(-1).fields.title, 'root again');
|
||||||
|
+ w.fake.deleteProject(w.project);
|
||||||
|
+ await assert.rejects(w.adapter.tick('demo'), { code: 'tracker-not-found' });
|
||||||
|
+});
|
||||||
|
+test('a reconcile that fails keeps the previous tree', async (t) => {
|
||||||
|
+ const w = await world(t, TREE);
|
||||||
|
+ const first = w.fake.project('First', w.owner, { parent: w.project });
|
||||||
|
+ const second = w.fake.project('Second', w.owner, { parent: w.project });
|
||||||
|
+ w.fake.install(second);
|
||||||
|
+ const task = await add(w, second, 'second');
|
||||||
|
+ await ready(w);
|
||||||
|
+ // The first child's install check fails, so the walk stops before it reaches the second.
|
||||||
|
+ w.fake.fault('GET', new RegExp(`^/projects/${first}/views$`), 503);
|
||||||
|
+ await assert.rejects(w.adapter.reconcile('demo'), { code: 'tracker-unavailable' });
|
||||||
|
+ await w.ui('PATCH', `/tasks/${task}`, { title: 'renamed' });
|
||||||
|
+ await sleep(20);
|
||||||
|
+ await w.adapter.tick('demo');
|
||||||
|
+ assert.equal(w.snapshots(ref(second, task)).at(-1).fields.title, 'renamed');
|
||||||
|
+ assert.deepEqual(missing(w), []);
|
||||||
|
+});
|
||||||
|
+test("a closed task the cursor missed takes its own project's done bucket", async (t) => {
|
||||||
|
+ // An hour behind, so the cursor never returns the close and missing() reads the task.
|
||||||
|
+ const w = await world(t, { ...TREE, fake: behind });
|
||||||
|
+ const child = w.fake.project('Child', w.owner, { parent: w.project });
|
||||||
|
+ const childBoard = w.fake.install(child);
|
||||||
|
+ const watched = w.fake.project('Watched', w.owner, { parent: w.project });
|
||||||
|
+ const tasks = { child: await add(w, child, 'child'), watched: await add(w, watched, 'watched') };
|
||||||
|
+ await ready(w);
|
||||||
|
+ for (const id of Object.values(tasks)) await w.ui('PATCH', `/tasks/${id}`, { done: true });
|
||||||
|
+ await sleep(20);
|
||||||
|
+ await w.adapter.tick('demo');
|
||||||
|
+ const child1 = w.snapshots(ref(child, tasks.child)).at(-1);
|
||||||
|
+ const watched1 = w.snapshots(ref(watched, tasks.watched)).at(-1);
|
||||||
|
+ assert.deepEqual([child1.via, child1.fields.done, child1.fields.bucket], ['task', true, childBoard.buckets.done]);
|
||||||
|
+ assert.deepEqual([watched1.via, watched1.fields.done, watched1.fields.bucket], ['task', true, WATCH_ONLY]);
|
||||||
|
+ assert.notEqual(childBoard.buckets.done, w.installed.buckets.done);
|
||||||
|
+});
|
||||||
|
+test("closed tasks read at startup take their own project's done bucket", async (t) => {
|
||||||
|
+ const w = await world(t, TREE);
|
||||||
|
+ const child = w.fake.project('Child', w.owner, { parent: w.project });
|
||||||
|
+ const childBoard = w.fake.install(child);
|
||||||
|
+ const watched = w.fake.project('Watched', w.owner, { parent: w.project });
|
||||||
|
+ const tasks = { child: await add(w, child, 'child'), watched: await add(w, watched, 'watched') };
|
||||||
|
+ for (const id of Object.values(tasks)) await w.ui('PATCH', `/tasks/${id}`, { done: true });
|
||||||
|
+ await ready(w);
|
||||||
|
+ const child1 = w.snapshots(ref(child, tasks.child)).at(-1);
|
||||||
|
+ const watched1 = w.snapshots(ref(watched, tasks.watched)).at(-1);
|
||||||
|
+ assert.deepEqual([child1.via, child1.fields.done, child1.fields.bucket], ['reconcile', true, childBoard.buckets.done]);
|
||||||
|
+ assert.deepEqual([watched1.via, watched1.fields.done, watched1.fields.bucket], ['reconcile', true, WATCH_ONLY]);
|
||||||
|
+});
|
||||||
|
+test("a closed task the cursor returns takes its own project's done bucket", async (t) => {
|
||||||
|
+ // Two closes per project: one before the tick, so the open read leaves it out, and one between the
|
||||||
|
+ // open read and the cursor walk, so both reads return it. `race` closes the second kind.
|
||||||
|
+ const race = new Map();
|
||||||
|
+ const w = await world(t, {
|
||||||
|
+ ...TREE,
|
||||||
|
+ fetch: async (url, init) => {
|
||||||
|
+ const r = await w.fake.fetch(url, init);
|
||||||
|
+ for (const [project, id] of race)
|
||||||
|
+ if (new URL(url).pathname.startsWith(`/api/v2/projects/${project}/`)) {
|
||||||
|
+ race.delete(project);
|
||||||
|
+ await w.ui('PATCH', `/tasks/${id}`, { done: true });
|
||||||
|
+ }
|
||||||
|
+ return r;
|
||||||
|
+ },
|
||||||
|
+ });
|
||||||
|
+ const child = w.fake.project('Child', w.owner, { parent: w.project });
|
||||||
|
+ const childBoard = w.fake.install(child);
|
||||||
|
+ const watched = w.fake.project('Watched', w.owner, { parent: w.project });
|
||||||
|
+ const before = { [child]: await add(w, child, 'child'), [watched]: await add(w, watched, 'watched') };
|
||||||
|
+ const during = { [child]: await add(w, child, 'child raced'), [watched]: await add(w, watched, 'watched raced') };
|
||||||
|
+ await ready(w);
|
||||||
|
+ for (const id of Object.values(before)) await w.ui('PATCH', `/tasks/${id}`, { done: true });
|
||||||
|
+ for (const [project, id] of Object.entries(during)) race.set(+project, id);
|
||||||
|
+ await sleep(20);
|
||||||
|
+ await w.adapter.tick('demo');
|
||||||
|
+ assert.equal(race.size, 0);
|
||||||
|
+ for (const [project, bucket] of [[child, childBoard.buckets.done], [watched, WATCH_ONLY]])
|
||||||
|
+ for (const id of [before[project], during[project]]) {
|
||||||
|
+ const s = w.snapshots(ref(project, id)).at(-1);
|
||||||
|
+ assert.deepEqual([s.via, s.fields.done, s.fields.bucket], ['cursor', true, bucket]);
|
||||||
|
+ }
|
||||||
|
+});
|
||||||
|
+test('a task moved out of a child that leaves the tree at the same time is moved, not no-access', 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, 'leaving');
|
||||||
|
+ await ready(w);
|
||||||
|
+ w.fake.archive(child);
|
||||||
|
+ w.fake.moveToProject(task, w.project);
|
||||||
|
+ await sleep(20);
|
||||||
|
+ await w.adapter.reconcile('demo');
|
||||||
|
+ assert.deepEqual(missing(w), [[ref(child, task), { reason: 'moved', project: w.project }]]);
|
||||||
|
+ assert.equal(w.snapshots(ref(w.project, task)).at(-1).fields.title, 'leaving');
|
||||||
|
+});
|
||||||
+test('a task in a bucket outside the five keeps its raw bucket id', async (t) => {
|
+test('a task in a bucket outside the five keeps its raw bucket id', async (t) => {
|
||||||
+ const w = await ready(await world(t, TREE));
|
+ const w = await ready(await world(t, TREE));
|
||||||
+ const backlog = w.fake.bucket(w.installed.view, 'Backlog');
|
+ const backlog = w.fake.bucket(w.installed.view, 'Backlog');
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ fcc95afc418823416c87475de0f6fcad33d312588d22e9d6fd5784d9e2478f0e packages/cli/s
|
|||||||
1737237bbf062f83eeffeb7fb1a604bc6676be9f9db1e6b91844587f77491a1c packages/cli/tests/config.test.mjs
|
1737237bbf062f83eeffeb7fb1a604bc6676be9f9db1e6b91844587f77491a1c packages/cli/tests/config.test.mjs
|
||||||
4ada6f215c8fb42bc95d46121e7c61c2477f7b1b1d7120675443b91cc4574fdc packages/cli/tests/trackers-boot.test.mjs
|
4ada6f215c8fb42bc95d46121e7c61c2477f7b1b1d7120675443b91cc4574fdc packages/cli/tests/trackers-boot.test.mjs
|
||||||
aad24a5db15943eba6c0bf5fd93add3f4f7b265ff5868c46e405e976bc26ce22 packages/tasks/live/README.md
|
aad24a5db15943eba6c0bf5fd93add3f4f7b265ff5868c46e405e976bc26ce22 packages/tasks/live/README.md
|
||||||
d31be986f6f5f972bb6166eee64b9ad2e3ad202c946b92af39ab69339d6225ac packages/tasks/README.md
|
0046d1f28fad5405ad2e27e1e350e20b1ab7df0b2795b5efa7bf11bb856d525b packages/tasks/README.md
|
||||||
02a802a9bb1306bf5f82b8e56ca2064c308f1b7574eebba0f47932cdea7f41d1 packages/tasks/src/adapter.mjs
|
02a802a9bb1306bf5f82b8e56ca2064c308f1b7574eebba0f47932cdea7f41d1 packages/tasks/src/adapter.mjs
|
||||||
aeb65af54d716a36bd7e790c41cad3c4ca516ab8384b288e23ecd04d82cdd366 packages/tasks/src/fake.mjs
|
7c10b7e24a34e04179465c372a2660141c2eeea8b5d954ea7f6372025f567166 packages/tasks/src/fake.mjs
|
||||||
e0c754aacf7e82c2fe18fb0548674044ac949ffc94435b353e4bd7aebcf797c2 packages/tasks/src/startup.mjs
|
e0c754aacf7e82c2fe18fb0548674044ac949ffc94435b353e4bd7aebcf797c2 packages/tasks/src/startup.mjs
|
||||||
4a329bc76c4909490ac064332ccd0083365a4f7fcbffcd69ef17c327a288fc82 packages/tasks/src/sync.mjs
|
9da4ab6ff55912e9d42c589fcb970493af1f3ada3056235f1d8c72c1cc62d597 packages/tasks/src/sync.mjs
|
||||||
e8798609ec1bf7d4901f7f94b6346b37c7ea4504ee2eb781b07c634001e3bf72 packages/tasks/tests/shapes.test.mjs
|
e8798609ec1bf7d4901f7f94b6346b37c7ea4504ee2eb781b07c634001e3bf72 packages/tasks/tests/shapes.test.mjs
|
||||||
82f6a673a59dc214a65d20af597096c6c3f86ad2c4d39bb25d617ad2515a9e4b packages/tasks/tests/tree.test.mjs
|
1ad138752c83159813eeb586af87d6ad0690922791d33baa28ca346538a24d85 packages/tasks/tests/tree.test.mjs
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
applied to 122b122374ce93523efa13cd30676918c416f106
|
||||||
|
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,82 @@
|
|||||||
|
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (218.462729ms)
|
||||||
|
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (354.617189ms)
|
||||||
|
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (372.926296ms)
|
||||||
|
✔ launch events require a human CLI capability; generic emit cannot forge authority events (341.363704ms)
|
||||||
|
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (411.683699ms)
|
||||||
|
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (224.338178ms)
|
||||||
|
✔ task action subjects and linked decision trail are complete and ordered (286.770879ms)
|
||||||
|
✔ launch binding is durable and reconnecting requires the identical trusted record (105.276106ms)
|
||||||
|
✔ business isolation includes inherited object names and cross-business message references (250.798968ms)
|
||||||
|
✔ authority never transfers between action, run, target, unresolved or replaced role holder (321.819141ms)
|
||||||
|
✔ task projection uses schema current view, skipping earlier and equal-start polls (171.819022ms)
|
||||||
|
✔ revocation permanently bars the old run from reclaiming first, including after broker restart (245.21846ms)
|
||||||
|
✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (166.700792ms)
|
||||||
|
✔ both arbiters require human resolution when their cross-role route is themselves (551.393275ms)
|
||||||
|
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (2.910974ms)
|
||||||
|
✔ only validated broker references load; returned data and exceptions cannot expose a known token (6.240569ms)
|
||||||
|
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (4.336874ms)
|
||||||
|
✔ expiry refuses use and env references never become client data (1.121475ms)
|
||||||
|
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.869207ms)
|
||||||
|
✔ opaque tokens shorter than 16 characters refuse before use (0.473295ms)
|
||||||
|
✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (177.605913ms)
|
||||||
|
✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (157.34579ms)
|
||||||
|
✔ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (223.974661ms)
|
||||||
|
✔ endLaunch leaves a claim another run took alone (230.499229ms)
|
||||||
|
✔ refuse records action.refused against the caller with the code only (165.998253ms)
|
||||||
|
✔ launches off refuses role.launch with launch-revoked until launches on (357.816434ms)
|
||||||
|
✔ broker process: launch ops authorize role.launch, record refusals and end runs (509.594782ms)
|
||||||
|
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (3.480324ms)
|
||||||
|
✔ process reader gets own kernel identity without exposing environment values (1.983378ms)
|
||||||
|
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (1.217437ms)
|
||||||
|
✔ real pid 1 remains inspectable when its environment is protected (0.490238ms)
|
||||||
|
✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (240.336247ms)
|
||||||
|
✔ missing and foreign-business citations refuse and roll back message and grant (245.838541ms)
|
||||||
|
✔ cross-role sends still need a matching resolved decision and consume it once (363.752707ms)
|
||||||
|
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (237.775427ms)
|
||||||
|
✔ startup token refusal returns safe code without value or partial listening broker (44.4025ms)
|
||||||
|
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (229.876291ms)
|
||||||
|
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (236.471994ms)
|
||||||
|
✔ trusted host registers later launches; socket clients never have a registration verb (327.76203ms)
|
||||||
|
✔ runtime excludes declared project roots even when host supplies no repoRoots (50.018254ms)
|
||||||
|
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (273.066116ms)
|
||||||
|
✔ v3b prototype refusals, views and append-only mutations (1741.541512ms)
|
||||||
|
✔ gated approval authorizes once, survives store reopen, and fresh approval works (332.044052ms)
|
||||||
|
✔ another run cannot consume an approval; a failed check leaves it usable (332.679389ms)
|
||||||
|
✔ two scheduled callers have exactly one grant and one consumed refusal (230.939095ms)
|
||||||
|
✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (660.797841ms)
|
||||||
|
✔ class drift gated to cross-role refuses before consumption (327.332719ms)
|
||||||
|
✔ class drift cross-role to gated refuses before consumption (315.671233ms)
|
||||||
|
✔ class drift gated to within-role refuses before consumption (268.976252ms)
|
||||||
|
✔ class drift cross-role to within-role refuses before consumption (275.814754ms)
|
||||||
|
✔ class drift within-role to gated refuses before consumption (197.24665ms)
|
||||||
|
✔ class drift within-role to cross-role refuses before consumption (235.493369ms)
|
||||||
|
✔ message.send consumes approval and prevents a later send or authorize (266.078196ms)
|
||||||
|
✔ role.revoke consumes approval and prevents a later revoke or authorize (572.874238ms)
|
||||||
|
✔ creates private WAL store and excludes a second writer until explicit close (168.224993ms)
|
||||||
|
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (216.05144ms)
|
||||||
|
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (278.084495ms)
|
||||||
|
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (221.900704ms)
|
||||||
|
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (231.193074ms)
|
||||||
|
✔ async transactions refuse before invoking their function (135.418981ms)
|
||||||
|
✔ recordTask keeps sync reads and a role write apart (236.70867ms)
|
||||||
|
✔ read_at must be one canonical UTC format, so the projection compares strings safely (139.263599ms)
|
||||||
|
✔ a bad entry refuses the whole record (145.9625ms)
|
||||||
|
✔ taskView reads the projection for one business (193.964985ms)
|
||||||
|
✔ requestTask hands only a holder and a task verb to the handler, and records refusals (417.685322ms)
|
||||||
|
✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (532.771002ms)
|
||||||
|
✔ without an adapter the server refuses every task verb (337.290883ms)
|
||||||
|
✔ the runtime refuses an invalid adapter and closes a valid one (234.789318ms)
|
||||||
|
✔ the process loads the S3 adapter from plain-data trackers (333.283191ms)
|
||||||
|
✔ socket capability stamps launch identity; shared views use wire, no SQL client (205.046759ms)
|
||||||
|
✔ two wire claims serialize; a lost reply never automatically retries (231.29236ms)
|
||||||
|
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (186.135632ms)
|
||||||
|
✔ client preserves UTF-8 when a response divides a multibyte character (12.872512ms)
|
||||||
|
✔ committed mutation followed by dropped reply reports unknown and is never retried (198.89444ms)
|
||||||
|
ℹ tests 74
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 74
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 4131.258628
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
✔ config directory and file path follow MOSAIC_CONFIG (1.978502ms)
|
||||||
|
✔ the fixture business validates and comes back frozen (7.384652ms)
|
||||||
|
✔ two instances may share a definition (1.434559ms)
|
||||||
|
✔ top-level refusals (5.157183ms)
|
||||||
|
✔ arbiters and projects (5.91723ms)
|
||||||
|
✔ role instances (4.939451ms)
|
||||||
|
✔ Vikunja bots (6.984924ms)
|
||||||
|
✔ a role without Vikunja takes no tracker block (2.30441ms)
|
||||||
|
✔ credential references match the definition's services (3.466293ms)
|
||||||
|
✔ launch (8.825317ms)
|
||||||
|
✔ loadBusiness: file checks (2.21601ms)
|
||||||
|
✔ loadBusiness: not a regular file (42.184267ms)
|
||||||
|
✔ loading writes nothing (1.066244ms)
|
||||||
|
✔ names that are Object.prototype properties don't count as declared (3.433313ms)
|
||||||
|
✔ the shipped example refuses as written and validates once filled in (0.874682ms)
|
||||||
|
✔ usage errors exit 4 (331.833341ms)
|
||||||
|
✔ validate: a good business exits 0 and prints instance digests (79.043475ms)
|
||||||
|
✔ validate: project files (384.043082ms)
|
||||||
|
✔ validate: missing files and a broken system config (320.51191ms)
|
||||||
|
✔ validate: credential reference problems exit 2 and name each one (90.427062ms)
|
||||||
|
✔ validate: a token file inside the repository is refused (85.909468ms)
|
||||||
|
✔ validate: role definitions come from MOSAIC_ROLES_DIR (300.476894ms)
|
||||||
|
✔ resolve: prints one instance's record (328.180843ms)
|
||||||
|
✔ resolve: refusals (477.790333ms)
|
||||||
|
✔ parse: exactly one of file or env, plus the service's date (2.520331ms)
|
||||||
|
✔ check: a good file has no problems (0.911946ms)
|
||||||
|
✔ check never opens the file: a write-only token passes (0.452923ms)
|
||||||
|
✔ check: file problems (0.955862ms)
|
||||||
|
✔ check: token files can't live in the repository or dataRoot, even through a linked directory (1.075624ms)
|
||||||
|
✔ check: dates and environment references (0.449484ms)
|
||||||
|
✔ path and load (2.168977ms)
|
||||||
|
✔ refusals (1.226372ms)
|
||||||
|
✔ systemVars flattens the validated config (2.835373ms)
|
||||||
|
✔ precedence: system, business, project, project role, agent (7.138645ms)
|
||||||
|
✔ limits narrow the definition and never widen it (3.318931ms)
|
||||||
|
✔ role.launch stays within-role only for the instance the launch block names (6.832042ms)
|
||||||
|
✔ limits.authority without role.launch leaves the launcher with no launch block (2.141851ms)
|
||||||
|
✔ limits.authority narrows cross-role actions too (1.045083ms)
|
||||||
|
✔ classify (1.199033ms)
|
||||||
|
✔ the record carries what the broker and launcher need (1.039442ms)
|
||||||
|
✔ digest: key order doesn't matter, any value change does (6.402912ms)
|
||||||
|
✔ refusals (2.267202ms)
|
||||||
|
✔ the four shipped version 2 roles load (3.286574ms)
|
||||||
|
✔ shipped role scopes match addendum B section 2 and the SR runbook (1.163255ms)
|
||||||
|
✔ shipped authority follows the note's table (0.540544ms)
|
||||||
|
✔ version 1 files keep loading with no authority (0.981928ms)
|
||||||
|
✔ the conductor policy isn't a role (0.219416ms)
|
||||||
|
✔ a missing role file is exit 4, a symbolic link too (0.387333ms)
|
||||||
|
✔ version 2 refusals (1.268899ms)
|
||||||
|
✔ authority: closed vocabulary, no gated-only action, no overlap (2.305145ms)
|
||||||
|
✔ credentials: Gitea scopes (0.90998ms)
|
||||||
|
✔ credentials: Vikunja scopes are a group-to-verbs map from the grantable list (1.082646ms)
|
||||||
|
✔ credentials: services (0.615176ms)
|
||||||
|
✔ contract: a non-empty regular Markdown file beside the role file (1.012181ms)
|
||||||
|
✔ every key names known layers and a merge rule (1.254516ms)
|
||||||
|
✔ unknown keys and wrong layers refuse (1.061933ms)
|
||||||
|
✔ types (2.78491ms)
|
||||||
|
✔ merge: defaults, then the most specific layer wins (0.419958ms)
|
||||||
|
✔ merge: limits only narrow, and provenance lists each source (0.538889ms)
|
||||||
|
✔ merge doesn't change its inputs (0.215515ms)
|
||||||
|
ℹ tests 60
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 60
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 2477.006598
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (162.925221ms)
|
||||||
|
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (182.251802ms)
|
||||||
|
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (166.25572ms)
|
||||||
|
✔ decide prints a declining choice as declining (147.278716ms)
|
||||||
|
✔ an unknown outcome is reported once and never resent (142.475415ms)
|
||||||
|
✔ a decision closed before the answer arrives exits 2 and points at its trail (130.260878ms)
|
||||||
|
✔ a prefix that matches two open decisions exits 2 and resolves neither (143.947093ms)
|
||||||
|
✔ without --business a command uses the live host's business, and a stale host.json is not a host (177.749907ms)
|
||||||
|
✔ every human command refuses inside an agent run before it touches the bus (108.895465ms)
|
||||||
|
✔ usage errors exit 4; no business and no host is a usage error (109.197427ms)
|
||||||
|
✔ agents and tasks print through the broker (112.594851ms)
|
||||||
|
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.463394ms)
|
||||||
|
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (53.698161ms)
|
||||||
|
✔ trackers come from the tracker.* variables of the one project that names a tracker project (81.53185ms)
|
||||||
|
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (37.932637ms)
|
||||||
|
✔ two projects that each name a tracker project refuse, since the boot shape holds one (41.661233ms)
|
||||||
|
✔ a business without tracker.baseUrl gets no trackers entry (31.653215ms)
|
||||||
|
✔ an unknown business and a broken system config refuse with exit 3 (66.990584ms)
|
||||||
|
✔ empty views say so (1.09277ms)
|
||||||
|
✔ the trail keeps the broker's order and names a decision's task without its rows (1.127964ms)
|
||||||
|
✔ tasks print the tracker fields the snapshot carries (0.188638ms)
|
||||||
|
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (992.314523ms)
|
||||||
|
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (334.682755ms)
|
||||||
|
✔ a second host for the same data root refuses with exit 3 while the first runs (176.732003ms)
|
||||||
|
✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1175.261138ms)
|
||||||
|
✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (288.254111ms)
|
||||||
|
✔ a broker reply with no request waiting breaks the channel and the host exits 1 (142.754572ms)
|
||||||
|
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (200.347497ms)
|
||||||
|
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (529.528805ms)
|
||||||
|
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (141.853909ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (197.512636ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (125.316072ms)
|
||||||
|
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (225.533088ms)
|
||||||
|
✔ watchChildren reports a child that died before it was called, and one that dies later (30.566092ms)
|
||||||
|
✔ bus stop refuses to signal a live pid that is not a bus host (203.494441ms)
|
||||||
|
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (280.081496ms)
|
||||||
|
✔ bus start refuses with exit 3 without a notifier config (105.033205ms)
|
||||||
|
✔ bus start runs until bus stop; status reports it while it runs (764.390227ms)
|
||||||
|
✔ bus-service.sh renders the unit and installs it into a given directory (43.567261ms)
|
||||||
|
✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1347.403957ms)
|
||||||
|
✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (853.680971ms)
|
||||||
|
✔ a runner that stops at once ends its launch with the runner's reason (224.405387ms)
|
||||||
|
✔ 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 (751.409243ms)
|
||||||
|
✔ 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 (3705.635931ms)
|
||||||
|
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (132.284495ms)
|
||||||
|
✔ an over-long launch request is refused at once, not at the 10 s idle timeout (139.693059ms)
|
||||||
|
✔ a launch client that never closes its side doesn't hold the host's close (259.187421ms)
|
||||||
|
✔ a runner that ignores SIGTERM is killed when the host closes (1266.925818ms)
|
||||||
|
✔ zoned uses the IANA zone across DST (17.516092ms)
|
||||||
|
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (152.230322ms)
|
||||||
|
✔ two blocking decisions get two DMs with different nonces (172.590193ms)
|
||||||
|
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.333792ms)
|
||||||
|
✔ a failed DM is journaled, backs off, and is retried until it lands (177.764083ms)
|
||||||
|
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (128.098051ms)
|
||||||
|
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (140.484015ms)
|
||||||
|
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (129.686384ms)
|
||||||
|
✔ 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 (226.3129ms)
|
||||||
|
✔ a restart after the second refusal does not send before that refusal's 30 min are up (213.97481ms)
|
||||||
|
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (127.497558ms)
|
||||||
|
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (117.442063ms)
|
||||||
|
✔ an inbox read failure is logged and the next poll retries (1.070536ms)
|
||||||
|
✔ no Discord id reaches the journal or the log (100.336974ms)
|
||||||
|
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (36.322406ms)
|
||||||
|
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (40.731632ms)
|
||||||
|
✔ the journal: a whole file that is one torn line truncates to empty (21.709268ms)
|
||||||
|
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.873378ms)
|
||||||
|
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.733563ms)
|
||||||
|
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.331385ms)
|
||||||
|
✔ the journal: a symlinked directory refuses and says it is a link (0.408101ms)
|
||||||
|
✔ 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.576654ms)
|
||||||
|
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.396195ms)
|
||||||
|
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.447894ms)
|
||||||
|
✔ digest content stays within Discord's 2000 characters (0.277436ms)
|
||||||
|
✔ runLoop never overlaps ticks and stops after the one in flight (110.874109ms)
|
||||||
|
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 (463.164325ms)
|
||||||
|
✔ the transport writes {business, verb, args} to the child and reads its JSON (49.283565ms)
|
||||||
|
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2168.481312ms)
|
||||||
|
✔ busExit and refuseInsideAgent (0.475616ms)
|
||||||
|
✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (277.535234ms)
|
||||||
|
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1169.155818ms)
|
||||||
|
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (116.277509ms)
|
||||||
|
✔ launches off and on go to the broker and change the business's launch state (136.842866ms)
|
||||||
|
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (91.465756ms)
|
||||||
|
ℹ tests 83
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 83
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 8804.490389
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
export TMPDIR=~/darkwing-scratch/r62/tmp
|
||||||
|
G=~/darkwing-scratch/r62/gate; O=~/darkwing-scratch/r62/out2; 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; 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,14 @@
|
|||||||
|
tasks rc=0
|
||||||
|
business rc=0
|
||||||
|
bus rc=0
|
||||||
|
cli 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,77 @@
|
|||||||
|
✔ the boot config is checked before anything starts (643.655096ms)
|
||||||
|
✔ a business with no tracker entry refuses task verbs (581.977517ms)
|
||||||
|
✔ credential.expiring and .expired are recorded once per instance (352.915926ms)
|
||||||
|
✔ a token file that changes on disk records credential.changed (745.419237ms)
|
||||||
|
✔ autostart polls, reconciles and retries a startup the tracker was down for (398.375871ms)
|
||||||
|
✔ a refusal a restart must clear is not retried by the poll (364.705049ms)
|
||||||
|
✔ a poll that fires while two are queued is dropped (246.79161ms)
|
||||||
|
✔ close waits for a running verb and refuses one that has not started (932.771822ms)
|
||||||
|
✔ the bundled Vikunja is the pinned upstream image the runbook names (2.033688ms)
|
||||||
|
✔ every published port is on 127.0.0.1, and no secret is in the file (0.660125ms)
|
||||||
|
✔ the fake answers each route with the statuses and shapes Vikunja v2.7.0 sent (1406.688865ms)
|
||||||
|
✔ the project list holds recorded project objects in the recorded list envelope (174.804414ms)
|
||||||
|
✔ the recorded task bodies pass the checks S3 applies to every read (0.847543ms)
|
||||||
|
✔ the client works against the fake over real HTTP with the platform fetch (1017.104557ms)
|
||||||
|
✔ a correct install starts, and the first reconcile records tasks that already exist (811.278726ms)
|
||||||
|
✔ verbs refuse while a business is starting and after startup refused it (440.026378ms)
|
||||||
|
✔ startup refuses a token that can do more than its role needs (1390.596831ms)
|
||||||
|
✔ startup refuses an unsupported version and flags an untested one (783.84498ms)
|
||||||
|
✔ startup refuses a board that the runbook did not install (1623.41702ms)
|
||||||
|
✔ startup refuses a project the sync bot cannot read (526.639287ms)
|
||||||
|
✔ startup refuses a configured label the pm bot cannot see (404.062165ms)
|
||||||
|
✔ startup refuses an expired credential and a missing sync credential (450.218341ms)
|
||||||
|
✔ an unreachable tracker refuses with tracker-unavailable (219.120343ms)
|
||||||
|
✔ an edit in the UI is recorded once, with the fields that changed (1139.80552ms)
|
||||||
|
✔ a move between open buckets is seen on the board, though updated does not change (261.364781ms)
|
||||||
|
✔ a person's comment is counted and a bot's is not (1033.64844ms)
|
||||||
|
✔ the hourly reconcile catches a comment through comment_count (494.61155ms)
|
||||||
|
✔ a task closed in the UI leaves the open view with its done bucket (1408.829152ms)
|
||||||
|
✔ a task that leaves the board is recorded as deleted, moved or out of reach (709.138576ms)
|
||||||
|
✔ a poll that read before a verb wrote does not overwrite the verb (785.91972ms)
|
||||||
|
✔ a tracker fault during a tick is reported and the next tick catches up (474.126722ms)
|
||||||
|
✔ a malformed answer refuses the tick with tracker-shape (295.591722ms)
|
||||||
|
✔ no token value reaches the database, the log or a refusal (505.66392ms)
|
||||||
|
✔ the first look at a task counts only comments inside the window (306.184749ms)
|
||||||
|
✔ with descendants off the adapter reads no project list and no child, as in v0 (673.262409ms)
|
||||||
|
✔ a three-level tree is read per project, and a project outside it is not (531.895447ms)
|
||||||
|
✔ a watch-only child keeps the business ready, and a failed child read refuses as transient (347.380776ms)
|
||||||
|
✔ a root without the installed buckets refuses with descendants on (468.024278ms)
|
||||||
|
✔ the project list refuses without read_all, and skips archived projects and saved filters (809.241784ms)
|
||||||
|
✔ descendants must be true or false (312.832088ms)
|
||||||
|
✔ a move inside the tree is a moved tombstone plus a snapshot under the new ref (642.778388ms)
|
||||||
|
✔ a child made after startup joins the tree at the next reconcile (553.603198ms)
|
||||||
|
✔ a child that leaves the tree has its tasks recorded missing, and the ledger keeps their history (307.23534ms)
|
||||||
|
✔ a child deleted or hidden between reconciles drops out at the next tick, and the root still refuses (572.747637ms)
|
||||||
|
✔ a reconcile that fails keeps the previous tree (713.775563ms)
|
||||||
|
✔ a closed task the cursor missed takes its own project's done bucket (339.618923ms)
|
||||||
|
✔ closed tasks read at startup take their own project's done bucket (183.005435ms)
|
||||||
|
✔ a closed task the cursor returns takes its own project's done bucket (273.256186ms)
|
||||||
|
✔ a task moved out of a child that leaves the tree at the same time is moved, not no-access (175.981532ms)
|
||||||
|
✔ a task in a bucket outside the five keeps its raw bucket id (204.599613ms)
|
||||||
|
✔ bucket 0 on a watch-only task never becomes a bucket or a state (344.624141ms)
|
||||||
|
✔ task.create needs a recorded human request and a requirement id (1178.145145ms)
|
||||||
|
✔ only labels named in the business file can be written (362.228377ms)
|
||||||
|
✔ task.schedule sets and clears a due date and relations (1219.247533ms)
|
||||||
|
✔ assign and reassign move the role bots and record task.assigned (1230.981451ms)
|
||||||
|
✔ task.update.assigned is for the assignee and records task.state (767.300117ms)
|
||||||
|
✔ a wrong expected digest records task.conflict and writes nothing (905.809307ms)
|
||||||
|
✔ a cross-role verb needs a resolved decision, used once (716.081644ms)
|
||||||
|
✔ task.close needs a verdict; after it every verb refuses with task-done (494.232939ms)
|
||||||
|
✔ a lost answer is settled by a re-read and never retried (488.608777ms)
|
||||||
|
✔ a create whose answer is lost is reported uncertain, and the poll finds the task (294.180591ms)
|
||||||
|
✔ a task the sync bot cannot read refuses and records nothing (166.991377ms)
|
||||||
|
✔ verbs and polls for one business run one at a time (220.421246ms)
|
||||||
|
✔ a due date with milliseconds is written to the second (223.178126ms)
|
||||||
|
✔ every write landed and the final read failed: the verb succeeds and records what it wrote (123.39974ms)
|
||||||
|
✔ some writes landed and the final read failed: write-uncertain, and nothing is recorded (111.854725ms)
|
||||||
|
✔ a create whose final read fails succeeds and records task.created (114.756088ms)
|
||||||
|
✔ an edit between the last write and the final read shows as external on the next poll (146.233806ms)
|
||||||
|
✔ task.created is recorded when a later label write fails (105.655805ms)
|
||||||
|
ℹ tests 69
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 69
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 9206.28559
|
||||||
@@ -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 '122b122374ce93523efa13cd30676918c416f106'.
|
||||||
|
|
||||||
|
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 '122b122374ce93523efa13cd30676918c416f106'.
|
||||||
|
|
||||||
|
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: 3052519 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
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Round 2. Same method as mutants.sh: each mutant edits one source file in the build worktree, runs
|
||||||
|
# the tests, then restores the file from the worktree index (the round 2 candidate). Round 1's five
|
||||||
|
# run again first. Each runs only the test file that should fail it, since the row 55 mutant run
|
||||||
|
# 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/mut2-$1.log 2>&1
|
||||||
|
echo "$1: rc=$? $(grep -E '^ℹ (pass|fail) ' ~/darkwing-scratch/r62/tmp/mut2-$1.log | tr '\n' ' ')"
|
||||||
|
grep -E '^✖ ' ~/darkwing-scratch/r62/tmp/mut2-$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, a child that answers 403 or 404 in a tick.
|
||||||
|
run r2-child-refuses $S 's/ if (!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/ project !== ctx.project \&\& e instanceof BusError/ e instanceof BusError/' $T
|
||||||
|
run r2-any-error-drops $S "s/e instanceof BusError \&\& (e.code === 'tracker-forbidden' || e.code === 'tracker-not-found');/e instanceof BusError;/" $T
|
||||||
|
run r2-403-only $S "s/ || e.code === 'tracker-not-found')/)/" $T
|
||||||
|
run r2-404-only $S "s/(e.code === 'tracker-forbidden' || /(/" $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
|
||||||
|
git diff --quiet -- $S $U packages/cli/src/config.mjs && echo "restored: sources match the candidate index"
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
no-leave-tree-check: rc=1 ℹ pass 16 ℹ 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 8 ℹ fail 9
|
||||||
|
✖ 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 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
|
||||||
|
✖ a three-level tree is read per project, and a project outside it is not
|
||||||
|
placed-by-id: rc=1 ℹ pass 16 ℹ 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 16 ℹ 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 2
|
||||||
|
✖ a child deleted or hidden between reconciles drops out at the next tick, and the root still refuses
|
||||||
|
✖ a child that leaves the tree has its tasks recorded missing, and the ledger keeps their history
|
||||||
|
✖ failing tests:
|
||||||
|
r2-skip-keeps-child: rc=1 ℹ pass 16 ℹ fail 1
|
||||||
|
✖ a child deleted or hidden between reconciles drops out at the next tick, and the root still refuses
|
||||||
|
✖ failing tests:
|
||||||
|
r2-root-drops: rc=1 ℹ pass 16 ℹ 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 16 ℹ fail 1
|
||||||
|
✖ a child deleted or hidden between reconciles drops out at the next tick, and the root still refuses
|
||||||
|
✖ failing tests:
|
||||||
|
r2-403-only: rc=1 ℹ pass 16 ℹ fail 1
|
||||||
|
✖ a child deleted or hidden between reconciles drops out at the next tick, and the root still refuses
|
||||||
|
✖ failing tests:
|
||||||
|
r2-404-only: rc=1 ℹ pass 15 ℹ fail 2
|
||||||
|
✖ a child deleted or hidden between reconciles drops out at the next tick, and the root still refuses
|
||||||
|
✖ a child that leaves the tree has its tasks recorded missing, and the ledger keeps their history
|
||||||
|
✖ failing tests:
|
||||||
|
tree-early: rc=1 ℹ pass 16 ℹ fail 1
|
||||||
|
✖ a reconcile that fails keeps the previous tree
|
||||||
|
✖ failing tests:
|
||||||
|
missing-done-root-bucket: rc=1 ℹ pass 16 ℹ 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 16 ℹ 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 16 ℹ fail 1
|
||||||
|
✖ closed tasks read at startup take their own project's done bucket
|
||||||
|
✖ failing tests:
|
||||||
|
tick-done-root-bucket: rc=1 ℹ pass 15 ℹ 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 16 ℹ fail 1
|
||||||
|
✖ a closed task the cursor returns takes its own project's done bucket
|
||||||
|
✖ failing tests:
|
||||||
|
tree-check-before-moved: rc=1 ℹ pass 16 ℹ fail 1
|
||||||
|
✖ a task moved out of a child that leaves the tree at the same time is moved, not no-access
|
||||||
|
✖ failing tests:
|
||||||
|
restored: sources match the candidate index
|
||||||
Reference in New Issue
Block a user