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