docs(queue-62): row 62 candidate packet, project trees (darkwing)
Candidate manifest sha256 ec3f7a42f3bf6436634a5a83a12bb5678ca1e58a10ff4f66a23dfca7ff3f375f, 13 files onedc5de71. Gate on6ae8b844: 13 suites green. The candidate itself is not committed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,225 @@
|
|||||||
|
# Row 62 (#1555): the adapter reads a project tree, candidate packet
|
||||||
|
|
||||||
|
Author: Darkwing. Reviewers: Dewey and Filbert. Brief:
|
||||||
|
`docs/plans/2026-10-11_multi-project-sync.md`, row A. Base: `edc5de71`
|
||||||
|
(`base.txt`). It applies unchanged to `6ae8b844`, where the gate ran: the
|
||||||
|
commits between the two touch no file in this candidate. The feature is
|
||||||
|
uncommitted; only this packet is committed. I pushed nothing.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
`files.txt` lists the 13 repository paths. `candidate-manifest.sha256`
|
||||||
|
has their sha256. `build.patch` is `git diff --cached --binary edc5de71`
|
||||||
|
over them: 13 files, +608/−73.
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|---|---|
|
||||||
|
| `packages/business/src/vars.mjs` | `tracker.descendants`: project layer, boolean, default false |
|
||||||
|
| `packages/business/tests/vars.test.mjs` | accepts true, refuses `"true"`, `1` and the business layer; default false with provenance `default` |
|
||||||
|
| `packages/cli/src/config.mjs` | one line, `descendants: v["tracker.descendants"]` in `trackerFor()` (decision 87) |
|
||||||
|
| `packages/cli/tests/config.test.mjs` | the two tracker tests expect `descendants: false`; one more assertion that `true` reaches the boot entry |
|
||||||
|
| `packages/cli/tests/trackers-boot.test.mjs` | line 44 expects `descendants: false`; see "Scope" |
|
||||||
|
| `packages/tasks/src/adapter.mjs` | `settings()` takes `descendants` (absent means false, a non-boolean refuses `tracker-config`); `ctx.tree` |
|
||||||
|
| `packages/tasks/src/startup.mjs` | `inspect(ctx, project)` is the install check for any project; new `discover(ctx)` |
|
||||||
|
| `packages/tasks/src/sync.mjs` | tick and reconcile read each project in `ctx.tree`; watch-only reads; the leave-tree rule in `missing()` |
|
||||||
|
| `packages/tasks/src/fake.mjs` | project trees: parents, the project list, archive, inherited access |
|
||||||
|
| `packages/tasks/tests/tree.test.mjs` | new: 11 tests, listed below |
|
||||||
|
| `packages/tasks/tests/shapes.test.mjs` | new test: the fake's project list against recorded shapes |
|
||||||
|
| `packages/tasks/README.md` | new section "Project trees"; the host contract and startup notes |
|
||||||
|
| `packages/tasks/live/README.md` | new section "Project trees" |
|
||||||
|
|
||||||
|
`verbs.mjs`, `vikunja.mjs`, `index.mjs` and `packages/bus/` are unchanged.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
`packages/cli/src/config.mjs` and `config.test.mjs` are outside the
|
||||||
|
brief's files. Sage added them as lead decision 87 (commit `9277de33`), a
|
||||||
|
decided scope addition: without the line, a host config with
|
||||||
|
`tracker.descendants` set never reaches the adapter, and the gate read
|
||||||
|
would need a packet script.
|
||||||
|
|
||||||
|
`packages/cli/tests/trackers-boot.test.mjs` is not named in decision 87.
|
||||||
|
It follows from it. Line 44 compares `boot.trackers` with an exact
|
||||||
|
object, so the config line breaks it: with the original line 44 and the
|
||||||
|
candidate `config.mjs` the test fails, 0 passed and 1 failed. The change
|
||||||
|
adds `descendants: false` to that object and nothing else. Reviewers and
|
||||||
|
Sage should treat it as part of decision 87's addition, or tell me to
|
||||||
|
take it out.
|
||||||
|
|
||||||
|
The extra `config.test.mjs` assertion, `true` reaching the boot entry, is
|
||||||
|
mine. Decision 87 asked for the two `false` updates. Without the new
|
||||||
|
assertion no test sent `true` through `config.mjs`, and the gate's live
|
||||||
|
run is the first place that value is used.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
With `descendants` off, `ctx.tree` holds the root and its view from
|
||||||
|
startup and nothing else. Every read is the v0 read with the root's
|
||||||
|
project id. The v0 tests pass unchanged, and tree test 1 checks that no
|
||||||
|
`GET /projects` and no read under a child happens.
|
||||||
|
|
||||||
|
With it on:
|
||||||
|
|
||||||
|
1. Startup is unchanged. The root's install check still refuses the
|
||||||
|
business with `tracker-install`.
|
||||||
|
2. Each reconcile, including the first one at start, calls `discover()`.
|
||||||
|
It reads `GET /projects`, every page, and walks `parent_project_id`
|
||||||
|
breadth-first from the root. It skips ids at or below 0 and archived
|
||||||
|
projects. Each child gets `inspect()`. A pass gives its view, a
|
||||||
|
`tracker-install` refusal gives `null` (watch-only), and any other
|
||||||
|
refusal propagates. `ctx.tree` is replaced only after the walk
|
||||||
|
finishes, so a failed reconcile keeps the previous tree.
|
||||||
|
3. Tick and reconcile read each project in `ctx.tree`. An installed
|
||||||
|
project is read through its board, as v0 reads the root. A watch-only
|
||||||
|
one is read through `walk(ctx, 'done = false', {}, project)`, and each
|
||||||
|
task gets bucket `WATCH_ONLY` (0).
|
||||||
|
4. Gone detection in a tick uses a set of refs placed by this tick's
|
||||||
|
reads, not task ids. A task moved from A to B shows on B's board under
|
||||||
|
the same id; keying by id would hide the old ref's tombstone. The
|
||||||
|
`placed-by-id` mutant below fails the move test.
|
||||||
|
5. `missing()` takes the project from the ref (`parseRef`). If the task
|
||||||
|
still reads 200 in the same project but that project is no longer in
|
||||||
|
`ctx.tree`, the task gets `no-access` with its last `updated`.
|
||||||
|
|
||||||
|
`fieldsFor()` builds the fields of a watch-only task with
|
||||||
|
`taskFields(task, 1)` and then sets `bucket: 0`. `taskFields` checks the
|
||||||
|
bucket is a positive integer, so the 1 is a placeholder that never
|
||||||
|
reaches the ledger. The bus schema accepts 0 (an integer).
|
||||||
|
|
||||||
|
One line in reconcile changed meaning: `if (!bucket) continue` became
|
||||||
|
`if (bucket === undefined) continue`. The old form would skip every
|
||||||
|
watch-only task, since 0 is falsy. `undefined` is still the case it was
|
||||||
|
written for, an open task the board didn't show.
|
||||||
|
|
||||||
|
## The brief, line by line
|
||||||
|
|
||||||
|
| Brief | Where |
|
||||||
|
|---|---|
|
||||||
|
| `tracker.descendants`, project-layer boolean, default false | `vars.mjs`; `vars.test.mjs` "types" and "merge" |
|
||||||
|
| The tree from `GET /projects` by `parent_project_id` | `discover()`; tree tests 2 and 5 |
|
||||||
|
| Discovery at startup and each reconcile, not each tick | tree test 2 (tick has no `GET /projects`, reconcile starts with it); test 8 (a late child joins at reconcile, not tick) |
|
||||||
|
| Archived projects are not in the tree | tree test 5 (with the list forced to include archived ones); test 9 |
|
||||||
|
| Leave-tree rule, recorded and tested | README "Project trees"; tree test 9, three ways |
|
||||||
|
| The ledger is never rewritten | tree test 9: snapshots read `open`, then `no-access` with the same `updated` |
|
||||||
|
| Root install fails: refuse | tree test 4 |
|
||||||
|
| Child passes: installed. Fails: watch-only, business ready | tree tests 2 and 3 |
|
||||||
|
| Watch-only records title, done, assignees, comments; no bucket state | tree test 11, decision 87's bucket 0 |
|
||||||
|
| Moves root to child, child to child, child to outside | tree test 7 |
|
||||||
|
| Poll cost stated | README; tree test 2 counts 6 GETs per tick and 11 per reconcile; probes.md P4 |
|
||||||
|
| Cross-project read probed | probes.md P4 and "Gate read" |
|
||||||
|
| The fake models the tree | `fake.mjs`; probes.md "Fake"; shapes test 2 |
|
||||||
|
| Descendants off matches v0 | tree test 1; every pre-existing tasks test unchanged |
|
||||||
|
| verbs read-only; a child task refuses `task-project` | `verbs.mjs` unchanged; tree test 11 |
|
||||||
|
| No change to `packages/bus/`; no Vikunja writes | `files.txt`; tree test 11 checks `sync.mjs` has no write method string |
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
`packages/tasks/tests/tree.test.mjs`:
|
||||||
|
|
||||||
|
1. With descendants off the adapter reads no project list and no child, as in v0.
|
||||||
|
2. A three-level tree is read per project, and a project outside it is
|
||||||
|
not. Root, an installed child, a watch-only grandchild and an outside
|
||||||
|
project shared with the sync bot. The tick's six requests are checked
|
||||||
|
in order; the reconcile is `GET /projects` plus 10.
|
||||||
|
3. A watch-only child keeps the business ready, and a failed child read
|
||||||
|
refuses as transient (a 503 on its views).
|
||||||
|
4. A root without the installed buckets refuses with descendants on.
|
||||||
|
5. The project list refuses without `read_all`, and skips archived
|
||||||
|
projects and saved filters. A fetch wrapper adds `is_archived=true`,
|
||||||
|
so the fake returns archived projects and the adapter's own check has
|
||||||
|
to drop them.
|
||||||
|
6. `descendants` must be true or false.
|
||||||
|
7. A move inside the tree is a moved tombstone plus a snapshot under the
|
||||||
|
new ref: root to A, A to B (watch-only), A to outside.
|
||||||
|
8. A child made after startup joins the tree at the next reconcile.
|
||||||
|
9. A child that leaves the tree has its tasks recorded missing, and the
|
||||||
|
ledger keeps their history: archived, reparented to the top with a
|
||||||
|
direct share, and reparented under a project the bot can't see. The
|
||||||
|
hidden parent's title never reaches the bus.
|
||||||
|
10. A task in a bucket outside the five keeps its raw bucket id.
|
||||||
|
11. Bucket 0 on a watch-only task never becomes a bucket or a state. A
|
||||||
|
move between open buckets records nothing; done then reopen records
|
||||||
|
`done` twice; every snapshot has bucket 0; no `task.state` event; a
|
||||||
|
verb refuses `task-project`; the source of `sync.mjs` has no
|
||||||
|
`.titles` or `.ids[` lookup and no write method string. This is the
|
||||||
|
test decision 87 asked for.
|
||||||
|
|
||||||
|
`shapes.test.mjs` adds "the project list holds recorded project objects
|
||||||
|
in the recorded list envelope". No recording holds a `GET /projects`
|
||||||
|
list, so it checks the envelope against the recorded views list and each
|
||||||
|
item against the recorded `GET /projects/{p}` body, keys and types.
|
||||||
|
|
||||||
|
### Mutants
|
||||||
|
|
||||||
|
`out/mutants.sh` edits one source file in the build worktree, runs the
|
||||||
|
tests, and restores the file from the candidate. Each mutant fails at
|
||||||
|
least one test (`out/mutants.txt`):
|
||||||
|
|
||||||
|
| Mutant | Fails |
|
||||||
|
|---|---|
|
||||||
|
| `no-leave-tree-check`: drop the `ctx.tree.has(project)` branch in `missing()` | tree 9 |
|
||||||
|
| `no-watch-only-bucket`: `fieldsFor` keeps the bucket it is given | tree 2, 3, 7, 8, 11 |
|
||||||
|
| `placed-by-id`: gone detection keyed by task id | tree 7 |
|
||||||
|
| `keep-archived`: `discover()` keeps archived projects | tree 5 |
|
||||||
|
| `config-drops-descendants`: `config.mjs` passes `false` | cli config "trackers come from..." |
|
||||||
|
|
||||||
|
## Things a reviewer should know
|
||||||
|
|
||||||
|
- The `p.id <= 0` skip guards shape only. The walk starts at the root
|
||||||
|
and follows positive parent ids, so it can't reach a negative id. The
|
||||||
|
skip stops a saved filter from failing the `parent_project_id` check.
|
||||||
|
- The fake's `GET /projects/{p}` body now has `parent_project_id`,
|
||||||
|
`position`, `created` and `updated`, from a shared `#projectJson`.
|
||||||
|
Shapes test 1 still matches it to the recorded body.
|
||||||
|
- `commentsAt` and `commentCount` stay keyed by task id. Vikunja task ids
|
||||||
|
are global, so a moved task keeps its comment state.
|
||||||
|
- A watch-only child's open tasks come from its task list, so a tick
|
||||||
|
costs the same two GETs as an installed project.
|
||||||
|
- I noticed in the v2.7.0 source that creating a project under v2 also
|
||||||
|
creates a default kanban with a backlog bucket. A child made in the UI
|
||||||
|
is then watch-only until someone installs the five buckets by hand.
|
||||||
|
Nothing in this row depends on it.
|
||||||
|
- No probe in this row touched a live Vikunja. probes.md has the source
|
||||||
|
evidence and the exact gate read.
|
||||||
|
|
||||||
|
## Gate
|
||||||
|
|
||||||
|
The gate ran in a detached worktree of `6ae8b844` with `build.patch`
|
||||||
|
applied and `node_modules` linked to the main checkout's
|
||||||
|
(`out/gate/apply.txt`, `out/gate/gate.sh`). Every suite ran in order and
|
||||||
|
each log is in `out/gate/`. test-task ran with
|
||||||
|
`DOCKER_HOST=unix:///nonexistent`.
|
||||||
|
|
||||||
|
Every suite passed on this candidate (`out/gate/summary.txt`):
|
||||||
|
|
||||||
|
| Suite | rc | Result |
|
||||||
|
|---|---|---|
|
||||||
|
| `packages/tasks` | 0 | 63 passed, 0 failed |
|
||||||
|
| `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 |
|
||||||
|
|
||||||
|
After the run I removed the gate worktree's `gate_default` Docker
|
||||||
|
network; it had no containers.
|
||||||
|
|
||||||
|
Two earlier gate runs were stopped. The first had no `node_modules` link,
|
||||||
|
so test-discord failed its "pi binary present" check, 57 passed and 1
|
||||||
|
failed. The second I stopped to add the `true` assertion to
|
||||||
|
`config.test.mjs`, which changed the candidate. Neither run's logs are
|
||||||
|
in this packet; they were on the earlier candidate.
|
||||||
|
|
||||||
|
## Gate read for Sage
|
||||||
|
|
||||||
|
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,
|
||||||
|
GETs only. The adapter reads per project whatever read 2 answers; a 200
|
||||||
|
there opens a follow-up row.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
edc5de71663062e1e1d2dd750fd0c15e522208a5
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
|||||||
|
a15c22de43c3ca4ea80fa6141fb2b8173f5a1eb31d939bad5137e36fc131c89d packages/business/src/vars.mjs
|
||||||
|
43ebec98d7cb474c4d65ff533c103250e10d1d5bf5530f23f3424ab5c265f73c packages/business/tests/vars.test.mjs
|
||||||
|
fcc95afc418823416c87475de0f6fcad33d312588d22e9d6fd5784d9e2478f0e packages/cli/src/config.mjs
|
||||||
|
1737237bbf062f83eeffeb7fb1a604bc6676be9f9db1e6b91844587f77491a1c packages/cli/tests/config.test.mjs
|
||||||
|
4ada6f215c8fb42bc95d46121e7c61c2477f7b1b1d7120675443b91cc4574fdc packages/cli/tests/trackers-boot.test.mjs
|
||||||
|
aad24a5db15943eba6c0bf5fd93add3f4f7b265ff5868c46e405e976bc26ce22 packages/tasks/live/README.md
|
||||||
|
d31be986f6f5f972bb6166eee64b9ad2e3ad202c946b92af39ab69339d6225ac packages/tasks/README.md
|
||||||
|
02a802a9bb1306bf5f82b8e56ca2064c308f1b7574eebba0f47932cdea7f41d1 packages/tasks/src/adapter.mjs
|
||||||
|
aeb65af54d716a36bd7e790c41cad3c4ca516ab8384b288e23ecd04d82cdd366 packages/tasks/src/fake.mjs
|
||||||
|
e0c754aacf7e82c2fe18fb0548674044ac949ffc94435b353e4bd7aebcf797c2 packages/tasks/src/startup.mjs
|
||||||
|
4a329bc76c4909490ac064332ccd0083365a4f7fcbffcd69ef17c327a288fc82 packages/tasks/src/sync.mjs
|
||||||
|
e8798609ec1bf7d4901f7f94b6346b37c7ea4504ee2eb781b07c634001e3bf72 packages/tasks/tests/shapes.test.mjs
|
||||||
|
82f6a673a59dc214a65d20af597096c6c3f86ad2c4d39bb25d617ad2515a9e4b packages/tasks/tests/tree.test.mjs
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
packages/business/src/vars.mjs
|
||||||
|
packages/business/tests/vars.test.mjs
|
||||||
|
packages/cli/src/config.mjs
|
||||||
|
packages/cli/tests/config.test.mjs
|
||||||
|
packages/cli/tests/trackers-boot.test.mjs
|
||||||
|
packages/tasks/live/README.md
|
||||||
|
packages/tasks/README.md
|
||||||
|
packages/tasks/src/adapter.mjs
|
||||||
|
packages/tasks/src/fake.mjs
|
||||||
|
packages/tasks/src/startup.mjs
|
||||||
|
packages/tasks/src/sync.mjs
|
||||||
|
packages/tasks/tests/shapes.test.mjs
|
||||||
|
packages/tasks/tests/tree.test.mjs
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
applied
|
||||||
|
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 (189.205781ms)
|
||||||
|
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (438.844355ms)
|
||||||
|
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (382.921188ms)
|
||||||
|
✔ launch events require a human CLI capability; generic emit cannot forge authority events (389.459955ms)
|
||||||
|
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (292.529916ms)
|
||||||
|
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (219.077084ms)
|
||||||
|
✔ task action subjects and linked decision trail are complete and ordered (390.586456ms)
|
||||||
|
✔ launch binding is durable and reconnecting requires the identical trusted record (134.628246ms)
|
||||||
|
✔ business isolation includes inherited object names and cross-business message references (215.705092ms)
|
||||||
|
✔ authority never transfers between action, run, target, unresolved or replaced role holder (309.853682ms)
|
||||||
|
✔ task projection uses schema current view, skipping earlier and equal-start polls (133.355659ms)
|
||||||
|
✔ revocation permanently bars the old run from reclaiming first, including after broker restart (481.213654ms)
|
||||||
|
✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (180.256758ms)
|
||||||
|
✔ both arbiters require human resolution when their cross-role route is themselves (241.16013ms)
|
||||||
|
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (3.251996ms)
|
||||||
|
✔ only validated broker references load; returned data and exceptions cannot expose a known token (7.071814ms)
|
||||||
|
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (6.492665ms)
|
||||||
|
✔ expiry refuses use and env references never become client data (1.740994ms)
|
||||||
|
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (7.820923ms)
|
||||||
|
✔ opaque tokens shorter than 16 characters refuse before use (0.700696ms)
|
||||||
|
✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (202.757684ms)
|
||||||
|
✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (234.939664ms)
|
||||||
|
✔ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (279.024904ms)
|
||||||
|
✔ endLaunch leaves a claim another run took alone (253.496157ms)
|
||||||
|
✔ refuse records action.refused against the caller with the code only (335.224505ms)
|
||||||
|
✔ launches off refuses role.launch with launch-revoked until launches on (315.632998ms)
|
||||||
|
✔ broker process: launch ops authorize role.launch, record refusals and end runs (429.799892ms)
|
||||||
|
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (4.279809ms)
|
||||||
|
✔ process reader gets own kernel identity without exposing environment values (0.893228ms)
|
||||||
|
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (1.337363ms)
|
||||||
|
✔ real pid 1 remains inspectable when its environment is protected (0.519345ms)
|
||||||
|
✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (277.997725ms)
|
||||||
|
✔ missing and foreign-business citations refuse and roll back message and grant (344.747497ms)
|
||||||
|
✔ cross-role sends still need a matching resolved decision and consume it once (353.002017ms)
|
||||||
|
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (273.37842ms)
|
||||||
|
✔ startup token refusal returns safe code without value or partial listening broker (60.632816ms)
|
||||||
|
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (356.371003ms)
|
||||||
|
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (256.867864ms)
|
||||||
|
✔ trusted host registers later launches; socket clients never have a registration verb (385.853343ms)
|
||||||
|
✔ runtime excludes declared project roots even when host supplies no repoRoots (55.296127ms)
|
||||||
|
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (253.530798ms)
|
||||||
|
✔ v3b prototype refusals, views and append-only mutations (1766.320602ms)
|
||||||
|
✔ gated approval authorizes once, survives store reopen, and fresh approval works (386.057154ms)
|
||||||
|
✔ another run cannot consume an approval; a failed check leaves it usable (402.945256ms)
|
||||||
|
✔ two scheduled callers have exactly one grant and one consumed refusal (274.267909ms)
|
||||||
|
✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (600.084673ms)
|
||||||
|
✔ class drift gated to cross-role refuses before consumption (391.791192ms)
|
||||||
|
✔ class drift cross-role to gated refuses before consumption (340.330132ms)
|
||||||
|
✔ class drift gated to within-role refuses before consumption (227.877646ms)
|
||||||
|
✔ class drift cross-role to within-role refuses before consumption (282.209668ms)
|
||||||
|
✔ class drift within-role to gated refuses before consumption (198.278025ms)
|
||||||
|
✔ class drift within-role to cross-role refuses before consumption (481.680122ms)
|
||||||
|
✔ message.send consumes approval and prevents a later send or authorize (242.199767ms)
|
||||||
|
✔ role.revoke consumes approval and prevents a later revoke or authorize (208.064707ms)
|
||||||
|
✔ creates private WAL store and excludes a second writer until explicit close (170.30296ms)
|
||||||
|
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (294.171606ms)
|
||||||
|
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (324.314159ms)
|
||||||
|
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (266.274964ms)
|
||||||
|
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (283.014865ms)
|
||||||
|
✔ async transactions refuse before invoking their function (186.302733ms)
|
||||||
|
✔ recordTask keeps sync reads and a role write apart (248.021791ms)
|
||||||
|
✔ read_at must be one canonical UTC format, so the projection compares strings safely (183.061837ms)
|
||||||
|
✔ a bad entry refuses the whole record (203.123255ms)
|
||||||
|
✔ taskView reads the projection for one business (187.279126ms)
|
||||||
|
✔ requestTask hands only a holder and a task verb to the handler, and records refusals (506.610296ms)
|
||||||
|
✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (511.4966ms)
|
||||||
|
✔ without an adapter the server refuses every task verb (427.506996ms)
|
||||||
|
✔ the runtime refuses an invalid adapter and closes a valid one (281.415721ms)
|
||||||
|
✔ the process loads the S3 adapter from plain-data trackers (301.335615ms)
|
||||||
|
✔ socket capability stamps launch identity; shared views use wire, no SQL client (221.529429ms)
|
||||||
|
✔ two wire claims serialize; a lost reply never automatically retries (338.606324ms)
|
||||||
|
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (163.296596ms)
|
||||||
|
✔ client preserves UTF-8 when a response divides a multibyte character (16.065369ms)
|
||||||
|
✔ committed mutation followed by dropped reply reports unknown and is never retried (193.337359ms)
|
||||||
|
ℹ tests 74
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 74
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 4202.497194
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
✔ config directory and file path follow MOSAIC_CONFIG (2.848351ms)
|
||||||
|
✔ the fixture business validates and comes back frozen (38.438289ms)
|
||||||
|
✔ two instances may share a definition (4.985988ms)
|
||||||
|
✔ top-level refusals (15.573259ms)
|
||||||
|
✔ arbiters and projects (14.939897ms)
|
||||||
|
✔ role instances (9.446596ms)
|
||||||
|
✔ Vikunja bots (12.111196ms)
|
||||||
|
✔ a role without Vikunja takes no tracker block (5.556326ms)
|
||||||
|
✔ credential references match the definition's services (18.578185ms)
|
||||||
|
✔ launch (40.298298ms)
|
||||||
|
✔ loadBusiness: file checks (17.280922ms)
|
||||||
|
✔ loadBusiness: not a regular file (151.91495ms)
|
||||||
|
✔ loading writes nothing (4.18165ms)
|
||||||
|
✔ names that are Object.prototype properties don't count as declared (9.614905ms)
|
||||||
|
✔ the shipped example refuses as written and validates once filled in (1.581714ms)
|
||||||
|
✔ usage errors exit 4 (685.533424ms)
|
||||||
|
✔ validate: a good business exits 0 and prints instance digests (100.237294ms)
|
||||||
|
✔ validate: project files (510.346462ms)
|
||||||
|
✔ validate: missing files and a broken system config (408.670972ms)
|
||||||
|
✔ validate: credential reference problems exit 2 and name each one (108.704355ms)
|
||||||
|
✔ validate: a token file inside the repository is refused (108.455448ms)
|
||||||
|
✔ validate: role definitions come from MOSAIC_ROLES_DIR (317.775665ms)
|
||||||
|
✔ resolve: prints one instance's record (323.99734ms)
|
||||||
|
✔ resolve: refusals (597.461021ms)
|
||||||
|
✔ parse: exactly one of file or env, plus the service's date (4.801613ms)
|
||||||
|
✔ check: a good file has no problems (14.046213ms)
|
||||||
|
✔ check never opens the file: a write-only token passes (0.660342ms)
|
||||||
|
✔ check: file problems (1.637196ms)
|
||||||
|
✔ check: token files can't live in the repository or dataRoot, even through a linked directory (6.311826ms)
|
||||||
|
✔ check: dates and environment references (0.767415ms)
|
||||||
|
✔ path and load (30.032437ms)
|
||||||
|
✔ refusals (14.888956ms)
|
||||||
|
✔ systemVars flattens the validated config (3.618812ms)
|
||||||
|
✔ precedence: system, business, project, project role, agent (8.824697ms)
|
||||||
|
✔ limits narrow the definition and never widen it (4.797614ms)
|
||||||
|
✔ role.launch stays within-role only for the instance the launch block names (15.636871ms)
|
||||||
|
✔ limits.authority without role.launch leaves the launcher with no launch block (6.532983ms)
|
||||||
|
✔ limits.authority narrows cross-role actions too (3.042241ms)
|
||||||
|
✔ classify (3.151387ms)
|
||||||
|
✔ the record carries what the broker and launcher need (2.556793ms)
|
||||||
|
✔ digest: key order doesn't matter, any value change does (67.721665ms)
|
||||||
|
✔ refusals (42.31728ms)
|
||||||
|
✔ the four shipped version 2 roles load (5.044342ms)
|
||||||
|
✔ shipped role scopes match addendum B section 2 and the SR runbook (2.371647ms)
|
||||||
|
✔ shipped authority follows the note's table (0.895367ms)
|
||||||
|
✔ version 1 files keep loading with no authority (3.271035ms)
|
||||||
|
✔ the conductor policy isn't a role (0.459604ms)
|
||||||
|
✔ a missing role file is exit 4, a symbolic link too (0.9243ms)
|
||||||
|
✔ version 2 refusals (2.45245ms)
|
||||||
|
✔ authority: closed vocabulary, no gated-only action, no overlap (4.053667ms)
|
||||||
|
✔ credentials: Gitea scopes (4.751198ms)
|
||||||
|
✔ credentials: Vikunja scopes are a group-to-verbs map from the grantable list (2.514347ms)
|
||||||
|
✔ credentials: services (1.082643ms)
|
||||||
|
✔ contract: a non-empty regular Markdown file beside the role file (1.531355ms)
|
||||||
|
✔ every key names known layers and a merge rule (2.114061ms)
|
||||||
|
✔ unknown keys and wrong layers refuse (1.394641ms)
|
||||||
|
✔ types (3.523914ms)
|
||||||
|
✔ merge: defaults, then the most specific layer wins (0.571531ms)
|
||||||
|
✔ merge: limits only narrow, and provenance lists each source (0.556813ms)
|
||||||
|
✔ merge doesn't change its inputs (0.682966ms)
|
||||||
|
ℹ tests 60
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 60
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 3285.96556
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (243.295827ms)
|
||||||
|
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (189.071888ms)
|
||||||
|
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (146.01894ms)
|
||||||
|
✔ decide prints a declining choice as declining (139.95998ms)
|
||||||
|
✔ an unknown outcome is reported once and never resent (111.046439ms)
|
||||||
|
✔ a decision closed before the answer arrives exits 2 and points at its trail (121.224944ms)
|
||||||
|
✔ a prefix that matches two open decisions exits 2 and resolves neither (140.654613ms)
|
||||||
|
✔ without --business a command uses the live host's business, and a stale host.json is not a host (102.510429ms)
|
||||||
|
✔ every human command refuses inside an agent run before it touches the bus (81.10258ms)
|
||||||
|
✔ usage errors exit 4; no business and no host is a usage error (117.127277ms)
|
||||||
|
✔ agents and tasks print through the broker (216.831611ms)
|
||||||
|
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (5.142126ms)
|
||||||
|
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (90.196977ms)
|
||||||
|
✔ trackers come from the tracker.* variables of the one project that names a tracker project (112.216406ms)
|
||||||
|
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (46.353189ms)
|
||||||
|
✔ two projects that each name a tracker project refuse, since the boot shape holds one (44.703145ms)
|
||||||
|
✔ a business without tracker.baseUrl gets no trackers entry (45.759211ms)
|
||||||
|
✔ an unknown business and a broken system config refuse with exit 3 (97.641512ms)
|
||||||
|
✔ empty views say so (1.390106ms)
|
||||||
|
✔ the trail keeps the broker's order and names a decision's task without its rows (1.518535ms)
|
||||||
|
✔ tasks print the tracker fields the snapshot carries (0.322287ms)
|
||||||
|
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (1080.334885ms)
|
||||||
|
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (277.169937ms)
|
||||||
|
✔ a second host for the same data root refuses with exit 3 while the first runs (321.92585ms)
|
||||||
|
✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1348.009528ms)
|
||||||
|
✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (394.60645ms)
|
||||||
|
✔ a broker reply with no request waiting breaks the channel and the host exits 1 (226.991929ms)
|
||||||
|
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (292.036695ms)
|
||||||
|
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (239.157285ms)
|
||||||
|
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (139.568023ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (208.017712ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (189.031877ms)
|
||||||
|
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (225.30536ms)
|
||||||
|
✔ watchChildren reports a child that died before it was called, and one that dies later (28.891377ms)
|
||||||
|
✔ bus stop refuses to signal a live pid that is not a bus host (203.296479ms)
|
||||||
|
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (317.654195ms)
|
||||||
|
✔ bus start refuses with exit 3 without a notifier config (122.953483ms)
|
||||||
|
✔ bus start runs until bus stop; status reports it while it runs (836.163088ms)
|
||||||
|
✔ bus-service.sh renders the unit and installs it into a given directory (45.822113ms)
|
||||||
|
✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1400.86653ms)
|
||||||
|
✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (1247.605114ms)
|
||||||
|
✔ a runner that stops at once ends its launch with the runner's reason (248.884842ms)
|
||||||
|
✔ 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 (1002.975701ms)
|
||||||
|
✔ 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 (3815.570834ms)
|
||||||
|
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (214.061284ms)
|
||||||
|
✔ an over-long launch request is refused at once, not at the 10 s idle timeout (150.366959ms)
|
||||||
|
✔ a launch client that never closes its side doesn't hold the host's close (164.53907ms)
|
||||||
|
✔ a runner that ignores SIGTERM is killed when the host closes (1317.405262ms)
|
||||||
|
✔ zoned uses the IANA zone across DST (41.143292ms)
|
||||||
|
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (259.674743ms)
|
||||||
|
✔ two blocking decisions get two DMs with different nonces (155.486762ms)
|
||||||
|
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.38307ms)
|
||||||
|
✔ a failed DM is journaled, backs off, and is retried until it lands (146.199866ms)
|
||||||
|
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (143.132605ms)
|
||||||
|
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (124.186841ms)
|
||||||
|
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (147.572487ms)
|
||||||
|
✔ 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 (214.561955ms)
|
||||||
|
✔ a restart after the second refusal does not send before that refusal's 30 min are up (125.378767ms)
|
||||||
|
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (301.025846ms)
|
||||||
|
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (243.748228ms)
|
||||||
|
✔ an inbox read failure is logged and the next poll retries (1.354297ms)
|
||||||
|
✔ no Discord id reaches the journal or the log (223.052008ms)
|
||||||
|
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (43.903741ms)
|
||||||
|
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (87.99139ms)
|
||||||
|
✔ the journal: a whole file that is one torn line truncates to empty (134.022562ms)
|
||||||
|
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (6.709958ms)
|
||||||
|
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (1.707582ms)
|
||||||
|
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (50.099977ms)
|
||||||
|
✔ the journal: a symlinked directory refuses and says it is a link (0.658324ms)
|
||||||
|
✔ 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.821595ms)
|
||||||
|
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.649684ms)
|
||||||
|
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.770337ms)
|
||||||
|
✔ digest content stays within Discord's 2000 characters (0.5096ms)
|
||||||
|
✔ runLoop never overlaps ticks and stops after the one in flight (110.894961ms)
|
||||||
|
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 (547.545108ms)
|
||||||
|
✔ the transport writes {business, verb, args} to the child and reads its JSON (72.833567ms)
|
||||||
|
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2235.327071ms)
|
||||||
|
✔ busExit and refuseInsideAgent (0.656759ms)
|
||||||
|
✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (355.542292ms)
|
||||||
|
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1156.500378ms)
|
||||||
|
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (226.331905ms)
|
||||||
|
✔ launches off and on go to the broker and change the business's launch state (276.154196ms)
|
||||||
|
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (190.755404ms)
|
||||||
|
ℹ tests 83
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 83
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 9694.655934
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
export TMPDIR=~/darkwing-scratch/r62/tmp
|
||||||
|
G=~/darkwing-scratch/r62/gate; O=~/darkwing-scratch/r62/out; 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,71 @@
|
|||||||
|
✔ the boot config is checked before anything starts (104.416076ms)
|
||||||
|
✔ a business with no tracker entry refuses task verbs (130.611086ms)
|
||||||
|
✔ credential.expiring and .expired are recorded once per instance (279.889027ms)
|
||||||
|
✔ a token file that changes on disk records credential.changed (208.764269ms)
|
||||||
|
✔ autostart polls, reconciles and retries a startup the tracker was down for (246.807203ms)
|
||||||
|
✔ a refusal a restart must clear is not retried by the poll (114.25003ms)
|
||||||
|
✔ a poll that fires while two are queued is dropped (139.221689ms)
|
||||||
|
✔ close waits for a running verb and refuses one that has not started (288.167564ms)
|
||||||
|
✔ the bundled Vikunja is the pinned upstream image the runbook names (1.586814ms)
|
||||||
|
✔ every published port is on 127.0.0.1, and no secret is in the file (0.644421ms)
|
||||||
|
✔ the fake answers each route with the statuses and shapes Vikunja v2.7.0 sent (409.424478ms)
|
||||||
|
✔ the project list holds recorded project objects in the recorded list envelope (157.698384ms)
|
||||||
|
✔ the recorded task bodies pass the checks S3 applies to every read (0.763433ms)
|
||||||
|
✔ the client works against the fake over real HTTP with the platform fetch (537.56183ms)
|
||||||
|
✔ a correct install starts, and the first reconcile records tasks that already exist (120.574047ms)
|
||||||
|
✔ verbs refuse while a business is starting and after startup refused it (145.811034ms)
|
||||||
|
✔ startup refuses a token that can do more than its role needs (632.395388ms)
|
||||||
|
✔ startup refuses an unsupported version and flags an untested one (368.034205ms)
|
||||||
|
✔ startup refuses a board that the runbook did not install (499.056967ms)
|
||||||
|
✔ startup refuses a project the sync bot cannot read (155.060339ms)
|
||||||
|
✔ startup refuses a configured label the pm bot cannot see (227.668874ms)
|
||||||
|
✔ startup refuses an expired credential and a missing sync credential (365.899809ms)
|
||||||
|
✔ an unreachable tracker refuses with tracker-unavailable (246.733826ms)
|
||||||
|
✔ an edit in the UI is recorded once, with the fields that changed (239.895408ms)
|
||||||
|
✔ a move between open buckets is seen on the board, though updated does not change (213.821318ms)
|
||||||
|
✔ a person's comment is counted and a bot's is not (483.335126ms)
|
||||||
|
✔ the hourly reconcile catches a comment through comment_count (264.005482ms)
|
||||||
|
✔ a task closed in the UI leaves the open view with its done bucket (570.154722ms)
|
||||||
|
✔ a task that leaves the board is recorded as deleted, moved or out of reach (536.585697ms)
|
||||||
|
✔ a poll that read before a verb wrote does not overwrite the verb (435.391707ms)
|
||||||
|
✔ a tracker fault during a tick is reported and the next tick catches up (597.848081ms)
|
||||||
|
✔ a malformed answer refuses the tick with tracker-shape (556.19897ms)
|
||||||
|
✔ no token value reaches the database, the log or a refusal (876.126984ms)
|
||||||
|
✔ the first look at a task counts only comments inside the window (607.031588ms)
|
||||||
|
✔ with descendants off the adapter reads no project list and no child, as in v0 (155.493456ms)
|
||||||
|
✔ a three-level tree is read per project, and a project outside it is not (150.614393ms)
|
||||||
|
✔ a watch-only child keeps the business ready, and a failed child read refuses as transient (315.51776ms)
|
||||||
|
✔ a root without the installed buckets refuses with descendants on (207.162752ms)
|
||||||
|
✔ the project list refuses without read_all, and skips archived projects and saved filters (270.37371ms)
|
||||||
|
✔ descendants must be true or false (128.628273ms)
|
||||||
|
✔ a move inside the tree is a moved tombstone plus a snapshot under the new ref (246.375569ms)
|
||||||
|
✔ a child made after startup joins the tree at the next reconcile (233.25224ms)
|
||||||
|
✔ a child that leaves the tree has its tasks recorded missing, and the ledger keeps their history (254.204244ms)
|
||||||
|
✔ a task in a bucket outside the five keeps its raw bucket id (295.854549ms)
|
||||||
|
✔ bucket 0 on a watch-only task never becomes a bucket or a state (488.248057ms)
|
||||||
|
✔ task.create needs a recorded human request and a requirement id (240.304486ms)
|
||||||
|
✔ only labels named in the business file can be written (290.929905ms)
|
||||||
|
✔ task.schedule sets and clears a due date and relations (517.445508ms)
|
||||||
|
✔ assign and reassign move the role bots and record task.assigned (426.774157ms)
|
||||||
|
✔ task.update.assigned is for the assignee and records task.state (416.845053ms)
|
||||||
|
✔ a wrong expected digest records task.conflict and writes nothing (443.969427ms)
|
||||||
|
✔ a cross-role verb needs a resolved decision, used once (873.662032ms)
|
||||||
|
✔ task.close needs a verdict; after it every verb refuses with task-done (1012.441292ms)
|
||||||
|
✔ a lost answer is settled by a re-read and never retried (877.307177ms)
|
||||||
|
✔ a create whose answer is lost is reported uncertain, and the poll finds the task (2200.018372ms)
|
||||||
|
✔ a task the sync bot cannot read refuses and records nothing (2699.391581ms)
|
||||||
|
✔ verbs and polls for one business run one at a time (1530.611798ms)
|
||||||
|
✔ a due date with milliseconds is written to the second (2348.684349ms)
|
||||||
|
✔ every write landed and the final read failed: the verb succeeds and records what it wrote (1049.370857ms)
|
||||||
|
✔ some writes landed and the final read failed: write-uncertain, and nothing is recorded (1037.216419ms)
|
||||||
|
✔ a create whose final read fails succeeds and records task.created (791.827816ms)
|
||||||
|
✔ an edit between the last write and the final read shows as external on the next poll (591.740537ms)
|
||||||
|
✔ task.created is recorded when a later label write fails (311.053548ms)
|
||||||
|
ℹ tests 63
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 63
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 17824.523183
|
||||||
@@ -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 '6ae8b844231244830b8b09e0a2ef4b325644ffca'.
|
||||||
|
|
||||||
|
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 '6ae8b844231244830b8b09e0a2ef4b325644ffca'.
|
||||||
|
|
||||||
|
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: 2151932 Killed "$@" > /dev/null 2>&1
|
||||||
|
OK forced interruption kills the replacing process
|
||||||
|
OK next invocation recovers old consistent installation
|
||||||
|
OK interrupted replacement rolled back
|
||||||
|
OK sync succeeds after interruption recovery
|
||||||
|
OK unlocked stale lock file does not block
|
||||||
|
OK active lock refuses a concurrent sync
|
||||||
|
OK source symlink fails closed
|
||||||
|
OK nested second entrypoint fails closed
|
||||||
|
|
||||||
|
extension package selftest: 18 passed, 0 failed
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
toolchain: node v26.8.1, python 3.12.8, jsonschema 4.26.0
|
||||||
|
|
||||||
|
OK syntax: scripts/foundation-inspect.mjs
|
||||||
|
OK syntax: scripts/foundation/strict-json.mjs
|
||||||
|
OK syntax: scripts/foundation/canonical.mjs
|
||||||
|
OK syntax: scripts/foundation/resolve.mjs
|
||||||
|
OK syntax: scripts/foundation/validate-record.mjs
|
||||||
|
OK syntax: scripts/foundation/fixtures/build-fixtures.mjs
|
||||||
|
OK syntax: scripts/foundation/canonical.test.mjs
|
||||||
|
OK syntax: scripts/foundation/cli.test.mjs
|
||||||
|
OK syntax: scripts/foundation/fixtures.test.mjs
|
||||||
|
OK syntax: scripts/foundation/resolve.test.mjs
|
||||||
|
OK syntax: scripts/foundation/strict-json.test.mjs
|
||||||
|
OK syntax: scripts/foundation/verify-schema.py (ast only; no bytecode written)
|
||||||
|
OK fixture generator runs
|
||||||
|
OK checked-in fixtures/bundles equal a fresh generation
|
||||||
|
OK checked-in fixtures/raw equal a fresh generation
|
||||||
|
OK checked-in fixtures/index.json equal a fresh generation
|
||||||
|
OK checked-in demo bundles equal a fresh generation
|
||||||
|
OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
|
||||||
|
OK node --test scripts/foundation/ (ℹ pass 80)
|
||||||
|
OK differential schema oracle: PASS: differential schema oracle (finite corpus; compatibility evidence, not equivalence proof)
|
||||||
|
platform witness: strftime('%Y') for year 999 -> '999' (pinned checker refuses years 0001..0999)
|
||||||
|
node v26.8.1; corpus 1568 records (38 pinned fixtures, 478 unique bundle records, 1052 typeCase/mutation/lexical cases)
|
||||||
|
schema column: agree-valid 540, agree-invalid 991, DISAGREEMENTS 0; strict-only (parser-bound) cases: 27; unsupported-kind records not schema-assessed by the inspector: 10
|
||||||
|
profile column (schema-valid records only): profile-valid 510, profile-invalid 30
|
||||||
|
profile refusals asserted: 30 schema-agreed-valid records refused only by the strict typed-string profile (rule profile-pattern-mismatch), 12 declared by name; 73 named probes verified against declared schema/profile columns
|
||||||
|
OK oracle: zero schema-column disagreements with the pinned checker
|
||||||
|
OK oracle: strict-only profile refusals are counted and asserted
|
||||||
|
OK demo: permitted read preview exits 0 (exit 0)
|
||||||
|
OK demo: permitted file.change preview exits 0 (exit 0)
|
||||||
|
OK demo: assignment.change proposal is unresolved (exit 3) (exit 3)
|
||||||
|
OK demo: revoked registration is refused (exit 3) (exit 3)
|
||||||
|
OK demo: message is not authority (exit 3) (exit 3)
|
||||||
|
OK usage: no arguments exits 2 (exit 2)
|
||||||
|
OK io: missing file exits 4 (exit 4)
|
||||||
|
OK io: directory exits 4 (exit 4)
|
||||||
|
OK io: symlink exits 4 (O_NOFOLLOW) (exit 4)
|
||||||
|
OK bound: oversize fixture exits 2 (exit 2)
|
||||||
|
OK profile: one final LF in a typed selection id is refused before admission (exit 2) (exit 2)
|
||||||
|
OK profile: two final LFs fail the schema pattern itself (exit 2) (exit 2)
|
||||||
|
OK profile: escaped newlines in free-form text stay allowed (exit 0) (exit 0)
|
||||||
|
OK profile refusal is invalid-request/profile-pattern-mismatch with selection and operation withheld, value not echoed
|
||||||
|
OK text output starts with the disclaimer
|
||||||
|
OK json output is valid JSON with result allowed and exactly the charter §7 fields
|
||||||
|
OK json golden matches byte-for-byte
|
||||||
|
OK sandboxed bundle run (env -i, PATH=/nonexistent) produced the unresolved proposal
|
||||||
|
OK sandbox inventory (path/type/size/mode/uid/gid/inode/mtime/sha256) unchanged by runs
|
||||||
|
OK canary never printed (bundle run and credential-file run)
|
||||||
|
OK a non-bundle JSON file is refused at the shape gate, not read into output
|
||||||
|
OK no field of the non-bundle file is echoed
|
||||||
|
|
||||||
|
selftest: 44 passed, 0 failed
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
toolchain: node v26.8.1, git version 2.55.0
|
||||||
|
|
||||||
|
OK syntax: packages/queue/src/cli.mjs
|
||||||
|
OK syntax: packages/queue/src/errors.mjs
|
||||||
|
OK syntax: packages/queue/src/io.mjs
|
||||||
|
OK syntax: packages/queue/src/lock.mjs
|
||||||
|
OK syntax: packages/queue/src/queue.mjs
|
||||||
|
OK syntax: packages/queue/src/review.mjs
|
||||||
|
OK syntax: packages/queue/src/store.mjs
|
||||||
|
OK syntax: packages/queue/tests/commit.test.mjs
|
||||||
|
OK syntax: packages/queue/tests/data.test.mjs
|
||||||
|
OK syntax: packages/queue/tests/dispatch.test.mjs
|
||||||
|
OK syntax: packages/queue/tests/helpers.mjs
|
||||||
|
OK syntax: packages/queue/tests/lock.test.mjs
|
||||||
|
OK syntax: packages/queue/tests/migration.test.mjs
|
||||||
|
OK syntax: packages/queue/tests/review.test.mjs
|
||||||
|
OK syntax: packages/queue/tests/store.test.mjs
|
||||||
|
OK syntax: packages/queue/tests/write.test.mjs
|
||||||
|
OK syntax: packages/queue/tests/fixtures/fake-gitea.mjs
|
||||||
|
OK syntax: packages/queue/tests/fixtures/kill-at.mjs
|
||||||
|
OK syntax: packages/queue/tests/fixtures/lock-child.mjs
|
||||||
|
OK syntax: packages/queue/tests/fixtures/mosaic-pre-a2.sh
|
||||||
|
OK syntax: scripts/queue-commit.sh
|
||||||
|
OK syntax: scripts/git-hooks/pre-commit
|
||||||
|
OK syntax: scripts/mosaic
|
||||||
|
OK queue-commit.sh, the guard and scripts/mosaic are executable
|
||||||
|
OK packages/queue declares no dependencies
|
||||||
|
ℹ tests 149
|
||||||
|
ℹ pass 149
|
||||||
|
ℹ fail 0
|
||||||
|
OK node --test packages/queue/tests/
|
||||||
|
OK scripts/mosaic queue help
|
||||||
|
skip queue verify and render --check: this checkout (/home/jwoltje/darkwing-scratch/r62/gate) is not the queue's canonical root (/mnt/storage/src/mosaic-stack)
|
||||||
|
|
||||||
|
queue suite: 27 passed, 0 failed
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
OK valid RELEASE resolves (exit 0)
|
||||||
|
OK invalid RELEASE exits 1 (exit 1)
|
||||||
|
OK missing RELEASE exits 1 (exit 1)
|
||||||
|
OK valid RELEASE leaves image tag consistent with version
|
||||||
|
OK status safe on empty state (exit 0)
|
||||||
|
OK status created no pointer
|
||||||
|
OK fault-injected activation refuses (exit 1)
|
||||||
|
OK refused activation wrote no pointer
|
||||||
|
OK refusal logged exactly once with valid fields
|
||||||
|
OK healthy activation succeeds (exit 0)
|
||||||
|
OK pointer written with valid fields
|
||||||
|
OK repeat activation succeeds (log grows) (exit 0)
|
||||||
|
OK log is append-only across activations
|
||||||
|
OK rollback without previous refuses (exit 1)
|
||||||
|
|
||||||
|
selftest: 14 passed, 0 failed
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
OK valid task validates (exit 0)
|
||||||
|
OK unknown task key exits 2 (exit 2)
|
||||||
|
OK unsupported taskVersion exits 2 (exit 2)
|
||||||
|
OK invalid task id exits 2 (exit 2)
|
||||||
|
OK empty prompt exits 2 (exit 2)
|
||||||
|
OK NUL in expectExact exits 2 (exit 2)
|
||||||
|
OK out-of-range timeout exits 2 (exit 2)
|
||||||
|
OK missing mission file exits 4 (exit 4)
|
||||||
|
OK task with valid mission validates (exit 0)
|
||||||
|
OK invalid mission exits 2 (exit 2)
|
||||||
|
OK validate missing task exits 4 (exit 4)
|
||||||
|
OK validation does not modify the task file
|
||||||
|
OK prune dry-run exits 0 (exit 0)
|
||||||
|
OK dry-run deleted nothing
|
||||||
|
OK prune --keep=2 --yes removes oldest (exit 0)
|
||||||
|
OK kept exactly 2 newest runs
|
||||||
|
OK newest run kept, oldest pruned
|
||||||
|
OK append-only receipt written (3 entries)
|
||||||
|
OK sessions/workspaces untouched by prune
|
||||||
|
OK prune with invalid keep exits 4 (exit 4)
|
||||||
|
skip adapter seam cases (docker daemon unavailable)
|
||||||
|
skip workspace/capability cases (docker daemon unavailable)
|
||||||
|
skip live task cases (docker unavailable)
|
||||||
|
OK onboard without name exits 4 (non-interactive) (exit 4)
|
||||||
|
OK onboard --name renders profile (exit 0)
|
||||||
|
OK profile written
|
||||||
|
OK canon structure: required filled, optional placeholdered
|
||||||
|
OK canon sections present
|
||||||
|
skip live recall pair: user recall run, recalled user name (docker daemon unavailable)
|
||||||
|
OK no agent identity on headless run
|
||||||
|
|
||||||
|
selftest: 26 passed, 0 failed
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Each mutant edits one source file in the build worktree, runs the tasks tests, then restores
|
||||||
|
# the file from the worktree index (the candidate). Expect every mutant to fail at least one test.
|
||||||
|
# The tasks tests run for each mutant; the cli tests too for the config.mjs one.
|
||||||
|
export TMPDIR=~/darkwing-scratch/r62/tmp
|
||||||
|
cd ~/darkwing-scratch/r62/build
|
||||||
|
S=packages/tasks/src/sync.mjs; U=packages/tasks/src/startup.mjs
|
||||||
|
run() { # name file sed-expr [test glob]
|
||||||
|
sed -i "$3" "$2"
|
||||||
|
if git diff --quiet -- "$2"; then echo "$1: MUTATION DID NOT APPLY"; return; fi
|
||||||
|
node --test "${4:-packages/tasks/tests/*.test.mjs}" > ~/darkwing-scratch/r62/tmp/mut-$1.log 2>&1
|
||||||
|
echo "$1: rc=$? $(grep -E '^ℹ (pass|fail) ' ~/darkwing-scratch/r62/tmp/mut-$1.log | tr '\n' ' ')"
|
||||||
|
grep -E '^not ok|^# Subtest|✖ ' ~/darkwing-scratch/r62/tmp/mut-$1.log | grep -v '^ℹ' | sed 's/^/ /' | sort -u | head -8
|
||||||
|
git checkout -q -- "$2"
|
||||||
|
}
|
||||||
|
run no-leave-tree-check $S 's/} else if (!ctx.tree.has(project)) {/} else if (false) {/'
|
||||||
|
run no-watch-only-bucket $S 's/^const fieldsFor = .*/const fieldsFor = (task, bucket) => taskFields(task, bucket);/'
|
||||||
|
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));/'
|
||||||
|
run keep-archived $U 's/if (p.is_archived || p.id === ctx.project) continue;/if (p.id === ctx.project) continue;/'
|
||||||
|
run config-drops-descendants packages/cli/src/config.mjs 's/ descendants: v\["tracker.descendants"\],/ descendants: false,/' 'packages/cli/tests/*.test.mjs'
|
||||||
|
git diff --quiet -- $S $U packages/cli/src/config.mjs && echo "restored: sources match the candidate index"
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
no-leave-tree-check: rc=1 ℹ pass 62 ℹ fail 1
|
||||||
|
✖ a child that leaves the tree has its tasks recorded missing, and the ledger keeps their history (160.370577ms)
|
||||||
|
✖ failing tests:
|
||||||
|
no-watch-only-bucket: rc=1 ℹ pass 58 ℹ fail 5
|
||||||
|
✖ a child made after startup joins the tree at the next reconcile (149.827337ms)
|
||||||
|
✖ a move inside the tree is a moved tombstone plus a snapshot under the new ref (162.487755ms)
|
||||||
|
✖ a three-level tree is read per project, and a project outside it is not (149.484335ms)
|
||||||
|
✖ a watch-only child keeps the business ready, and a failed child read refuses as transient (162.164514ms)
|
||||||
|
✖ bucket 0 on a watch-only task never becomes a bucket or a state (133.187732ms)
|
||||||
|
✖ failing tests:
|
||||||
|
placed-by-id: rc=1 ℹ pass 62 ℹ fail 1
|
||||||
|
✖ a move inside the tree is a moved tombstone plus a snapshot under the new ref (163.981091ms)
|
||||||
|
✖ failing tests:
|
||||||
|
keep-archived: rc=1 ℹ pass 62 ℹ fail 1
|
||||||
|
✖ failing tests:
|
||||||
|
✖ the project list refuses without read_all, and skips archived projects and saved filters (434.524613ms)
|
||||||
|
config-drops-descendants: rc=1 ℹ pass 82 ℹ fail 1
|
||||||
|
✖ failing tests:
|
||||||
|
✖ trackers come from the tracker.* variables of the one project that names a tracker project (178.137587ms)
|
||||||
|
restored: sources match the candidate index
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
# Row 62 probes: reading a Vikunja project tree
|
||||||
|
|
||||||
|
Row 62 (#1555) has the tasks adapter read a project and its descendants.
|
||||||
|
No probe in this row touched a live Vikunja. Sage told me not to touch any
|
||||||
|
board or project, live or scratch, during the build. The evidence below is
|
||||||
|
the v2.7.0 source, Sage's live records, and the fake. Sage runs the reads
|
||||||
|
in "Gate read" with the sync key at the gate.
|
||||||
|
|
||||||
|
## Source
|
||||||
|
|
||||||
|
Vikunja tag `v2.7.0`, commit `a16be96aa454671fdf213b0fbe411dd38a098418`,
|
||||||
|
read from a local checkout. Paths are relative to the repository root.
|
||||||
|
|
||||||
|
### P1. The project list
|
||||||
|
|
||||||
|
`pkg/routes/api/v2/projects.go:39-46` registers `GET /projects`
|
||||||
|
(`projects-list`). Its description: "Returns the projects the
|
||||||
|
authenticated user has access to (owned plus shared, with child projects
|
||||||
|
of accessible parents), paginated. Archived projects are excluded unless
|
||||||
|
is_archived=true." The adapter reads it with the standard envelope
|
||||||
|
(`items`, `total`, `page`, `per_page`, `total_pages`), every page.
|
||||||
|
|
||||||
|
`pkg/models/project.go:620-651`: `getArchived` false adds
|
||||||
|
`is_archived = false` to the query.
|
||||||
|
`pkg/models/project.go:154-158`: pseudo-project ids are negative. -1 is
|
||||||
|
Favorites, -2 and below are saved filters. The adapter skips any id at or
|
||||||
|
below 0.
|
||||||
|
|
||||||
|
The recorded `GET /projects/{p}` body in
|
||||||
|
`packages/tasks/tests/fixtures/v2-shapes.json` has `parent_project_id`
|
||||||
|
and `is_archived`, so the field names and types come from a v2.7.0
|
||||||
|
answer, not only from source. No recording holds a `GET /projects` list.
|
||||||
|
`shapes.test.mjs` checks the fake's list items against the recorded
|
||||||
|
project object and its envelope against the recorded views list.
|
||||||
|
|
||||||
|
### P2. Archive state reaches every descendant
|
||||||
|
|
||||||
|
`pkg/models/project.go:884-888`: "is_archived is materialized down the
|
||||||
|
tree". `pkg/models/project.go:1216-1217` calls
|
||||||
|
`SetArchiveStateForProjectDescendants` when the flag changes, and
|
||||||
|
`:1634` is the update that writes it. An archived parent's children come
|
||||||
|
back with `is_archived: true` too, so skipping archived items drops the
|
||||||
|
whole subtree. The adapter also checks the flag itself, in case a list
|
||||||
|
ever includes archived projects.
|
||||||
|
|
||||||
|
### P3. Access inherits down the tree
|
||||||
|
|
||||||
|
`pkg/models/project_access.go:31-75`. `projectAccess` holds every project
|
||||||
|
a user can reach. The query joins each grant to `project_ancestors`, so
|
||||||
|
a share on a parent reaches every descendant. An inherited admin wins
|
||||||
|
anywhere in the chain. Below admin, the nearest grant wins. The sync bot
|
||||||
|
needs one share on the root. The fake's `#access` follows the same rule.
|
||||||
|
|
||||||
|
### P4. One task read across projects
|
||||||
|
|
||||||
|
`pkg/routes/api/v2/task_collection.go:134-141` registers `GET /tasks`
|
||||||
|
(`tasks-list`): "Returns the tasks the authenticated user can see across
|
||||||
|
every project they have access to, paginated and flat."
|
||||||
|
|
||||||
|
`pkg/models/api_routes.go:495-501`, in `tokenAuthorizesRoute`: a token
|
||||||
|
with `tasks` `read_all` may call `GET /api/v2/tasks` as well as
|
||||||
|
`/api/v2/projects/:project/tasks`. The sync key already has that scope.
|
||||||
|
|
||||||
|
`pkg/models/task_collection.go:220-230`: with no project id, the project
|
||||||
|
set is `getRawProjectsForUser` with the default options, so archived
|
||||||
|
projects are left out (P1).
|
||||||
|
|
||||||
|
`pkg/models/task_collection_filter.go:157-158` maps a filter field
|
||||||
|
`project` to `project_id`, so `project in 19, 18, 20, 21` filters by
|
||||||
|
project.
|
||||||
|
|
||||||
|
The adapter doesn't use this read. It reads per project, as the brief
|
||||||
|
says, until Sage's live read shows the cross-project read works for the
|
||||||
|
sync key. If it does, a tick is two GETs whatever the tree's size, and a
|
||||||
|
switch is a follow-up row.
|
||||||
|
|
||||||
|
### P5. Writes on buckets and views
|
||||||
|
|
||||||
|
The adapter writes no project, view or bucket. Sage's first install
|
||||||
|
attempt on #19
|
||||||
|
(`agents/sage/work/vikunja-setup/2026-10-11_install-19-attempt1.json`)
|
||||||
|
found that on v2.7.0 bucket and view writes answer 405 under `/api/v2` and work only
|
||||||
|
under `/api/v1`. Nothing in this row depends on that. It matters for the
|
||||||
|
runbook, which installs boards by hand.
|
||||||
|
|
||||||
|
## Live facts used
|
||||||
|
|
||||||
|
From Sage, with the records under `agents/sage/work/vikunja-setup/`:
|
||||||
|
|
||||||
|
- The bus is on project #19. Its kanban view is 76. The five buckets are
|
||||||
|
todo 159, in-progress 161, in-review 248, blocked 162, done 57.
|
||||||
|
- View 76 has four more buckets: 55, 56, 158 (`Backlog`) and 160
|
||||||
|
(`Pending`). The install check ignores extra buckets. A task in one is
|
||||||
|
recorded with that raw bucket id (`tree.test.mjs`, "a task in a bucket
|
||||||
|
outside the five keeps its raw bucket id").
|
||||||
|
- #19's children are 18, 20 and 21. None has the five buckets, so all
|
||||||
|
three are watch-only.
|
||||||
|
- Sync key 9 has `projects` `read_all` and lists 29 projects
|
||||||
|
(`2026-10-11_shares-read.json`).
|
||||||
|
|
||||||
|
## Fake
|
||||||
|
|
||||||
|
`packages/tasks/src/fake.mjs` models:
|
||||||
|
|
||||||
|
- `parent_project_id` on every project, set by `project(..., {parent})`
|
||||||
|
and changed by `reparent()`
|
||||||
|
- `GET /projects` with `projects` `read_all`, archived projects left out
|
||||||
|
unless `is_archived=true`, saved filters and Favorites as negative ids
|
||||||
|
(`pseudoProject()`)
|
||||||
|
- `archive()`, which marks the project and every descendant
|
||||||
|
- access through an ancestor's share, as in P3, and `unshare()`
|
||||||
|
|
||||||
|
`packages/tasks/tests/tree.test.mjs` runs the adapter against it. The
|
||||||
|
three-level test counts requests. A quiet tick on a root, one installed
|
||||||
|
child and one watch-only grandchild is 6 GETs, two per project. A
|
||||||
|
reconcile is 11: the project list, views and buckets for each of the two
|
||||||
|
children, and two per project. For #19 (root plus three watch-only
|
||||||
|
children) a quiet tick is 8 GETs, plus one per extra page of 50 tasks.
|
||||||
|
|
||||||
|
## Gate read
|
||||||
|
|
||||||
|
Two GETs, sync key 9, against the live instance. Record the status, the
|
||||||
|
item count on the page and `total`. Record no titles. Spaces in a filter
|
||||||
|
go as `%20`.
|
||||||
|
|
||||||
|
1. The tree.
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v2/projects
|
||||||
|
query: per_page=50&page=1
|
||||||
|
key: sync key 9
|
||||||
|
```
|
||||||
|
|
||||||
|
Expect 200 and `total` 29 (Sage's earlier read). Also record, for ids
|
||||||
|
19, 18, 20 and 21 only, `parent_project_id` and `is_archived`. Expect
|
||||||
|
18, 20 and 21 to have parent 19, and all four `is_archived: false`.
|
||||||
|
This is the read `discover()` makes at startup and at each reconcile.
|
||||||
|
|
||||||
|
2. One task read across the tree.
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v2/tasks
|
||||||
|
query: filter=project in 19, 18, 20, 21&sort_by=id&order_by=asc&per_page=50&page=1
|
||||||
|
key: sync key 9
|
||||||
|
```
|
||||||
|
|
||||||
|
A 200 means the cross-project read works for the sync key, and a
|
||||||
|
follow-up row can move a tick to it. 401 or 403 means the scope check
|
||||||
|
in P4 doesn't hold for this key, and the adapter keeps reading per
|
||||||
|
project. For comparison, the per-project open counts are what the
|
||||||
|
adapter reads today, one per project:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v2/projects/{19,18,20,21}/tasks
|
||||||
|
query: filter=done = false&sort_by=id&order_by=asc&per_page=50&page=1
|
||||||
|
key: sync key 9
|
||||||
|
```
|
||||||
|
|
||||||
|
These four are optional. They let the gate compare `total` from read 2
|
||||||
|
with the sum the adapter would see. Read 2 has no `done` filter, so
|
||||||
|
its `total` counts closed tasks too.
|
||||||
|
|
||||||
|
The host config with `tracker.descendants` set reaches the adapter
|
||||||
|
through `packages/cli/src/config.mjs` (lead decision 87), so the gate
|
||||||
|
needs no packet script.
|
||||||
Reference in New Issue
Block a user