docs(cli): row 45 S4 follow-up round 2 candidate packet (rocko)
build.patch c8cec070…6756, candidate-manifest 5b067a9d…0e0e, base d539d8d2.
Covers Darkwing 26872, Filbert 26880 and lead decision 73.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -1,35 +1,222 @@
|
||||
# Row 45 (#1527): S4 follow-up, candidate packet
|
||||
# Row 45 (#1527): S4 follow-up, candidate packet, round 2
|
||||
|
||||
Author: Rocko. Reviewers: Filbert and Darkwing. Brief:
|
||||
`docs/plans/2026-10-09_s4-follow-up-and-cohort.md`, section "S4 follow-up".
|
||||
Rulings: lead decision 72. Base: `521597bb` (`base.txt`). This packet is
|
||||
Rulings: lead decision 72, and lead decision 73 with comment 26879 for
|
||||
round 2. Base: `d539d8d2` (`base.txt`). Round 1 was over `521597bb`. None
|
||||
of the 8 files changed upstream between the two bases. This packet is
|
||||
uncommitted. There are no commits, pushes or Gitea calls, and no token or
|
||||
private binding was read.
|
||||
|
||||
Round 2 answers Darkwing's changes verdict (comment 26872, rev 208) and
|
||||
Filbert's (comment 26880, rev 211). The round 1 text follows the round 2
|
||||
section and still holds, except where round 2 says otherwise.
|
||||
|
||||
## Files (`files.txt`, 8)
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `packages/cli/src/notifier.mjs` | F2 refusal limit, J4 type check, directory and file error messages |
|
||||
| `packages/cli/src/host.mjs` | `broker.connected` guards on both `close` sends (old :144 and :150) |
|
||||
| `packages/cli/README.md` | digest mark, definite-refusal bullet, type-check list, refusal wording |
|
||||
| `packages/cli/src/notifier.mjs` | F2 refusal limit, J4 type check, error messages; round 2: the 30-minute refusal wait from the journal, a real-date `day` check, three more error paths |
|
||||
| `packages/cli/src/host.mjs` | `broker.connected` guards on both start-failure `close` sends; round 2: a callback on all four sends (:144, :150, :184, :188) |
|
||||
| `packages/cli/README.md` | digest mark, definite-refusal bullet, type-check list, refusal wording; round 2: the refusal wait, manual recovery, the digest-mark sentence, the new error paths |
|
||||
| `scripts/bus-service.sh` | install message gains the `mkdir -m 0700 -p` line |
|
||||
| `packages/cli/tests/notifier.test.mjs` | F2, J4, N2, N4 and error-path tests |
|
||||
| `packages/cli/tests/host.test.mjs` | reader-cap exposure, M28, N5, both broker-death guards, `t.after` in "a notifier that dies" |
|
||||
| `packages/cli/tests/notifier.test.mjs` | F2, J4, N2, N4 and error-path tests; round 2: spacing, restart, X9, day validity, note 1 paths |
|
||||
| `packages/cli/tests/host.test.mjs` | reader-cap exposure, M28, N5, both broker-death guards, `t.after`; round 2: three EPIPE tests, the X14 assertion |
|
||||
| `packages/cli/tests/trackers-boot.test.mjs` | new: Sage's trackers boot test, byte-identical copy |
|
||||
| `packages/discord/tests/journal.test.mjs` | `deadPid()` replaces the fixed pid `2 ** 22 - 7` |
|
||||
|
||||
`build.patch`: `git diff --cached --binary 521597bb` over those files, +459/−44.
|
||||
It applies cleanly to 521597bb (see the gate tree's `out/manifest-check-tree.txt`).
|
||||
`build.patch`: `git diff --cached --binary d539d8d2` over those files,
|
||||
+635/−56. It applies cleanly to d539d8d2 (`out/manifest-check-tree-r2.txt`).
|
||||
|
||||
## What changed
|
||||
## Round 2
|
||||
|
||||
### R1: a callback on every send that can race a death (both reviews)
|
||||
|
||||
`host.mjs` :144, :150 and :188 now read
|
||||
`if (broker.connected) broker.send({ op: "close" }, () => {});`, and :184
|
||||
reads `if (notify.connected) notify.send({ op: "stop" }, () => {});`.
|
||||
|
||||
`connected` stays true after a child dies until Node processes the
|
||||
disconnect. A send in that window fails with EPIPE. Without a callback,
|
||||
Node emits that as an `error` event on the next tick. The `once(child,
|
||||
"exit")` in `ended()` rejects on `error`, so the raw EPIPE replaces the
|
||||
real outcome. With a callback, the error goes to the callback and nothing
|
||||
else sees it. The `exit` still settles `ended()`.
|
||||
|
||||
What the raw EPIPE cost (Filbert's point):
|
||||
|
||||
- **:150**, the refusal path. A notifier refusal is a CliError with exit
|
||||
3, which `RestartPreventExitStatus=2 3 4` keeps down. A raw EPIPE is not
|
||||
a CliError, so `cli.mjs` exits 1 with a stack trace, and the unit
|
||||
restarts the host every 15 s against a refusal that won't go away.
|
||||
- **:144**, the no-reply path. The exit code is 1 either way, but the
|
||||
operator gets the EPIPE instead of the notifier's message.
|
||||
- **:184 and :188**, in `close()`. A raw EPIPE rejects `close()`, so the
|
||||
host never removes its state file or finishes `done`.
|
||||
|
||||
Tests (Darkwing's deterministic form). `failSends(t, fake)` in
|
||||
`host.test.mjs` traps the `send` setter of each new child, as the
|
||||
reader-cap spy does. When `fake(child, m)` returns an error, the send
|
||||
fails the way Node fails a write to a dead child: the error goes to the
|
||||
callback if there is one, or else to an `error` event on the next tick.
|
||||
`epipe(child)` SIGKILLs the child and returns an EPIPE error. The real
|
||||
window is about a millisecond wide, so a test that waits for it would be
|
||||
flaky. This one is not.
|
||||
|
||||
- A close send that fails with EPIPE after the notifier refuses: the host
|
||||
rejects with exit 3 and `notifier refused to start: … no dmRecipient`.
|
||||
- A close send that fails with EPIPE after the notifier dies without a
|
||||
reply: the host rejects with exit 1 and `notifier exited (null) before it
|
||||
replied`.
|
||||
- `close()` whose stop and close sends both fail with EPIPE resolves with
|
||||
1 and removes the state file.
|
||||
|
||||
All three passed 15 of 15 runs. Without a callback, each one fails (G144cb,
|
||||
G150cb, G184 and G188 in the mutant table).
|
||||
|
||||
A correction from my first round 2 mutant run: G184 hung the host test file
|
||||
for its 900 s timeout instead of failing. The `close()` test had
|
||||
`t.after(() => host.close(0))`. When the mutant made `close()` reject,
|
||||
`closing` was already set, so the cleanup's second `close()` waited on a
|
||||
`done` that never settles. The broker it never closed also kept the file
|
||||
alive. The fix: `failSends` now SIGKILLs every child it saw after each
|
||||
test, and that test has no `t.after` close. With the fix, G184 and G188 each
|
||||
fail that test in about 20 s. I stopped the first run, restored the mutated
|
||||
`host.mjs` from its `.orig` copy (the manifest checked OK), then rebuilt the
|
||||
patch and re-ran the whole gate and every mutant from scratch.
|
||||
|
||||
### R2 / B1: a definite refusal waits the full 30 minutes (decision 73)
|
||||
|
||||
In round 1, a refusal backed off the same way as an unknown outcome: 30 s,
|
||||
doubling. So five refusals took 7.5 minutes, and a binding typo gave up
|
||||
every blocking DM before anyone could fix it. Now:
|
||||
|
||||
- After a definite refusal, the next attempt waits `BACKOFF_MAX_MS`
|
||||
(30 min). The log line reads `refused (HTTP 403); retry in 1800 s`.
|
||||
- The wait is counted from the refusal's `at` in the journal. `openJournal`
|
||||
keeps `refusedAt`, the time of each decision's last definite refusal, and
|
||||
updates it on every append. `tick` skips a decision until 30 minutes after
|
||||
that time. A restart rebuilds the map from the file, so it can't shorten
|
||||
the wait.
|
||||
- Unknown outcomes keep the doubling backoff from 30 s, with no limit.
|
||||
|
||||
The five sends therefore go at 0, 30, 60, 90 and 120 minutes, and the DM
|
||||
gives up at 2 hours.
|
||||
|
||||
Tests:
|
||||
|
||||
- **Spacing.** Polled every `POLL_MS` on a fake clock against a permanent
|
||||
403: sends at 0, 30, 60, 90 and 120 min, no gave-up line before 120 min,
|
||||
one at 120 min, and four `retry in 1800 s` log lines.
|
||||
- **Restart.** Refusals at 0 and 30 min, then a restart at 31 min. The next
|
||||
send comes at 60 min, not at once.
|
||||
- The existing backoff test now checks both waits. An unknown outcome logs
|
||||
`retry in 30 s`. A 403 sends nothing at +29 min, sends at +30 min, and
|
||||
logs `retry in 1800 s`.
|
||||
|
||||
### README
|
||||
|
||||
- **Recovery after a give-up is manual.** Fixing the binding does not
|
||||
resend the DM, which stays given up. The decision stays open in `mosaic
|
||||
inbox`, the digest lists it, and the operator decides it with `mosaic
|
||||
decide`.
|
||||
- The definite-refusal bullet gives the 30-minute wait from the journal and
|
||||
the 2-hour span. The `unknown` bullet keeps the doubling backoff.
|
||||
- Darkwing's note 4: the digest-mark sentence now quotes its three states,
|
||||
and the long line is rewrapped.
|
||||
- The `day` field and the refusal list name the round 2 checks.
|
||||
|
||||
### Optional items taken
|
||||
|
||||
- **X9 (Filbert):** a 0400 `sent.jsonl` in a good directory refuses with
|
||||
`notify journal is not writable (EACCES): <file>`.
|
||||
- **X14 (Filbert) and Darkwing's note 2:** the bus-service install test
|
||||
asserts the `mkdir -m 0700 -p <dataRoot>/notify/<business>` line.
|
||||
- **Darkwing's note 3:** a `day` must be a real date. `2026-13-45` and
|
||||
`2026-02-30` now refuse as `(day)`. The check is the `YYYY-MM-DD` shape,
|
||||
then `Date.parse`, then a `toISOString()` round trip.
|
||||
- **Darkwing's note 1:** the three raw-error paths now refuse with a
|
||||
CliError and exit 3:
|
||||
- A dangling symlink in place of the directory: `mkdir` fails with
|
||||
ENOENT, and an `lstat` finds a link, so the message is `notify journal
|
||||
directory must not be a symlink`.
|
||||
- A parent that is a file: ENOTDIR gives `notify journal directory
|
||||
cannot be created (ENOTDIR)`.
|
||||
- A `sent.jsonl` that is a directory: EISDIR gives `notify journal must
|
||||
be a regular file, mode 0600, owned by this user`.
|
||||
|
||||
### Declined: a type check inside `append` (Filbert's note 1)
|
||||
|
||||
Filbert left this to me, and I left it out. A failing check in `append`
|
||||
would throw inside `attempt`'s catch, which journals and logs the send as
|
||||
`unknown`. The DM was in fact sent. The next tick would send it again, and
|
||||
then again on every later tick, so a bad `messageId` would become a loop of
|
||||
duplicate DMs. Today the writer can't produce a bad line: `rest.mjs`
|
||||
`createMessage` throws `unknown` on a 2xx without a string id. If that
|
||||
changes, the open-time check refuses the bad line with exit 3. That is a
|
||||
loud failure, and it fits fail-closed better than a send loop.
|
||||
|
||||
Darkwing's note 5 (`accessSync` is advisory) needs no change. `open` still
|
||||
refuses, and only the message could differ.
|
||||
|
||||
### Round 2 gate
|
||||
|
||||
The gate tree `/mnt/storage/scratch/rocko-r45/tree` is now a detached
|
||||
worktree at d539d8d2 with `build.patch` applied. The manifest checks 8/8 OK
|
||||
(`out/manifest-check-tree-r2.txt`). `run.sh` ran every node suite and then
|
||||
every `test-*.sh`, one at a time, with Docker withheld from test-task and
|
||||
test-release. The outputs are `out/node-*-r2.txt` and `out/suite-*-r2.txt`.
|
||||
It ran 14:02:34Z to 14:05:56Z, and load average was 1.01 at the start and
|
||||
2.44 at the end.
|
||||
|
||||
| Suite | Pass / fail |
|
||||
|---|---|
|
||||
| node business / bus / cli / control-board | 60/0 · 67/0 · 66/0 · 124/0 |
|
||||
| node conversation / discord / ledger / mosaic | 152/0 · 178/0 · 78/0 · 69/0 |
|
||||
| node queue / seat / tasks / webui | 148/0 · 19/0 · 51/0 · 14/0 |
|
||||
| test-auth / conductor / config / discord | 15/0 · 17/0 · 24/0 · 66/0 |
|
||||
| test-extension-package / foundation / queue / release | 18/0 · 44/0 · 27/0 · 4/0 (Docker cases skipped) |
|
||||
| test-task | 26/2: "user recall run succeeds", "recalled user name" |
|
||||
|
||||
Conversation passes in full now that row 46 has landed (2d308abd). The two
|
||||
test-task failures are the live-recall cases that need Docker. They failed
|
||||
the same way on the round 1 base. I did not rerun the base on d539d8d2,
|
||||
because none of the 8 files changed between the two bases.
|
||||
|
||||
### Round 2 mutants
|
||||
|
||||
`mutants.sh` re-ran all 18 round 1 mutants and 13 new ones in the gate
|
||||
tree after the gate (`out/r2/mutants.txt`, `out/r2/mut-*.txt`). It ran
|
||||
14:05:56Z to 14:08:50Z, and load average was 2.44 at the start and 3.85 at
|
||||
the end. All **31 of 31** were killed, each by a failing test, with 0
|
||||
cancelled. The round 1 mutants were killed by the same tests as in round 1.
|
||||
F2a and F2f are now also killed by the spacing test. Afterwards the tree
|
||||
checked 8/8 OK (`out/r2/manifest-check-mut.txt`).
|
||||
|
||||
| Id | Mutation | Killed by |
|
||||
|---|---|---|
|
||||
| G144cb | no callback on the :144 close send | EPIPE after the notifier dies unanswered |
|
||||
| G150cb | no callback on the :150 close send | EPIPE after the notifier refuses |
|
||||
| G184 | no callback on the `close()` stop send | `close()` with EPIPE on stop and close |
|
||||
| G188 | no callback on the `close()` close send | `close()` with EPIPE on stop and close |
|
||||
| R2a | a definite refusal backs off like an unknown | backoff test (the 1800 s log); spacing test |
|
||||
| R2b | `tick` ignores `refusedAt` | restart after the second refusal |
|
||||
| R2c | `refusedAt` never set | restart after the second refusal |
|
||||
| X9 | no CliError for an unwritable `sent.jsonl` | the unwritable-journal test |
|
||||
| X14 | install message drops the `mkdir -m 0700` line | bus-service.sh install test |
|
||||
| D3 | `day` is a shape check only | wrong-type table (`2026-13-45`, `2026-02-30`) |
|
||||
| N1a | no symlink message for a dangling directory link | dangling link, file parent, directory journal |
|
||||
| N1b | ENOTDIR not turned into a CliError | the same test |
|
||||
| N1c | EISDIR not turned into a CliError | the same test |
|
||||
|
||||
## Round 1 (still holds unless round 2 says otherwise)
|
||||
|
||||
### F2: the refusal limit (decision 72)
|
||||
|
||||
- `REFUSAL_LIMIT = 5`, exported. A definite refusal is a `dm` journal line
|
||||
with outcome `refused` and an integer status from 400 to 499 other than
|
||||
429. A 429, an `unknown` outcome and a refusal without a status never
|
||||
count. Unknown outcomes keep retrying at the 30-minute cap, with no limit.
|
||||
count. Unknown outcomes keep retrying with no limit.
|
||||
- The count comes from the journal: `openJournal` keeps a `refusals` map
|
||||
and a `gaveUp` set and updates both on every append. A restart rebuilds
|
||||
them from the file.
|
||||
@@ -55,22 +242,22 @@ or the first field that fails:
|
||||
- `at`: not an ISO string that round-trips through `toISOString()`.
|
||||
- `kind`: not `dm` or `digest`.
|
||||
- `decision`: for a dm, a non-empty string; for a digest, null or absent.
|
||||
- `day`: required and `YYYY-MM-DD` for a digest; optional for a dm, but
|
||||
well-formed if present.
|
||||
- `day`: required and a real `YYYY-MM-DD` date for a digest (round 2);
|
||||
optional for a dm, but valid if present.
|
||||
- `outcome`: one of confirmed, refused, unknown or gave-up. gave-up is
|
||||
allowed only on a dm.
|
||||
- `messageId`: a string on `confirmed`; null or a string otherwise.
|
||||
- `status`: an integer when present.
|
||||
|
||||
Choices a reviewer may contest: a digest with a decision is refused,
|
||||
gave-up is dm-only, and `confirmed` requires a string messageId. The test
|
||||
table also proves that the good fixture lines still load.
|
||||
Both reviewers agreed with the contested choices: every line is checked, a
|
||||
digest with a decision is refused, gave-up is dm-only, and `confirmed`
|
||||
requires a string messageId.
|
||||
|
||||
### Error messages (the brief's error items)
|
||||
|
||||
- mkdir fails with EACCES, EPERM or EROFS:
|
||||
- mkdir fails with EACCES, EPERM or EROFS (round 2: or ENOTDIR):
|
||||
`notify journal directory cannot be created (CODE): <dir>`.
|
||||
- Directory is a symlink (checked with lstat):
|
||||
- Directory is a symlink (checked with lstat; round 2: a dangling one too):
|
||||
`notify journal directory must not be a symlink: <dir>`. The 0700 check
|
||||
follows it as before.
|
||||
- Directory not writable (`accessSync` W_OK|X_OK):
|
||||
@@ -85,19 +272,11 @@ already states the 0700 rule.
|
||||
### host.mjs guards
|
||||
|
||||
Both `broker.send({ op: "close" })` sends on the notifier start-failure
|
||||
paths are now `if (broker.connected) …`. :144 is the no-reply path (a start
|
||||
paths are `if (broker.connected) …`. :144 is the no-reply path (a start
|
||||
timeout, or the notifier exiting before it replies). :150 is the refusal
|
||||
path. On a closed channel, `ChildProcess.send` emits `error`
|
||||
(ERR_IPC_CHANNEL_CLOSED).
|
||||
|
||||
There is a remaining window, which I'm recording but did not change.
|
||||
`connected` stays true after the broker dies until Node processes the
|
||||
disconnect. If the broker is SIGKILLed and the notifier exits a moment
|
||||
later, the :144 close can still be sent. When I probed it, the late send
|
||||
emitted no `error` event in 5 of 5 runs and `connected` was false shortly
|
||||
afterwards. Passing a callback to that `send` would close the window
|
||||
completely. I left it out because the brief asked for the guard and no
|
||||
failure was observed. It is a candidate follow-up if a reviewer wants it.
|
||||
(ERR_IPC_CHANNEL_CLOSED). Round 1 recorded the remaining window after a
|
||||
SIGKILL and left it open. Round 2 closes it (R1 above).
|
||||
|
||||
### Tests
|
||||
|
||||
@@ -115,93 +294,33 @@ failure was observed. It is a candidate follow-up if a reviewer wants it.
|
||||
swapped for a symlink fails with ELOOP and writes nothing through it.
|
||||
- **Guard (G150):** the spy SIGKILLs the broker synchronously on the start
|
||||
send. The notifier still refuses with `/no dmRecipient/`, there is no
|
||||
`error` event and there is no `close` send. 15/15 runs passed.
|
||||
`error` event and there is no `close` send.
|
||||
- **Guard (G144):** the spy SIGSTOPs the notifier, SIGKILLs the broker,
|
||||
and SIGKILLs the notifier once the broker's `disconnect` fires. The host
|
||||
rejects with `notifier exited (null) before it replied`, exit 1, with no
|
||||
`error` event and no `close` send. 15/15 runs passed, and 10/10 after a
|
||||
small cleanup. My first version killed both children at once, and the
|
||||
test caught the window described above.
|
||||
`error` event and no `close` send.
|
||||
- **`t.after`** is added to "a notifier that dies".
|
||||
- **0500 directory and 0500 parent:** modes are restored in try/finally,
|
||||
not `t.after`. A `t.after` restore runs after `tmp()` cleanup and leaked a
|
||||
directory once; I removed that directory. Both tests skip when running as
|
||||
root.
|
||||
not `t.after`, because a `t.after` restore runs after `tmp()` cleanup.
|
||||
Both tests skip when running as root.
|
||||
- **Discord lock test:** `deadPid()` spawns a node child, reaps it, and
|
||||
loops until `!pidAlive(pid)`. pid_max here is 4194304, so `2 ** 22 - 7`
|
||||
can be a live pid. I replaced all five uses in that file, not only :120.
|
||||
can be a live pid. All five uses in that file are replaced.
|
||||
- **Trackers boot test:** copied byte-identical to
|
||||
`packages/cli/tests/trackers-boot.test.mjs`. No import changes were needed.
|
||||
Sage's tracked original in `agents/sage/work/s4-follow-up/` is left for
|
||||
Sage to remove, because I don't edit another seat's directory.
|
||||
`packages/cli/tests/trackers-boot.test.mjs`. Sage's tracked original in
|
||||
`agents/sage/work/s4-follow-up/` is left for Sage to remove at landing.
|
||||
|
||||
## Gate
|
||||
### Round 1 gate and mutants
|
||||
|
||||
Round 1 ran the full gate. I then added the G144 test, which changes only
|
||||
`host.test.mjs`. I rebuilt the patch, re-applied it to a clean checkout of
|
||||
the tree (manifest OK), and re-ran the cli node suite: **60/0**
|
||||
(`out/node-cli.txt`). The other node suites and the `test-*.sh` suites below
|
||||
are from round 1, on the identical source files.
|
||||
|
||||
Detached worktree `/mnt/storage/scratch/rocko-r45/tree` at 521597bb with
|
||||
`build.patch` applied; manifest check OK (`out/manifest-check-tree.txt`).
|
||||
`run.sh` ran every `packages/*/tests` suite, then every `scripts/test-*.sh`,
|
||||
in sequence. test-task and test-release ran with
|
||||
`DOCKER_HOST=unix:///nonexistent-rocko-r45-docker.sock`. Load average was
|
||||
4.34 at the start and 5.65 at the end.
|
||||
|
||||
| Suite | Pass / fail |
|
||||
|---|---|
|
||||
| node business / bus / cli / control-board | 60/0 · 67/0 · 60/0 (round 2; 59/0 in round 1) · 124/0 |
|
||||
| node discord / ledger / mosaic / queue | 178/0 · 78/0 · 69/0 · 148/0 |
|
||||
| node seat / tasks / webui | 19/0 · 51/0 · 14/0 |
|
||||
| node conversation | 149/3: K1, K3, K10 (row 46, expected) |
|
||||
| test-auth / conductor / config / discord | 15/0 · 17/0 · 24/0 · 66/0 |
|
||||
| test-extension-package / foundation / queue / release | 18/0 · 44/0 · 27/0 · 4/0 |
|
||||
| test-task | 26/2: "user recall run succeeds", "recalled user name" |
|
||||
|
||||
Unpatched base, worktree `/mnt/storage/scratch/rocko-r45/base` at 521597bb
|
||||
(`out/base-*.txt`):
|
||||
|
||||
- test-task: 26/2, the same two live-recall failures. They need Docker, which
|
||||
the gate withholds. They are not caused by this patch.
|
||||
- conversation: 150/2 (K1, K3). The patch doesn't touch
|
||||
`packages/conversation`, so K10 is intermittent. It is one of the brief's
|
||||
row 46 exceptions.
|
||||
|
||||
## Mutants (`mutants.sh`, `out/mutants.txt`, `out/mut-*.txt`)
|
||||
|
||||
Each mutant is one perl substitution in the gate tree, followed by a run of
|
||||
the cli and discord node suites. Only a `fail` counts as a kill. Every
|
||||
failing test is named, and none is a flaky bystander. The table comes from
|
||||
the round 2 run, where all 18 were killed. In round 1, before its test
|
||||
existed, G144 survived. After the run the tree re-checked OK
|
||||
(`out/manifest-check-mut.txt`). Load average was 3.98 at the start and 5.38
|
||||
at the end.
|
||||
|
||||
| Id | Mutation | Result | Killed by |
|
||||
|---|---|---|---|
|
||||
| N2 | `lstatSync(dir)` → `statSync(dir)` | killed | symlinked directory refuses |
|
||||
| N4 | drop `O_NOFOLLOW` from the append open | killed | append after symlink swap |
|
||||
| N5 | death check ignores `signalCode` | killed | watchChildren, child died before call |
|
||||
| M28 | drop the `prior?.live` refusal | killed | second host refuses with exit 3 |
|
||||
| G150 | drop the guard on the refusal-path close | killed | notifier refuses after broker died |
|
||||
| G144 | drop the guard on the no-reply-path close | killed | notifier dies before it replies |
|
||||
| F2a | limit 5 → 6 | killed | five definite refusals stop a DM |
|
||||
| F2b | 429 counts as definite | killed | 429s/unknowns never count |
|
||||
| F2c | tick ignores `gaveUp` | killed (2) | crash recovery; five refusals |
|
||||
| F2d | no crash-recovery give-up in tick | killed | crash recovery |
|
||||
| F2e | no digest mark | killed | five refusals (digest check) |
|
||||
| F2f | no give-up after the fifth refusal | killed | five refusals |
|
||||
| J4a | `at` round-trip check removed | killed | wrong-type table |
|
||||
| J4b | dm decision type check removed | killed | wrong-type table |
|
||||
| J4c | `status` integer check removed | killed | wrong-type table |
|
||||
| J4d | confirmed messageId check removed | killed | wrong-type table |
|
||||
| E1 | `accessSync` writability check removed | killed | unwritable directory/parent |
|
||||
| E2 | symlink refusal removed | killed | symlinked directory refuses |
|
||||
Round 1, over 521597bb, was green except for conversation K1, K3 and K10
|
||||
(row 46) and test-task's two live-recall failures, which need Docker and
|
||||
fail the same way on the base. All 18 round 1 mutants were killed. The
|
||||
files are `out/node-*.txt`, `out/suite-*.txt`, `out/base-*.txt` and
|
||||
`out/mut-*.txt` without the `-r2` suffix. Both reviewers reproduced them.
|
||||
|
||||
## Hashes
|
||||
|
||||
- `build.patch`: `4ed9ff61b94b4c9d2426e5703c2e6c0dbcd6bc0c64a88d59347a8b47f4933408`
|
||||
- `candidate-manifest.sha256`: `b329fdcbdf8cb62659f239359570dfa9771559e3112b743df55429244769ed14`
|
||||
- `build.patch`: `c8cec070da60d8297f1ee5b006a1099ab9376fd3abf32fa4461f967750ad6756`
|
||||
- `candidate-manifest.sha256`: `5b067a9dad645c31d99e1ea02575cd2fc1ba547ce011ea81c56b17ae29da0d0e`
|
||||
- `packet-manifest.sha256`: a sorted sha256 list of every packet file except itself.
|
||||
- Round 1, for reference: `build.patch` `4ed9ff61…3408`, `candidate-manifest.sha256` `b329fdcb…ed14`.
|
||||
|
||||
@@ -1 +1 @@
|
||||
521597bbe0cf51a0a1aa3b7a16f13fc7b59c98c6
|
||||
d539d8d2cc930ff8707f8d778a78b1fe661a3d99
|
||||
|
||||
@@ -1,28 +1,44 @@
|
||||
diff --git a/packages/cli/README.md b/packages/cli/README.md
|
||||
index 589078c3..67dc6618 100644
|
||||
index 589078c3..0eb1423c 100644
|
||||
--- a/packages/cli/README.md
|
||||
+++ b/packages/cli/README.md
|
||||
@@ -145,8 +145,8 @@ inbox through the reader capability and does two things:
|
||||
@@ -145,9 +145,10 @@ inbox through the reader capability and does two things:
|
||||
- **Digest.** It sends one digest a day at 08:00 America/Chicago. The hour
|
||||
comes from the IANA zone, so daylight saving time is handled. If the host
|
||||
starts after 08:00 and the day has no digest yet, the digest goes at once.
|
||||
- The digest lists the inbox and marks each blocking decision as DM sent or
|
||||
- DM pending. An empty inbox gets one line. Each message stays within
|
||||
+ The digest lists the inbox and marks each blocking decision as DM sent,
|
||||
+ DM pending, or DM refused, not retried. An empty inbox gets one line. Each message stays within
|
||||
Discord's 2000 characters.
|
||||
- Discord's 2000 characters.
|
||||
+ The digest lists the inbox. It marks each blocking decision with one of
|
||||
+ three states: "DM sent", "DM pending" or "DM refused, not retried". An
|
||||
+ empty inbox gets one line. Each message stays within Discord's 2000
|
||||
+ characters.
|
||||
|
||||
The notifier only reads the bus. Its memory is the journal
|
||||
@@ -165,16 +165,29 @@ one line per send attempt:
|
||||
`<dataRoot>/notify/<business>/sent.jsonl` (directory 0700, file 0600), with
|
||||
@@ -160,21 +161,41 @@ one line per send attempt:
|
||||
|
||||
- A decision, or a day's digest, counts as sent once it has a `confirmed`
|
||||
line.
|
||||
-- A `refused` or `unknown` send is retried. The wait starts at 30 s and
|
||||
- doubles up to 30 min. A duplicate costs less than a miss. Every retry
|
||||
+- A `refused` or `unknown` send is retried. After an `unknown` outcome the
|
||||
+ wait starts at 30 s and doubles up to 30 min. A duplicate costs less than
|
||||
+ a miss. Every retry
|
||||
carries the same Discord nonce, so a retry inside Discord's dedupe window
|
||||
returns the first message. A DM's nonce comes from the decision id. A
|
||||
digest's comes from the business and the day.
|
||||
+- A definite refusal is a DM refused with an HTTP 4xx other than 429. After
|
||||
+ 5 of them for one decision, the notifier appends one `gave-up` line, logs
|
||||
+ it once and never sends that DM again; the digest marks the decision "DM
|
||||
+ refused, not retried". The count comes from the journal, so a restart
|
||||
+ keeps it. An `unknown` outcome (network, 5xx or 429) retries without a
|
||||
+ limit (lead decision 72).
|
||||
+- A definite refusal is a DM refused with an HTTP 4xx other than 429. The
|
||||
+ next attempt waits the full 30 min, counted from the refusal's `at` in
|
||||
+ the journal, so a restart does not shorten it. After 5 of them for one
|
||||
+ decision, at least 2 hours apart end to end, the notifier appends one
|
||||
+ `gave-up` line, logs it once and never sends that DM again. The digest
|
||||
+ marks the decision "DM refused, not retried". The count comes from the
|
||||
+ journal, so a restart keeps it. An `unknown` outcome (network, 5xx or
|
||||
+ 429) retries without a limit (lead decisions 72 and 73).
|
||||
+- Recovery after a give-up is manual. Fixing the binding does not resend
|
||||
+ the DM, which stays given up. The decision stays open in `mosaic inbox`,
|
||||
+ the digest lists it, and the operator decides it with `mosaic decide`.
|
||||
- On open, a final line without its newline is a torn write. The notifier
|
||||
copies those bytes to `torn-<UTC stamp>.bin` in the same directory (0600,
|
||||
a new file, synced), then truncates `sent.jsonl` to its last newline and
|
||||
@@ -35,20 +51,21 @@ index 589078c3..67dc6618 100644
|
||||
- regular 0600 file you own.
|
||||
+- A malformed complete line refuses with exit 3 and changes nothing. Each
|
||||
+ line is type-checked: `at` an ISO timestamp, `kind` `dm` or `digest`,
|
||||
+ `decision` a string (required for `dm`, null for `digest`), `day`
|
||||
+ `YYYY-MM-DD` (required for `digest`), `outcome` one of `confirmed`,
|
||||
+ `decision` a string (required for `dm`, null for `digest`), `day` a real
|
||||
+ `YYYY-MM-DD` date (required for `digest`), `outcome` one of `confirmed`,
|
||||
+ `refused`, `unknown` or `gave-up` (`gave-up` only for `dm`), `messageId`
|
||||
+ a string (required when confirmed) or null, `status` an integer when
|
||||
+ present.
|
||||
+- The journal refuses with exit 3 when its directory is looser than 0700,
|
||||
+ not yours, a symlink or not writable, when `sent.jsonl` is a symlink or
|
||||
+ not writable, or when the file is not a regular 0600 file you own. The
|
||||
+ message names the path.
|
||||
+ not yours, a symlink (dangling or not) or not writable, or cannot be
|
||||
+ created (for example, a parent is a file). It also refuses when
|
||||
+ `sent.jsonl` is a symlink, a directory or not writable, or is not a
|
||||
+ regular 0600 file you own. The message names the path.
|
||||
- No Discord channel or user id goes in the journal, a log line or an
|
||||
error.
|
||||
|
||||
diff --git a/packages/cli/src/host.mjs b/packages/cli/src/host.mjs
|
||||
index 159160c0..26553cba 100644
|
||||
index 159160c0..083f8f1d 100644
|
||||
--- a/packages/cli/src/host.mjs
|
||||
+++ b/packages/cli/src/host.mjs
|
||||
@@ -141,13 +141,13 @@ export async function startHost({ boot, business, notifier = null, bootTimeoutMs
|
||||
@@ -56,22 +73,36 @@ index 159160c0..26553cba 100644
|
||||
notify.kill("SIGTERM");
|
||||
await ended(notify, 5000);
|
||||
- broker.send({ op: "close" });
|
||||
+ if (broker.connected) broker.send({ op: "close" });
|
||||
+ if (broker.connected) broker.send({ op: "close" }, () => {});
|
||||
await ended(broker, CLOSE_TIMEOUT_MS);
|
||||
throw e;
|
||||
}
|
||||
if (ok?.ok !== true) {
|
||||
await ended(notify, 5000);
|
||||
- broker.send({ op: "close" });
|
||||
+ if (broker.connected) broker.send({ op: "close" });
|
||||
+ if (broker.connected) broker.send({ op: "close" }, () => {});
|
||||
await ended(broker, CLOSE_TIMEOUT_MS);
|
||||
throw new CliError(`notifier refused to start: ${typeof ok?.error === "string" ? ok.error : "notifier-refused"}`, 3);
|
||||
}
|
||||
@@ -181,11 +181,11 @@ export async function startHost({ boot, business, notifier = null, bootTimeoutMs
|
||||
closing = true;
|
||||
let result = code;
|
||||
if (notify) {
|
||||
- if (notify.connected) notify.send({ op: "stop" });
|
||||
+ if (notify.connected) notify.send({ op: "stop" }, () => {});
|
||||
if ((await ended(notify, CLOSE_TIMEOUT_MS)) !== 0 && result === 0) result = 1;
|
||||
}
|
||||
await queue;
|
||||
- if (broker.connected) broker.send({ op: "close" });
|
||||
+ if (broker.connected) broker.send({ op: "close" }, () => {});
|
||||
if ((await ended(broker, CLOSE_TIMEOUT_MS)) !== 0 && result === 0) result = 1;
|
||||
const now = readHostState(dataRoot);
|
||||
if (now && now.pid === state.pid && now.startTime === state.startTime) rmSync(hostFile(dataRoot), { force: true });
|
||||
diff --git a/packages/cli/src/notifier.mjs b/packages/cli/src/notifier.mjs
|
||||
index a4bb78d2..0610e1f5 100644
|
||||
index a4bb78d2..2a392b83 100644
|
||||
--- a/packages/cli/src/notifier.mjs
|
||||
+++ b/packages/cli/src/notifier.mjs
|
||||
@@ -4,17 +4,22 @@
|
||||
@@ -4,17 +4,25 @@
|
||||
// bus; its memory is the journal `<dataRoot>/notify/<business>/sent.jsonl`
|
||||
// (0600, in a 0700 directory), one line per send attempt:
|
||||
//
|
||||
@@ -83,10 +114,13 @@ index a4bb78d2..0610e1f5 100644
|
||||
// doubling to 30 min): a duplicate costs less than a miss, and Discord's
|
||||
// nonce folds a retry inside its dedupe window into the first message.
|
||||
+// The exception is a definite refusal, an HTTP 4xx other than 429 (lead
|
||||
+// decision 72): after five for one decision, counted from the journal so a
|
||||
+// restart keeps the count, the notifier appends one `gave-up` line, logs
|
||||
+// once and stops sending that DM. The digest marks it "DM refused, not
|
||||
+// retried". Unknown outcomes (network, 5xx, 429) retry without a limit.
|
||||
+// decision 72): the next try waits the full 30 min, counted from the
|
||||
+// refusal's line so a restart cannot shorten it (lead decision 73), and
|
||||
+// after five for one decision, counted from the journal, the notifier
|
||||
+// appends one `gave-up` line, logs once and stops sending that DM. Five
|
||||
+// tries span two hours, long enough to fix a binding or token typo. The
|
||||
+// digest marks it "DM refused, not retried"; recovery is the operator
|
||||
+// deciding it. Unknown outcomes (network, 5xx, 429) retry without a limit.
|
||||
// No Discord channel or user id goes in the journal, a log line or an
|
||||
// error; the Discord side (packages/discord/src/notify.mjs) keeps them.
|
||||
|
||||
@@ -96,7 +130,7 @@ index a4bb78d2..0610e1f5 100644
|
||||
import { dirname, join } from "node:path";
|
||||
import { CliError } from "./errors.mjs";
|
||||
import { authorizationLines, optionsLine, shortId } from "./format.mjs";
|
||||
@@ -25,6 +30,7 @@ export const POLL_MS = 30000;
|
||||
@@ -25,6 +33,7 @@ export const POLL_MS = 30000;
|
||||
const BACKOFF_MS = 30000;
|
||||
const BACKOFF_MAX_MS = 30 * 60 * 1000;
|
||||
const LIMIT = 2000;
|
||||
@@ -104,7 +138,7 @@ index a4bb78d2..0610e1f5 100644
|
||||
|
||||
export const journalPath = (dataRoot, business) => join(dataRoot, "notify", business, "sent.jsonl");
|
||||
|
||||
@@ -62,14 +68,14 @@ export function dmContent(business, d) {
|
||||
@@ -62,14 +71,14 @@ export function dmContent(business, d) {
|
||||
return clip(lines.join("\n"), LIMIT);
|
||||
}
|
||||
|
||||
@@ -121,12 +155,13 @@ index a4bb78d2..0610e1f5 100644
|
||||
const line = `- ${mark}${shortId(d.id)} ${d.action}: ${clip(d.question.replace(/\s+/g, " "), 160)}`;
|
||||
const more = inbox.length - shown - 1;
|
||||
const reserve = more > 0 ? `\n… and ${more} more.`.length : 0;
|
||||
@@ -116,9 +122,31 @@ function copyTorn(dir, bytes, date) {
|
||||
@@ -116,9 +125,32 @@ function copyTorn(dir, bytes, date) {
|
||||
}
|
||||
}
|
||||
|
||||
+const OUTCOMES = ["confirmed", "refused", "unknown", "gave-up"];
|
||||
+const DAY = /^\d{4}-\d{2}-\d{2}$/;
|
||||
+const isDay = (d) => typeof d === "string" && DAY.test(d) && !Number.isNaN(Date.parse(d)) && new Date(d).toISOString().startsWith(d);
|
||||
+const UNWRITABLE = ["EACCES", "EPERM", "EROFS"];
|
||||
+
|
||||
+// The first field of a journal record that has the wrong type, or null
|
||||
@@ -136,7 +171,7 @@ index a4bb78d2..0610e1f5 100644
|
||||
+ if (typeof r.at !== "string" || Number.isNaN(Date.parse(r.at)) || new Date(r.at).toISOString() !== r.at) return "at";
|
||||
+ if (!["dm", "digest"].includes(r.kind)) return "kind";
|
||||
+ if (r.kind === "dm" ? typeof r.decision !== "string" || r.decision === "" : r.decision !== null && r.decision !== undefined) return "decision";
|
||||
+ if (r.kind === "digest" ? typeof r.day !== "string" || !DAY.test(r.day) : r.day !== undefined && (typeof r.day !== "string" || !DAY.test(r.day))) return "day";
|
||||
+ if (r.kind === "digest" ? !isDay(r.day) : r.day !== undefined && !isDay(r.day)) return "day";
|
||||
+ if (!OUTCOMES.includes(r.outcome) || (r.outcome === "gave-up" && r.kind !== "dm")) return "outcome";
|
||||
+ if (r.outcome === "confirmed" ? typeof r.messageId !== "string" : r.messageId !== null && typeof r.messageId !== "string") return "messageId";
|
||||
+ if (r.status !== undefined && !Number.isInteger(r.status)) return "status";
|
||||
@@ -155,7 +190,7 @@ index a4bb78d2..0610e1f5 100644
|
||||
// A final line without its newline is a write that never finished (lead
|
||||
// decision 71): its bytes are copied to torn-<stamp>.bin, then the journal
|
||||
// is truncated to its last newline and fsynced, and both steps are logged.
|
||||
@@ -126,20 +154,42 @@ function copyTorn(dir, bytes, date) {
|
||||
@@ -126,20 +158,49 @@ function copyTorn(dir, bytes, date) {
|
||||
// both; the second copy is harmless.
|
||||
export function openJournal(file, { log = () => {}, now = () => new Date() } = {}) {
|
||||
const dir = dirname(file);
|
||||
@@ -163,7 +198,9 @@ index a4bb78d2..0610e1f5 100644
|
||||
+ try {
|
||||
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||
+ } catch (e) {
|
||||
+ if (UNWRITABLE.includes(e.code)) throw new CliError(`notify journal directory cannot be created (${e.code}): ${dir}`, 3);
|
||||
+ // A dangling link in place of the directory fails mkdir with ENOENT.
|
||||
+ if (e.code === "ENOENT" && lstatSync(dir, { throwIfNoEntry: false })?.isSymbolicLink()) throw new CliError(`notify journal directory must not be a symlink: ${dir}`, 3);
|
||||
+ if ([...UNWRITABLE, "ENOTDIR"].includes(e.code)) throw new CliError(`notify journal directory cannot be created (${e.code}): ${dir}`, 3);
|
||||
+ if (e.code !== "EEXIST") throw e;
|
||||
+ }
|
||||
const ds = lstatSync(dir);
|
||||
@@ -183,15 +220,20 @@ index a4bb78d2..0610e1f5 100644
|
||||
} catch (e) {
|
||||
if (e.code === "ELOOP") throw new CliError(`notify journal must not be a symlink: ${file}`, 3);
|
||||
+ if (UNWRITABLE.includes(e.code)) throw new CliError(`notify journal is not writable (${e.code}): ${file}`, 3);
|
||||
+ if (e.code === "EISDIR") throw new CliError(`notify journal must be a regular file, mode 0600, owned by this user: ${file}`, 3);
|
||||
throw e;
|
||||
}
|
||||
const sent = new Set();
|
||||
const days = new Set();
|
||||
+ const refusals = new Map();
|
||||
+ const gaveUp = new Set();
|
||||
+ const refusedAt = new Map();
|
||||
+ const count = (r) => {
|
||||
+ if (r.kind === "dm" && r.outcome === "gave-up") gaveUp.add(r.decision);
|
||||
+ if (definite(r)) refusals.set(r.decision, (refusals.get(r.decision) ?? 0) + 1);
|
||||
+ if (definite(r)) {
|
||||
+ refusals.set(r.decision, (refusals.get(r.decision) ?? 0) + 1);
|
||||
+ refusedAt.set(r.decision, Date.parse(r.at));
|
||||
+ }
|
||||
+ if (r.outcome !== "confirmed") return;
|
||||
+ if (r.kind === "dm") sent.add(r.decision);
|
||||
+ else days.add(r.day);
|
||||
@@ -199,7 +241,7 @@ index a4bb78d2..0610e1f5 100644
|
||||
try {
|
||||
const st = fstatSync(fd);
|
||||
if (!st.isFile() || st.uid !== process.getuid() || (st.mode & 0o777) !== 0o600) {
|
||||
@@ -156,12 +206,9 @@ export function openJournal(file, { log = () => {}, now = () => new Date() } = {
|
||||
@@ -156,12 +217,9 @@ export function openJournal(file, { log = () => {}, now = () => new Date() } = {
|
||||
} catch {
|
||||
r = null;
|
||||
}
|
||||
@@ -215,16 +257,17 @@ index a4bb78d2..0610e1f5 100644
|
||||
});
|
||||
if (end < bytes.length) {
|
||||
const name = copyTorn(dir, bytes.subarray(end), now());
|
||||
@@ -176,6 +223,8 @@ export function openJournal(file, { log = () => {}, now = () => new Date() } = {
|
||||
@@ -176,6 +234,9 @@ export function openJournal(file, { log = () => {}, now = () => new Date() } = {
|
||||
return {
|
||||
sent,
|
||||
days,
|
||||
+ refusals,
|
||||
+ gaveUp,
|
||||
+ refusedAt,
|
||||
append(record) {
|
||||
const afd = openSync(file, O_WRONLY | O_APPEND | O_NOFOLLOW);
|
||||
try {
|
||||
@@ -183,9 +232,7 @@ export function openJournal(file, { log = () => {}, now = () => new Date() } = {
|
||||
@@ -183,9 +244,7 @@ export function openJournal(file, { log = () => {}, now = () => new Date() } = {
|
||||
} finally {
|
||||
closeSync(afd);
|
||||
}
|
||||
@@ -235,7 +278,7 @@ index a4bb78d2..0610e1f5 100644
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -202,6 +249,13 @@ export function createNotifier({ business, dataRoot, inbox, direct, now = () =>
|
||||
@@ -202,6 +261,13 @@ export function createNotifier({ business, dataRoot, inbox, direct, now = () =>
|
||||
return b !== undefined && t < b.next;
|
||||
}
|
||||
|
||||
@@ -249,18 +292,27 @@ index a4bb78d2..0610e1f5 100644
|
||||
async function attempt(key, record, message) {
|
||||
const t = now().getTime();
|
||||
try {
|
||||
@@ -215,6 +269,10 @@ export function createNotifier({ business, dataRoot, inbox, direct, now = () =>
|
||||
@@ -212,10 +278,16 @@ export function createNotifier({ business, dataRoot, inbox, direct, now = () =>
|
||||
} catch (err) {
|
||||
const kind = err?.kind === "refused" ? "refused" : "unknown";
|
||||
const status = Number.isInteger(err?.details?.status) ? err.details.status : null;
|
||||
+ const line = { at: new Date(t).toISOString(), ...record, outcome: kind, messageId: null, ...(status !== null ? { status } : {}) };
|
||||
const n = (backoff.get(key)?.n ?? -1) + 1;
|
||||
backoff.set(key, { n, next: t + Math.min(BACKOFF_MS * 2 ** n, BACKOFF_MAX_MS) });
|
||||
journal.append({ at: new Date(t).toISOString(), ...record, outcome: kind, messageId: null, ...(status !== null ? { status } : {}) });
|
||||
- backoff.set(key, { n, next: t + Math.min(BACKOFF_MS * 2 ** n, BACKOFF_MAX_MS) });
|
||||
- journal.append({ at: new Date(t).toISOString(), ...record, outcome: kind, messageId: null, ...(status !== null ? { status } : {}) });
|
||||
- log(`notify: ${record.kind} ${kind}${status !== null ? ` (HTTP ${status})` : ""}; retry in ${Math.round(Math.min(BACKOFF_MS * 2 ** n, BACKOFF_MAX_MS) / 1000)} s`);
|
||||
+ const wait = definite(line) ? BACKOFF_MAX_MS : Math.min(BACKOFF_MS * 2 ** n, BACKOFF_MAX_MS);
|
||||
+ backoff.set(key, { n, next: t + wait });
|
||||
+ journal.append(line);
|
||||
+ if (record.kind === "dm" && (journal.refusals.get(record.decision) ?? 0) >= REFUSAL_LIMIT) {
|
||||
+ giveUp(record.decision, t);
|
||||
+ return false;
|
||||
+ }
|
||||
log(`notify: ${record.kind} ${kind}${status !== null ? ` (HTTP ${status})` : ""}; retry in ${Math.round(Math.min(BACKOFF_MS * 2 ** n, BACKOFF_MAX_MS) / 1000)} s`);
|
||||
+ log(`notify: ${record.kind} ${kind}${status !== null ? ` (HTTP ${status})` : ""}; retry in ${Math.round(wait / 1000)} s`);
|
||||
return false;
|
||||
}
|
||||
@@ -232,7 +290,12 @@ export function createNotifier({ business, dataRoot, inbox, direct, now = () =>
|
||||
}
|
||||
@@ -232,7 +304,15 @@ export function createNotifier({ business, dataRoot, inbox, direct, now = () =>
|
||||
}
|
||||
const t = now();
|
||||
for (const d of list) {
|
||||
@@ -271,10 +323,13 @@ index a4bb78d2..0610e1f5 100644
|
||||
+ giveUp(d.id, t.getTime());
|
||||
+ continue;
|
||||
+ }
|
||||
+ // A definite refusal waits the full cap from its journal line, which
|
||||
+ // the in-memory backoff loses on a restart.
|
||||
+ if (t.getTime() < (journal.refusedAt.get(d.id) ?? -Infinity) + BACKOFF_MAX_MS) continue;
|
||||
const key = `dm:${d.id}`;
|
||||
if (waiting(key, t.getTime())) continue;
|
||||
if (await attempt(key, { kind: "dm", decision: d.id }, { content: dmContent(business, d), nonce: dmNonce(d.id) })) done.dms++;
|
||||
@@ -241,7 +304,7 @@ export function createNotifier({ business, dataRoot, inbox, direct, now = () =>
|
||||
@@ -241,7 +321,7 @@ export function createNotifier({ business, dataRoot, inbox, direct, now = () =>
|
||||
const { day, hour } = zoned(t, zone);
|
||||
const key = `digest:${day}`;
|
||||
if (hour >= DIGEST_HOUR && !journal.days.has(day) && !waiting(key, t.getTime())) {
|
||||
@@ -284,7 +339,7 @@ index a4bb78d2..0610e1f5 100644
|
||||
else done.failed++;
|
||||
}
|
||||
diff --git a/packages/cli/tests/host.test.mjs b/packages/cli/tests/host.test.mjs
|
||||
index 8d20b47f..649031ae 100644
|
||||
index 8d20b47f..f8f11ec9 100644
|
||||
--- a/packages/cli/tests/host.test.mjs
|
||||
+++ b/packages/cli/tests/host.test.mjs
|
||||
@@ -1,6 +1,7 @@
|
||||
@@ -295,7 +350,7 @@ index 8d20b47f..649031ae 100644
|
||||
import { createServer } from "node:http";
|
||||
import { once } from "node:events";
|
||||
import { existsSync, readFileSync, statSync, writeFileSync, mkdirSync } from "node:fs";
|
||||
@@ -48,6 +49,31 @@ async function until(fn, ms = 8000) {
|
||||
@@ -48,6 +49,70 @@ async function until(fn, ms = 8000) {
|
||||
throw new Error("timed out waiting");
|
||||
}
|
||||
|
||||
@@ -323,11 +378,50 @@ index 8d20b47f..649031ae 100644
|
||||
+ t.after(() => unsubscribe("child_process", onChild));
|
||||
+ return sent;
|
||||
+}
|
||||
+
|
||||
+// Fails this process's sends to new children the way node fails a write to a
|
||||
+// child that has just died: `fake(child, m)` returns an error to fail that
|
||||
+// send (to its callback if it has one, otherwise as an 'error' event on the
|
||||
+// next tick), or nothing to send it for real. Deterministic where the real
|
||||
+// window after a SIGKILL is a millisecond wide (Darkwing R1, lead decision 73).
|
||||
+// Every child it saw is killed after the test, so a host left half closed
|
||||
+// fails the test rather than hang it.
|
||||
+function failSends(t, fake) {
|
||||
+ const children = [];
|
||||
+ const onChild = ({ process: child }) => {
|
||||
+ children.push(child);
|
||||
+ let send;
|
||||
+ Object.defineProperty(child, "send", {
|
||||
+ configurable: true,
|
||||
+ get: () => send,
|
||||
+ set(fn) {
|
||||
+ send = function (m, ...rest) {
|
||||
+ const err = fake(this, m);
|
||||
+ if (!err) return fn.call(this, m, ...rest);
|
||||
+ const callback = rest.find((a) => typeof a === "function");
|
||||
+ if (callback) process.nextTick(callback, err);
|
||||
+ else process.nextTick(() => this.emit("error", err));
|
||||
+ return false;
|
||||
+ };
|
||||
+ },
|
||||
+ });
|
||||
+ };
|
||||
+ subscribe("child_process", onChild);
|
||||
+ t.after(() => {
|
||||
+ unsubscribe("child_process", onChild);
|
||||
+ for (const child of children) child.kill("SIGKILL");
|
||||
+ });
|
||||
+}
|
||||
+
|
||||
+const epipe = (child) => {
|
||||
+ child.kill("SIGKILL");
|
||||
+ return Object.assign(new Error("write EPIPE"), { code: "EPIPE", errno: -32, syscall: "write" });
|
||||
+};
|
||||
+
|
||||
const procText = (pid, what) => {
|
||||
try {
|
||||
return readFileSync(`/proc/${pid}/${what}`, "utf8");
|
||||
@@ -64,8 +90,12 @@ test("the host boots the broker, binds a launch in process, and the notifier DMs
|
||||
@@ -64,8 +129,12 @@ test("the host boots the broker, binds a launch in process, and the notifier DMs
|
||||
const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env });
|
||||
assert.equal("trackers" in boot, false);
|
||||
const logs = [];
|
||||
@@ -340,7 +434,7 @@ index 8d20b47f..649031ae 100644
|
||||
|
||||
const state = JSON.parse(readFileSync(hostFile(f.dataRoot), "utf8"));
|
||||
assert.equal(statSync(hostFile(f.dataRoot)).mode & 0o777, 0o600);
|
||||
@@ -84,12 +114,16 @@ test("the host boots the broker, binds a launch in process, and the notifier DMs
|
||||
@@ -84,12 +153,16 @@ test("the host boots the broker, binds a launch in process, and the notifier DMs
|
||||
assert.ok(discord.requests.every((r) => r.authorized));
|
||||
assert.match(discord.dms()[0].body.content, new RegExp(`mosaic decide ${d.id.slice(0, 8)}`));
|
||||
|
||||
@@ -362,7 +456,7 @@ index 8d20b47f..649031ae 100644
|
||||
|
||||
assert.equal(await host.close(0), 0);
|
||||
const journal = readFileSync(journalPath(f.dataRoot, "acme"), "utf8");
|
||||
@@ -107,6 +141,7 @@ test("a notifier that dies takes the host down with exit 1, so the unit restarts
|
||||
@@ -107,6 +180,7 @@ test("a notifier that dies takes the host down with exit 1, so the unit restarts
|
||||
const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env });
|
||||
const logs = [];
|
||||
const host = await startHost({ boot, business: "acme", notifier: { binding: "test-seat", base: discord.base, pollMs: 100 }, log: (l) => logs.push(l) });
|
||||
@@ -370,7 +464,7 @@ index 8d20b47f..649031ae 100644
|
||||
process.kill(host.pids.notifier, "SIGKILL");
|
||||
assert.equal(await host.done, 1);
|
||||
assert.match(logs.join("\n"), /notifier exited \(SIGKILL\); stopping the host/);
|
||||
@@ -114,6 +149,21 @@ test("a notifier that dies takes the host down with exit 1, so the unit restarts
|
||||
@@ -114,6 +188,21 @@ test("a notifier that dies takes the host down with exit 1, so the unit restarts
|
||||
assert.equal(existsSync(hostFile(f.dataRoot)), false);
|
||||
});
|
||||
|
||||
@@ -392,7 +486,7 @@ index 8d20b47f..649031ae 100644
|
||||
test("a notifier that refuses stops the broker and the host refuses with exit 3", async (t) => {
|
||||
const root = tmp(t);
|
||||
const f = fixture(root);
|
||||
@@ -127,9 +177,68 @@ test("a notifier that refuses stops the broker and the host refuses with exit 3"
|
||||
@@ -127,9 +216,111 @@ test("a notifier that refuses stops the broker and the host refuses with exit 3"
|
||||
assert.equal(existsSync(hostFile(f.dataRoot)), false);
|
||||
});
|
||||
|
||||
@@ -447,6 +541,49 @@ index 8d20b47f..649031ae 100644
|
||||
+ assert.deepEqual(errors, [], "no close was sent over the closed channel");
|
||||
+ assert.equal(sends.filter((m) => m?.op === "close").length, 0);
|
||||
+});
|
||||
+
|
||||
+test("a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3", async (t) => {
|
||||
+ const root = tmp(t);
|
||||
+ const f = fixture(root);
|
||||
+ makeDeployment(root);
|
||||
+ const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env });
|
||||
+ failSends(t, (child, m) => (m?.op === "close" ? epipe(child) : null));
|
||||
+ const started = startHost({ boot, business: "acme", notifier: { binding: "test-seat" }, log: () => {} });
|
||||
+ t.after(async () => (await started.catch(() => null))?.close(0));
|
||||
+ await assert.rejects(started, (e) => e.exitCode === 3 && /^notifier refused to start: .*no dmRecipient/.test(e.message));
|
||||
+});
|
||||
+
|
||||
+test("a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error", async (t) => {
|
||||
+ const root = tmp(t);
|
||||
+ const f = fixture(root);
|
||||
+ makeDeployment(root);
|
||||
+ const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env });
|
||||
+ failSends(t, (child, m) => {
|
||||
+ if (m?.op === "close") return epipe(child);
|
||||
+ if (m?.op === "start") {
|
||||
+ // Stopped, it cannot reply; then it dies.
|
||||
+ child.kill("SIGSTOP");
|
||||
+ setImmediate(() => child.kill("SIGKILL"));
|
||||
+ }
|
||||
+ return null;
|
||||
+ });
|
||||
+ const started = startHost({ boot, business: "acme", notifier: { binding: "test-seat" }, log: () => {} });
|
||||
+ t.after(async () => (await started.catch(() => null))?.close(0));
|
||||
+ await assert.rejects(started, (e) => e.exitCode === 1 && /^notifier exited \(null\) before it replied/.test(e.message));
|
||||
+});
|
||||
+
|
||||
+test("close() whose stop and close sends fail with EPIPE still finishes, with exit 1", async (t) => {
|
||||
+ const root = tmp(t);
|
||||
+ const f = fixture(root);
|
||||
+ makeDeployment(root, { dmRecipient: IDS.owner });
|
||||
+ const discord = await fakeDiscord(t);
|
||||
+ const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env });
|
||||
+ failSends(t, (child, m) => (m?.op === "stop" || m?.op === "close" ? epipe(child) : null));
|
||||
+ const host = await startHost({ boot, business: "acme", notifier: { binding: "test-seat", base: discord.base, pollMs: 100 }, log: () => {} });
|
||||
+ // No t.after close: a close() that rejected leaves `done` pending forever.
|
||||
+ assert.equal(await host.close(0), 1, "both children died by signal");
|
||||
+ assert.equal(existsSync(hostFile(f.dataRoot)), false);
|
||||
+});
|
||||
+
|
||||
test("watchChildren reports a child that died before it was called, and one that dies later", async (t) => {
|
||||
const early = spawn(process.execPath, ["-e", "process.exit(7)"], { stdio: "ignore" });
|
||||
@@ -461,8 +598,16 @@ index 8d20b47f..649031ae 100644
|
||||
const late = spawn(process.execPath, ["-e", "setTimeout(() => {}, 60000)"], { stdio: "ignore" });
|
||||
t.after(() => late.kill("SIGKILL"));
|
||||
await once(late, "spawn");
|
||||
@@ -215,6 +406,7 @@ test("bus-service.sh renders the unit and installs it into a given directory", (
|
||||
const first = spawnSync(script, ["install", "--dir", dir, "--no-reload"], { encoding: "utf8" });
|
||||
assert.equal(first.status, 0, first.stderr);
|
||||
assert.match(first.stdout, /written: /);
|
||||
+ assert.match(first.stdout, /^ {2}mkdir -m 0700 -p <dataRoot>\/notify\/<business> +the notifier refuses a looser directory$/m);
|
||||
assert.equal(readFileSync(join(dir, "[email protected]"), "utf8"), render.stdout);
|
||||
assert.match(spawnSync(script, ["install", "--dir", dir, "--no-reload"], { encoding: "utf8" }).stdout, /unchanged: /);
|
||||
assert.match(spawnSync(script, ["uninstall", "--dir", dir, "--no-reload"], { encoding: "utf8" }).stdout, /removed: /);
|
||||
diff --git a/packages/cli/tests/notifier.test.mjs b/packages/cli/tests/notifier.test.mjs
|
||||
index 99127662..aa81ac69 100644
|
||||
index 99127662..3b65af47 100644
|
||||
--- a/packages/cli/tests/notifier.test.mjs
|
||||
+++ b/packages/cli/tests/notifier.test.mjs
|
||||
@@ -1,12 +1,15 @@
|
||||
@@ -472,7 +617,7 @@ index 99127662..aa81ac69 100644
|
||||
+import { appendFileSync, chmodSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
-import { createNotifier, digestContent, digestNonce, dmNonce, journalPath, openJournal, runLoop, zoned } from "../src/notifier.mjs";
|
||||
+import { createNotifier, digestContent, digestNonce, dmNonce, journalPath, openJournal, REFUSAL_LIMIT, runLoop, zoned } from "../src/notifier.mjs";
|
||||
+import { createNotifier, digestContent, digestNonce, dmNonce, journalPath, openJournal, POLL_MS, REFUSAL_LIMIT, runLoop, zoned } from "../src/notifier.mjs";
|
||||
import { RestOutcome } from "../../discord/src/rest.mjs";
|
||||
import { broker, tmp } from "./helpers.mjs";
|
||||
|
||||
@@ -493,10 +638,25 @@ index 99127662..aa81ac69 100644
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -101,6 +105,72 @@ test("a failed DM is journaled, backs off, and is retried until it lands", async
|
||||
assert.ok(s.logs.some((l) => /retry in 30 s/.test(l)));
|
||||
});
|
||||
|
||||
@@ -91,14 +95,117 @@ test("a failed DM is journaled, backs off, and is retried until it lands", async
|
||||
assert.equal((await s.notifier.tick()).failed, 0, "inside the first 30 s backoff");
|
||||
s.time.advance(25_000);
|
||||
assert.equal((await s.notifier.tick()).failed, 1, "second attempt refused");
|
||||
- s.time.advance(45_000);
|
||||
- assert.equal((await s.notifier.tick()).dms, 0, "inside the 60 s backoff");
|
||||
- s.time.advance(20_000);
|
||||
+ s.time.advance(29 * 60_000);
|
||||
+ assert.equal((await s.notifier.tick()).dms, 0, "a definite refusal waits the full 30 min, not the 60 s step");
|
||||
+ s.time.advance(60_000);
|
||||
assert.equal((await s.notifier.tick()).dms, 1);
|
||||
assert.deepEqual(s.journal().map((r) => r.outcome), ["unknown", "refused", "confirmed"]);
|
||||
assert.equal(s.journal()[1].status, 403);
|
||||
assert.equal(new Set(s.direct.sends.map((m) => m.nonce)).size, 1, "every retry reuses the nonce");
|
||||
- assert.ok(s.logs.some((l) => /retry in 30 s/.test(l)));
|
||||
+ assert.ok(s.logs.some((l) => /unknown; retry in 30 s/.test(l)));
|
||||
+ assert.ok(s.logs.some((l) => /refused \(HTTP 403\); retry in 1800 s/.test(l)));
|
||||
+});
|
||||
+
|
||||
+// 05:00Z is 00:00 Chicago: eight hours of polls before the digest is due.
|
||||
+const MIDNIGHT = "2026-10-08T05:00:00Z";
|
||||
+const PAST_BACKOFF = 31 * 60_000;
|
||||
@@ -563,10 +723,44 @@ index 99127662..aa81ac69 100644
|
||||
+ assert.equal(s.journal().length, REFUSAL_LIMIT + 1, "one gave-up line only");
|
||||
+});
|
||||
+
|
||||
+// Polls every POLL_MS from the current clock up to `until` ms later; returns
|
||||
+// the minute (from `from`) of each new send.
|
||||
+async function pollUntil(s, notifier, from, until, onPoll = () => {}) {
|
||||
+ const minutes = [];
|
||||
+ for (let elapsed = s.time.clock.t.getTime() - from; elapsed <= until; elapsed += POLL_MS) {
|
||||
+ const before = s.direct.sends.length;
|
||||
+ await notifier.tick();
|
||||
+ if (s.direct.sends.length > before) minutes.push(elapsed / 60_000);
|
||||
+ onPoll(elapsed);
|
||||
+ s.time.advance(POLL_MS);
|
||||
+ }
|
||||
+ return minutes;
|
||||
+}
|
||||
+
|
||||
+test("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", async (t) => {
|
||||
+ const s = setup(t, MIDNIGHT, Array(10).fill("refused"));
|
||||
+ s.raise("git.push.protected", { target: "refactor", blocking: true, task_ref: "vikunja:1/7" });
|
||||
+ const from = s.time.clock.t.getTime();
|
||||
+ const gaveUp = () => s.journal().some((r) => r.outcome === "gave-up");
|
||||
+ const minutes = await pollUntil(s, s.notifier, from, 120 * 60_000 + POLL_MS, (elapsed) => {
|
||||
+ assert.equal(gaveUp(), elapsed >= 120 * 60_000, `gave-up line at +${elapsed / 1000} s`);
|
||||
+ });
|
||||
+ assert.deepEqual(minutes, [0, 30, 60, 90, 120]);
|
||||
+ assert.equal(s.logs.filter((l) => /refused \(HTTP 403\); retry in 1800 s/.test(l)).length, 4);
|
||||
+});
|
||||
+
|
||||
+test("a restart after the second refusal does not send before that refusal's 30 min are up", async (t) => {
|
||||
+ const s = setup(t, MIDNIGHT, Array(10).fill("refused"));
|
||||
+ s.raise("git.push.protected", { target: "refactor", blocking: true, task_ref: "vikunja:1/7" });
|
||||
+ const from = s.time.clock.t.getTime();
|
||||
+ assert.deepEqual(await pollUntil(s, s.notifier, from, 30 * 60_000), [0, 30]);
|
||||
+ s.time.advance(60_000);
|
||||
+ const after = s.make();
|
||||
+ assert.deepEqual(await pollUntil(s, after, from, 60 * 60_000), [60], "nothing between the restart and +60 min");
|
||||
});
|
||||
|
||||
test("the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd", async (t) => {
|
||||
const s = setup(t, "2026-10-08T12:59:00Z");
|
||||
const d = s.raise("git.push.protected", { target: "refactor", blocking: true, task_ref: "vikunja:1/7" });
|
||||
@@ -150,7 +220,7 @@ const FRAGMENT = '{"at":"x","kind":"dm","dec';
|
||||
@@ -150,7 +257,7 @@ const FRAGMENT = '{"at":"x","kind":"dm","dec';
|
||||
|
||||
test("the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly", (t) => {
|
||||
const file = journalPath(tmp(t), "demo");
|
||||
@@ -575,7 +769,7 @@ index 99127662..aa81ac69 100644
|
||||
const good = readFileSync(file);
|
||||
appendFileSync(file, FRAGMENT);
|
||||
const logs = [];
|
||||
@@ -164,7 +234,7 @@ test("the journal: a torn tail is copied out and truncated, so an append after i
|
||||
@@ -164,7 +271,7 @@ test("the journal: a torn tail is copied out and truncated, so an append after i
|
||||
assert.equal(logs.length, 2);
|
||||
assert.match(logs[0], /copied a torn final line \(26 bytes\) to torn-20261008T235212345Z\.bin/);
|
||||
assert.match(logs[1], /truncated .* to its last newline/);
|
||||
@@ -584,7 +778,7 @@ index 99127662..aa81ac69 100644
|
||||
const again = [];
|
||||
assert.deepEqual([...openJournal(file, { log: (l) => again.push(l) }).sent], ["a", "b"]);
|
||||
assert.deepEqual(again, [], "nothing torn the second time");
|
||||
@@ -172,7 +242,7 @@ test("the journal: a torn tail is copied out and truncated, so an append after i
|
||||
@@ -172,7 +279,7 @@ test("the journal: a torn tail is copied out and truncated, so an append after i
|
||||
|
||||
test("the journal: a crash between the copy and the truncate leaves a tail the next open repairs", (t) => {
|
||||
const file = journalPath(tmp(t), "demo");
|
||||
@@ -593,7 +787,7 @@ index 99127662..aa81ac69 100644
|
||||
appendFileSync(file, FRAGMENT);
|
||||
const now = () => new Date("2026-10-08T23:52:12.345Z");
|
||||
// The log after step 1 throws: the process dies before step 2.
|
||||
@@ -182,7 +252,7 @@ test("the journal: a crash between the copy and the truncate leaves a tail the n
|
||||
@@ -182,7 +289,7 @@ test("the journal: a crash between the copy and the truncate leaves a tail the n
|
||||
const j = openJournal(file, { now });
|
||||
assert.deepEqual(tornFiles(file), ["torn-20261008T235212345Z-1.bin", "torn-20261008T235212345Z.bin"], "a second copy, the first kept");
|
||||
for (const n of tornFiles(file)) assert.equal(readFileSync(join(dirname(file), n), "utf8"), FRAGMENT);
|
||||
@@ -602,7 +796,7 @@ index 99127662..aa81ac69 100644
|
||||
assert.deepEqual([...openJournal(file).sent], ["a", "b"]);
|
||||
});
|
||||
|
||||
@@ -222,6 +292,89 @@ test("the journal: a loose file mode, a loose directory or a symlinked journal r
|
||||
@@ -222,6 +329,108 @@ test("the journal: a loose file mode, a loose directory or a symlinked journal r
|
||||
assert.throws(() => openJournal(linked), (e) => e.exitCode === 3 && /must not be a symlink/.test(e.message));
|
||||
});
|
||||
|
||||
@@ -622,6 +816,8 @@ index 99127662..aa81ac69 100644
|
||||
+ ["decision", { ...digest, decision: "a" }],
|
||||
+ ["day", { ...digest, day: undefined }],
|
||||
+ ["day", { ...digest, day: "2026-10-8" }],
|
||||
+ ["day", { ...digest, day: "2026-13-45" }],
|
||||
+ ["day", { ...digest, day: "2026-02-30" }],
|
||||
+ ["day", { ...dm, day: 20261008 }],
|
||||
+ ["outcome", { ...dm, outcome: "sent" }],
|
||||
+ ["outcome", { ...digest, outcome: "gave-up", messageId: null }],
|
||||
@@ -656,6 +852,20 @@ index 99127662..aa81ac69 100644
|
||||
+ assert.deepEqual(readdirSync(real), [], "nothing was created through the link");
|
||||
+});
|
||||
+
|
||||
+test("the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3", (t) => {
|
||||
+ const root = tmp(t);
|
||||
+ const dangling = journalPath(root, "demo");
|
||||
+ mkdirSync(dirname(dirname(dangling)), { mode: 0o700 });
|
||||
+ symlinkSync(join(root, "missing"), dirname(dangling));
|
||||
+ assert.throws(() => openJournal(dangling), (e) => e.exitCode === 3 && e.message === `notify journal directory must not be a symlink: ${dirname(dangling)}`);
|
||||
+ const under = join(root, "plain", "notify", "demo", "sent.jsonl");
|
||||
+ writeFileSync(join(root, "plain"), "");
|
||||
+ assert.throws(() => openJournal(under), (e) => e.exitCode === 3 && e.message === `notify journal directory cannot be created (ENOTDIR): ${dirname(under)}`);
|
||||
+ const asDir = journalPath(root, "acme");
|
||||
+ mkdirSync(asDir, { recursive: true, mode: 0o700 });
|
||||
+ assert.throws(() => openJournal(asDir), (e) => e.exitCode === 3 && e.message === `notify journal must be a regular file, mode 0600, owned by this user: ${asDir}`);
|
||||
+});
|
||||
+
|
||||
+test("the journal: an append after the file was swapped for a symlink refuses and writes nothing through it", (t) => {
|
||||
+ const root = tmp(t);
|
||||
+ const file = journalPath(root, "demo");
|
||||
@@ -679,6 +889,9 @@ index 99127662..aa81ac69 100644
|
||||
+ } finally {
|
||||
+ chmodSync(dirname(file), 0o700);
|
||||
+ }
|
||||
+ openJournal(file);
|
||||
+ chmodSync(file, 0o400);
|
||||
+ assert.throws(() => openJournal(file), (e) => e.exitCode === 3 && e.message === `notify journal is not writable (EACCES): ${file}`);
|
||||
+ const parent = join(root, "notify");
|
||||
+ const other = journalPath(root, "acme");
|
||||
+ chmodSync(parent, 0o500);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
6c777639fae9a9332726260c18ba62c5b942d56601a7d0e7ffe969b214a6f960 packages/cli/README.md
|
||||
e63b592d249ab1bebb1b3fbe7e61372270b7858c9b1230e3f2ecf48476ec053e packages/cli/src/host.mjs
|
||||
3f5bbbfcd3a64db07ceff7aef183f7843d6d541246cc51bf17ae3879eb59ebbd packages/cli/src/notifier.mjs
|
||||
628deae836f9d79e646bc8bba99ad05ab587339cc3f057ad42caa600b0f00c7b packages/cli/tests/host.test.mjs
|
||||
ec6dce1b2bf0af9635c0128b11542db8bcdb5f2dae846ce5a7005fa71c9a9b59 packages/cli/tests/notifier.test.mjs
|
||||
33835702af268d8337c68a9d62ab7340174ec6ec990a86422c099a922feba61d packages/cli/README.md
|
||||
24461cccd45d08cf4b5b77d2b44bd1792002bcaca028844b1f0f013052d097d6 packages/cli/src/host.mjs
|
||||
cb27929bd2d17704ff7db42a0b001d97cb626ec79fa833add4bdaff2e304a4c3 packages/cli/src/notifier.mjs
|
||||
b998202b4872c4929b1aeb601ed26da4bdaef8549ae0676ce650a23fa4f0f38c packages/cli/tests/host.test.mjs
|
||||
38d499cf3f2a0b78fb08d473946ea6517cc8a6b35681bd077695ddb507fb3cc6 packages/cli/tests/notifier.test.mjs
|
||||
f015ef5ed6ae2fa6ec40b2b0d5148baf23436917f71a7fa5521249864fdde0c4 packages/cli/tests/trackers-boot.test.mjs
|
||||
219924be715db3cbfc6030c4eafd57b27189d6d971b6da26f719b2f86599d37a packages/discord/tests/journal.test.mjs
|
||||
bc7abfb98e0ffa8c0d1111069c05ac19d9cc9be3b0ed4d457c4f135f97d08bb1 scripts/bus-service.sh
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# Row 45 mutants: N2, N4, N5 and M28 from the row 39 reviews, the F2 limit,
|
||||
# J4 and the new error paths. Same harness as
|
||||
# J4 and the new error paths. Round 2 adds the send callbacks (R1), the
|
||||
# refusal spacing and restart (R2, lead decision 73), X9, X14, the day check
|
||||
# and the error paths from Darkwing's note 1. Same harness as
|
||||
# agents/filbert/work/slice1-s4-review/mutants.sh: one perl substitution
|
||||
# each, restored after its run. A kill counts only `fail`; the cancelled
|
||||
# count is printed too, and the output names every failing test.
|
||||
@@ -38,3 +40,18 @@ run J4c $NT 's/if \(r\.status !== undefined && !Number\.isInteger\(r\.status\)\
|
||||
run J4d $NT 's/r\.outcome === "confirmed" \? typeof r\.messageId !== "string" : //'
|
||||
run E1 $NT 's/accessSync\(dir, constants\.W_OK \| constants\.X_OK\);//'
|
||||
run E2 $NT 's/ if \(ds\.isSymbolicLink\(\)\) throw[^\n]*\n//'
|
||||
BS=scripts/bus-service.sh
|
||||
# Round 2.
|
||||
run G144cb $HS 's/(notify\.kill\("SIGTERM"\);\n\s*await ended\(notify, 5000\);\n\s*if \(broker\.connected\) broker\.send\(\{ op: "close" \}), \(\) => \{\}\)/$1)/'
|
||||
run G150cb $HS 's/(if \(ok\?\.ok !== true\) \{\n\s*await ended\(notify, 5000\);\n\s*if \(broker\.connected\) broker\.send\(\{ op: "close" \}), \(\) => \{\}\)/$1)/'
|
||||
run G184 $HS 's/notify\.send\(\{ op: "stop" \}, \(\) => \{\}\)/notify.send({ op: "stop" })/'
|
||||
run G188 $HS 's/(await queue;\n\s*if \(broker\.connected\) broker\.send\(\{ op: "close" \}), \(\) => \{\}\)/$1)/'
|
||||
run R2a $NT 's/const wait = definite\(line\) \? BACKOFF_MAX_MS : /const wait = /'
|
||||
run R2b $NT 's/ if \(t\.getTime\(\) < \(journal\.refusedAt\.get\(d\.id\)[^\n]*\n//'
|
||||
run R2c $NT 's/ refusedAt\.set\(r\.decision, Date\.parse\(r\.at\)\);\n//'
|
||||
run X9 $NT 's/ if \(UNWRITABLE\.includes\(e\.code\)\) throw new CliError\(`notify journal is not writable[^\n]*\n//'
|
||||
run X14 $BS 's/ mkdir -m 0700 -p <dataRoot>\/notify\/<business>[^\n]*\n//'
|
||||
run D3 $NT 's/ && !Number\.isNaN\(Date\.parse\(d\)\) && new Date\(d\)\.toISOString\(\)\.startsWith\(d\)//'
|
||||
run N1a $NT 's/ if \(e\.code === "ENOENT" && lstatSync\(dir[^\n]*\n//'
|
||||
run N1b $NT 's/\[\.\.\.UNWRITABLE, "ENOTDIR"\]/UNWRITABLE/'
|
||||
run N1c $NT 's/ if \(e\.code === "EISDIR"\)[^\n]*\n//'
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
2.44 1.79 2.16 1/12979 779335
|
||||
2026-10-09T14:05:56Z
|
||||
@@ -0,0 +1,21 @@
|
||||
node-business exit 0
|
||||
node-bus exit 0
|
||||
node-cli exit 0
|
||||
node-control-board exit 0
|
||||
node-conversation exit 0
|
||||
node-discord exit 0
|
||||
node-ledger exit 0
|
||||
node-mosaic exit 0
|
||||
node-queue exit 0
|
||||
node-seat exit 0
|
||||
node-tasks exit 0
|
||||
node-webui exit 0
|
||||
suite-auth exit 0
|
||||
suite-conductor exit 0
|
||||
suite-config exit 0
|
||||
suite-discord exit 0
|
||||
suite-extension-package exit 0
|
||||
suite-foundation exit 0
|
||||
suite-queue exit 0
|
||||
suite-release exit 0
|
||||
suite-task exit 1
|
||||
@@ -0,0 +1,2 @@
|
||||
1.01 0.95 2.02 7/12984 683241
|
||||
2026-10-09T14:02:34Z
|
||||
@@ -0,0 +1,8 @@
|
||||
packages/cli/README.md: OK
|
||||
packages/cli/src/host.mjs: OK
|
||||
packages/cli/src/notifier.mjs: OK
|
||||
packages/cli/tests/host.test.mjs: OK
|
||||
packages/cli/tests/notifier.test.mjs: OK
|
||||
packages/cli/tests/trackers-boot.test.mjs: OK
|
||||
packages/discord/tests/journal.test.mjs: OK
|
||||
scripts/bus-service.sh: OK
|
||||
@@ -0,0 +1,75 @@
|
||||
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (20.217177ms)
|
||||
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (23.913734ms)
|
||||
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (27.418015ms)
|
||||
✔ launch events require a human CLI capability; generic emit cannot forge authority events (20.095476ms)
|
||||
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (15.028242ms)
|
||||
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (13.46164ms)
|
||||
✔ task action subjects and linked decision trail are complete and ordered (20.841526ms)
|
||||
✔ launch binding is durable and reconnecting requires the identical trusted record (8.013879ms)
|
||||
✔ business isolation includes inherited object names and cross-business message references (14.650376ms)
|
||||
✔ authority never transfers between action, run, target, unresolved or replaced role holder (18.622725ms)
|
||||
✔ task projection uses schema current view, skipping earlier and equal-start polls (9.427439ms)
|
||||
✔ revocation permanently bars the old run from reclaiming first, including after broker restart (15.623429ms)
|
||||
✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (10.401472ms)
|
||||
✔ both arbiters require human resolution when their cross-role route is themselves (15.860762ms)
|
||||
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (1.726404ms)
|
||||
✔ only validated broker references load; returned data and exceptions cannot expose a known token (3.477977ms)
|
||||
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (2.348156ms)
|
||||
✔ expiry refuses use and env references never become client data (0.808375ms)
|
||||
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.64772ms)
|
||||
✔ opaque tokens shorter than 16 characters refuse before use (0.37342ms)
|
||||
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (2.132056ms)
|
||||
✔ process reader gets own kernel identity without exposing environment values (1.287166ms)
|
||||
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (0.788136ms)
|
||||
✔ real pid 1 remains inspectable when its environment is protected (0.359006ms)
|
||||
✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (24.288305ms)
|
||||
✔ missing and foreign-business citations refuse and roll back message and grant (17.011547ms)
|
||||
✔ cross-role sends still need a matching resolved decision and consume it once (28.381359ms)
|
||||
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (66.723278ms)
|
||||
✔ startup token refusal returns safe code without value or partial listening broker (38.97226ms)
|
||||
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (49.827986ms)
|
||||
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (82.849468ms)
|
||||
✔ trusted host registers later launches; socket clients never have a registration verb (54.101581ms)
|
||||
✔ runtime excludes declared project roots even when host supplies no repoRoots (37.386095ms)
|
||||
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (48.361311ms)
|
||||
✔ v3b prototype refusals, views and append-only mutations (82.523597ms)
|
||||
✔ gated approval authorizes once, survives store reopen, and fresh approval works (27.788071ms)
|
||||
✔ another run cannot consume an approval; a failed check leaves it usable (20.309447ms)
|
||||
✔ two scheduled callers have exactly one grant and one consumed refusal (20.492887ms)
|
||||
✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (29.50709ms)
|
||||
✔ class drift gated to cross-role refuses before consumption (19.187248ms)
|
||||
✔ class drift cross-role to gated refuses before consumption (14.978956ms)
|
||||
✔ class drift gated to within-role refuses before consumption (16.443241ms)
|
||||
✔ class drift cross-role to within-role refuses before consumption (16.072617ms)
|
||||
✔ class drift within-role to gated refuses before consumption (15.064451ms)
|
||||
✔ class drift within-role to cross-role refuses before consumption (15.61633ms)
|
||||
✔ message.send consumes approval and prevents a later send or authorize (15.913262ms)
|
||||
✔ role.revoke consumes approval and prevents a later revoke or authorize (16.694773ms)
|
||||
✔ creates private WAL store and excludes a second writer until explicit close (15.558522ms)
|
||||
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (15.837274ms)
|
||||
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (15.430354ms)
|
||||
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (51.488693ms)
|
||||
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (9.684961ms)
|
||||
✔ async transactions refuse before invoking their function (7.964162ms)
|
||||
✔ recordTask keeps sync reads and a role write apart (20.233791ms)
|
||||
✔ read_at must be one canonical UTC format, so the projection compares strings safely (9.909358ms)
|
||||
✔ a bad entry refuses the whole record (9.524417ms)
|
||||
✔ taskView reads the projection for one business (16.739565ms)
|
||||
✔ requestTask hands only a holder and a task verb to the handler, and records refusals (21.811716ms)
|
||||
✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (269.121186ms)
|
||||
✔ without an adapter the server refuses every task verb (15.529542ms)
|
||||
✔ the runtime refuses an invalid adapter and closes a valid one (14.405829ms)
|
||||
✔ the process loads the S3 adapter from plain-data trackers (87.38528ms)
|
||||
✔ socket capability stamps launch identity; shared views use wire, no SQL client (24.374712ms)
|
||||
✔ two wire claims serialize; a lost reply never automatically retries (18.879726ms)
|
||||
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (16.264802ms)
|
||||
✔ client preserves UTF-8 when a response divides a multibyte character (11.772894ms)
|
||||
✔ committed mutation followed by dropped reply reports unknown and is never retried (13.790622ms)
|
||||
ℹ tests 67
|
||||
ℹ suites 0
|
||||
ℹ pass 67
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 577.642302
|
||||
@@ -0,0 +1,68 @@
|
||||
✔ config directory and file path follow MOSAIC_CONFIG (1.452137ms)
|
||||
✔ the fixture business validates and comes back frozen (5.578344ms)
|
||||
✔ two instances may share a definition (1.441253ms)
|
||||
✔ top-level refusals (5.152839ms)
|
||||
✔ arbiters and projects (7.563057ms)
|
||||
✔ role instances (3.855653ms)
|
||||
✔ Vikunja bots (9.49824ms)
|
||||
✔ a role without Vikunja takes no tracker block (3.98548ms)
|
||||
✔ credential references match the definition's services (5.343946ms)
|
||||
✔ launch (10.588077ms)
|
||||
✔ loadBusiness: file checks (3.090109ms)
|
||||
✔ loadBusiness: not a regular file (42.83915ms)
|
||||
✔ loading writes nothing (1.21552ms)
|
||||
✔ names that are Object.prototype properties don't count as declared (2.449986ms)
|
||||
✔ the shipped example refuses as written and validates once filled in (1.466161ms)
|
||||
✔ usage errors exit 4 (285.939347ms)
|
||||
✔ validate: a good business exits 0 and prints instance digests (65.835211ms)
|
||||
✔ validate: project files (316.787862ms)
|
||||
✔ validate: missing files and a broken system config (242.410619ms)
|
||||
✔ validate: credential reference problems exit 2 and name each one (66.429598ms)
|
||||
✔ validate: a token file inside the repository is refused (61.975689ms)
|
||||
✔ validate: role definitions come from MOSAIC_ROLES_DIR (189.828413ms)
|
||||
✔ resolve: prints one instance's record (192.088605ms)
|
||||
✔ resolve: refusals (370.817334ms)
|
||||
✔ parse: exactly one of file or env, plus the service's date (2.912184ms)
|
||||
✔ check: a good file has no problems (0.697025ms)
|
||||
✔ check never opens the file: a write-only token passes (0.352511ms)
|
||||
✔ check: file problems (0.754102ms)
|
||||
✔ check: token files can't live in the repository or dataRoot, even through a linked directory (0.720844ms)
|
||||
✔ check: dates and environment references (0.368981ms)
|
||||
✔ path and load (2.114907ms)
|
||||
✔ refusals (1.225659ms)
|
||||
✔ systemVars flattens the validated config (2.575859ms)
|
||||
✔ precedence: system, business, project, project role, agent (8.530151ms)
|
||||
✔ limits narrow the definition and never widen it (3.350867ms)
|
||||
✔ role.launch stays within-role only for the instance the launch block names (7.962531ms)
|
||||
✔ limits.authority without role.launch leaves the launcher with no launch block (2.886369ms)
|
||||
✔ limits.authority narrows cross-role actions too (2.092685ms)
|
||||
✔ classify (2.292167ms)
|
||||
✔ the record carries what the broker and launcher need (1.011055ms)
|
||||
✔ digest: key order doesn't matter, any value change does (7.193711ms)
|
||||
✔ refusals (2.432054ms)
|
||||
✔ the four shipped version 2 roles load (3.955928ms)
|
||||
✔ shipped role scopes match addendum B section 2 and the SR runbook (1.776154ms)
|
||||
✔ shipped authority follows the note's table (1.111235ms)
|
||||
✔ version 1 files keep loading with no authority (1.386411ms)
|
||||
✔ the conductor policy isn't a role (0.428507ms)
|
||||
✔ a missing role file is exit 4, a symbolic link too (0.540084ms)
|
||||
✔ version 2 refusals (5.32113ms)
|
||||
✔ authority: closed vocabulary, no gated-only action, no overlap (3.077258ms)
|
||||
✔ credentials: Gitea scopes (1.661659ms)
|
||||
✔ credentials: Vikunja scopes are a group-to-verbs map from the grantable list (2.161649ms)
|
||||
✔ credentials: services (0.922355ms)
|
||||
✔ contract: a non-empty regular Markdown file beside the role file (0.714248ms)
|
||||
✔ every key names known layers and a merge rule (1.083157ms)
|
||||
✔ unknown keys and wrong layers refuse (0.688039ms)
|
||||
✔ types (2.09786ms)
|
||||
✔ merge: defaults, then the most specific layer wins (0.434102ms)
|
||||
✔ merge: limits only narrow, and provenance lists each source (0.444915ms)
|
||||
✔ merge doesn't change its inputs (0.176633ms)
|
||||
ℹ tests 60
|
||||
ℹ suites 0
|
||||
ℹ pass 60
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 1866.960358
|
||||
@@ -0,0 +1,76 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (15.406365ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (19.889859ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (11.082826ms)
|
||||
✔ decide prints a declining choice as declining (10.900547ms)
|
||||
✔ an unknown outcome is reported once and never resent (9.993144ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (9.741018ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (11.82987ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (10.354325ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (9.997008ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (10.647727ms)
|
||||
✔ agents and tasks print through the broker (10.313741ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.332501ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (39.17658ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (33.4277ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (28.461035ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (33.735066ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (40.658634ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (62.480257ms)
|
||||
✔ empty views say so (0.745845ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (0.7702ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.123489ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (861.401799ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (134.37757ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (72.16873ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (124.331709ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (118.368837ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (78.525932ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (123.149211ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (76.627673ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (130.111723ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (22.373132ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (203.144574ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (204.506438ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (80.300161ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (589.870543ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (28.306311ms)
|
||||
✔ zoned uses the IANA zone across DST (12.414673ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (15.361146ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (12.226799ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.171141ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (10.006057ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (13.970726ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (15.636064ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (11.408405ms)
|
||||
✔ 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 (99.487844ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (42.033245ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (12.180808ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (6.843471ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.566595ms)
|
||||
✔ no Discord id reaches the journal or the log (7.102793ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (1.680487ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (1.967108ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.242875ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.399269ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.47903ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.788018ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.28774ms)
|
||||
✔ 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.448896ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.329134ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.398665ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.232068ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (110.778579ms)
|
||||
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 (253.53672ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (29.875889ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2148.77842ms)
|
||||
✔ busExit and refuseInsideAgent (0.531925ms)
|
||||
ℹ tests 66
|
||||
ℹ suites 0
|
||||
ℹ pass 66
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 2933.537466
|
||||
@@ -0,0 +1,132 @@
|
||||
✔ explicit request, Seen, ordinary completion and a new request have distinct attention states (3.621501ms)
|
||||
✔ attention convention ignores reasoning/quoted examples and permits leading blank lines (0.239892ms)
|
||||
✔ completed smoke replies and ordinary questions are idle, not human blockers (0.725294ms)
|
||||
✔ only an explicit first-line input request makes a finished reply waiting (0.150697ms)
|
||||
✔ tool activity, user text, errors and unfinished turns override attention text (0.122357ms)
|
||||
✔ STOP access failure is unknown, not absence, under a non-root identity (34.557499ms)
|
||||
✔ connector Task never inherits Discord routing envelopes; ordinary Task still uses user text (3.106752ms)
|
||||
✔ connector discovery keeps only safe identity; rejects modes, mismatches, links and traversal (1.34525ms)
|
||||
✔ canonical owner identity and STOP are independent; no tmux fallback or forged registration (5.383925ms)
|
||||
✔ connector reply refusal precedes forged live tmux registration; ordinary agent still sends (0.560572ms)
|
||||
✔ server rescans connector discovery and refuses HTTP reply without transport (30.899922ms)
|
||||
✔ connector session links and linked directories are not read (1.012519ms)
|
||||
✔ newer live matching launch marks old activity, preserves history/attention/attribution, then clears on new activity (3.354834ms)
|
||||
✔ CLI print uses the relaunch notice instead of old current preview (56.057003ms)
|
||||
✔ connector owner and fixed task never inherit a native relaunch notice (1.818617ms)
|
||||
✔ equality, stale/unknown/offline, mismatched registration and unknown activity do not assert relaunch (1.438209ms)
|
||||
✔ loadConfig: missing file throws ConfigError (1.410362ms)
|
||||
✔ loadConfig: invalid JSON throws ConfigError (0.27626ms)
|
||||
✔ loadConfig: missing dataRoot throws ConfigError (0.206544ms)
|
||||
✔ loadConfig: relative dataRoot throws ConfigError (0.198034ms)
|
||||
✔ loadConfig: valid config returns dataRoot (0.604677ms)
|
||||
✔ findNewestSession: picks the newest by mtime among two files (0.434068ms)
|
||||
✔ findNewestSession: finds files in nested subdirectories (0.284813ms)
|
||||
✔ findNewestSession: returns null for a missing dir (0.117471ms)
|
||||
✔ readSession: extracts fields, collapses/truncates text, counts a truncated final line (0.587025ms)
|
||||
✔ readSession: model and provider follow the latest model_change entry or assistant turn; null when the log names neither; scanAgent carries them (1.170303ms)
|
||||
✔ readSession: lastError carries the assistant errorMessage only when the last assistant turn errored (0.419397ms)
|
||||
✔ findNewestSession/scan: never read sibling auth or secrets next to a sessions dir (0.997913ms)
|
||||
✔ deriveState: full state table (0.16249ms)
|
||||
✔ rule: newest entry is an assistant message with a tool call, after a question-looking text, is working (0.304815ms)
|
||||
✔ rule: newest entry is a tool result with no assistant text after it is working (0.260324ms)
|
||||
✔ rule: a finished ordinary turn is idle, even if it says your move (0.245797ms)
|
||||
✔ task: the first user message of the session, from text blocks (0.234725ms)
|
||||
✔ task: a plain-string user content is accepted, whitespace collapsed and long text capped (0.237149ms)
|
||||
✔ task: no user message in the log means null (shown as unknown), never a guess (0.240797ms)
|
||||
✔ workspace: the live tmux pane path wins; the session cwd is the fallback; neither means null (0.338475ms)
|
||||
✔ activeProject: basename of the nearest .git directory or .git file above the workspace; none means null (0.465721ms)
|
||||
✔ scan: the written record carries task, workspace and activeProject (0.455056ms)
|
||||
✔ registration: overrides task, project and workspace; every source says registration; registered carries the launch fields; the grouping column is untouched (0.52772ms)
|
||||
✔ registration: empty task and null project/workspace leave the derived values in place; registered is still non-null (0.347052ms)
|
||||
✔ registration: a record whose pid is gone is stale; derived values win, sources say derived, registered stays with alive false; a pid the probe cannot decide is not stale; pidAlive itself (0.94347ms)
|
||||
✔ registration: no registration leaves the Gate A fields exactly as before, and registered is null (0.392815ms)
|
||||
✔ loadRegistrations: a missing seatsDir gives empty lists (0.129706ms)
|
||||
✔ loadRegistrations: one good record, one malformed JSON, one with an unknown field; a stray file under seatsDir is ignored (0.66035ms)
|
||||
✔ matchRegistration: matches by sessionsDir, and by realpath through a symlink; sessionsDir null never matches; same seat name with a different sessionsDir does not match (fleet vs repo darkwing) (0.29492ms)
|
||||
✔ scan: writes the registration override to disk; index.json carries registered and registrationErrors (0.630719ms)
|
||||
✔ scan: a relative seatsDir throws ConfigError; an omitted seatsDir behaves as before (0.297576ms)
|
||||
✔ scanAgent: waitingOnYou is true for waiting/error and false otherwise (0.514123ms)
|
||||
✔ scanAgent: ageSeconds is computed from the injected now (0.253019ms)
|
||||
✔ scanAgent: sessionFile null and state idle when sessions dir is empty but alive (0.149203ms)
|
||||
✔ discoverRepoAgents: finds agents with a sessions dir, skips those without, sorted by name (0.315371ms)
|
||||
✔ discoverFleetAgents: finds agents with a sessions dir, sorted by name, fleet tmux fields (0.439033ms)
|
||||
✔ scan: writes per-agent files and index.json, rerun overwrites, no leftover tmp files (0.987942ms)
|
||||
✔ scan: relative boardDir throws ConfigError (0.098999ms)
|
||||
✔ CLI: scan with assume-alive liveness exits 0, prints board summary, writes board files (54.602612ms)
|
||||
✔ CLI: missing config exits 2 with a refused: message (49.586258ms)
|
||||
✔ CLI: unknown command exits 2 (48.912305ms)
|
||||
✔ CLI: unknown --liveness value exits 2 (51.658356ms)
|
||||
✔ panesRunPi: true when any trimmed line equals 'pi' (0.19844ms)
|
||||
✔ panesRunPi: false for bash-only, claude, empty, or node-pi-style lines (0.069606ms)
|
||||
✔ tmuxIsAlive: a pane running pi is alive (0.181262ms)
|
||||
✔ tmuxIsAlive: session exists but pi has exited is not alive (0.074403ms)
|
||||
✔ tmuxIsAlive: no such tmux session is not alive (0.079362ms)
|
||||
✔ tmuxIsAlive: tmux could not be run at all is unknown (null), never assumed alive (0.069174ms)
|
||||
✔ tmuxIsAlive: passes -L <socket> only when a socket is given (0.098014ms)
|
||||
✔ parsePanes: one pane per line, command and optional tab-separated path (0.078915ms)
|
||||
✔ tmuxInspect: reports the path of the pane running pi, not of a shell pane (0.078816ms)
|
||||
✔ tmuxInspect: no pi pane, no session, or no tmux gives no workspace and the matching liveness (0.095051ms)
|
||||
✔ loadSeen: missing file returns {} (0.159643ms)
|
||||
✔ loadSeen: invalid JSON throws ConfigError (0.188476ms)
|
||||
✔ loadSeen: a JSON array throws ConfigError (0.153238ms)
|
||||
✔ loadSeen: a non-string value throws ConfigError (0.185738ms)
|
||||
✔ markSeen: seen true adds the key and writes seen.json mode 0600, no leftover tmp files (0.376136ms)
|
||||
✔ markSeen: seen false deletes the key (0.286277ms)
|
||||
✔ markSeen: missing, empty, or non-string fields throw ConfigError (0.206712ms)
|
||||
✔ markSeen: project containing '/' throws ConfigError (0.119844ms)
|
||||
✔ markSeen: non-boolean seen throws ConfigError (0.114391ms)
|
||||
✔ scanAgent: a seen mark matching the waiting session's lastTimestamp clears waitingOnYou (0.297461ms)
|
||||
✔ scanAgent: a stale mark (agent wrote something newer) is not seen and waitingOnYou is true (0.222805ms)
|
||||
✔ scanAgent: a working session with a matching mark is not seen (marks only apply to waiting/error) (0.224168ms)
|
||||
✔ scanAgent: an error-state session with a matching mark is seen (0.239582ms)
|
||||
✔ scan: index.seen and waitingOnYou reflect seen.json, which scan never rewrites or deletes (0.530782ms)
|
||||
✔ scan: a corrupt seen.json makes scan throw ConfigError (fail closed) (0.201505ms)
|
||||
✔ taskSetBy: a registered task carries the record's setter; a record without the field (pre-#1511) reads unknown; the value is not copied into registered (0.657081ms)
|
||||
✔ taskSetBy: null whenever the task shown is not the registered one: no registration, an empty registered task, a stale registration; the field is always present (0.555664ms)
|
||||
✔ taskSetBy: scan() reads the field from disk through the seat package (bounded there), writes it to the per-agent record and index, and an invalid on-disk value is a registrationError, never a row value (1.135361ms)
|
||||
✔ isLoopbackHost: recognizes loopback hosts (1.193759ms)
|
||||
✔ isLoopbackHost: rejects non-loopback hosts (4.175629ms)
|
||||
✔ startServer: refuses a non-loopback host with ConfigError, never opens a socket (2.931585ms)
|
||||
✔ startServer: serves page, healthz, and a rescanning /api/board (34.632692ms)
|
||||
✔ startServer: a seatsDir registration overrides the row and index.registered reflects it (5.469118ms)
|
||||
✔ startServer: /api/board returns 500 JSON with an error field when scan throws (2.396976ms)
|
||||
✔ CLI: serve refuses a non-loopback host with exit 2 and a refused: message (51.22562ms)
|
||||
✔ CLI: serve rejects a non-numeric --port with exit 2 (50.883701ms)
|
||||
✔ CLI: scan still works after the async cli refactor (54.813228ms)
|
||||
✔ CLI: live serve prints its URL and answers /healthz (58.398192ms)
|
||||
✔ page.html: esc() escapes every HTML-significant character (0.659275ms)
|
||||
✔ POST /api/seen marks a row; GET /api/board still shows it seen; seen:false clears it (8.07859ms)
|
||||
✔ POST /api/seen without a JSON content-type returns 400 and does not write a mark (2.037422ms)
|
||||
✔ POST /api/seen with invalid JSON returns 400 (2.835152ms)
|
||||
✔ POST /api/seen with a body over 4096 bytes returns 400 (or resets the connection) and writes no mark (2.019713ms)
|
||||
✔ POST /api/seen with a missing agent returns 400 (1.295617ms)
|
||||
✔ POST /api/board returns 405; PUT /api/seen returns 405 (1.669646ms)
|
||||
✔ CLI: scan --print marks a seen row with 's' and the summary line ends with 'N seen)' (50.449813ms)
|
||||
✔ page.html: seenControl() escapes rec.project/agent/lastActivity, and the POST uses a JSON content-type (0.337992ms)
|
||||
✔ page.html: has a collapsed Seen section that lists seen rows with the shared row builder (0.270884ms)
|
||||
✔ page.html: each project has a Hide seen checkbox (default on) beside Hide offline, with a hidden-count note (0.16532ms)
|
||||
✔ page.html: a project header reads "N of N" only while a checkbox hides rows (0.13303ms)
|
||||
✔ page.html: every row shows Task and Active project, derived or the word unknown, with the workspace in the detail (0.309982ms)
|
||||
✔ page.html: task and active project cells show their source via sourceTag(); the detail has a Registered row via registeredText(); SOURCE_LABEL maps registration to registered; every dynamic value in sourceTag/fromSource/registeredText is escaped (0.577047ms)
|
||||
✔ POST /api/reply: runs agent-send.sh with -s from the registration, -S <host>:control-board, -m text plus the fixed trailer, no -L on the default socket, MOSAIC_TMUX_SOCKET stripped; answers delivered with the exit code and both streams (28.948657ms)
|
||||
✔ POST /api/reply: a registration with a tmux socket adds -L <socket> (25.686903ms)
|
||||
✔ POST /api/reply: a non-zero tool exit is a 200 with delivered false, the exit code and the stderr verbatim (29.374986ms)
|
||||
✔ POST /api/reply: refusals before the tool runs: empty or blank or long text 400, unknown row 404, no registration 409, stale registration 409, no tmux session 409, bad JSON 400; the tool is never called (14.264179ms)
|
||||
✔ POST /api/reply: a missing agent-send.sh is a 500 with the path in the error, not a crash (4.271884ms)
|
||||
✔ replyToRow: DEFAULT_AGENT_SEND is the repository's tools/tmux/agent-send.sh and it is executable (0.155668ms)
|
||||
✔ page.html: the reply box appears only where canReply() holds (live registration with a tmux session), the detail has a Reply row, the submit posts JSON to /api/reply, receipts and drafts survive a refresh, and every receipt value is escaped (0.584762ms)
|
||||
✔ startServer: /api/board carries taskSetBy from a live registration and null for the derived rows (2.429038ms)
|
||||
✔ page.html: the task cell and detail show who set a registered task via setByTag()/setByText(), both escaped, only from rec.taskSetBy; the reply gate does not read it (0.302417ms)
|
||||
✔ Host/Origin guard: GET /api/board and POST /api/reply refuse a foreign Host, a wrong port and a cross-origin Origin with 403 JSON, before any scan or send, and never send CORS headers (7.946952ms)
|
||||
✔ Host/Origin guard: loopback names on this port are accepted, with or without a same-origin Origin (28.283933ms)
|
||||
✔ conversation routes (F16): a foreign Host, a wrong port and a cross-origin Origin get 403 before the reader runs, with no CORS headers (5.489636ms)
|
||||
✔ every refusal code the reader can raise has an HTTP status (2.556468ms)
|
||||
✔ conversation routes: catalogue, first page, next page and follow over HTTP; refusals map to 4xx with their code; nothing is written (47.006525ms)
|
||||
ℹ tests 124
|
||||
ℹ suites 0
|
||||
ℹ pass 124
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 616.847495
|
||||
@@ -0,0 +1,160 @@
|
||||
✔ W1: two processes acquire the same pair at once; exactly one claim (138.999793ms)
|
||||
✔ W1: two writers publish the same revision at once: one wins, the other gets null, the winner's record stays (8.506643ms)
|
||||
✔ W1: a revision name appears only after its bytes are synced; before that, only a temp file exists (3.652426ms)
|
||||
✔ W2: acquire while a claim is reserved or active refuses already-active (139.65297ms)
|
||||
✔ W3: acquire while stopping, uncertain, or stopped without proof refuses unsafe-replacement (208.781861ms)
|
||||
✔ W4: same session with another seat tuple, and the reverse, both refuse; a loser on the seat key closes it no-unit (146.913673ms)
|
||||
✔ W4: a hard link of one session under another seat is the same session: the second controller refuses already-active and launches nothing (16.758077ms)
|
||||
✔ W4: a copy of one session under another seat is the same session: the second controller refuses already-active and launches nothing (15.265081ms)
|
||||
✔ W4: a session header ID that changes after construction refuses target; nothing is claimed or launched (2.423659ms)
|
||||
✔ W5: SIGKILL between every publication barrier of acquire and transition; restart never finds two holders or a lost claim (5564.882556ms)
|
||||
✔ W5: SIGKILL between every publication barrier of release; restart finishes or holds the release (21290.183026ms)
|
||||
✔ W6: controller killed mid-turn while the engine lives; restart is uncertain, no launch, prompts refuse (148.837324ms)
|
||||
✔ W12: a live owner paused with SIGSTOP; a second controller refuses already-active and changes nothing (90.038212ms)
|
||||
✔ W13: crash after the engine spawns, before active; restart finds the live unit: uncertain, no second spawn, force stop only (222.59503ms)
|
||||
✔ W14: crash after reservation, before the spawn marker: stopped with a no-unit observation; the pair is free (180.796554ms)
|
||||
✔ W20: crash after the spawn marker, scope collected; uncertain in both runs, the marker is copied, no launch until a boot proof (211.80975ms)
|
||||
✔ W15: crash between the two keys during release; restart finishes it under the same claim ID (33.370238ms)
|
||||
✔ W7: recorded boot ID differs on the same machine: stopped with a boot proof; open tool calls become uncertain (95.958842ms)
|
||||
✔ W8: resume after a proven stop with the same pins: new claim ID, generation +1, same conversation, branch and leaf (31.22442ms)
|
||||
✔ W9: resume with a changed binary, argv digest, branch or leaf is refused and the claim is unchanged (90.183997ms)
|
||||
✔ W11: the controller writes no session file; only the fake engine's own appends appear (22.32039ms)
|
||||
✔ W16: a highest revision that won't parse holds the pair uncertain; the older stopped revision is not reused (51.795196ms)
|
||||
✔ W17: a claim root copied from another host refuses foreign-host and promotes nothing (57.941598ms)
|
||||
✔ G1: a session path or claim root under .pi/state, ~/.claude, the data root or a registration refuses at construction (3.484511ms)
|
||||
✔ G2: a symlink inside the fixture root to a live session file is refused by the real-path check (1.066388ms)
|
||||
✔ G3: a fixture path swapped for a live path after construction is refused at bind (1.763114ms)
|
||||
✔ K1: force stop kills a tool child that called setsid; stopped with a verified proof (2549.276526ms)
|
||||
✔ K2: K1 on the process-group fallback ends uncertain, never stopped (144.547438ms)
|
||||
✔ K3: SIGTERM acknowledged while a member lives: stopping until the kill phase, never stopped from TERM (2428.985502ms)
|
||||
✔ K4: two engines; force stop one; the other survives by independent observation (4301.9598ms)
|
||||
✔ K5: a stop during a tool call leaves the effect uncertain, and it is shown (2149.163268ms)
|
||||
✔ K12: a member forking in a loop: the freeze stops it, enumeration is complete, populated 0 after cgroup.kill (2246.94703ms)
|
||||
✔ K13: a member writing its pid into another cgroup is refused by the namespace; the kill is complete (2148.008577ms)
|
||||
✔ K15: the shim gone, engine/cgroup.events unreadable, or the engine cgroup missing: evidence unavailable, not empty; uncertain (4339.87992ms)
|
||||
✔ K10: controller killed between the TERM and kill phases: restart checks the invocation ID and re-runs from TERM for the same stop (385.636316ms)
|
||||
✔ K11: controller killed after the confirmation is recorded, before TERM: restart checks the invocation ID and re-runs from TERM for the same stop (320.160393ms)
|
||||
✔ K14: a unit with the recorded name but another invocation ID: evidence unavailable, no signals, uncertain (259.118506ms)
|
||||
✔ K6: recover without proof, without confirmation, or with changed pins is refused (65.979593ms)
|
||||
✔ K7: recover after proof, then launch: new claim and execution, generation +1, same leaf; the cancelled prompt is not replayed (30.958052ms)
|
||||
✔ K8: an engine that loads another leaf on resume is refused before admission; it stays claimed until a proven stop (38.927223ms)
|
||||
✔ K9: an interrupt that never settles stays uncertain; force stop stays available; takeover is refused while fenced (3025.311243ms)
|
||||
✔ K16: a claim from another machine ID refuses foreign-host; no boot proof is issued (5.577284ms)
|
||||
✔ K17: two launcher calls with one eligibility record: one launch, the other refuses, no second engine (31.074742ms)
|
||||
✔ K18: the leaf changes after eligibility: launch refused; the reservation stays until released with proof (25.6351ms)
|
||||
✔ S1: `/goal x`, with leading spaces or a tab, refuses text-policy at admission; zero engine bytes (32.684296ms)
|
||||
✔ S2: every prefix pinned Pi interprets is refused, from the list the code uses; the rest reach the engine exactly (31.894859ms)
|
||||
✔ S3: `/goal` on the second line is pinned from the source: Pi checks only index 0, so it is admitted and sent exactly (25.404556ms)
|
||||
✔ S4: a `/` left in the composer is cleared when control transfers and returns; the next submit sends only the new text (41.567832ms)
|
||||
✔ S5: an observer terminal gets a paste then Enter, as send-message.sh does: not admitted: controller, nothing sent (18.393873ms)
|
||||
✔ S6: a mediated-shaped registration (no tmux) passed to the board's replyToRow: 409 no tmux session; exec never runs (0.681211ms)
|
||||
✔ S7: ESC, bracketed-paste markers and U+2028/U+2029 travel as one JSON string; the engine receives the exact text in one record (23.740901ms)
|
||||
✔ P3: a Pi confirm, select, input or editor dialog is shown disabled with a reason and never answered (125.240068ms)
|
||||
✔ E1: send, ack, user, toolCall, toolResult, final answer: shown once, no refresh, draft and reading position kept (40.541369ms)
|
||||
✔ E2: U+2028, U+2029 inside JSON strings and CRLF line ends each parse as one record, on the splitter and through the controller (20.472634ms)
|
||||
✔ E3: a multipart final, two blocks, null request correlation and duplicate delivery (30.998105ms)
|
||||
✔ E4: a page read after message_end but before its entry is persisted: marker at the seam, re-read after run-settled, each message once (35.918921ms)
|
||||
✔ E4: a gap or a new epoch also reconciles; nothing is concatenated across a gap (11.250458ms)
|
||||
✔ E5: an unknown native event gives no client event; evidence records its type and bytes; the terminal count goes up (32.533965ms)
|
||||
✔ E6: a tool result delayed across a pause and a reconnect is reconciled without a manual refresh (47.276026ms)
|
||||
✔ E7: the terminal renders the same stream as the library client, as observer and then as controller, and submits only as controller (41.965175ms)
|
||||
✔ terminal: engine control characters are made visible; a lost connection refuses submit (24.174742ms)
|
||||
✔ terminal: outcome unknown is shown as such, with no resend offer, and nothing is resent (0.562867ms)
|
||||
✔ terminal: text after Enter in the same input chunk starts the next message; it never joins the one submitted (0.2942ms)
|
||||
✔ terminal: a paste-start marker split right after its ESC still opens the paste; the Enter inside it never submits (0.422497ms)
|
||||
✔ terminal: invisible and bidi characters are made visible; head, status and notice lines stay one line (0.120017ms)
|
||||
✔ every record these fixtures produced is a valid CHAT-01 record (E5: no record fails the schema) (332.391101ms)
|
||||
✔ H1: two takeovers with the same expected generation: one wins, +1; the other refuses generation (54.806462ms)
|
||||
✔ H2: the old controller's prompt after a takeover commits is refused with zero engine bytes (82.43351ms)
|
||||
✔ H3: a takeover while a prompt holds the dispatch lock: written under the old actor, or refused; never both (132.498415ms)
|
||||
✔ H4: self-takeover is refused (20.051491ms)
|
||||
✔ H9: Interrupt racing a prompt's dispatch: before the write, dispatch-refused and no-turn; after, §3 rules (93.211892ms)
|
||||
✔ H10: Interrupt and force stop together: one stop chain, force stop supersedes (87.129707ms)
|
||||
✔ H10: an overlap during the pause before the abort: no abort, the stop ends uncertain (23.395035ms)
|
||||
✔ H10: a no-turn Interrupt lifts only its own fence; admission stays closed under force stop, overlap or revocation (78.376918ms)
|
||||
✔ H11: the controller disconnects mid-turn: work continues, the claim is unchanged, control stays put (129.210058ms)
|
||||
✔ H12: an exact retry after reconnecting to the same incarnation returns the same receipt; one dispatch (14.867287ms)
|
||||
✔ H13: a retry with the same request ID and different text is refused (15.797975ms)
|
||||
✔ H14: late stdout from the old engine after a replacement is dropped by incarnation, counted, never rendered (140.500735ms)
|
||||
✔ H15: a revoked connection's command is refused; the revocation fence holds (70.40603ms)
|
||||
✔ H16: a second controller for the same session refuses already-active; the first is untouched (20.607613ms)
|
||||
✔ H10: a second force stop while the first escalation runs refuses fenced; one escalation, and the claim records only the first stop's phases (57.752007ms)
|
||||
✔ H17: a confirmation reused, answered from another connection, or used after the stop changed is refused (59.76822ms)
|
||||
✔ H18: two prompts before any native output: the second refuses busy; one engine write (15.350836ms)
|
||||
✔ H19: the pipe fails mid-line under a large prompt: delivery-unknown transport-unknown, poisoned, no later write (120.166377ms)
|
||||
✔ H19: the link itself never writes again after an unknown outcome, whoever calls it (0.694913ms)
|
||||
✔ H19: the controller dies mid-write of a large line: after restart the outcome is unknown and nothing is resent (466.178154ms)
|
||||
✔ H20: the line is written but the ack is lost when the controller dies: orphan, outcome unknown, nothing resent (359.880623ms)
|
||||
✔ H21: a retry of the exact request with the old token after a crash is stale-incarnation; no second write (319.856375ms)
|
||||
✔ H22: after H21 and a valid recovery, a new request with the new token is admitted (2390.814685ms)
|
||||
✔ H23: requests pending at a restart are not resent; each shows outcome unknown (461.597177ms)
|
||||
✔ a plain conversation: catalogue row, one page, CHAT-01 records (6.495309ms)
|
||||
✔ native entries map to blocks: tools, thinking, bash, notices, ids that do not fit (1.845716ms)
|
||||
✔ F1: a malformed line is an unavailable part at its position, and reading continues (1.75288ms)
|
||||
✔ F1: a missing parent stops the history with a notice that names the unreadable lines (2.618534ms)
|
||||
✔ F1: an unreadable fork is never merged into another branch's history (1.689415ms)
|
||||
✔ F1: a follow stays on its branch when the next entry's parent is unreadable (2.008519ms)
|
||||
✔ F1: a file whose entries are all unreadable shows a notice per line (0.784078ms)
|
||||
✔ F2: a truncated trailing line marks the view incomplete, not an error (1.223817ms)
|
||||
✔ pagination: 100 parts, then the rest; parts concatenate to the whole branch (3.751945ms)
|
||||
✔ F3: a replaced file (new inode) refuses old cursors with reconcile (4.317439ms)
|
||||
✔ F4: a same-inode rewrite of the prefix refuses old cursors with reconcile (4.775366ms)
|
||||
✔ F5: growth between pages keeps the epoch and the page stops at the pinned length (5.184904ms)
|
||||
✔ F6: unknown, foreign and expired cursors refuse and leave the cursor usable (7.002224ms)
|
||||
✔ F7: a symlinked file and a symlinked directory component are refused, never opened (8.106285ms)
|
||||
✔ F8: a file swapped for a symlink after the catalogue is refused (2.045143ms)
|
||||
✔ F9: registrations never add or redirect a root (1.483833ms)
|
||||
✔ F10: a header cwd naming another project is refused (5.213162ms)
|
||||
✔ F11: parentSession renders with a marker and the parent is never opened (1.019762ms)
|
||||
✔ F12: two leaves: the default leaf is shown and the other branch reads alone (5.584279ms)
|
||||
✔ F12: a follow refuses when an appended duplicate id changes the branch's earlier parts (3.432934ms)
|
||||
✔ F12: a second root (Pi's resetLeaf) starts its own branch (1.517508ms)
|
||||
✔ F13: compaction is a marker in place, then the retained content (0.908764ms)
|
||||
✔ F14: long strings split into fragments and parts, reassemble exactly, and pages respect the byte cap (598.605287ms)
|
||||
✔ fragments never cut a surrogate pair and keep an empty string (4.606857ms)
|
||||
✔ F15: a Claude seat is an unsupported-harness placeholder whose directory is never read (1.428557ms)
|
||||
✔ unknown conversations, empty files and non-Pi files refuse (2.335997ms)
|
||||
✔ an unreadable file or root inside the roots is refused per row, not a failed catalogue (1.050082ms)
|
||||
✔ a seat directory without search permission refuses that root, not the catalogue (1.859272ms)
|
||||
✔ every page and cursor is a valid CHAT-01 record (875.578923ms)
|
||||
✔ the engine pin holds for the installed package (2.598793ms)
|
||||
✔ pinned Pi, sealed and without credentials, answers the controller's commands with the shapes the fake models (324.810483ms)
|
||||
✔ pinned Pi appends thinking_level_change at start when the branch lacks one, so the leaf moves (K8 then fails closed) (296.978768ms)
|
||||
✔ N25: ordinary Interrupt reconciles; a non-empty queue_update in the window is O5 (74.83861ms)
|
||||
✔ N1: an extension's follow-up queued after the fence is cleared before any abort; O5, Unknown (54.1458ms)
|
||||
✔ N1: a follow-up queued before the fence is O5 at once; the Interrupt refuses fenced (22.98321ms)
|
||||
✔ N2: with abort first, the fake runs the external item (the ordering guard has teeth) (22.268534ms)
|
||||
✔ N3: the fence lands in preflight, preflight errors, no run: failed, No run, uncertain (36.45886ms)
|
||||
✔ N4: the ack arrives after the first abort and a run starts: clear and abort again; Interrupted (21.68886ms)
|
||||
✔ N5: an input handler takes the prompt: ack, no run, delivery-unknown handled-without-run (116.161204ms)
|
||||
✔ N6: an extension queues between clear_queue and abort: O5 and O6, Unknown (50.613061ms)
|
||||
✔ N7: clear_queue times out: no abort, nativeQueue unknown, force stop still ends it (1533.053947ms)
|
||||
✔ N7: clear_queue answers an error: no abort, nativeQueue unknown, the link not poisoned (13.930099ms)
|
||||
✔ N8: an extension prompt starts a run during Mosaic preflight; the losing settle is O3 (70.710695ms)
|
||||
✔ N9: a run that started before the fence and ends aborted: failed interrupted, Interrupted (13.637596ms)
|
||||
✔ N9: decision 34: a run that ends aborted with no stop in progress: aborted-without-stop, uncertain, outcome unknown (14.878805ms)
|
||||
✔ N9: an aborted that lands after the fence but before any abort is written: aborted-without-stop, Unknown (31.802435ms)
|
||||
✔ N10: fake conformance (33.058444ms)
|
||||
✔ N11: the run fails before any user message_start: delivery-unknown ack-without-start, never failed (29.244906ms)
|
||||
✔ N12: input that starts a run after the final empty clear is O1 and not part of the stop's proof (20.118833ms)
|
||||
✔ N13: agent_start with no slot held is O1; a later prompt refuses with zero engine bytes (64.60306ms)
|
||||
✔ N14: the run completes while clear_queue is in flight: finished, Completed first, uncertain (29.196526ms)
|
||||
✔ N14: the run completes after the abort is written, before Pi applies it: finished, never relabelled (26.842034ms)
|
||||
✔ N15: the fence lands in preflight, then an input handler takes it: handled-without-run, No run (22.453252ms)
|
||||
✔ N16: Interrupt with no slot and no run refuses no-turn: no stop, no bytes, admission open (12.680362ms)
|
||||
✔ N17: the run fails on its own during the exchange: failed, Failed on its own (28.392304ms)
|
||||
✔ N18: no final assistant message_end, or a lost line: working stays working; before working, transport-unknown (105.211014ms)
|
||||
✔ N19: a losing extension prompt settles inside the Mosaic run before its user message: O3, run-overlap (137.549503ms)
|
||||
✔ N20: an extension triggerTurn during Mosaic preflight starts first; while streaming it queues with no signal (88.798093ms)
|
||||
✔ N21: a losing settle after the receipt settled finished is O2; the receipt stays finished (17.677504ms)
|
||||
✔ N22: an agent-level custom message is dropped by the clear with no signal; evidence names the seal (13.362159ms)
|
||||
✔ N23: a nextTurn message survives clear and abort and attaches to the next prompt, with no signal (14.431043ms)
|
||||
✔ N24: the seal is an allow-list: --extension, a missing --no-* flag, a second --mode or --session, a session or output flag, or a stray word refuses unsealed-engine; no engine starts (37.309547ms)
|
||||
ℹ tests 152
|
||||
ℹ suites 0
|
||||
ℹ pass 152
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 30796.131987
|
||||
@@ -0,0 +1,186 @@
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (2.903913ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.454462ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.78376ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.466664ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (19.513829ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (7.416436ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (6.819423ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.289907ms)
|
||||
✔ authorize: open channel, listed user (2.310838ms)
|
||||
✔ authorize: wrong guild (0.217528ms)
|
||||
✔ authorize: no guild (DM) (0.836277ms)
|
||||
✔ authorize: unlisted channel (0.187908ms)
|
||||
✔ authorize: unknown channel, no info (0.193527ms)
|
||||
✔ authorize: thread of listed parent (0.188433ms)
|
||||
✔ authorize: thread of unlisted parent (0.179334ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.144764ms)
|
||||
✔ authorize: unlisted user (0.911939ms)
|
||||
✔ authorize: no author (0.534731ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.146689ms)
|
||||
✔ authorize: system author (0.115937ms)
|
||||
✔ authorize: the bot itself (0.11845ms)
|
||||
✔ authorize: webhook (0.092028ms)
|
||||
✔ authorize: mention channel without mention (0.125109ms)
|
||||
✔ authorize: mention channel with bot mention (0.127117ms)
|
||||
✔ authorize: mention channel with @everyone only (0.101821ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.163548ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.109588ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.088943ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.068659ms)
|
||||
✔ authorize: thread in another guild per channel info (0.073402ms)
|
||||
✔ authorize: not an object (0.058343ms)
|
||||
✔ authorize: no id (0.061006ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.068543ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.064352ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.438614ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.142735ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.739096ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.234553ms)
|
||||
✔ binding: empty allowlists refuse (0.404158ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.2463ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.966692ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (1.087639ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.8907ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.257554ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (85.300078ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (4.469484ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (302.831421ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (201.036945ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (143.365084ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.880658ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.418763ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (17.920871ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (12.913425ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.477812ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.129505ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.251699ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.479765ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (33.370296ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (2.673987ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.049364ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (0.633413ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (40.210132ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (31.715359ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (5.397667ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (3.75225ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (2.232436ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (3.891573ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.084563ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.457993ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (1.039986ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (2.343573ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (3.663203ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.06878ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (3.810866ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.269622ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (1.370235ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.793196ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.19291ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.351993ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.123168ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.634407ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.429101ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.391398ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (53.446831ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (37.021802ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (29.426679ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (334.474382ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (234.697449ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (103.829241ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (230.393176ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (127.923407ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (212.349312ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (615.508555ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.459239ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.530617ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (430.037768ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (23.965826ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (43.019075ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.435203ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.590545ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.577269ms)
|
||||
✔ gateway: op 9 resumable resumes (0.365212ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.924753ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.575025ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.554273ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (68.40222ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (35.509171ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (63.337673ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.268017ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (70.998308ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (79.601099ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (88.922832ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (192.561515ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (70.937641ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (109.73177ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (188.202912ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (713.795919ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.865057ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (69.910142ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (0.703285ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (2.57031ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (34.852011ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (278.3905ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.422817ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (23.179621ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (0.995738ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (36.881048ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (143.185142ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (85.098925ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.507347ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.001579ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.379525ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.684432ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (40.714778ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (32.096076ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (30.979192ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (68.06152ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (660.288689ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.514157ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (2.949474ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.716796ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.867047ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (1.117036ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.069278ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (2.897525ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.593861ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.103284ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (19.607646ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (7.030442ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (6.646527ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (2.898618ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.530955ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (1.757769ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1010.059937ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.418211ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (6.24409ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.188286ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.190836ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.232401ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (2.757129ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (2.584477ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (3.375799ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (3.125719ms)
|
||||
✔ tools: listing and search caps hold (7.523794ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.589112ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (3.598801ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (0.817416ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (0.84635ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (3.75086ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (1.844748ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.243029ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (2.615671ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (1.494227ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1021.780474ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.288246ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.212091ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.584111ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.198462ms)
|
||||
ℹ tests 178
|
||||
ℹ suites 0
|
||||
ℹ pass 178
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 2592.580644
|
||||
@@ -0,0 +1,86 @@
|
||||
✔ a raw token file, with or without one trailing newline, reaches curl only through the config stream (118.594239ms)
|
||||
✔ the raw path accepts nothing else, and refuses before curl runs (367.73863ms)
|
||||
✔ the file checks still apply on the raw path: mode, symlink, missing, directory (277.674018ms)
|
||||
✔ the raw path base URL has no override (52.543854ms)
|
||||
✔ the JSON path is unchanged, and JSON never falls through to the raw path (244.478185ms)
|
||||
✔ a file that changes between the two reads refuses before curl runs, with or without a body (711.198471ms)
|
||||
✔ the token reaches no child environment, even with an inherited CFG or SHELLOPTS=allexport (672.287612ms)
|
||||
✔ real helper GET HTTP 200 preserves exit 0 without credentials (21.392231ms)
|
||||
✔ real helper POST HTTP 201 preserves exit 0 without credentials (10.999711ms)
|
||||
✔ real helper GET HTTP 403 preserves exit 1 without credentials (8.244951ms)
|
||||
✔ fixture git subjects only, follow-ups and three session kinds (105.850533ms)
|
||||
✔ text and JSON carry same numbers, open and truncated title (169.021156ms)
|
||||
✔ missing credentials exit 2, no-issues never calls API and shows unknown (134.774246ms)
|
||||
✔ empty range gives no rows and zero totals (96.267627ms)
|
||||
✔ inclusive UTC dates, first-line preamble only, role and seat boundaries (101.481753ms)
|
||||
✔ close-only issue included, even median, missing metadata stays unknown (93.922638ms)
|
||||
✔ unique commits but per-issue links count multiple tags once each (101.841089ms)
|
||||
✔ page cap refuses rather than silently undercounting (87.159091ms)
|
||||
✔ bad API payload not JSON refuses (89.245891ms)
|
||||
✔ bad API payload {} refuses (87.247327ms)
|
||||
✔ bad API payload [{"number":1}] refuses (82.516885ms)
|
||||
✔ partial or malformed session log refuses with location, not content (94.357432ms)
|
||||
✔ a U+2028 or U+2029 inside a session string is one line, not a malformed record (99.882598ms)
|
||||
✔ no sessions is an empty table; symlink source refuses (163.470236ms)
|
||||
✔ reads only refactor even when another branch is checked out (98.041163ms)
|
||||
✔ invalid dates, reverse dates and duplicate options refuse (58.6285ms)
|
||||
✔ T3 agent assignments do not count as human in Table 2 (95.217595ms)
|
||||
✔ preamble parsing and issue number boundaries (0.46154ms)
|
||||
✔ T3 header: agent, or board from control-board; anything short of the full header is human (0.141378ms)
|
||||
✔ no closed issues with human messages means undefined ratio, not invented zero (0.197281ms)
|
||||
✔ T3: seat, archived, unmapped and Researcher threads count; imported, deleted and other-project threads do not (185.160035ms)
|
||||
✔ T3: the default path is read from HOME and prints no path line; --no-t3 says so (399.779566ms)
|
||||
✔ T3: a HOME with no database exits 1 and names --no-t3 (90.585884ms)
|
||||
✔ T3: a file that is not a database exits 1 and names --no-t3 (95.28352ms)
|
||||
✔ T3: a seat thread renamed to another seat exits 1 naming thread, title and roles (113.166308ms)
|
||||
✔ T3: an unmapped thread addressed as a seat exits 1 (111.036626ms)
|
||||
✔ T3: a header to another thread id is not cross-checked (114.716086ms)
|
||||
✔ T3: no project, or two, for this root exits 1 (290.567987ms)
|
||||
✔ T3: a removed column exits 1 and names it (101.565435ms)
|
||||
✔ T3: a missing table exits 1 and names it (99.926989ms)
|
||||
✔ T3: a counted row with an unknown role exits 1 without its text (111.131468ms)
|
||||
✔ T3: a counted row with non-text content exits 1 without its text (116.180417ms)
|
||||
✔ T3: a counted row with an unparseable created_at exits 1 without its text (110.968807ms)
|
||||
✔ T3: a missing orchestration_events makes the diagnostic unknown and keeps the counts (197.389671ms)
|
||||
✔ T3: a human message with no event counts in humanWithoutEvent (113.950005ms)
|
||||
✔ T3: an unparseable event makes the diagnostic unknown and keeps the counts (204.679915ms)
|
||||
✔ T3: an event with no string messageId makes the diagnostic unknown and keeps the counts (213.732484ms)
|
||||
✔ T3: an error that is not from SQLite is rethrown, not reported as a database failure (59.38188ms)
|
||||
✔ T3: a symlink at ~/.t3 exits 1 (83.748007ms)
|
||||
✔ T3: a symlink at ~/.t3/userdata exits 1 (87.789686ms)
|
||||
✔ T3: a symlink at ~/.t3/userdata/state.sqlite exits 1 (83.319008ms)
|
||||
✔ T3: with --t3-db, a symlinked file or directory exits 1 (170.291468ms)
|
||||
✔ T3 WAL: the newest message only in -wal, writer attached, is counted (118.18355ms)
|
||||
✔ T3 WAL: stopped cleanly, counts are correct and the main file is unchanged (119.273698ms)
|
||||
✔ T3 WAL: -wal without -shm in a writable directory is read (138.87176ms)
|
||||
✔ T3 WAL: -wal without -shm in a read-only directory exits 1 (132.838681ms)
|
||||
✔ T3 WAL: stopped cleanly in a read-only directory exits 1 (118.205917ms)
|
||||
✔ T3: a lock held past the 5 s busy timeout exits 1 and names --no-t3 (5120.326126ms)
|
||||
✔ a done row whose closing issue is open is a violation; a row that is not done is not (2.500601ms)
|
||||
✔ an issue several rows close is expected closed only once all of them are done (0.73534ms)
|
||||
✔ closure needs positive evidence: unknown is undecided, and so is a skipped or short issue check (0.53954ms)
|
||||
✔ each owner of an in-progress or in-review row gets one liveness class (13.651359ms)
|
||||
✔ a required row not done after 14 days is a violation; a legacy row uses genesis as its lower bound (0.587481ms)
|
||||
✔ an ISO requiredSince, as `set required` writes it, ages from its UTC day; one that does not parse is a violation (0.315708ms)
|
||||
✔ the text section always ends in a count and a result, and never prints a full pass (0.480685ms)
|
||||
✔ pidAlive: a running pid is present, an exited one is gone, and EPERM still means present (23.386605ms)
|
||||
✔ issue states: open list first, then the metric page, then at most 10 lookups (284.666989ms)
|
||||
✔ a full open list: lookups settle what it leaves out, and only an unsettled issue keeps it undecided (365.749797ms)
|
||||
✔ the open list refuses on a failed call or a bad record, and never echoes the helper (169.211568ms)
|
||||
✔ a helper call past the deadline is killed with its child, and the call reports it (2007.38853ms)
|
||||
✔ readQueue loads queue.json through the queue validator and refuses anything else (91.484931ms)
|
||||
✔ protected changes list every in-range entry that changes a required or parked row (233.342814ms)
|
||||
✔ the CLI prints the queue section above the weekly table and under a queue key in --json (486.462843ms)
|
||||
✔ a queue with nothing wrong prints 0 violations and a reduced pass, never a full pass (199.386399ms)
|
||||
✔ --no-issues makes no call and leaves the issue checks undecided; --no-queue skips the section (258.105562ms)
|
||||
✔ the CLI refuses a bad queue before any call, and a failed open list with exit 2 (234.925552ms)
|
||||
✔ --unsupported-runtime repeats once per seat and takes a seat name (301.420891ms)
|
||||
✔ an unreadable config makes every owner invalid instead of passing them (124.051793ms)
|
||||
ℹ tests 78
|
||||
ℹ suites 0
|
||||
ℹ pass 78
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 10715.167538
|
||||
@@ -0,0 +1,77 @@
|
||||
✔ pure resolution selects current default or explicit enrolled account (1.919587ms)
|
||||
✔ scope is explicit, bounded and never inferred (1.579855ms)
|
||||
✔ fork pin is preserved against default change, override, missing account and revocation (0.710634ms)
|
||||
✔ unenrolled account/provider, missing harness, model expansion and native model ceiling refuse (0.798155ms)
|
||||
✔ only explicit synthetic credential forms and internal fixture stores admitted (5.446372ms)
|
||||
✔ two concurrent workspaces of the same agent publish distinct complete private generations (58.573612ms)
|
||||
✔ same execution ID is exclusively claimed and cannot overwrite a published generation (36.350298ms)
|
||||
✔ failed generation after-auth preserves prior files, records failure and refuses blind same-ID retry (45.854624ms)
|
||||
✔ failed generation before-publish preserves prior files, records failure and refuses blind same-ID retry (55.323414ms)
|
||||
✔ credential lock contention refuses without duplicate side effects (11.347744ms)
|
||||
✔ symlinked pre-existing final target is refused and never followed (31.749281ms)
|
||||
✔ invalid registry cannot resolve; no fallback to supplied partial entries (0.398185ms)
|
||||
✔ post-publication failure records uncertainty, preserves complete generation and prevents replay (26.197757ms)
|
||||
✔ expired credentials refresh under transaction and subsequent generation reuses rotation (85.726793ms)
|
||||
✔ refresh failure retains prior generation and store state (67.795926ms)
|
||||
✔ refresh timeout retains prior generation and store state (147.743057ms)
|
||||
✔ refresh malformed retains prior generation and store state (65.246806ms)
|
||||
✔ concurrent refresh on same account refuses contention while unrelated account proceeds (222.470507ms)
|
||||
✔ invalid refresh options refuse before burning claim (38.452168ms)
|
||||
✔ fixed fake process rotates both OAuth fields without mutating caller input (35.609313ms)
|
||||
✔ concurrent isolated processes preserve separate provider credentials (26.763497ms)
|
||||
✔ fake failure is refused with fixed diagnostics (28.24135ms)
|
||||
✔ fake malformed is refused with fixed diagnostics (23.593098ms)
|
||||
✔ fake timeout is refused with fixed diagnostics (103.490693ms)
|
||||
✔ fake unchanged is refused with fixed diagnostics (25.131315ms)
|
||||
✔ caller executable/environment injection is rejected before spawning (0.300073ms)
|
||||
✔ valid fixture tree validates and lists without secrets (68.081332ms)
|
||||
✔ unknown-field refuses (0.371759ms)
|
||||
✔ invalid-id refuses uppercase and traversal shapes (0.268678ms)
|
||||
✔ plain-http baseUrl requires allowInsecureTransport (0.296086ms)
|
||||
✔ native provider rejects allowInsecureTransport (0.140857ms)
|
||||
✔ unsupported credential type and kind refuse (0.147129ms)
|
||||
✔ account provider-path mismatch refuses (0.10235ms)
|
||||
✔ profile account refs must be provider/account shaped (0.625078ms)
|
||||
✔ seat selection accepts fork pin field, validates account refs (0.217339ms)
|
||||
✔ harness manifest id must equal executable (gate 1) (0.19595ms)
|
||||
✔ CLI validate: duplicate provider id across files refuses (28.672577ms)
|
||||
✔ CLI validate: missing referenced provider/account refuse (32.402305ms)
|
||||
✔ CLI validate: broken JSON refuses without secret echo (28.101555ms)
|
||||
✔ CLI usage errors exit 2 (55.542875ms)
|
||||
✔ credential.json sibling presence does not break validation and is never read (65.193222ms)
|
||||
✔ D1 missing, empty and structurally empty roots refuse, no list projection (168.473897ms)
|
||||
✔ D1 required directory auth cannot be absent (53.820205ms)
|
||||
✔ D1 required directory auth/providers cannot be absent (55.518075ms)
|
||||
✔ D1 required directory auth/accounts cannot be absent (62.62363ms)
|
||||
✔ D1 required directory auth/settings cannot be absent (68.901345ms)
|
||||
✔ D1 required directory harnesses cannot be absent (62.781661ms)
|
||||
✔ D1 root file and unreadable metadata refuse (113.227159ms)
|
||||
✔ D2 no symlink traversal at auth/providers/openai-codex.json (56.362661ms)
|
||||
✔ D2 no symlink traversal at auth/accounts/openai-codex/homelab-openai (61.353629ms)
|
||||
✔ D2 no symlink traversal at auth/providers (53.495206ms)
|
||||
✔ D2 no symlink traversal at auth (53.821032ms)
|
||||
✔ D2 root and ancestor symlinks and lexical traversal refuse (162.253987ms)
|
||||
✔ private filesystem modes enforced for root (52.511966ms)
|
||||
✔ private filesystem modes enforced for auth (56.016871ms)
|
||||
✔ private filesystem modes enforced for auth/providers/openai-codex.json (55.381156ms)
|
||||
✔ private filesystem modes enforced for auth/accounts/openai-codex/homelab-openai/account.json (62.616506ms)
|
||||
✔ D3 numeric version 1 only across all record kinds (1.233148ms)
|
||||
✔ D4 nested unknown keys and missing per-kind required fields refuse (60.640569ms)
|
||||
✔ D5 unenrolled default refuses even when account exists (64.638105ms)
|
||||
✔ D6 provider/account credential type must match (62.814507ms)
|
||||
✔ D7 every harness endpoint enforces HTTP opt-in and shape (0.437418ms)
|
||||
✔ D8 URLs reject embedded credentials and unsupported protocols without echo (167.695284ms)
|
||||
✔ D9 malformed JSON diagnostics contain no content excerpt (61.482042ms)
|
||||
✔ D10 missing metadata is missing-path, not invalid-json (62.199152ms)
|
||||
✔ D10 library returns no partial entries on any invalid record (74.715426ms)
|
||||
✔ null/scalar/array metadata refuses without stack or echo (249.850278ms)
|
||||
✔ credential sibling is never opened, even when an unreadable symlink (32.363117ms)
|
||||
✔ oversized metadata refuses before parsing (54.461836ms)
|
||||
ℹ tests 69
|
||||
ℹ suites 0
|
||||
ℹ pass 69
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 2143.653413
|
||||
@@ -0,0 +1,158 @@
|
||||
✔ F1: an ordinary commit after update-ref is refused until step 8; then it commits and the queue stays at C (1123.09366ms)
|
||||
✔ F1: a plain `commit -e` whose guard ran before update-ref fails at its own HEAD update (906.626911ms)
|
||||
ℹ git commit -e: index.lock free during the editor
|
||||
✔ F1: a `commit -e -- path` whose guard ran before update-ref fails at its own HEAD update (878.906969ms)
|
||||
ℹ git commit -e -- src.txt: index.lock held during the editor
|
||||
✔ F1: step 8 with index.lock held exits 3, and ordinary commits stay refused until the printed command runs (961.495951ms)
|
||||
✔ F1: HEAD moving after the step-7 guard check and before update-ref: refused, nothing published (958.097988ms)
|
||||
✔ F1: H is recorded before the canary, so HEAD moving during the step-1 canary is refused at step 7 (898.953729ms)
|
||||
✔ F1: a queue commit landing after H is recorded: step 1 says HEAD moved, not the guard (1186.759984ms)
|
||||
✔ F1: a queue commit landing between the HEAD check and the canary: the failed clean run is reported as HEAD moved (793.011208ms)
|
||||
✔ F1: a shared-index change during the procedure is not committed (912.048115ms)
|
||||
✔ F1: a queue path staged after update-ref: step 8 stops and touches nothing (893.352413ms)
|
||||
✔ F1: a missing or a different hook refuses (623.462627ms)
|
||||
✔ F1: same bytes without the exec bit, a symlinked hook, and core.hooksPath in the local or global scope each refuse before update-ref (1085.782739ms)
|
||||
✔ F1: the canary refuses a hook that git would not run (528.831803ms)
|
||||
✔ F1: the guard deactivated after step 1 is refused at the step-7 recheck (806.638181ms)
|
||||
✔ bootstrap: implementation-only HEAD, the guard, genesis, the --genesis commit, then an extending commit (773.656262ms)
|
||||
✔ bootstrap: --genesis with a base present, no base without --genesis, an op before the first commit, a changed map, another branch (813.111045ms)
|
||||
✔ bootstrap: the archived tests and validator run outside any repository (773.607392ms)
|
||||
✔ general: an unrelated staged file stays staged, and the committed blobs are the snapshot bytes (752.831693ms)
|
||||
✔ general: a queue write after the snapshot is not committed (1090.50174ms)
|
||||
✔ general: a snapshot whose log does not extend the base refuses (812.53443ms)
|
||||
✔ general: install-hook privilege, repair of a same-bytes hook, and its refusals (162.956804ms)
|
||||
✔ general: environment overrides, a linked worktree and usage (504.473454ms)
|
||||
✔ general: a queue path staged before the run refuses at step 1 (554.652009ms)
|
||||
✔ general: HEAD's queue tests failing in the archive refuse (745.828019ms)
|
||||
✔ genesis document serializes deterministically and replays (4.087765ms)
|
||||
✔ a hand edit that stays valid JSON fails replay; a formatting-only edit fails re-serialization (2.733583ms)
|
||||
✔ a tampered result, receipt or viewSha fails replay (2.516496ms)
|
||||
✔ op ids: 8 to 72 characters for callers, 80 in the log for .outcome entries (0.199785ms)
|
||||
✔ add: defaults for an ordinary seat, privileged extras, refusals (3.594784ms)
|
||||
✔ matrix: queued→briefed privileged; briefed→in-progress owner with after satisfied (4.995694ms)
|
||||
✔ matrix: release, review round, changes requested and waiting-on-jason (11.698919ms)
|
||||
✔ matrix J5: in-review→done by the gate owner with evidence naming the current round (7.40716ms)
|
||||
✔ review issue, lead decision 23: none refuses, one is used, several need --issue, later rounds keep it (18.043572ms)
|
||||
✔ the row schema refuses a round with a null issue, and the A1 review shape (P2) (2.375591ms)
|
||||
✔ matrix R1: every state × target × actor class matches 8.7, gate owner jason or not, required or not (1327.424727ms)
|
||||
✔ matrix: blocked keeps the claim and returns only to previousState (4.179038ms)
|
||||
✔ matrix J4: parking is Jason's, refused while required; unpark returns to queued (2.796213ms)
|
||||
✔ field edits: who may change what (5.608282ms)
|
||||
✔ set issues keeps a logged narrowing of closes (N10) (2.806362ms)
|
||||
✔ text the table shows refuses \ and <, everywhere it enters (N8) (1.925822ms)
|
||||
✔ every accepted text renders to nine cells on every row (N8) (22.358458ms)
|
||||
✔ genesis: the map refuses an owner among its row's reviewers; replay doesn't (2026-10-04) (0.497087ms)
|
||||
✔ times and dates must be calendar values, not just the shape (2026-10-04) (1.624743ms)
|
||||
✔ replay holds every op id to the caller's rule (N11) (4.934225ms)
|
||||
✔ note: owner, listed reviewer or privileged; empty clears (1.171347ms)
|
||||
✔ assign moves the claim with the owner; done clears it (2.628432ms)
|
||||
✔ render is byte-stable and escapes pipes (0.766776ms)
|
||||
✔ view classification: current, genuine stale, edited stale marker, changed current body, markers (0.621348ms)
|
||||
✔ next: resume, then review, then start, then wait, then nothing; lowest id first (18.560244ms)
|
||||
✔ canonical args make a retry's identity independent of list order (0.303241ms)
|
||||
✔ manifests, headings and blob ids (0.414845ms)
|
||||
✔ the migration map: one queue-map block, exact keys (0.702117ms)
|
||||
✔ every call but `queue` reaches the seat CLI exactly as before A2 (604.612956ms)
|
||||
✔ `queue` reaches the queue CLI with the rest of the arguments (194.810538ms)
|
||||
✔ the pre-A2 fixture is the script A2 changed (0.301444ms)
|
||||
✔ acquire publishes the record by link; release removes only its own lock (4.229467ms)
|
||||
✔ a kill between the temp write and the link leaves no lock (36.935103ms)
|
||||
✔ a short or failed temp write refuses and leaves no lock and no temp (1.871304ms)
|
||||
✔ a link error other than EEXIST refuses (1.181228ms)
|
||||
✔ an error after the link releases the lock: unreadable gate, failing temp stat (2.907727ms)
|
||||
✔ a release that fails on a gate path is reported, never a stack trace (P1) (4.290814ms)
|
||||
✔ a paused holder: another writer waits 10 s, then refuses naming it live (10059.982969ms)
|
||||
✔ two concurrent unlockers: the second refuses on the gate (22.476194ms)
|
||||
✔ a writer publishing during an unlock, lock first: unlock sees it live and refuses (1.927059ms)
|
||||
✔ a writer publishing during an unlock, gate first: the writer releases and refuses (1.722825ms)
|
||||
✔ a gate swapped while held is left in place and reported, on success and on refusal (N1) (1.977909ms)
|
||||
✔ a reused pid within one boot is mismatch; unlock removes the lock and never signals the process (3.383035ms)
|
||||
✔ the same pid and start on a different boot is mismatch (0.741045ms)
|
||||
✔ a foreign host is unknown whatever the local pid says; unlock refuses (46.957194ms)
|
||||
✔ unreadable /proc: classification is unknown and acquire refuses (0.625757ms)
|
||||
✔ invalid records: empty, unparsable, wrong keys, bad start or boot (0.23317ms)
|
||||
✔ a stale gate blocks writers; --check-gate says mismatch for a reused pid (3.57997ms)
|
||||
✔ a delayed release by a dead owner, after unlock and a new owner: the inode check keeps the new lock (2.147497ms)
|
||||
✔ release checks the inode too: a byte-identical lock file with a new inode is left in place (1.012782ms)
|
||||
✔ unlock refuses a live, unknown or invalid lock, and does nothing without one (3.955126ms)
|
||||
✔ the migration map validates and renders the golden genesis table (3.302219ms)
|
||||
✔ the marked QUEUE.md holds every row and parked item between its markers (1.156618ms)
|
||||
✔ map-check reports each kind of drift (4.654279ms)
|
||||
✔ a request posts once as the requester; a retry sends nothing (589.349761ms)
|
||||
✔ each transport answer maps to posted, failed or uncertain (8.9 step 3) (2965.786909ms)
|
||||
✔ the pre-send checks: GET user must name the requester, under the deadline (942.495573ms)
|
||||
✔ the lead's request refuses a token for login sage (502.310836ms)
|
||||
✔ the credential file: the seat's own, 0600, no symlink, never the shared default (429.46681ms)
|
||||
✔ an unresolved request blocks a new request, a new round, waiting-on-jason and done (1329.366392ms)
|
||||
✔ a same-op retry after a kill sends nothing, even with a stale view (2033.657318ms)
|
||||
✔ a held lock at the outcome exits 3 and names what the transport said (534.566132ms)
|
||||
✔ late outcomes: after an abandon, and after a resolve with the same or another id (1367.135232ms)
|
||||
✔ resolve checks the comment: issue, markers, round, candidate and author (1390.301985ms)
|
||||
✔ the lead resolves a seat's request: the comment's author is the requester, fetched with the lead's token (462.37746ms)
|
||||
✔ validateRow checks a request round's shape, which every replayed entry must keep (310.297933ms)
|
||||
✔ request, changes, a new candidate, approval: every round pinned; no review files (1268.058829ms)
|
||||
✔ a row with no reviewers opens a round that posts nothing (748.981551ms)
|
||||
✔ verify-commit: a prospective tree must hold exactly the candidate's paths (1054.353712ms)
|
||||
✔ semantics: v1 entries replay as before; review entries need v2 (309.071342ms)
|
||||
✔ set reviewers refuses the row's owner (2026-09-28) (240.118293ms)
|
||||
✔ the owner records no verdict, even as a listed reviewer (311.114905ms)
|
||||
✔ a request comment over the length limit is not sent (351.569715ms)
|
||||
✔ a late POST on a closed row leaves a conflict nothing can resolve, and resolve asks nothing (356.668232ms)
|
||||
✔ a Jason-gated row reaches waiting-on-jason only on every reviewer's approval (1642.075608ms)
|
||||
✔ genesis: refusals before anything is written (440.209638ms)
|
||||
✔ genesis: the map must be committed, well formed, with committed briefs and seats (552.297366ms)
|
||||
✔ genesis: markers, a stray witness, once only; a retry returns the receipt (463.823172ms)
|
||||
✔ genesis: a file holding genesis alone with no witness is confirmed by sync or a retry (279.997243ms)
|
||||
✔ canonical checks: worktree, second clone, detached HEAD, wrong branch, GIT_DIR, foreign code; a symlink works (570.928229ms)
|
||||
✔ --by that differs from MOSAIC_AGENT_NAME warns on stderr and logs nothing more (N12) (480.71357ms)
|
||||
✔ op ids: missing, too long, reserved; a retry answers; another payload refuses (567.401069ms)
|
||||
✔ a retried add returns the id it first allocated, after reassignment and after done (589.036439ms)
|
||||
✔ Rocko's S4 schedule: a lost result, another writer, then the retry opens no second round (419.946608ms)
|
||||
✔ the review issue and the evidence round through the CLI (lead decision 23, 8.7) (783.814932ms)
|
||||
✔ claims and add defaults through the CLI; candidates are manifests or reachable commits (575.769966ms)
|
||||
✔ add, set reviewers and assign refuse the row's owner as a reviewer (330.238827ms)
|
||||
✔ the working-brief check: a changed working copy refuses the start and flags next (512.003779ms)
|
||||
✔ next: resume first, then nothing for an idle seat; needs a seat (264.440746ms)
|
||||
✔ view stale: new ops and verify refuse naming the unshown op; retries answer; reads warn; render fixes (508.26713ms)
|
||||
✔ view unknown: a hand edit, an old marker over an edited body, missing or duplicate markers (928.114596ms)
|
||||
✔ a hand edit to queue.json refuses every verb, reads included (482.195771ms)
|
||||
✔ verify and render --check leave bytes and mtimes unchanged (358.437948ms)
|
||||
✔ render is byte-stable across runs and repositories (209.006408ms)
|
||||
✔ snapshot and verify --snapshot (616.971903ms)
|
||||
✔ usage errors exit 4 (517.000537ms)
|
||||
✔ a short write, ENOSPC or a file fsync failure: nothing visible, temp removed (161.525767ms)
|
||||
✔ a rename failure: nothing visible, temp removed (130.615185ms)
|
||||
✔ a directory fsync failure: uncertain, exit 3, no receipt; the tail refuses new ops; a retry confirms (153.433215ms)
|
||||
✔ a directory fsync failure, then sync names the op (271.248573ms)
|
||||
✔ a witness write failure: uncertain, durable, exit 3; the view is untouched (138.332545ms)
|
||||
✔ the .git fsync after the witness rename fails: uncertain, exit 3, the witness says so (122.960464ms)
|
||||
✔ confirming a tail fsyncs queue.json and docs/plans before the witness; either failure changes nothing (131.07291ms)
|
||||
✔ the docs/plans fsync after the view rename fails: the op stands, the view is written, a warning says so (109.887142ms)
|
||||
✔ a lock swapped while held is left in place and reported, on a receipt and on a refusal (120.207877ms)
|
||||
✔ a lock that cannot be released after an op is reported, on a receipt and on a refusal (130.250122ms)
|
||||
✔ unlock prints a swapped gate's warning on stderr, the result on stdout (104.778697ms)
|
||||
✔ a view write that fails keeps the op and reports a stale view (105.263596ms)
|
||||
✔ SIGKILL before the rename: nothing recorded; the retry removes the leftover temp (544.001901ms)
|
||||
✔ SIGKILL after the rename, before the witness: the tail refuses new ops and sync names the op (610.622764ms)
|
||||
✔ SIGKILL after the witness, before the view: the stale refusal names the op (569.060997ms)
|
||||
✔ SIGKILL after the view, before the receipt: the retry returns the receipt (574.825452ms)
|
||||
✔ git checkout between steps 1 and 7: step 7 refuses and nothing is written (132.228675ms)
|
||||
✔ git stash restoring an older valid pair: history lost; accept-history needs privilege, a reason and --yes (782.093087ms)
|
||||
✔ a deleted witness: refused after the locked recheck; accept-history records it absent (169.363828ms)
|
||||
✔ a header edit during a write: the op stands, the view write is skipped with a warning (108.565309ms)
|
||||
✔ a reader paused between the witness and the file while a writer finishes: no lost-history report (117.868726ms)
|
||||
✔ file-then-witness order forced by a hook: the locked recheck prevents a false report (120.002769ms)
|
||||
✔ a writer paused before and after the witness rename: readers see a tail, then a match (117.337284ms)
|
||||
✔ a true rollback is reported only after the locked recheck; a held lock names its holder instead (472.231055ms)
|
||||
✔ an accept-history in progress: an unlocked reader waits on the lock and never reports lost history (433.987458ms)
|
||||
✔ the platform check refuses other filesystems (97.893225ms)
|
||||
✔ tmpfs passes only a test layer that allows it (N5) (126.487748ms)
|
||||
✔ unlock keeps a multi-line lock record on stdout (P3) (149.811729ms)
|
||||
ℹ tests 148
|
||||
ℹ suites 0
|
||||
ℹ pass 148
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 19601.279263
|
||||
@@ -0,0 +1,27 @@
|
||||
✔ resolveSeat: by name under --repo resolves the repo layout (1.201331ms)
|
||||
✔ resolveSeat: by path resolves the fleet layout (0.336482ms)
|
||||
✔ resolveSeat: refusals for missing dir, missing launch.sh, non-executable launch.sh, invalid name, and unknown layout (0.70358ms)
|
||||
✔ tmuxContext: outside tmux, default socket, custom socket, and exec failure (0.63805ms)
|
||||
✔ makeRegistration produces a record that validates; each shape violation throws SeatError (0.787247ms)
|
||||
✔ writeRegistration/readRegistration: round trip, permissions, absence, and malformed records (1.280028ms)
|
||||
✔ updateTask: changes task, taskSetBy and updatedAt only, and refuses appropriately (1.097861ms)
|
||||
✔ CLI launch: registers, execs the fake launch script, and passes args through (29.046665ms)
|
||||
✔ CLI launch: --harness lands in the record (28.749652ms)
|
||||
✔ CLI launch: the launch script's own exit code passes through (27.528268ms)
|
||||
✔ CLI launch: relaunching a seat rewrites the one registration record (58.113927ms)
|
||||
✔ CLI launch: omitting --task records an empty string, not null (30.100879ms)
|
||||
✔ CLI seat task: updates only the task after a launch, and refuses on an unlaunched seat (78.310326ms)
|
||||
✔ CLI refusals: no args, unknown flag, missing config, already-registered env, and exec failure (135.586594ms)
|
||||
✔ samePath: equal paths, symlinked dirs, distinct dirs, and non-strings (0.371918ms)
|
||||
✔ resolveSetBy: explicit --by wins over the environment; absent or empty environment gives unknown; invalid explicit or environment values refuse with exit 4 (0.564905ms)
|
||||
✔ validateRegistration/readRegistration: taskSetBy is optional; a record without it (written before #1511) still loads unchanged; an invalid one is refused; the version does not change (0.694039ms)
|
||||
✔ updateTask: records setBy, preserves startedAt and every unrelated field, upgrades an old record in place only when the task is set, and replaces a previous attribution (0.860769ms)
|
||||
✔ CLI seat task: --by beats MOSAIC_AGENT_NAME, the environment beats nothing, empty environment is unknown, invalid --by or environment refuses with exit 4 and leaves the record byte for byte (264.773945ms)
|
||||
ℹ tests 19
|
||||
ℹ suites 0
|
||||
ℹ pass 19
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 711.455044
|
||||
@@ -0,0 +1,59 @@
|
||||
✔ the boot config is checked before anything starts (13.525058ms)
|
||||
✔ a business with no tracker entry refuses task verbs (11.657263ms)
|
||||
✔ credential.expiring and .expired are recorded once per instance (28.491107ms)
|
||||
✔ a token file that changes on disk records credential.changed (12.173554ms)
|
||||
✔ autostart polls, reconciles and retries a startup the tracker was down for (35.116846ms)
|
||||
✔ a refusal a restart must clear is not retried by the poll (7.934045ms)
|
||||
✔ a poll that fires while two are queued is dropped (12.221434ms)
|
||||
✔ close waits for a running verb and refuses one that has not started (40.109683ms)
|
||||
✔ the bundled Vikunja is the pinned upstream image the runbook names (0.854356ms)
|
||||
✔ every published port is on 127.0.0.1, and no secret is in the file (0.320283ms)
|
||||
✔ the fake answers each route with the statuses and shapes Vikunja v2.7.0 sent (89.076359ms)
|
||||
✔ the recorded task bodies pass the checks S3 applies to every read (0.496214ms)
|
||||
✔ the client works against the fake over real HTTP with the platform fetch (93.784381ms)
|
||||
✔ a correct install starts, and the first reconcile records tasks that already exist (28.196029ms)
|
||||
✔ verbs refuse while a business is starting and after startup refused it (15.096596ms)
|
||||
✔ startup refuses a token that can do more than its role needs (35.919401ms)
|
||||
✔ startup refuses an unsupported version and flags an untested one (25.521099ms)
|
||||
✔ startup refuses a board that the runbook did not install (33.062122ms)
|
||||
✔ startup refuses a project the sync bot cannot read (6.775512ms)
|
||||
✔ startup refuses a configured label the pm bot cannot see (7.808352ms)
|
||||
✔ startup refuses an expired credential and a missing sync credential (15.64388ms)
|
||||
✔ an unreachable tracker refuses with tracker-unavailable (6.503175ms)
|
||||
✔ an edit in the UI is recorded once, with the fields that changed (79.122007ms)
|
||||
✔ a move between open buckets is seen on the board, though updated does not change (37.816485ms)
|
||||
✔ a person's comment is counted and a bot's is not (65.42748ms)
|
||||
✔ the hourly reconcile catches a comment through comment_count (75.271868ms)
|
||||
✔ a task closed in the UI leaves the open view with its done bucket (108.166875ms)
|
||||
✔ a task that leaves the board is recorded as deleted, moved or out of reach (60.936271ms)
|
||||
✔ a poll that read before a verb wrote does not overwrite the verb (32.571499ms)
|
||||
✔ a tracker fault during a tick is reported and the next tick catches up (34.564496ms)
|
||||
✔ a malformed answer refuses the tick with tracker-shape (11.06167ms)
|
||||
✔ no token value reaches the database, the log or a refusal (42.85857ms)
|
||||
✔ the first look at a task counts only comments inside the window (35.713216ms)
|
||||
✔ task.create needs a recorded human request and a requirement id (37.987302ms)
|
||||
✔ only labels named in the business file can be written (25.590207ms)
|
||||
✔ task.schedule sets and clears a due date and relations (30.728864ms)
|
||||
✔ assign and reassign move the role bots and record task.assigned (51.194862ms)
|
||||
✔ task.update.assigned is for the assignee and records task.state (49.737162ms)
|
||||
✔ a wrong expected digest records task.conflict and writes nothing (16.178182ms)
|
||||
✔ a cross-role verb needs a resolved decision, used once (19.604384ms)
|
||||
✔ task.close needs a verdict; after it every verb refuses with task-done (19.639506ms)
|
||||
✔ a lost answer is settled by a re-read and never retried (22.484145ms)
|
||||
✔ a create whose answer is lost is reported uncertain, and the poll finds the task (34.524476ms)
|
||||
✔ a task the sync bot cannot read refuses and records nothing (13.739975ms)
|
||||
✔ verbs and polls for one business run one at a time (83.390667ms)
|
||||
✔ a due date with milliseconds is written to the second (60.693013ms)
|
||||
✔ every write landed and the final read failed: the verb succeeds and records what it wrote (36.698399ms)
|
||||
✔ some writes landed and the final read failed: write-uncertain, and nothing is recorded (13.750686ms)
|
||||
✔ a create whose final read fails succeeds and records task.created (33.557305ms)
|
||||
✔ an edit between the last write and the final read shows as external on the next poll (35.962535ms)
|
||||
✔ task.created is recorded when a later label write fails (11.383503ms)
|
||||
ℹ tests 51
|
||||
ℹ suites 0
|
||||
ℹ pass 51
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 683.275814
|
||||
@@ -0,0 +1,23 @@
|
||||
✔ browser edge states: loading, empty, malformed, stale, hostile/long values, in-flight reply and appearance fallback (2176.618252ms)
|
||||
Rendered contrast: {"failures":[],"count":330,"lowest":4.504658476260286}
|
||||
✔ served Console browser: real board fixtures, keyboard, drafts, receipts, themes, 320px and failures (2178.390524ms)
|
||||
✔ conversation view: full history, collapsed tools, hidden thinking, inert hostile content, malformed and reconcile markers (1980.754608ms)
|
||||
✔ conversation view: a fork keeps the open branch, says so, and opens the new one on request (1284.842187ms)
|
||||
✔ conversation view: a newer session with no readable history keeps the marker (854.620038ms)
|
||||
✔ conversation view: seats without history say so and offer no reply (559.16708ms)
|
||||
✔ Discord row through real board/WebUI: independent brake/liveness, no Reply, literal content (1658.06713ms)
|
||||
✔ return flow through the conversation view: send, tool call, delayed result, peer message, exact long answers, relaunch (52597.312061ms)
|
||||
✔ both presentations replace old activity with relaunch notice, label retained history, then resume after new activity (1714.825956ms)
|
||||
✔ reported return flow and relative Age: reply sent from the inspector, then the new answer appears there without manual refresh (21573.357273ms)
|
||||
✔ loopback host and board origin fail closed (6.341086ms)
|
||||
✔ real board fixture passes through WebUI; assets and isolated seen/reply work (76.074161ms)
|
||||
✔ proxy preserves exact request bytes, status and receipt, rejects forms and malformed JSON, never follows redirect (47.426114ms)
|
||||
✔ unreachable board reports URL; CLI rejects unsupported options (411.36665ms)
|
||||
ℹ tests 14
|
||||
ℹ suites 0
|
||||
ℹ pass 14
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 52851.310746
|
||||
@@ -0,0 +1,8 @@
|
||||
packages/cli/README.md: OK
|
||||
packages/cli/src/host.mjs: OK
|
||||
packages/cli/src/notifier.mjs: OK
|
||||
packages/cli/tests/host.test.mjs: OK
|
||||
packages/cli/tests/notifier.test.mjs: OK
|
||||
packages/cli/tests/trackers-boot.test.mjs: OK
|
||||
packages/discord/tests/journal.test.mjs: OK
|
||||
scripts/bus-service.sh: OK
|
||||
@@ -0,0 +1,273 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (29.760605ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (40.387701ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (19.630873ms)
|
||||
✔ decide prints a declining choice as declining (18.559833ms)
|
||||
✔ an unknown outcome is reported once and never resent (19.251059ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (17.774078ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (18.392506ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (19.895737ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (21.584274ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (17.797736ms)
|
||||
✔ agents and tasks print through the broker (16.859557ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.04971ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (74.7938ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (58.571683ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (60.48769ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (57.400902ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (60.437397ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (85.017421ms)
|
||||
✔ empty views say so (1.072336ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.521006ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.24846ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (977.003584ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (140.658962ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (79.178326ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (133.82799ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (125.348276ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (98.619915ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (129.898413ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (75.795733ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (137.043961ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (27.113002ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.695286ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (208.18818ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (88.154146ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (602.606373ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (26.695514ms)
|
||||
✔ zoned uses the IANA zone across DST (28.539915ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (28.090678ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (27.568839ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.336713ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (20.089529ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (25.438663ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (26.87194ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (21.403229ms)
|
||||
✔ 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 (143.282607ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (65.831031ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (16.145753ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (13.046842ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.793326ms)
|
||||
✔ no Discord id reaches the journal or the log (12.404002ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.007638ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (2.188473ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.627255ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.496929ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.561601ms)
|
||||
✖ the journal: a line with a wrong type refuses with exit 3 and names the field (1.770868ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.339127ms)
|
||||
✔ 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.511845ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.423929ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.491457ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.29813ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (112.067241ms)
|
||||
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 (369.773535ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (58.915322ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2285.30706ms)
|
||||
✔ busExit and refuseInsideAgent (0.408639ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.188646ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.756135ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.597795ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.624234ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (19.999595ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (7.349429ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (7.400713ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.732256ms)
|
||||
✔ authorize: open channel, listed user (2.101826ms)
|
||||
✔ authorize: wrong guild (0.209367ms)
|
||||
✔ authorize: no guild (DM) (0.162187ms)
|
||||
✔ authorize: unlisted channel (0.179388ms)
|
||||
✔ authorize: unknown channel, no info (0.382868ms)
|
||||
✔ authorize: thread of listed parent (0.197633ms)
|
||||
✔ authorize: thread of unlisted parent (0.160359ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.155157ms)
|
||||
✔ authorize: unlisted user (0.991643ms)
|
||||
✔ authorize: no author (0.295573ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.140998ms)
|
||||
✔ authorize: system author (0.55616ms)
|
||||
✔ authorize: the bot itself (0.099859ms)
|
||||
✔ authorize: webhook (0.113831ms)
|
||||
✔ authorize: mention channel without mention (0.136751ms)
|
||||
✔ authorize: mention channel with bot mention (0.145852ms)
|
||||
✔ authorize: mention channel with @everyone only (0.104218ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.10662ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.150781ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.094834ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.077781ms)
|
||||
✔ authorize: thread in another guild per channel info (0.081945ms)
|
||||
✔ authorize: not an object (0.059755ms)
|
||||
✔ authorize: no id (0.06135ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.066517ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.066867ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.472156ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.154326ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.522631ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.552805ms)
|
||||
✔ binding: empty allowlists refuse (0.474243ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.795377ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.668973ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.432969ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.577308ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.402369ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (122.273784ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (1.82173ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (397.984547ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (198.528071ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (139.989087ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.79887ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.297895ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (19.070442ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (17.563682ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.483077ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.077325ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.172985ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.471663ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (31.650079ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.809468ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.128839ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.115303ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.156008ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (31.454802ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (6.007986ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (4.213715ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (4.508761ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (5.672546ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (1.949083ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.817555ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (2.550261ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (5.493833ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (7.417453ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.881282ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (4.910144ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.472195ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.849994ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.776002ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.159095ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.417185ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.240973ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.659696ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (3.087237ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.685236ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (71.638604ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (45.725937ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (56.573313ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (369.23746ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (234.018661ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (104.249644ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (226.457903ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (127.47982ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (213.341342ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.421315ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.317539ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.505815ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (426.748202ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (24.530213ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (52.15548ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.592007ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.584807ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.499142ms)
|
||||
✔ gateway: op 9 resumable resumes (0.368765ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.76534ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.741377ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.408938ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (73.103743ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (66.480583ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (130.774678ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.470491ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (97.943837ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (88.474131ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (95.746066ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (200.028001ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (71.359196ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (82.998207ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (194.931382ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (759.877143ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.051919ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (95.710236ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.103282ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (3.743815ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (70.748104ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (339.267476ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.478894ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (23.98535ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (0.986754ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (45.428384ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (140.01096ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (86.917507ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.479905ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (4.712815ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.022034ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.770634ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (56.608994ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (57.629133ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (42.713779ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (84.057658ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (665.388638ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.224846ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (2.491371ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.855465ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.855279ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.965654ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.49081ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (2.944926ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.625227ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.165291ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (26.520967ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (12.387858ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (8.198956ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (5.556572ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.614136ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.8708ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1012.976724ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.479047ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (6.557741ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.258712ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.206733ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.564429ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.434266ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (5.960139ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.559793ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (4.934678ms)
|
||||
✔ tools: listing and search caps hold (11.939271ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.984883ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (6.626118ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.136801ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.307826ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.524896ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.015756ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.701988ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.340098ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (1.825342ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1022.850229ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.215255ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.220765ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.577992ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.207962ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3201.814177
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:332:1
|
||||
✖ the journal: a line with a wrong type refuses with exit 3 and names the field (1.770868ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception: day: {"at":"2026-10-08T12:00:00.000Z","kind":"digest","decision":null,"day":"2026-13-45","outcome":"confirmed","messageId":"2"}
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:361:12)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: undefined,
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (34.933054ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (40.586516ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (17.071468ms)
|
||||
✔ decide prints a declining choice as declining (20.782453ms)
|
||||
✔ an unknown outcome is reported once and never resent (15.525174ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (15.507269ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (16.66138ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (15.815819ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (13.792254ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (22.532964ms)
|
||||
✔ agents and tasks print through the broker (16.142383ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.867672ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (93.712278ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (55.603432ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (54.026832ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (59.84363ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (51.074233ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (78.386348ms)
|
||||
✔ empty views say so (1.271252ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.3584ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.215963ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (955.861046ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (147.043559ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (72.581752ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (127.282474ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (120.073724ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (74.485241ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (128.852161ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (74.254731ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (154.961828ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (23.825489ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (203.095679ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (199.499294ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (84.245811ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (601.832061ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (26.056634ms)
|
||||
✔ zoned uses the IANA zone across DST (25.127665ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (36.414145ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (18.357286ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.315889ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (16.223452ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (23.260855ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (26.808591ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (21.001178ms)
|
||||
✔ 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 (149.53931ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (52.44175ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (13.60806ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (11.889479ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.84053ms)
|
||||
✔ no Discord id reaches the journal or the log (11.681828ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.054497ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (2.500186ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.68153ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.521073ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.65964ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.43279ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.41309ms)
|
||||
✔ 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.554531ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.440515ms)
|
||||
✖ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.878258ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.290256ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (109.820304ms)
|
||||
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 (357.256831ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (62.429113ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2263.493502ms)
|
||||
✔ busExit and refuseInsideAgent (0.404764ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.21458ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.657051ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.627824ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.478332ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (21.332276ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (5.963611ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (5.08662ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.496166ms)
|
||||
✔ authorize: open channel, listed user (2.030134ms)
|
||||
✔ authorize: wrong guild (0.193645ms)
|
||||
✔ authorize: no guild (DM) (0.175263ms)
|
||||
✔ authorize: unlisted channel (0.190754ms)
|
||||
✔ authorize: unknown channel, no info (0.180484ms)
|
||||
✔ authorize: thread of listed parent (0.415865ms)
|
||||
✔ authorize: thread of unlisted parent (0.258485ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.147594ms)
|
||||
✔ authorize: unlisted user (0.394631ms)
|
||||
✔ authorize: no author (0.296242ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.1591ms)
|
||||
✔ authorize: system author (0.143776ms)
|
||||
✔ authorize: the bot itself (0.091701ms)
|
||||
✔ authorize: webhook (0.113823ms)
|
||||
✔ authorize: mention channel without mention (0.166028ms)
|
||||
✔ authorize: mention channel with bot mention (2.248183ms)
|
||||
✔ authorize: mention channel with @everyone only (0.258813ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.1227ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.096363ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.087809ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.070144ms)
|
||||
✔ authorize: thread in another guild per channel info (0.081757ms)
|
||||
✔ authorize: not an object (0.066607ms)
|
||||
✔ authorize: no id (0.057509ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.064267ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.057648ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.485833ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.137947ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.609785ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.333335ms)
|
||||
✔ binding: empty allowlists refuse (0.387055ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.5243ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.943568ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (1.214392ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (2.328198ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.399884ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (115.048247ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (2.047758ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (384.18446ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (186.331009ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (130.382041ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.777309ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.201288ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (27.547373ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (18.250452ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.607876ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.117282ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.197262ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.377091ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (33.636836ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (2.461827ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.585965ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.42583ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (41.695013ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (31.728311ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (6.742455ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (5.046873ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (1.741964ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (5.967723ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.054239ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.757555ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (3.440423ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (3.246304ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (5.360643ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (3.12885ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (4.921058ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.720076ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (1.092018ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.005291ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.209847ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.579851ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (2.193647ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.732547ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.872288ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.46565ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (51.958902ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (50.280945ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (43.633685ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (363.478196ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (239.226412ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (104.390613ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (229.487928ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (137.513919ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (214.478157ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (615.2532ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.322802ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.5683ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (428.877002ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (25.269469ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (46.061713ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.405247ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.554267ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.51209ms)
|
||||
✔ gateway: op 9 resumable resumes (0.33714ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.785509ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.502693ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.385894ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (74.382799ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (58.763881ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (121.730986ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.350252ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (95.164082ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (82.677506ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (83.789469ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (200.720392ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (73.770788ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (86.440968ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (201.00243ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (765.686154ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.665994ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (82.834681ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.045811ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.737672ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (65.452895ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (305.142207ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.395347ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (25.560325ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.028335ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (47.715684ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (147.940445ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (93.267207ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.485469ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (4.702771ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.033328ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.585238ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (49.94715ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (51.181476ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (38.667192ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (74.673209ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (633.782043ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.715976ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (5.224635ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.593037ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.652803ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.88761ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.471402ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (2.888135ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.469303ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (1.845624ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (28.381107ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (12.91462ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (8.297829ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (3.778139ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.473772ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (2.461485ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1013.148874ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.430035ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (6.570636ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.258537ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.209404ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.661587ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.225792ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (3.677205ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (4.675501ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (3.702477ms)
|
||||
✔ tools: listing and search caps hold (10.94082ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.855241ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (5.139971ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.249232ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.112909ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.751631ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.873977ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.880282ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.292493ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (1.842155ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1028.430881ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.252925ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.248327ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.722402ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.616528ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3127.553943
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:410:1
|
||||
✖ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.878258ms)
|
||||
AssertionError [ERR_ASSERTION]: The validation function is expected to return "true". Received false
|
||||
|
||||
Caught error:
|
||||
|
||||
CliError: notify journal is not writable (EACCES): /mnt/storage/scratch/tmp/mosaic-cli-5J6JN7/notify/demo/sent.jsonl
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:417:12)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: CliError: notify journal is not writable (EACCES): /mnt/storage/scratch/tmp/mosaic-cli-5J6JN7/notify/demo/sent.jsonl
|
||||
at openJournal (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/src/notifier.mjs:185:44)
|
||||
at file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:417:25
|
||||
at getActual (node:assert:580:5)
|
||||
at strict.throws (node:assert:728:24)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:417:12)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12),
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (30.80592ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (37.941349ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (24.665665ms)
|
||||
✔ decide prints a declining choice as declining (16.704628ms)
|
||||
✔ an unknown outcome is reported once and never resent (18.362108ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (22.967575ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (17.921798ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (19.349994ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (23.929134ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (19.440884ms)
|
||||
✔ agents and tasks print through the broker (18.568282ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.487931ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (69.7928ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (71.846687ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (57.044383ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (58.644273ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (60.90405ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (84.738424ms)
|
||||
✔ empty views say so (0.990899ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.31507ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.201898ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (980.69948ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (141.208608ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (74.75781ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (128.932544ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (119.885869ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (84.033505ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (125.225389ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (79.318402ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (127.759584ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (20.647585ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.650482ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (211.881612ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (86.249024ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (596.571005ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (25.992768ms)
|
||||
✔ zoned uses the IANA zone across DST (26.355309ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (28.970798ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (20.501669ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.333487ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (20.656415ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (22.497303ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (29.097279ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (22.478863ms)
|
||||
✔ 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 (152.705588ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (63.602167ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (16.846273ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (10.902115ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.696838ms)
|
||||
✔ no Discord id reaches the journal or the log (10.193279ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.286875ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (2.83902ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.342191ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.548612ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.599609ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.983624ms)
|
||||
✖ the journal: a symlinked directory refuses and says it is a link (0.811472ms)
|
||||
✔ 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.509637ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.39318ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.466583ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.845689ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (110.857557ms)
|
||||
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 (376.993838ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (60.080609ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2271.743692ms)
|
||||
✔ busExit and refuseInsideAgent (3.612956ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.25813ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.294402ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.549751ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.47292ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (14.969516ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (6.513918ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (7.227274ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.701524ms)
|
||||
✔ authorize: open channel, listed user (1.959092ms)
|
||||
✔ authorize: wrong guild (0.216453ms)
|
||||
✔ authorize: no guild (DM) (0.170078ms)
|
||||
✔ authorize: unlisted channel (0.195683ms)
|
||||
✔ authorize: unknown channel, no info (0.23938ms)
|
||||
✔ authorize: thread of listed parent (0.185313ms)
|
||||
✔ authorize: thread of unlisted parent (0.159428ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.150142ms)
|
||||
✔ authorize: unlisted user (0.962821ms)
|
||||
✔ authorize: no author (0.270791ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.148066ms)
|
||||
✔ authorize: system author (0.140795ms)
|
||||
✔ authorize: the bot itself (0.105239ms)
|
||||
✔ authorize: webhook (0.093834ms)
|
||||
✔ authorize: mention channel without mention (0.155979ms)
|
||||
✔ authorize: mention channel with bot mention (0.156979ms)
|
||||
✔ authorize: mention channel with @everyone only (0.209198ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.572704ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.093317ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.087315ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.063573ms)
|
||||
✔ authorize: thread in another guild per channel info (0.062634ms)
|
||||
✔ authorize: not an object (0.059682ms)
|
||||
✔ authorize: no id (0.059357ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.789309ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.086833ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.472122ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.187836ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.594047ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.43093ms)
|
||||
✔ binding: empty allowlists refuse (0.640449ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.346187ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.700946ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (1.223966ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.536256ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.450629ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (110.20281ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (1.845798ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (437.195233ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (198.834255ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (140.07413ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (2.065311ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.126689ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (17.238463ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (18.816258ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.53663ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.12218ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.512308ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.88486ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (32.236004ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (4.001692ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (2.373969ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.243393ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.635205ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (35.994945ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (9.116644ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (5.278894ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.234631ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (5.540188ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.880503ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.732181ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (4.767746ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (6.139385ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (5.41613ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (3.126854ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (4.543543ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.348612ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.798247ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.750549ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.15992ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.468416ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.285245ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (1.184191ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.58938ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.469604ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (57.493171ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (51.29494ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (51.675881ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (363.088551ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (228.510724ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (104.701379ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (232.035177ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (135.507868ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (212.449236ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.742805ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.235627ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.502668ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (427.168525ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (24.983317ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (45.06491ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.570315ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.813386ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.550125ms)
|
||||
✔ gateway: op 9 resumable resumes (0.390354ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.833395ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.468572ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.411776ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (75.207396ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (65.182611ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (146.161346ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.419711ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (93.371319ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (114.644759ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (92.718272ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (215.787199ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (67.738301ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (81.54714ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (194.467721ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (735.065155ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.732554ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (100.769657ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.129857ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (7.58865ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (63.822334ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (374.981472ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.597039ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (32.402789ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.201646ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (42.397274ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (137.466081ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (86.666807ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.40965ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.665911ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.356813ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.838863ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (49.890843ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (60.66429ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (46.115231ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (96.310687ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (670.803332ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.278146ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (2.892925ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.539036ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (1.440505ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.845807ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.476087ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.307226ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.887012ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.236875ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (29.831211ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (13.545179ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (9.060634ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.631006ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.676231ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (4.481228ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1016.264905ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.41958ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (5.921007ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.26201ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.337149ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.32135ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.367336ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.324124ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.206261ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (5.556235ms)
|
||||
✔ tools: listing and search caps hold (14.90763ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.871245ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (6.010288ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.369828ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.393609ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.344622ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.153513ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.476181ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (4.200898ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.395614ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1032.626989ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.188592ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.227019ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.619079ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.172735ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3138.692892
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:373:1
|
||||
✖ the journal: a symlinked directory refuses and says it is a link (0.811472ms)
|
||||
AssertionError [ERR_ASSERTION]: The validation function is expected to return "true". Received false
|
||||
|
||||
Caught error:
|
||||
|
||||
CliError: notify journal directory must be mode 0700 and owned by this user: /mnt/storage/scratch/tmp/mosaic-cli-vNPN55/notify/demo
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:380:10)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: CliError: notify journal directory must be mode 0700 and owned by this user: /mnt/storage/scratch/tmp/mosaic-cli-vNPN55/notify/demo
|
||||
at openJournal (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/src/notifier.mjs:171:11)
|
||||
at file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:380:23
|
||||
at getActual (node:assert:580:5)
|
||||
at strict.throws (node:assert:728:24)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:380:10)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12),
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (34.457669ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (39.776212ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (27.600945ms)
|
||||
✔ decide prints a declining choice as declining (15.940212ms)
|
||||
✔ an unknown outcome is reported once and never resent (16.749822ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (15.791857ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (13.234687ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (19.37181ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (23.136445ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (20.99572ms)
|
||||
✔ agents and tasks print through the broker (17.491638ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.941933ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (79.89992ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (58.021301ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (57.661152ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (58.772844ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (62.196252ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (99.376599ms)
|
||||
✔ empty views say so (1.116849ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.287586ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.38398ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (970.94765ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (141.475956ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (70.074611ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (128.594938ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (122.05514ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (83.471111ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (130.578086ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (80.773985ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (129.291133ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (23.951038ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (201.98111ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (214.706423ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (83.562569ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (591.731588ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (26.066894ms)
|
||||
✔ zoned uses the IANA zone across DST (27.340622ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (36.416407ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (31.196857ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.317673ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (28.754351ms)
|
||||
✖ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (1.583579ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (34.761097ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (18.059918ms)
|
||||
✖ 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 (153.389297ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (71.565471ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (17.956347ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (14.297155ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.821125ms)
|
||||
✔ no Discord id reaches the journal or the log (15.565565ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.563285ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (3.30832ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.759443ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.711908ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.691527ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.787096ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.413294ms)
|
||||
✔ 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.60859ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.473971ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.594826ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.330817ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.534829ms)
|
||||
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 (386.924727ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (59.064308ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2271.756605ms)
|
||||
✔ busExit and refuseInsideAgent (0.430765ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.227106ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.540684ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.62685ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.479888ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (17.627827ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (6.471183ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (6.524196ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.657375ms)
|
||||
✔ authorize: open channel, listed user (2.064009ms)
|
||||
✔ authorize: wrong guild (0.22122ms)
|
||||
✔ authorize: no guild (DM) (0.181135ms)
|
||||
✔ authorize: unlisted channel (0.22082ms)
|
||||
✔ authorize: unknown channel, no info (0.170662ms)
|
||||
✔ authorize: thread of listed parent (0.151932ms)
|
||||
✔ authorize: thread of unlisted parent (0.154559ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.132508ms)
|
||||
✔ authorize: unlisted user (0.22058ms)
|
||||
✔ authorize: no author (1.165386ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.334929ms)
|
||||
✔ authorize: system author (0.214155ms)
|
||||
✔ authorize: the bot itself (0.12664ms)
|
||||
✔ authorize: webhook (0.175548ms)
|
||||
✔ authorize: mention channel without mention (0.181537ms)
|
||||
✔ authorize: mention channel with bot mention (0.151418ms)
|
||||
✔ authorize: mention channel with @everyone only (0.096097ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.079812ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.067777ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.089663ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.076655ms)
|
||||
✔ authorize: thread in another guild per channel info (0.105833ms)
|
||||
✔ authorize: not an object (0.073392ms)
|
||||
✔ authorize: no id (0.075666ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.072669ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.074601ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.48884ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.200971ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.476085ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.226904ms)
|
||||
✔ binding: empty allowlists refuse (0.368308ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.348988ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.751437ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.550265ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (2.38952ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.983162ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (110.66854ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (2.066087ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (432.827415ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (206.863956ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (136.110665ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.686294ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.092118ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (18.053257ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (18.088716ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.630096ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.245316ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.333303ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.411725ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (32.777624ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.04499ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.084472ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.654419ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (41.170181ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (33.153366ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (8.181913ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (5.288311ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.203992ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (7.428206ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (4.39863ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (2.096518ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (3.067026ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (5.268096ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (4.007624ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.652781ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (6.435234ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.61294ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.799959ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.948758ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.388242ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.46722ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.170258ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.659488ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.437656ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.418608ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (64.442493ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (54.892935ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (49.692748ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (361.393325ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (241.635748ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (104.348408ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (229.414968ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (126.967524ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (212.927643ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.718972ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.317189ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.489047ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (428.586787ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (23.691376ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (46.180303ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.533128ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (2.885873ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.491052ms)
|
||||
✔ gateway: op 9 resumable resumes (0.536362ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.821569ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.681609ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.392197ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (94.317873ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (58.533787ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (139.984006ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.442403ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (110.2365ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (93.031839ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (93.281368ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (196.280919ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (72.931362ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (78.976717ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (192.498725ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (746.924677ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.877264ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (82.470299ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.467376ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.535194ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (61.667284ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (368.701422ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.422508ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (22.696297ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (0.900262ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (45.148368ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (144.394417ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (82.2528ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.410635ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (4.335288ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (1.767914ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.372908ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (55.475108ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (57.162387ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (52.573354ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (98.92471ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (665.11408ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (1.919958ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (5.139676ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.583988ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.734173ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.818297ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.721563ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.351536ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.819202ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.235031ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (28.702181ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (12.829086ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (8.098975ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (3.763734ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.844764ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.518365ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1017.65669ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.399851ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (5.949877ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.228252ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.193367ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.779223ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (3.994224ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.81424ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (7.962176ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (8.218101ms)
|
||||
✔ tools: listing and search caps hold (13.779685ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.849999ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (6.298225ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.483929ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.991859ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (6.629344ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.361629ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.889447ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (4.019815ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.134085ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1026.529497ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.186143ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.230577ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.75257ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.254833ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 242
|
||||
ℹ fail 2
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3146.217285
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:113:1
|
||||
✖ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (1.583579ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||
|
||||
6 !== 5
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:114:10)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 6,
|
||||
expected: 5,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:189:1
|
||||
✖ 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 (153.389297ms)
|
||||
AssertionError [ERR_ASSERTION]: gave-up line at +7200 s
|
||||
|
||||
false !== true
|
||||
|
||||
at file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:195:12
|
||||
at pollUntil (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:183:5)
|
||||
at async TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:194:19)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: false,
|
||||
expected: true,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (27.146304ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (38.009662ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (18.5141ms)
|
||||
✔ decide prints a declining choice as declining (18.628883ms)
|
||||
✔ an unknown outcome is reported once and never resent (15.650932ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (15.287044ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (14.844071ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (14.900019ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (15.275902ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (13.997118ms)
|
||||
✔ agents and tasks print through the broker (14.975116ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.754515ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (91.691289ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (57.69124ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (54.331019ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (51.439347ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (65.184913ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (77.553723ms)
|
||||
✔ empty views say so (1.135468ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.399915ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.233921ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (962.551965ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (138.83737ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (75.153813ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (128.923681ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (124.460836ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (71.170747ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (124.842029ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (71.963873ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (136.068405ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (22.434272ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.119082ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (196.768748ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (85.259772ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (591.2875ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (26.213333ms)
|
||||
✔ zoned uses the IANA zone across DST (25.593634ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (28.853407ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (26.512062ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.340155ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (21.202885ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (25.705688ms)
|
||||
✖ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (28.349376ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (19.973765ms)
|
||||
✔ 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 (149.387782ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (55.829593ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (15.426272ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (12.611342ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.719418ms)
|
||||
✔ no Discord id reaches the journal or the log (9.566831ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.769208ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (2.676623ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (2.491676ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.554745ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.597171ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.126252ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.357005ms)
|
||||
✔ 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.532459ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.439354ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.508479ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.292882ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.349787ms)
|
||||
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 (355.844502ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (58.458297ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2249.525617ms)
|
||||
✔ busExit and refuseInsideAgent (0.44317ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.175311ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.678547ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.627134ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.500035ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (18.059746ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (6.855178ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (7.079715ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (3.047131ms)
|
||||
✔ authorize: open channel, listed user (1.881817ms)
|
||||
✔ authorize: wrong guild (0.199221ms)
|
||||
✔ authorize: no guild (DM) (0.16743ms)
|
||||
✔ authorize: unlisted channel (0.175829ms)
|
||||
✔ authorize: unknown channel, no info (0.197697ms)
|
||||
✔ authorize: thread of listed parent (0.185077ms)
|
||||
✔ authorize: thread of unlisted parent (0.129646ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.309046ms)
|
||||
✔ authorize: unlisted user (0.937763ms)
|
||||
✔ authorize: no author (0.257256ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.133568ms)
|
||||
✔ authorize: system author (0.123916ms)
|
||||
✔ authorize: the bot itself (0.091698ms)
|
||||
✔ authorize: webhook (0.102782ms)
|
||||
✔ authorize: mention channel without mention (0.156143ms)
|
||||
✔ authorize: mention channel with bot mention (0.753095ms)
|
||||
✔ authorize: mention channel with @everyone only (0.233616ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.084316ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.091267ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.082723ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.064701ms)
|
||||
✔ authorize: thread in another guild per channel info (0.073075ms)
|
||||
✔ authorize: not an object (0.083615ms)
|
||||
✔ authorize: no id (0.067531ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.103362ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.063306ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.54641ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.155689ms)
|
||||
✔ binding: a complete binding validates and is frozen (3.013746ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.455639ms)
|
||||
✔ binding: empty allowlists refuse (0.404366ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.109516ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.918847ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (1.501386ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.386796ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.44632ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (106.593035ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (2.086633ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (383.108209ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (183.287555ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (130.941356ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.626377ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.149413ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (18.081637ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (19.049916ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.587682ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.14477ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.299863ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.373868ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (33.784768ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.620864ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.173775ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.301099ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.660229ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.305697ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (6.665299ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (4.540588ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (2.873305ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (8.22036ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.538817ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.74908ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (4.939788ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (2.400576ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (4.188017ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (3.494767ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (7.690648ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.504002ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.967218ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.092757ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.257843ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.451567ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.750337ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.669448ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.327601ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.398387ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (64.031478ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (47.950717ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (62.691644ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (352.153145ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (226.802245ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (104.529544ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (229.402728ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (124.690358ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (213.142582ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.032089ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.341782ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.49986ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (431.522471ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (27.058553ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (44.995574ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.419962ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.71561ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.466603ms)
|
||||
✔ gateway: op 9 resumable resumes (0.316897ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.961517ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.522763ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.362003ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (63.942108ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (59.4284ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (126.799841ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.476755ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (92.874679ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (84.349771ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (80.517048ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (184.282742ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (66.455619ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (84.865589ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (199.420563ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (739.218079ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.214298ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (80.777969ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.71809ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.816216ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (57.795153ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (333.939421ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.394607ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (25.228986ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.016161ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (37.755237ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (144.249473ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (86.936735ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.463881ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (4.922807ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.318563ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.672544ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (44.198484ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (50.913696ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (45.419862ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (82.671127ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (628.964212ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.388558ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (5.651155ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.637493ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.754378ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.86022ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.755052ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.092522ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.536178ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.111131ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (26.24324ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (13.209537ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (7.678632ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (3.778833ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.746281ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (2.707642ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1011.316703ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.447999ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (6.354564ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.37848ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.207054ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.357033ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (3.783929ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (3.102159ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (4.706791ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (4.239873ms)
|
||||
✔ tools: listing and search caps hold (10.962071ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.786456ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (4.629923ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.284867ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.299665ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.346846ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.866692ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.853825ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (2.484076ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (1.602765ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1021.991413ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.151466ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.215167ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.625186ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.234811ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3086.142048
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:141:1
|
||||
✖ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (28.349376ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||
|
||||
5 !== 15
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:155:10)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 5,
|
||||
expected: 15,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (31.007842ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (35.866359ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (20.391436ms)
|
||||
✔ decide prints a declining choice as declining (19.387843ms)
|
||||
✔ an unknown outcome is reported once and never resent (19.143644ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (21.741923ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (17.358926ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (18.612049ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (18.413219ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (18.748875ms)
|
||||
✔ agents and tasks print through the broker (17.446897ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.341661ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (70.12014ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (56.660283ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (65.793117ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (74.584839ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (75.007179ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (77.310781ms)
|
||||
✔ empty views say so (1.165592ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (3.504443ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.260713ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (983.254691ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (140.487326ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (67.116026ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (127.533849ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (122.697523ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (71.66171ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (119.158434ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (69.160923ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (140.213827ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (21.294332ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.685519ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (199.922757ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (85.877495ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (598.438975ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (33.100005ms)
|
||||
✔ zoned uses the IANA zone across DST (25.377136ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (29.556827ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (22.368874ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.267481ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (17.861226ms)
|
||||
✖ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (24.111021ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (24.280414ms)
|
||||
✖ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (20.672223ms)
|
||||
✔ 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 (158.174917ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (63.611281ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (15.884116ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (12.683243ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.775468ms)
|
||||
✔ no Discord id reaches the journal or the log (10.520459ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.37734ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (3.169474ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.665332ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.798672ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.852685ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.641297ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.415441ms)
|
||||
✔ 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.582724ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.413852ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.570654ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.350281ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (110.808332ms)
|
||||
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 (369.692362ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (58.761125ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2284.511013ms)
|
||||
✔ busExit and refuseInsideAgent (3.544211ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.079533ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.677794ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.583406ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.4371ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (19.715242ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (6.759233ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (8.419207ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.241644ms)
|
||||
✔ authorize: open channel, listed user (1.660324ms)
|
||||
✔ authorize: wrong guild (0.207454ms)
|
||||
✔ authorize: no guild (DM) (0.169451ms)
|
||||
✔ authorize: unlisted channel (0.177561ms)
|
||||
✔ authorize: unknown channel, no info (0.204644ms)
|
||||
✔ authorize: thread of listed parent (0.202797ms)
|
||||
✔ authorize: thread of unlisted parent (0.221182ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.114174ms)
|
||||
✔ authorize: unlisted user (0.162636ms)
|
||||
✔ authorize: no author (0.23081ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.133959ms)
|
||||
✔ authorize: system author (0.12471ms)
|
||||
✔ authorize: the bot itself (0.086604ms)
|
||||
✔ authorize: webhook (0.114468ms)
|
||||
✔ authorize: mention channel without mention (1.089119ms)
|
||||
✔ authorize: mention channel with bot mention (0.145785ms)
|
||||
✔ authorize: mention channel with @everyone only (0.189883ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.069336ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.08391ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.084706ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.09793ms)
|
||||
✔ authorize: thread in another guild per channel info (0.074934ms)
|
||||
✔ authorize: not an object (0.0622ms)
|
||||
✔ authorize: no id (0.058571ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.071718ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.064527ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.469365ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.165844ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.510966ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.229942ms)
|
||||
✔ binding: empty allowlists refuse (0.384401ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.510869ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.434975ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.638298ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.767753ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.330397ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (109.04945ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (3.442108ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (397.439876ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (186.926601ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (137.427307ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.768241ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.217834ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (18.518944ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (16.017566ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.27069ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.107805ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.260684ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.164596ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (34.4887ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.597669ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.911539ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.046037ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.201889ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (33.086235ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (7.394232ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (5.377634ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (4.620731ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (6.616873ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (5.391233ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (1.018179ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (5.171072ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (1.978544ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (6.662906ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.957156ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (5.654373ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.323015ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.928322ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.352459ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.191875ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.48947ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.479037ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.707397ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.432097ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.417485ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (59.088799ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (51.192571ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (50.383243ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (355.020551ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (228.921125ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (105.633755ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (228.578243ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (127.578494ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (212.595517ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (615.372594ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.318042ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.53139ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (427.860818ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (25.345959ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (46.570271ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.38393ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.620558ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.507665ms)
|
||||
✔ gateway: op 9 resumable resumes (0.366657ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.917196ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.547144ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.434264ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (70.772752ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (79.984173ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (147.588211ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.489374ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (90.947522ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (78.97579ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (82.346823ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (187.690137ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (70.506242ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (88.728135ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (198.772126ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (733.543221ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.727733ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (92.866591ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.271926ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.955178ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (68.79092ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (318.106323ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (3.525371ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (22.428769ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (0.918306ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (43.410185ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (134.866998ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (91.239056ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.475589ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.387924ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.333288ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.846356ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (66.546212ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (54.64825ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (44.944134ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (73.643758ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (639.914514ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.563478ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (5.876985ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.883849ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (1.046819ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.903085ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (6.133513ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.531952ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (3.973928ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.711905ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (28.708008ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (11.824706ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (10.641994ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (5.062501ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.628745ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.415296ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1012.325492ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.495638ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (6.169644ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.237913ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.194271ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.287926ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.039662ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.636906ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.344053ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (3.921436ms)
|
||||
✔ tools: listing and search caps hold (10.83548ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.886942ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (5.785223ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.340195ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.43212ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (6.644596ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.085522ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.870085ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.645311ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.578727ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1026.227949ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.258363ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.227004ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.49931ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.252013ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 242
|
||||
ℹ fail 2
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3112.839501
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:113:1
|
||||
✖ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (24.111021ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||
|
||||
8 !== 6
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:132:10)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 8,
|
||||
expected: 6,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:161:1
|
||||
✖ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (20.672223ms)
|
||||
AssertionError [ERR_ASSERTION]: one gave-up line only
|
||||
|
||||
7 !== 6
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:172:10)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 7,
|
||||
expected: 6,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (35.907671ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (33.828051ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (19.564022ms)
|
||||
✔ decide prints a declining choice as declining (23.123139ms)
|
||||
✔ an unknown outcome is reported once and never resent (16.844214ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (17.757332ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (17.102004ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (15.783173ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (18.352242ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (17.123042ms)
|
||||
✔ agents and tasks print through the broker (19.02021ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.12139ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (65.167648ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (63.185177ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (67.754525ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (59.879169ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (63.30319ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (91.113368ms)
|
||||
✔ empty views say so (1.042294ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.288402ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.21112ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (964.781264ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (147.235114ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (76.35978ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (125.618169ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (122.116694ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (72.92106ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (125.31243ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (75.404511ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (134.518737ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (24.183271ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.825005ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (200.699607ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (85.010098ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (599.278098ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (33.967937ms)
|
||||
✔ zoned uses the IANA zone across DST (25.819193ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (26.259947ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (21.336884ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.310211ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (19.798177ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (25.335423ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (42.562172ms)
|
||||
✖ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (18.796835ms)
|
||||
✔ 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 (147.398441ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (74.246448ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (16.563056ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (9.075605ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.714626ms)
|
||||
✔ no Discord id reaches the journal or the log (8.910446ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.238968ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (2.977015ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.331085ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.556547ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.93961ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.160316ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.412259ms)
|
||||
✔ 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.504315ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.424702ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.489339ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.308606ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.245016ms)
|
||||
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 (377.305092ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (61.886594ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2282.888322ms)
|
||||
✔ busExit and refuseInsideAgent (0.396029ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.182482ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.495384ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.591696ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.516961ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (17.191805ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (7.896753ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (5.758235ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.653996ms)
|
||||
✔ authorize: open channel, listed user (1.937768ms)
|
||||
✔ authorize: wrong guild (0.19303ms)
|
||||
✔ authorize: no guild (DM) (0.164671ms)
|
||||
✔ authorize: unlisted channel (0.215374ms)
|
||||
✔ authorize: unknown channel, no info (0.178852ms)
|
||||
✔ authorize: thread of listed parent (0.190196ms)
|
||||
✔ authorize: thread of unlisted parent (0.249874ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.122206ms)
|
||||
✔ authorize: unlisted user (0.179826ms)
|
||||
✔ authorize: no author (0.25968ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (3.498479ms)
|
||||
✔ authorize: system author (0.454962ms)
|
||||
✔ authorize: the bot itself (0.10011ms)
|
||||
✔ authorize: webhook (0.103339ms)
|
||||
✔ authorize: mention channel without mention (0.130293ms)
|
||||
✔ authorize: mention channel with bot mention (0.145646ms)
|
||||
✔ authorize: mention channel with @everyone only (0.105035ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.099156ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.094127ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.089244ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.07785ms)
|
||||
✔ authorize: thread in another guild per channel info (0.078183ms)
|
||||
✔ authorize: not an object (0.067403ms)
|
||||
✔ authorize: no id (0.069164ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.072886ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.129506ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (2.508958ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.185243ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.55613ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (2.119311ms)
|
||||
✔ binding: empty allowlists refuse (0.42883ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.595041ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (2.080632ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (1.262084ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.637671ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.313326ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (120.14796ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (5.464029ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (386.124028ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (205.191133ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (130.871351ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.703233ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.089932ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (18.387326ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (15.796369ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.699771ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.132473ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.412887ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.515809ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (34.367621ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.085594ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.055758ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.46701ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (44.293865ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.753552ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (8.05122ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (5.716244ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (2.365769ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (5.144234ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (1.834329ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.908962ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (3.630321ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (3.381295ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (5.355993ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.782928ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (5.127064ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.435241ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.788725ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.508875ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.169816ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.479615ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.257794ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.635396ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.390889ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.391207ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (59.415341ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (61.451709ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (49.217009ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (352.992402ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (227.904937ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (103.911725ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (226.980309ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (131.690081ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (212.287434ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.728019ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.306726ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.509402ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (425.816542ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (25.212649ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (46.235066ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.780269ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.789716ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.464944ms)
|
||||
✔ gateway: op 9 resumable resumes (0.390011ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.78067ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.495854ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.41733ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (70.383607ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (67.176795ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (152.699022ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.465056ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (94.365656ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (87.974451ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (84.478469ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (211.105093ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (65.591525ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (82.639005ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (196.923492ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (732.40599ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.823601ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (90.98917ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.117745ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (3.804433ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (62.068597ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (330.460986ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.40533ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (26.944861ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (0.995838ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (43.039273ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (146.950752ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (84.591369ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.440065ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.278016ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.047214ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.770043ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (60.225454ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (50.438925ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (48.919357ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (77.559074ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (644.227422ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.610182ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (2.945677ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (1.025202ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.732875ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (1.053592ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.302412ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.850869ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.867226ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.541066ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (27.741019ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (14.919383ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (7.898873ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.028128ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.679448ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.291423ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1014.093062ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.414768ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (5.972973ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.189231ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.199192ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.219535ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.023123ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.68059ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (6.089698ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (4.554985ms)
|
||||
✔ tools: listing and search caps hold (13.116831ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.705526ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (7.182148ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.260686ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.220933ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.45877ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.804999ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (2.046206ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.53915ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.273334ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1025.200986ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.323866ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.22155ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.613232ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.19972ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3114.589808
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:161:1
|
||||
✖ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (18.796835ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ 'refused'
|
||||
- 'gave-up'
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:169:10)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 'refused',
|
||||
expected: 'gave-up',
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (28.621283ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (34.418507ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (20.995275ms)
|
||||
✔ decide prints a declining choice as declining (22.836862ms)
|
||||
✔ an unknown outcome is reported once and never resent (15.164053ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (15.187221ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (21.632191ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (14.957876ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (15.069813ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (21.033209ms)
|
||||
✔ agents and tasks print through the broker (16.857571ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (3.228282ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (66.96735ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (59.574632ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (57.733999ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (65.292768ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (53.459236ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (90.547387ms)
|
||||
✔ empty views say so (1.061084ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.431715ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.217874ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (961.827261ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (144.385665ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (69.599438ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (124.221887ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (119.2421ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (75.874011ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (120.182931ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (74.915202ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (131.184086ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (21.819526ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.474489ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (209.010171ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (83.345606ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (592.614018ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (26.484325ms)
|
||||
✔ zoned uses the IANA zone across DST (27.698692ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (31.164826ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (21.953427ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.374516ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (20.330818ms)
|
||||
✖ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (33.696536ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (24.452153ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (17.698773ms)
|
||||
✔ 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 (147.101493ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (53.663954ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (18.172301ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (10.095754ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.75751ms)
|
||||
✔ no Discord id reaches the journal or the log (11.053261ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (1.910362ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (2.04545ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.120175ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.461928ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.523453ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.001405ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.313132ms)
|
||||
✔ 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.453771ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.363517ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.441764ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.265474ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (110.780231ms)
|
||||
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 (361.507281ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (61.19871ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2254.738866ms)
|
||||
✔ busExit and refuseInsideAgent (0.418056ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (2.979819ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.41399ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.536152ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.942113ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (18.18381ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (6.478598ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (8.082299ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.086992ms)
|
||||
✔ authorize: open channel, listed user (1.868361ms)
|
||||
✔ authorize: wrong guild (0.339308ms)
|
||||
✔ authorize: no guild (DM) (0.355702ms)
|
||||
✔ authorize: unlisted channel (0.216537ms)
|
||||
✔ authorize: unknown channel, no info (0.169905ms)
|
||||
✔ authorize: thread of listed parent (0.18777ms)
|
||||
✔ authorize: thread of unlisted parent (0.132027ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.141871ms)
|
||||
✔ authorize: unlisted user (0.179529ms)
|
||||
✔ authorize: no author (0.662374ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.139891ms)
|
||||
✔ authorize: system author (0.13039ms)
|
||||
✔ authorize: the bot itself (0.093409ms)
|
||||
✔ authorize: webhook (0.159174ms)
|
||||
✔ authorize: mention channel without mention (0.133873ms)
|
||||
✔ authorize: mention channel with bot mention (0.158685ms)
|
||||
✔ authorize: mention channel with @everyone only (0.122146ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.087096ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.086022ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.100617ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.075227ms)
|
||||
✔ authorize: thread in another guild per channel info (0.074536ms)
|
||||
✔ authorize: not an object (0.066614ms)
|
||||
✔ authorize: no id (0.086367ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.075215ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.074069ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.44869ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (1.276891ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.578559ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.732603ms)
|
||||
✔ binding: empty allowlists refuse (0.375719ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.323385ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.651178ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.79816ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (2.016077ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.602531ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (115.297034ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (2.831267ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (367.802337ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (187.692232ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (127.309151ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.537031ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.020843ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (17.547014ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (16.720471ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.564107ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.096813ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.272855ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.61944ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (32.390387ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (4.30189ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.200616ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (2.180326ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.461218ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (31.916724ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (9.127388ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (5.259657ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.305258ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (6.722387ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (3.125464ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.652024ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (5.727573ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (1.535223ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (5.050562ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (3.428525ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (5.172906ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (3.319807ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.79182ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.908327ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.213356ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.322505ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.381823ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (1.2548ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.445298ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.507578ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (60.985253ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (51.847787ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (50.762557ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (351.806729ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (240.035379ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (103.509997ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (225.043251ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (128.43759ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (214.54819ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.924449ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.308751ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.531149ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (425.910117ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (25.935939ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (44.315462ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.452899ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.7736ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.611255ms)
|
||||
✔ gateway: op 9 resumable resumes (0.341012ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.877711ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.516388ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.447957ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (68.486843ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (59.495346ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (134.453382ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.458105ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (89.916991ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (75.23857ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (85.488293ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (192.418581ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (66.165731ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (77.232416ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (196.747174ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (731.926544ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.755713ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (82.249044ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.127505ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (5.18822ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (57.270022ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (333.069339ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.454178ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (22.855622ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (0.979474ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (34.582006ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (129.468697ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (79.569227ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.411317ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (8.930572ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.046757ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.780801ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (57.67267ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (52.373179ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (44.581135ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (69.44625ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (614.78035ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.500519ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (6.121771ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.691956ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.707289ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.874534ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (6.147746ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.489601ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.579195ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (3.59566ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (29.720314ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (12.519307ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (7.762805ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (5.01743ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.796999ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.226122ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1012.079624ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.444775ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (6.03868ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.289155ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.206254ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.316366ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.437806ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (3.630721ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (4.564635ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (4.425491ms)
|
||||
✔ tools: listing and search caps hold (11.546249ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (1.435662ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (13.51128ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (0.972228ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.345303ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (6.886211ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.701352ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.690755ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.529186ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.258573ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1025.389937ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.242156ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.226014ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.526761ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.78514ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3082.015709
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:113:1
|
||||
✖ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (33.696536ms)
|
||||
AssertionError [ERR_ASSERTION]: The input did not match the regular expression /\[blocking, DM refused, not retried\] a8df1921 git\.push\.protected/. Input:
|
||||
|
||||
'Mosaic digest (demo, 2026-10-08): 1 open decision(s).\n' +
|
||||
'- [blocking, DM pending] a8df1921 git.push.protected: Push the release?\n' +
|
||||
'Run mosaic inbox for the full list.'
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:138:10)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 'Mosaic digest (demo, 2026-10-08): 1 open decision(s).\n- [blocking, DM pending] a8df1921 git.push.protected: Push the release?\nRun mosaic inbox for the full list.',
|
||||
expected: /\[blocking, DM refused, not retried\] a8df1921 git\.push\.protected/,
|
||||
operator: 'match',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (56.885504ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (34.287014ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (17.265434ms)
|
||||
✔ decide prints a declining choice as declining (14.985414ms)
|
||||
✔ an unknown outcome is reported once and never resent (16.212657ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (14.862663ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (14.512272ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (22.653118ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (24.838053ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (18.465952ms)
|
||||
✔ agents and tasks print through the broker (14.697332ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.42752ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (74.09765ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (57.52387ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (56.072894ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (52.204553ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (63.35558ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (79.893582ms)
|
||||
✔ empty views say so (1.21304ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.380745ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.22152ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (942.790779ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (155.209653ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (75.197442ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (123.043863ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (121.441269ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (81.126446ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (127.683225ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (86.137999ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (131.315693ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (23.172735ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.028145ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (202.631136ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (83.066616ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (602.933961ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (25.736454ms)
|
||||
✔ zoned uses the IANA zone across DST (26.367569ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (61.280969ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (20.343789ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.326101ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (21.98434ms)
|
||||
✖ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (19.818272ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (25.372ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (18.619543ms)
|
||||
✖ 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 (139.966927ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (49.035235ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (14.995437ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (13.299279ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.773214ms)
|
||||
✔ no Discord id reaches the journal or the log (11.954278ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.146364ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (3.010685ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.809056ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.57811ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.710816ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.548265ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.388793ms)
|
||||
✔ 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.528707ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.413836ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.505336ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.31769ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.5451ms)
|
||||
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 (350.945743ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (51.215414ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2235.504837ms)
|
||||
✔ busExit and refuseInsideAgent (0.45267ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (2.985741ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.678746ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.640068ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.470898ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (19.894024ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (6.126932ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (7.108522ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.147988ms)
|
||||
✔ authorize: open channel, listed user (1.828706ms)
|
||||
✔ authorize: wrong guild (0.185022ms)
|
||||
✔ authorize: no guild (DM) (0.177938ms)
|
||||
✔ authorize: unlisted channel (0.211176ms)
|
||||
✔ authorize: unknown channel, no info (0.184219ms)
|
||||
✔ authorize: thread of listed parent (0.178391ms)
|
||||
✔ authorize: thread of unlisted parent (0.177869ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.14313ms)
|
||||
✔ authorize: unlisted user (0.182067ms)
|
||||
✔ authorize: no author (0.294058ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.171007ms)
|
||||
✔ authorize: system author (0.126633ms)
|
||||
✔ authorize: the bot itself (0.0969ms)
|
||||
✔ authorize: webhook (0.088551ms)
|
||||
✔ authorize: mention channel without mention (0.137281ms)
|
||||
✔ authorize: mention channel with bot mention (0.128178ms)
|
||||
✔ authorize: mention channel with @everyone only (0.118136ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.095369ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.086469ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.084808ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.068016ms)
|
||||
✔ authorize: thread in another guild per channel info (0.072532ms)
|
||||
✔ authorize: not an object (0.062472ms)
|
||||
✔ authorize: no id (0.060983ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.066501ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.062761ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.455448ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.138271ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.508739ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.24338ms)
|
||||
✔ binding: empty allowlists refuse (0.387825ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.288192ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.587089ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.442067ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.614418ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.442121ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (108.988243ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (3.913193ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (385.981748ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (198.132962ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (125.979165ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.608937ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.173621ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (15.874528ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (16.952335ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.473261ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.592667ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.341225ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.70334ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (33.782675ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (4.044352ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.374301ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.042096ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (41.734104ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (33.251025ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (6.473614ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (9.356673ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.261338ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (9.648391ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (3.555416ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.771529ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (4.321657ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (4.769728ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (7.345102ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.643734ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (3.470697ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.347587ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.797221ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.765022ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.185081ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.362878ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.149406ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.642868ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.960776ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.43147ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (49.204733ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (47.370258ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (45.890133ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (349.744044ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (230.082374ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (106.185494ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (228.972299ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (129.172254ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (212.782508ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (616.080922ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.279921ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.544898ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (429.71944ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (23.42561ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (43.133759ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.881829ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.556568ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.536634ms)
|
||||
✔ gateway: op 9 resumable resumes (0.545814ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.870018ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.598496ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.619697ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (65.19501ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (58.813551ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (138.390132ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.312712ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (94.387699ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (94.891888ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (86.79196ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (203.185544ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (64.081443ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (86.495759ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (209.998925ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (733.972789ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.091593ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (82.294687ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.166123ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (5.122215ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (60.012183ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (331.721233ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.396141ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (22.525606ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (0.995717ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (34.459796ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (148.210668ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (82.366295ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.472883ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.238448ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.216863ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.639008ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (52.236586ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (52.83315ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (47.624887ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (79.969359ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (679.403112ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.421208ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (5.031282ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.640044ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.703364ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.817813ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.170314ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (2.858315ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.405603ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.326149ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (27.642422ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (11.030959ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (8.350764ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.986666ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.607414ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.468382ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1016.286571ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.464161ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (6.485864ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.537642ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.199155ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.324383ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.380325ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.196114ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.302093ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (3.673284ms)
|
||||
✔ tools: listing and search caps hold (12.447422ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.895993ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (6.84361ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (2.269688ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.375267ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.547906ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.774322ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.854376ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (5.010109ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.513761ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1030.900527ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.378903ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.23307ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (3.234016ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.811915ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 242
|
||||
ℹ fail 2
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3114.074441
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:113:1
|
||||
✖ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (19.818272ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
|
||||
[
|
||||
'refused',
|
||||
'refused',
|
||||
'refused',
|
||||
'refused',
|
||||
'refused',
|
||||
- 'gave-up'
|
||||
]
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:127:10)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: [ 'refused', 'refused', 'refused', 'refused', 'refused' ],
|
||||
expected: [ 'refused', 'refused', 'refused', 'refused', 'refused', 'gave-up' ],
|
||||
operator: 'deepStrictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:189:1
|
||||
✖ 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 (139.966927ms)
|
||||
AssertionError [ERR_ASSERTION]: gave-up line at +7200 s
|
||||
|
||||
false !== true
|
||||
|
||||
at file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:195:12
|
||||
at pollUntil (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:183:5)
|
||||
at async TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:194:19)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: false,
|
||||
expected: true,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (29.095741ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (34.923888ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (15.985505ms)
|
||||
✔ decide prints a declining choice as declining (20.314714ms)
|
||||
✔ an unknown outcome is reported once and never resent (13.899644ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (17.224943ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (17.276258ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (16.604299ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (19.184063ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (19.317868ms)
|
||||
✔ agents and tasks print through the broker (17.139908ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.565535ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (72.467314ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (49.84195ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (52.10819ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (62.053938ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (62.308204ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (87.000304ms)
|
||||
✔ empty views say so (1.115034ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.454864ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.226238ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (950.497988ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (141.486172ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (74.041411ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (127.53729ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (126.267882ms)
|
||||
✖ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (73.29616ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (119.74423ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (69.877328ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (131.504147ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (23.031934ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (201.922926ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (201.441639ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (87.594342ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (594.89957ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (33.862868ms)
|
||||
✔ zoned uses the IANA zone across DST (27.366217ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (30.324315ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (20.355728ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.27787ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (24.206386ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (26.174087ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (29.841159ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (17.824088ms)
|
||||
✔ 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 (152.302442ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (51.172318ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (14.327499ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (10.355138ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.718549ms)
|
||||
✔ no Discord id reaches the journal or the log (9.433057ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.174015ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (2.297969ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.184247ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.484994ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.640183ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.202926ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.320092ms)
|
||||
✔ 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.460622ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.390474ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.437026ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.268666ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.030194ms)
|
||||
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 (369.137409ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (61.50929ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2249.586124ms)
|
||||
✔ busExit and refuseInsideAgent (0.41445ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.137569ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (2.194828ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.586076ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.51967ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (15.685404ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (8.000277ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (7.734034ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.779899ms)
|
||||
✔ authorize: open channel, listed user (1.969847ms)
|
||||
✔ authorize: wrong guild (0.198678ms)
|
||||
✔ authorize: no guild (DM) (0.197448ms)
|
||||
✔ authorize: unlisted channel (0.182072ms)
|
||||
✔ authorize: unknown channel, no info (0.182055ms)
|
||||
✔ authorize: thread of listed parent (0.210424ms)
|
||||
✔ authorize: thread of unlisted parent (0.156645ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.137805ms)
|
||||
✔ authorize: unlisted user (0.18579ms)
|
||||
✔ authorize: no author (0.296704ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.145194ms)
|
||||
✔ authorize: system author (0.312494ms)
|
||||
✔ authorize: the bot itself (0.094985ms)
|
||||
✔ authorize: webhook (0.752715ms)
|
||||
✔ authorize: mention channel without mention (0.134448ms)
|
||||
✔ authorize: mention channel with bot mention (0.116537ms)
|
||||
✔ authorize: mention channel with @everyone only (0.083491ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.077775ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.082699ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.088658ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.073754ms)
|
||||
✔ authorize: thread in another guild per channel info (0.088674ms)
|
||||
✔ authorize: not an object (0.054784ms)
|
||||
✔ authorize: no id (0.063928ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.074415ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.073527ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (2.03148ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.140374ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.424948ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.243753ms)
|
||||
✔ binding: empty allowlists refuse (0.377074ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.271095ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.481653ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.552278ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (2.326417ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.594778ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (108.406928ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (1.781137ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (402.476611ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (187.532858ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (125.515579ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.741472ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.461955ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (16.566057ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (17.85597ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (2.728884ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.456175ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (2.03462ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.702603ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (33.958556ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.860861ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.012946ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.352594ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.615618ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.052034ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (9.503955ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (4.821725ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.474703ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (6.069742ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (3.144499ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.741144ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (7.842408ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (1.432428ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (3.965582ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.973037ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (4.621111ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.447485ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.819355ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.235456ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.253733ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.55866ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.282132ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.637805ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.643009ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.422296ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (52.264335ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (52.951878ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (57.502895ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (382.727095ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (228.256983ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (114.165961ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (227.288806ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (125.812203ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (212.627369ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (615.402369ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.226298ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.508602ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (426.083474ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (23.897817ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (45.112548ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.412089ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.631675ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.449641ms)
|
||||
✔ gateway: op 9 resumable resumes (0.348812ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.929052ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.611223ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.45368ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (67.794757ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (62.659566ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (128.676422ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.366912ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (89.399851ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (83.745301ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (77.88815ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (199.054856ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (65.829609ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (76.923938ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (194.349209ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (733.403237ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.521431ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (83.973747ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (2.295484ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (3.099574ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (73.940635ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (317.910765ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.409595ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (24.29891ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.056676ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (35.587816ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (135.680834ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (82.764193ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.408213ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.075093ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.182406ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.7002ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (51.868684ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (58.914471ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (42.049844ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (88.275118ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (626.350216ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.622214ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (5.062453ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (1.240388ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.715139ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.852064ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (6.335935ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.818495ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.779181ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (3.000309ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (28.904269ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (12.925509ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (7.554198ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.364328ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.632652ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.753915ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1014.403495ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.42263ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (5.964611ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.225257ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.217242ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.34837ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (5.074763ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.775756ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.131013ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (4.821373ms)
|
||||
✔ tools: listing and search caps hold (12.725447ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.936192ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (5.826119ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.864891ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.6133ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (6.489664ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.861216ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.853406ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (2.892454ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (1.799945ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1029.586208ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (6.060918ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.356494ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (3.239184ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.209551ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3096.592038
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/host.test.mjs:243:1
|
||||
✖ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (73.29616ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||
|
||||
1 !== 0
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:268:10)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 1,
|
||||
expected: 0,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (26.1368ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (46.745011ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (16.100877ms)
|
||||
✔ decide prints a declining choice as declining (16.379141ms)
|
||||
✔ an unknown outcome is reported once and never resent (17.447369ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (14.858296ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (14.287926ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (15.549323ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (16.259379ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (21.570527ms)
|
||||
✔ agents and tasks print through the broker (16.178902ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.259346ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (74.942581ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (57.961992ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (54.682985ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (60.085701ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (58.386835ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (81.737818ms)
|
||||
✔ empty views say so (0.967447ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.493325ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.215181ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (957.7209ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (146.582909ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (76.136108ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (128.968102ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (118.781911ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (69.989998ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (122.555871ms)
|
||||
✖ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (70.183687ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (133.670931ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (26.088198ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (203.173423ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (205.297138ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (84.370611ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (592.037185ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (26.73236ms)
|
||||
✔ zoned uses the IANA zone across DST (27.008417ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (23.208353ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (19.924515ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.451612ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (19.04242ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (21.225929ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (24.649025ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (16.345159ms)
|
||||
✔ 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 (140.119999ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (62.387294ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (14.681064ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (14.434875ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.837843ms)
|
||||
✔ no Discord id reaches the journal or the log (12.389468ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.696068ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (4.681866ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.93143ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.809339ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.819608ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.484426ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.44415ms)
|
||||
✔ 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.566352ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.441192ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.525ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.539639ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (110.651376ms)
|
||||
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 (360.952342ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (53.7094ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2240.251825ms)
|
||||
✔ busExit and refuseInsideAgent (0.416736ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.308892ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.522903ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.645219ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.483178ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (17.376906ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (7.457576ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (7.343405ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.979697ms)
|
||||
✔ authorize: open channel, listed user (1.743225ms)
|
||||
✔ authorize: wrong guild (0.189864ms)
|
||||
✔ authorize: no guild (DM) (0.268705ms)
|
||||
✔ authorize: unlisted channel (0.171038ms)
|
||||
✔ authorize: unknown channel, no info (0.150431ms)
|
||||
✔ authorize: thread of listed parent (0.183056ms)
|
||||
✔ authorize: thread of unlisted parent (0.13136ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.118599ms)
|
||||
✔ authorize: unlisted user (1.06446ms)
|
||||
✔ authorize: no author (0.237253ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.593525ms)
|
||||
✔ authorize: system author (0.125774ms)
|
||||
✔ authorize: the bot itself (0.116418ms)
|
||||
✔ authorize: webhook (0.077649ms)
|
||||
✔ authorize: mention channel without mention (0.12093ms)
|
||||
✔ authorize: mention channel with bot mention (0.117308ms)
|
||||
✔ authorize: mention channel with @everyone only (0.089981ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.097472ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.114099ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.934377ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.10492ms)
|
||||
✔ authorize: thread in another guild per channel info (0.08405ms)
|
||||
✔ authorize: not an object (0.07059ms)
|
||||
✔ authorize: no id (0.067651ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.075171ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.064778ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.469229ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.138129ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.667906ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (2.00733ms)
|
||||
✔ binding: empty allowlists refuse (0.429084ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.30046ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.540602ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.471465ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.866653ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.336394ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (103.007293ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (3.545742ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (395.75902ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (190.884765ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (134.217422ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.734418ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.181598ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (15.455274ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (18.43262ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.673261ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.407837ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.500103ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.579746ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (33.438276ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.533333ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (0.990361ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.394028ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (41.831246ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.960726ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (9.141976ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (5.363426ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (4.774124ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (5.97473ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.172664ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.817185ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (7.004484ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (2.134845ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (5.717394ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (3.680791ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (4.720045ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.528464ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.787622ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.868034ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.237296ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (2.345611ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.179714ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.653019ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.542918ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.394399ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (51.982367ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (54.041696ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (52.543706ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (365.980906ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (226.717036ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (114.841503ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (228.378286ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (126.395957ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (213.982971ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.502266ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.28494ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.455757ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (425.889445ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (26.144857ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (47.886129ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.475985ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.50105ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.447689ms)
|
||||
✔ gateway: op 9 resumable resumes (0.359406ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.904707ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.512303ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.40511ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (69.338568ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (59.822598ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (139.054817ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.443794ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (97.524835ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (87.910796ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (82.886667ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (189.922796ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (65.437666ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (84.476317ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (189.720326ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (734.069404ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.48591ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (81.743585ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.043907ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.96132ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (57.800739ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (340.613444ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.369071ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (25.297509ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (0.978547ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (36.117537ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (137.026522ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (81.489355ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.459774ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.033554ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.574369ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.694562ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (47.965925ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (46.456693ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (44.562243ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (76.745919ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (632.318303ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.507474ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (5.51626ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.641018ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.735424ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.814947ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.569093ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.671653ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.797279ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.193978ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (30.796964ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (13.823469ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (6.984046ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.841136ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.603736ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.303258ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1016.621822ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.452632ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (6.514694ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.337529ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.210321ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.898733ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.162243ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (3.90781ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (4.823158ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (3.699656ms)
|
||||
✔ tools: listing and search caps hold (12.142446ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.913146ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (5.991941ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.311991ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.285283ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (6.303826ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.770847ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (2.446206ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.261044ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.095035ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1027.890644ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.623231ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.271071ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.880563ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.241811ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 21892.726774
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/host.test.mjs:282:1
|
||||
✖ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (70.183687ms)
|
||||
AssertionError [ERR_ASSERTION]: The validation function is expected to return "true". Received false
|
||||
|
||||
Caught error:
|
||||
|
||||
Error: write EPIPE
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:298:3)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: Error: write EPIPE
|
||||
at epipe (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:113:24)
|
||||
at file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:288:35
|
||||
at ChildProcess.send (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:94:23)
|
||||
at startHost (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/src/host.mjs:144:36)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async waitForActual (node:assert:615:5)
|
||||
at async strict.rejects (node:assert:738:25)
|
||||
at async TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:298:3)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7),
|
||||
operator: 'rejects',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (36.137224ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (32.80617ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (20.446641ms)
|
||||
✔ decide prints a declining choice as declining (22.61046ms)
|
||||
✔ an unknown outcome is reported once and never resent (18.116597ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (14.993634ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (16.118092ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (14.030829ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (16.339592ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (16.636658ms)
|
||||
✔ agents and tasks print through the broker (23.719494ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.803717ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (67.682761ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (63.452436ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (60.125592ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (55.93854ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (63.122419ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (82.102685ms)
|
||||
✔ empty views say so (1.078813ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.428027ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.206948ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (952.526967ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (154.740474ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (78.082734ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (128.007999ms)
|
||||
✖ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (134.711417ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (86.036456ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (124.9122ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (75.737395ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (138.083174ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (21.677089ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.200301ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (199.31374ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (85.678937ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (598.861125ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (26.648972ms)
|
||||
✔ zoned uses the IANA zone across DST (25.186616ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (31.115707ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (23.120288ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.295168ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (19.592593ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (34.223143ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (25.865874ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (15.390973ms)
|
||||
✔ 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 (149.241935ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (61.050772ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (17.366519ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (10.444671ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.794404ms)
|
||||
✔ no Discord id reaches the journal or the log (9.407856ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.757199ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (2.164529ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.329633ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.606127ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.762642ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.845129ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.623139ms)
|
||||
✔ 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.570491ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.434198ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.62767ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.337211ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (110.23101ms)
|
||||
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 (357.441666ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (66.744628ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2257.702429ms)
|
||||
✔ busExit and refuseInsideAgent (0.401325ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.122055ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.395797ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.557231ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.470742ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (17.128501ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (5.948692ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (7.001664ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.839765ms)
|
||||
✔ authorize: open channel, listed user (1.905814ms)
|
||||
✔ authorize: wrong guild (0.194095ms)
|
||||
✔ authorize: no guild (DM) (0.331274ms)
|
||||
✔ authorize: unlisted channel (0.186257ms)
|
||||
✔ authorize: unknown channel, no info (0.255594ms)
|
||||
✔ authorize: thread of listed parent (0.175622ms)
|
||||
✔ authorize: thread of unlisted parent (0.24057ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.146466ms)
|
||||
✔ authorize: unlisted user (1.074803ms)
|
||||
✔ authorize: no author (0.480332ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.227986ms)
|
||||
✔ authorize: system author (0.118093ms)
|
||||
✔ authorize: the bot itself (0.088781ms)
|
||||
✔ authorize: webhook (0.095951ms)
|
||||
✔ authorize: mention channel without mention (0.125265ms)
|
||||
✔ authorize: mention channel with bot mention (0.1328ms)
|
||||
✔ authorize: mention channel with @everyone only (0.100235ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.150046ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.104864ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.100161ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.088155ms)
|
||||
✔ authorize: thread in another guild per channel info (0.079789ms)
|
||||
✔ authorize: not an object (0.072272ms)
|
||||
✔ authorize: no id (0.12767ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.079619ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.074017ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.465493ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.146439ms)
|
||||
✔ binding: a complete binding validates and is frozen (3.168897ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.552052ms)
|
||||
✔ binding: empty allowlists refuse (0.522861ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.345365ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.735368ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (1.189422ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.825939ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.850965ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (109.72649ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (2.939662ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (382.730718ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (209.193251ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (135.875836ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (2.16764ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (2.42409ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (19.243776ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (17.980429ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.535753ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.148879ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.529248ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.88324ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (33.555626ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.226429ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.11603ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.054395ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.21392ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.449019ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (6.234295ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (3.867423ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.231827ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (6.131194ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (3.656934ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.787371ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (2.163094ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (6.415917ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (5.456546ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.055967ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (3.92862ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.694643ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.781465ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.875644ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.209294ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (2.804381ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (4.91737ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.813091ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.566532ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.636779ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (61.082941ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (53.986925ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (49.689667ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (357.99446ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (231.366643ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (104.917301ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (236.778841ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (128.503987ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (212.70331ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.106109ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.275196ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.53512ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (429.244086ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (30.163175ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (46.049539ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.741781ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.527813ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.754199ms)
|
||||
✔ gateway: op 9 resumable resumes (0.359216ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.906629ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.503822ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.432442ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (69.647007ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (65.493273ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (120.122151ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.424613ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (94.670584ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (78.869019ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (86.12904ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (218.325974ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (81.564606ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (95.476163ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (210.664251ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (753.956112ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.227289ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (88.618108ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.186297ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (3.277209ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (61.174792ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (333.275407ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.377945ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (24.615643ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.033357ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (44.387525ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (155.641805ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (104.850567ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.497782ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (3.731311ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (1.79075ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.825007ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (51.237049ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (50.659425ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (47.28091ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (74.125674ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (704.036932ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.316203ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (6.632113ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.610573ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (1.213551ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (1.407844ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.684309ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.034095ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.741239ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.165727ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (26.55187ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (11.232172ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (7.824441ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (6.488054ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.659137ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.330052ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1013.245534ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.604235ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (6.280297ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.252352ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.363548ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.277544ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.343008ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.452761ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.450104ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (4.691864ms)
|
||||
✔ tools: listing and search caps hold (9.44943ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.738477ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (5.475546ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.280617ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.474486ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.151292ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.23079ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.745754ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (2.97183ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.224102ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1025.702489ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.230836ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.261957ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.620959ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.493582ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3139.768113
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/host.test.mjs:219:1
|
||||
✖ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (134.711417ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||
|
||||
1 !== 0
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:240:10)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 1,
|
||||
expected: 0,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (26.600514ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (34.705587ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (16.769242ms)
|
||||
✔ decide prints a declining choice as declining (15.135272ms)
|
||||
✔ an unknown outcome is reported once and never resent (15.031737ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (16.435094ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (18.768125ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (17.572267ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (15.354229ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (21.055411ms)
|
||||
✔ agents and tasks print through the broker (23.168222ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.376503ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (64.768109ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (52.772937ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (53.69811ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (55.925369ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (56.035949ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (92.967208ms)
|
||||
✔ empty views say so (0.953993ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.154696ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.221803ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (962.276869ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (143.48731ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (70.299392ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (126.799875ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (123.002067ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (76.269733ms)
|
||||
✖ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (141.222031ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (73.507758ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (131.554124ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (25.680169ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.905447ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (194.944408ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (85.043443ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (594.029709ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (25.50059ms)
|
||||
✔ zoned uses the IANA zone across DST (29.044084ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (27.900183ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (20.115312ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.286355ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (19.363176ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (22.232889ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (21.679329ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (20.100012ms)
|
||||
✔ 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 (144.371044ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (58.039443ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (16.025702ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (9.831656ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.64579ms)
|
||||
✔ no Discord id reaches the journal or the log (11.503299ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.144653ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (3.265131ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.494834ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.682916ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.681227ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.011013ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.337943ms)
|
||||
✔ 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.522107ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.371251ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.462659ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.279541ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (110.507806ms)
|
||||
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 (359.906235ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (59.911226ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2254.18594ms)
|
||||
✔ busExit and refuseInsideAgent (0.39255ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (2.97925ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.362717ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.55241ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.464538ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (14.744728ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (6.923578ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (5.872745ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.252777ms)
|
||||
✔ authorize: open channel, listed user (1.698945ms)
|
||||
✔ authorize: wrong guild (0.1847ms)
|
||||
✔ authorize: no guild (DM) (0.15856ms)
|
||||
✔ authorize: unlisted channel (0.215869ms)
|
||||
✔ authorize: unknown channel, no info (0.189214ms)
|
||||
✔ authorize: thread of listed parent (0.143098ms)
|
||||
✔ authorize: thread of unlisted parent (0.123843ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.095182ms)
|
||||
✔ authorize: unlisted user (0.162611ms)
|
||||
✔ authorize: no author (0.271775ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.147219ms)
|
||||
✔ authorize: system author (0.116192ms)
|
||||
✔ authorize: the bot itself (0.066576ms)
|
||||
✔ authorize: webhook (0.077195ms)
|
||||
✔ authorize: mention channel without mention (0.142467ms)
|
||||
✔ authorize: mention channel with bot mention (0.132994ms)
|
||||
✔ authorize: mention channel with @everyone only (0.117532ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.076602ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.106192ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.062647ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.040465ms)
|
||||
✔ authorize: thread in another guild per channel info (0.042334ms)
|
||||
✔ authorize: not an object (0.058914ms)
|
||||
✔ authorize: no id (0.062515ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.069489ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.069454ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.561842ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.149422ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.4084ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.368569ms)
|
||||
✔ binding: empty allowlists refuse (0.374772ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.468965ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.6009ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.92465ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.615372ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.411698ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (106.654393ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (6.38343ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (403.239559ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (180.539463ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (118.674373ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.816907ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.367893ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (17.692579ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (18.058789ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (2.374568ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.107055ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.261762ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.934659ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (34.277506ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.171679ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (2.378805ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.154108ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.159283ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.356294ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (6.220538ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (4.597752ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.570117ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (6.052237ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (4.873245ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (2.223108ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (4.623839ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (2.797385ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (6.379171ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.834078ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (5.345558ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.5404ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.759062ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.97261ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.173579ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.33179ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.233988ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.672229ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.400304ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.415409ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (46.132355ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (56.214513ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (53.595986ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (365.304662ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (226.989441ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (112.933314ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (227.476041ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (125.467088ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (212.312892ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.425816ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.242552ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.45962ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (428.337404ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (24.377686ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (46.338052ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.530913ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.794592ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.478666ms)
|
||||
✔ gateway: op 9 resumable resumes (0.332182ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.763926ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.432209ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.382755ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (68.524866ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (63.94703ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (136.516427ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.527505ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (91.998853ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (88.154245ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (88.624448ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (186.984633ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (63.999475ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (81.504909ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (189.19431ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (752.117747ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (3.995149ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (84.923031ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.893848ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.096948ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (57.919952ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (335.710733ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.356444ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (22.858589ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.00731ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (39.905088ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (132.683955ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (78.784082ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.42906ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.314265ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.029434ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.771295ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (57.344136ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (49.411468ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (45.986284ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (73.96153ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (631.660709ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.746742ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (5.212156ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.81499ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.734504ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.931204ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (6.372629ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.548172ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (2.40931ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (3.034413ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (32.390237ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (16.599589ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (7.916278ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (3.815668ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.748293ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.21768ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1015.152052ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.428072ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (5.8981ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.208583ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.203371ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.229417ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (7.811889ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.532553ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (4.825032ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (4.437295ms)
|
||||
✔ tools: listing and search caps hold (15.185943ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.963757ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (5.725773ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.280921ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.069305ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.297714ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.666493ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.866652ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.112619ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.209453ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1029.690379ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.100777ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.217931ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.551203ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.197871ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 21848.550915
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/host.test.mjs:271:1
|
||||
✖ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (141.222031ms)
|
||||
AssertionError [ERR_ASSERTION]: The validation function is expected to return "true". Received false
|
||||
|
||||
Caught error:
|
||||
|
||||
Error: write EPIPE
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:279:3)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: Error: write EPIPE
|
||||
at epipe (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:113:24)
|
||||
at file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:276:51
|
||||
at ChildProcess.send (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:94:23)
|
||||
at startHost (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/src/host.mjs:150:36)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async waitForActual (node:assert:615:5)
|
||||
at async strict.rejects (node:assert:738:25)
|
||||
at async TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:279:3)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7),
|
||||
operator: 'rejects',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (30.190261ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (34.551942ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (19.199556ms)
|
||||
✔ decide prints a declining choice as declining (17.919345ms)
|
||||
✔ an unknown outcome is reported once and never resent (21.964825ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (26.414966ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (18.731365ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (23.234187ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (18.288267ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (21.811173ms)
|
||||
✔ agents and tasks print through the broker (16.927124ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.522903ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (77.396725ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (60.485574ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (56.355736ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (63.169257ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (60.166926ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (95.495542ms)
|
||||
✔ empty views say so (1.140766ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.363868ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.220362ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (971.553376ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (159.429205ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (77.209615ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (129.794763ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (125.291949ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (87.645136ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (121.483554ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (90.276229ms)
|
||||
✖ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (138.303819ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (25.158056ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (203.221938ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (214.257266ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (87.538076ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (604.522432ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (26.482392ms)
|
||||
✔ zoned uses the IANA zone across DST (27.362837ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (33.004841ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (21.768645ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.421146ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (19.006717ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (27.839774ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (25.77886ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (25.041516ms)
|
||||
✔ 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 (161.21144ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (61.112773ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (20.296917ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (11.449409ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.869409ms)
|
||||
✔ no Discord id reaches the journal or the log (10.32983ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.884583ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (2.513173ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.403405ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.464089ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.587224ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.591962ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.388675ms)
|
||||
✔ 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.545456ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.429635ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.477638ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.340235ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.375699ms)
|
||||
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 (372.337838ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (57.246794ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2297.274981ms)
|
||||
✔ busExit and refuseInsideAgent (0.421121ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.030284ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (2.102798ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.552857ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.517368ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (18.089251ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (8.925536ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (7.027423ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.33504ms)
|
||||
✔ authorize: open channel, listed user (1.931272ms)
|
||||
✔ authorize: wrong guild (0.189982ms)
|
||||
✔ authorize: no guild (DM) (0.157779ms)
|
||||
✔ authorize: unlisted channel (0.18683ms)
|
||||
✔ authorize: unknown channel, no info (0.146678ms)
|
||||
✔ authorize: thread of listed parent (0.197969ms)
|
||||
✔ authorize: thread of unlisted parent (0.145916ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.122708ms)
|
||||
✔ authorize: unlisted user (0.183533ms)
|
||||
✔ authorize: no author (0.264893ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.259968ms)
|
||||
✔ authorize: system author (0.14774ms)
|
||||
✔ authorize: the bot itself (0.116946ms)
|
||||
✔ authorize: webhook (0.106236ms)
|
||||
✔ authorize: mention channel without mention (0.150081ms)
|
||||
✔ authorize: mention channel with bot mention (0.176439ms)
|
||||
✔ authorize: mention channel with @everyone only (0.499201ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.110156ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.122991ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.3239ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.096298ms)
|
||||
✔ authorize: thread in another guild per channel info (0.075588ms)
|
||||
✔ authorize: not an object (0.062351ms)
|
||||
✔ authorize: no id (0.06053ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.073567ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.058582ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.485945ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.152046ms)
|
||||
✔ binding: a complete binding validates and is frozen (3.425684ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (2.141945ms)
|
||||
✔ binding: empty allowlists refuse (0.590751ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.751669ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.988316ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (1.730047ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.795508ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.241415ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (116.502152ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (4.661386ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (422.471569ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (214.927913ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (150.721805ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.951144ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.570816ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (17.646576ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (17.812594ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.558125ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.177876ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.170751ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.6356ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (33.578277ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.157117ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.54556ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.128202ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.80801ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (34.412926ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (6.548021ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (4.511124ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.63614ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (7.130849ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (4.028945ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.862027ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (4.820621ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (3.905224ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (4.344559ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (3.159632ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (4.504756ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.422853ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.883512ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.965183ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.334097ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.562132ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.225122ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.667398ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.600949ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.441715ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (63.624535ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (57.05796ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (61.662112ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (366.995448ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (235.440028ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (104.064014ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (231.265489ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (127.256069ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (213.85529ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (613.568922ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.335897ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.525471ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (428.342611ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (25.113013ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (47.5138ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.318652ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.510866ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.491385ms)
|
||||
✔ gateway: op 9 resumable resumes (0.353346ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.836841ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.54704ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.61774ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (75.467916ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (77.217847ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (134.680187ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.483544ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (99.008648ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (90.548843ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (92.799709ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (229.565788ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (92.029415ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (97.074074ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (204.321635ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (743.152909ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.713142ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (96.468494ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.607066ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (5.301534ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (70.262641ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (336.611061ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.39441ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (24.448638ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.485653ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (49.85054ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (154.040521ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (103.798693ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.644128ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.043981ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.403883ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (2.16611ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (61.108012ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (61.196009ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (46.242697ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (75.100233ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (699.071346ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.419085ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (5.005535ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.613088ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.674523ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (1.041611ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (6.097134ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.337198ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (2.203316ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.903674ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (26.819093ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (12.05144ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (8.018042ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (5.178702ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.773509ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.541534ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1016.06891ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.427365ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (5.948754ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.180273ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.199006ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.337322ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.560483ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.837649ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (4.925993ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (3.759524ms)
|
||||
✔ tools: listing and search caps hold (10.773015ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.929124ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (5.225338ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.293259ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.213271ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.591889ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.975545ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.905601ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.360302ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.263509ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1027.728519ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.167855ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.226639ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.576987ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.185515ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 22115.416853
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/host.test.mjs:301:1
|
||||
✖ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (138.303819ms)
|
||||
Error: write EPIPE
|
||||
at epipe (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:113:24)
|
||||
at file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:307:71
|
||||
at ChildProcess.send (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:94:23)
|
||||
at Object.close (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/src/host.mjs:184:36)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:310:27)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
code: 'EPIPE',
|
||||
errno: -32,
|
||||
syscall: 'write'
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (27.725004ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (34.861808ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (28.564691ms)
|
||||
✔ decide prints a declining choice as declining (25.178783ms)
|
||||
✔ an unknown outcome is reported once and never resent (15.039452ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (17.756226ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (19.747332ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (14.956081ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (21.351565ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (18.257531ms)
|
||||
✔ agents and tasks print through the broker (14.04227ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.094164ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (73.122818ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (55.622341ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (63.195833ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (56.787227ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (56.402734ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (89.866092ms)
|
||||
✔ empty views say so (0.866121ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.127362ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.255718ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (967.30061ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (150.187073ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (78.707426ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (127.111373ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (129.814801ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (78.327248ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (133.345574ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (75.733107ms)
|
||||
✖ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (133.706712ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (23.956798ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.478231ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (204.490025ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (91.936624ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (599.011152ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (26.458927ms)
|
||||
✔ zoned uses the IANA zone across DST (25.33944ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (35.749269ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (24.558137ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (1.37865ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (19.234281ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (34.395104ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (25.592443ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (19.345267ms)
|
||||
✔ 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 (142.375961ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (59.048147ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (15.977223ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (10.041438ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.798456ms)
|
||||
✔ no Discord id reaches the journal or the log (10.086742ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.456131ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (2.563806ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.348498ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.498688ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.574324ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.095986ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.376607ms)
|
||||
✔ 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.564945ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.436412ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.4563ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.417583ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.049151ms)
|
||||
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 (372.651182ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (62.998163ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2274.624121ms)
|
||||
✔ busExit and refuseInsideAgent (3.612584ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.003148ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.443646ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.543017ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.584522ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (17.38783ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (7.810789ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (6.60538ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.270888ms)
|
||||
✔ authorize: open channel, listed user (1.81043ms)
|
||||
✔ authorize: wrong guild (0.186899ms)
|
||||
✔ authorize: no guild (DM) (0.179106ms)
|
||||
✔ authorize: unlisted channel (0.198354ms)
|
||||
✔ authorize: unknown channel, no info (0.153258ms)
|
||||
✔ authorize: thread of listed parent (0.195673ms)
|
||||
✔ authorize: thread of unlisted parent (0.1863ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.125952ms)
|
||||
✔ authorize: unlisted user (0.257968ms)
|
||||
✔ authorize: no author (0.253408ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.910052ms)
|
||||
✔ authorize: system author (0.226458ms)
|
||||
✔ authorize: the bot itself (0.090903ms)
|
||||
✔ authorize: webhook (0.086991ms)
|
||||
✔ authorize: mention channel without mention (0.119171ms)
|
||||
✔ authorize: mention channel with bot mention (0.140139ms)
|
||||
✔ authorize: mention channel with @everyone only (0.104111ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.088684ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.124394ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.100233ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.072504ms)
|
||||
✔ authorize: thread in another guild per channel info (0.091162ms)
|
||||
✔ authorize: not an object (0.06125ms)
|
||||
✔ authorize: no id (0.059814ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.061261ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.063628ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.461563ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.141433ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.477734ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.305401ms)
|
||||
✔ binding: empty allowlists refuse (0.329257ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.110092ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.344477ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.450987ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.523266ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.334094ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (115.303312ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (4.802188ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (391.525902ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (180.216922ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (138.675771ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.874097ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.095084ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (19.792175ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (16.836111ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.616706ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.202123ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.529595ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.5809ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (32.199597ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (4.930766ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.210198ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.108887ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.637519ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.43494ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (7.552117ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (5.27629ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.204142ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (5.167342ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.837091ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (1.494918ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (2.722664ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (3.707717ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (5.258541ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.610759ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (5.113588ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.848003ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.764722ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.860669ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.164086ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.391768ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.456197ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.982474ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.511074ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.405558ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (60.224358ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (51.874251ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (54.732804ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (370.960466ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (229.920416ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (104.197407ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (228.740568ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (128.697407ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (214.476669ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.00822ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.349876ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.482971ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (428.004547ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (24.437217ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (48.025521ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.418776ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.587165ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.958786ms)
|
||||
✔ gateway: op 9 resumable resumes (0.316817ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.896081ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.53155ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.719708ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (72.577945ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (64.073997ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (136.680182ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.466574ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (98.52451ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (89.332896ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (83.826305ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (198.687787ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (70.744788ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (80.054235ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (207.681334ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (758.537557ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.8765ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (100.147953ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (2.902376ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (6.615975ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (56.645221ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (329.179783ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.39827ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (24.821806ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.165319ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (46.28033ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (141.204747ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (82.427663ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.411306ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.321794ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.193377ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (2.956844ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (49.026651ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (51.93831ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (58.71983ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (77.918761ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (639.543651ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.402223ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (6.070935ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.621755ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.720534ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.812191ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (8.506332ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.353446ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.566329ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.108639ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (27.257144ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (10.800698ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (8.090073ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.561691ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.673874ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (4.623866ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1015.500646ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.424022ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (6.004408ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.212984ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.226511ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.263028ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.407763ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.480263ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (6.143727ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (4.402021ms)
|
||||
✔ tools: listing and search caps hold (8.754398ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.858684ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (6.786359ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.46187ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.253895ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (6.957183ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.82702ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.767986ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.611241ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.410561ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1028.827543ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.196143ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.235084ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.83305ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.244223ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 22074.480532
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/host.test.mjs:301:1
|
||||
✖ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (133.706712ms)
|
||||
Error: write EPIPE
|
||||
at epipe (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:113:24)
|
||||
at file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:307:71
|
||||
at ChildProcess.send (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:94:23)
|
||||
at Object.close (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/src/host.mjs:188:34)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:310:16)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
code: 'EPIPE',
|
||||
errno: -32,
|
||||
syscall: 'write'
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (22.726199ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (40.099193ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (18.669567ms)
|
||||
✔ decide prints a declining choice as declining (25.574997ms)
|
||||
✔ an unknown outcome is reported once and never resent (16.609845ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (18.22855ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (24.848441ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (14.341316ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (31.557999ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (21.149079ms)
|
||||
✔ agents and tasks print through the broker (21.441955ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (3.723751ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (67.432148ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (60.256213ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (61.543467ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (57.032146ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (68.288178ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (84.188563ms)
|
||||
✔ empty views say so (0.981318ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.374963ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.344917ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (978.255238ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (138.645401ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (72.426735ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (125.339275ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (120.370682ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (68.565388ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (121.631302ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (75.252395ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (133.43766ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (28.224116ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.350116ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (197.47672ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (85.754313ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (594.820116ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (25.981795ms)
|
||||
✔ zoned uses the IANA zone across DST (31.364199ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (25.098743ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (24.076035ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.355916ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (22.682652ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (26.736546ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (28.784885ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (22.541675ms)
|
||||
✔ 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 (158.299538ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (56.517676ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (16.572217ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (10.163424ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.713737ms)
|
||||
✔ no Discord id reaches the journal or the log (12.146805ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.384862ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (2.726507ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.314435ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.671033ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.648588ms)
|
||||
✖ the journal: a line with a wrong type refuses with exit 3 and names the field (1.368848ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.426037ms)
|
||||
✔ 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.586933ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.403667ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.50309ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.284395ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (110.786515ms)
|
||||
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 (377.492026ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (73.721744ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2284.728472ms)
|
||||
✔ busExit and refuseInsideAgent (0.411573ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.119289ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.484911ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.557944ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.490382ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (16.016332ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (7.648142ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (7.801007ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.466221ms)
|
||||
✔ authorize: open channel, listed user (1.495214ms)
|
||||
✔ authorize: wrong guild (0.184132ms)
|
||||
✔ authorize: no guild (DM) (0.158573ms)
|
||||
✔ authorize: unlisted channel (0.187997ms)
|
||||
✔ authorize: unknown channel, no info (0.172406ms)
|
||||
✔ authorize: thread of listed parent (0.206847ms)
|
||||
✔ authorize: thread of unlisted parent (0.136909ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.135584ms)
|
||||
✔ authorize: unlisted user (0.182579ms)
|
||||
✔ authorize: no author (0.259783ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.151355ms)
|
||||
✔ authorize: system author (0.13244ms)
|
||||
✔ authorize: the bot itself (0.09681ms)
|
||||
✔ authorize: webhook (0.124532ms)
|
||||
✔ authorize: mention channel without mention (0.180748ms)
|
||||
✔ authorize: mention channel with bot mention (3.388141ms)
|
||||
✔ authorize: mention channel with @everyone only (0.246928ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.100541ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.091404ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.106613ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.077901ms)
|
||||
✔ authorize: thread in another guild per channel info (0.075693ms)
|
||||
✔ authorize: not an object (0.077901ms)
|
||||
✔ authorize: no id (0.069592ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.074525ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.068144ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.544098ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.174489ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.666765ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.251329ms)
|
||||
✔ binding: empty allowlists refuse (0.367955ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.247392ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.852648ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (1.231469ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.680945ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.274394ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (108.009805ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (2.019763ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (408.305407ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (198.458301ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (119.546867ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (2.185243ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.101053ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (20.666651ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (16.91693ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.482484ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.16619ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.390299ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.838456ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (33.60475ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.143404ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.880149ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.824679ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.245108ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.364214ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (8.800024ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (6.313398ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.949918ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (9.378475ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (3.77505ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.826286ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (5.029445ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (2.002168ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (4.599938ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (3.425683ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (7.105805ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.39627ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.730082ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.790504ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.175493ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.460952ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.103831ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.771749ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.603922ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.431817ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (50.800684ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (59.155256ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (56.618599ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (351.382076ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (235.187205ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (112.804001ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (226.221691ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (125.729691ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (213.9388ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.299749ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.274359ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.955363ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (429.833023ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (24.641738ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (43.387829ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.294965ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.623289ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.473724ms)
|
||||
✔ gateway: op 9 resumable resumes (0.338716ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.839054ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.486022ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.50621ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (68.972907ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (67.428241ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (154.569672ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.470823ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (96.889348ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (89.306211ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (90.186108ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (198.413848ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (62.187887ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (75.702707ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (188.888624ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (731.68452ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.796711ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (97.691222ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.596545ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.860128ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (64.069801ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (336.680712ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.409477ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (22.121286ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (0.945829ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (35.735586ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (142.138184ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (80.656688ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.408453ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.32331ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.75518ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (2.184563ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (83.350178ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (55.261995ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (44.622915ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (71.811221ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (627.650094ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (4.382351ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (3.196873ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.656126ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.737402ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.79421ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.166104ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.553735ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (2.499601ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (5.618046ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (33.45824ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (14.305094ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (9.194548ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (5.053531ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.793119ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (4.206796ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1015.545737ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.432412ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (6.039886ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.194603ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.199809ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.23459ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (5.436958ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.085675ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.861864ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (4.887542ms)
|
||||
✔ tools: listing and search caps hold (14.009015ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (1.219245ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (5.909847ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.444722ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.502471ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.795596ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.316308ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.824395ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.516548ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.433468ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1024.75181ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.250146ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.228631ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (3.02676ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.286185ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3104.897055
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:332:1
|
||||
✖ the journal: a line with a wrong type refuses with exit 3 and names the field (1.368848ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception: at: {"at":"2026-10-08","kind":"dm","decision":"a","outcome":"confirmed","messageId":"1"}
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:361:12)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: undefined,
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (24.158742ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (36.553469ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (18.076978ms)
|
||||
✔ decide prints a declining choice as declining (24.162789ms)
|
||||
✔ an unknown outcome is reported once and never resent (15.341305ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (17.762754ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (18.842692ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (16.674793ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (17.1648ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (20.246485ms)
|
||||
✔ agents and tasks print through the broker (22.345053ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.083256ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (69.210037ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (55.212696ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (55.68185ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (57.395285ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (66.335789ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (81.547046ms)
|
||||
✔ empty views say so (1.154157ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.286046ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.200556ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (963.804965ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (144.952239ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (78.042193ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (122.383088ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (120.271078ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (73.289915ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (118.88565ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (75.403813ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (135.474054ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (22.19171ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.699633ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (203.124317ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (88.133209ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (602.377329ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (28.269352ms)
|
||||
✔ zoned uses the IANA zone across DST (24.967725ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (24.332286ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (19.766757ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.35656ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (22.417887ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (24.46273ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (27.250478ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (18.451332ms)
|
||||
✔ 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 (146.832363ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (68.028414ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (15.778487ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (9.677167ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.706508ms)
|
||||
✔ no Discord id reaches the journal or the log (9.774456ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.082679ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (2.926156ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.240621ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.50467ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.591536ms)
|
||||
✖ the journal: a line with a wrong type refuses with exit 3 and names the field (1.443028ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.364006ms)
|
||||
✔ 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.513258ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.383339ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.581415ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.290206ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.30446ms)
|
||||
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 (359.572088ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (53.589497ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2257.255025ms)
|
||||
✔ busExit and refuseInsideAgent (0.469498ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.123227ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.639687ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.555273ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.465523ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (17.86442ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (6.901415ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (5.9026ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.03818ms)
|
||||
✔ authorize: open channel, listed user (1.737541ms)
|
||||
✔ authorize: wrong guild (0.192858ms)
|
||||
✔ authorize: no guild (DM) (0.271658ms)
|
||||
✔ authorize: unlisted channel (0.190478ms)
|
||||
✔ authorize: unknown channel, no info (0.17056ms)
|
||||
✔ authorize: thread of listed parent (0.196923ms)
|
||||
✔ authorize: thread of unlisted parent (0.145197ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.130481ms)
|
||||
✔ authorize: unlisted user (0.851026ms)
|
||||
✔ authorize: no author (0.279706ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.133807ms)
|
||||
✔ authorize: system author (0.118535ms)
|
||||
✔ authorize: the bot itself (0.073182ms)
|
||||
✔ authorize: webhook (0.083921ms)
|
||||
✔ authorize: mention channel without mention (0.125346ms)
|
||||
✔ authorize: mention channel with bot mention (0.707573ms)
|
||||
✔ authorize: mention channel with @everyone only (0.175719ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.167736ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.096688ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.090619ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.07697ms)
|
||||
✔ authorize: thread in another guild per channel info (0.075341ms)
|
||||
✔ authorize: not an object (0.067585ms)
|
||||
✔ authorize: no id (0.061902ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.068441ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.061633ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.442616ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.149945ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.726218ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.352258ms)
|
||||
✔ binding: empty allowlists refuse (0.383053ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.288902ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (2.280412ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.4798ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.927939ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.551198ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (109.193081ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (2.05798ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (391.396888ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (184.213524ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (146.290668ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (2.149828ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.245039ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (18.063291ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (16.092513ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.651459ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.204628ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.470906ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.732986ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (33.235227ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (4.081434ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.190745ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.666723ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.90144ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.083308ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (9.88305ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (4.109129ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (5.294589ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (7.460322ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (4.066995ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.987533ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (9.087361ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (3.278507ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (4.432537ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (3.228616ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (4.262337ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.639558ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.86637ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.510851ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.195851ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.346978ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.187305ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.643748ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.594534ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.408496ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (52.577893ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (51.071115ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (55.303446ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (365.328274ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (235.660705ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (104.173269ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (227.480275ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (126.243511ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (213.363735ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.192422ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.329328ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.493796ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (426.018107ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (24.134374ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (46.391758ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (1.992776ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.647812ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.477697ms)
|
||||
✔ gateway: op 9 resumable resumes (0.336546ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.767568ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.604046ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.560699ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (66.767761ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (65.891655ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (134.261181ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.34243ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (88.725305ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (78.848385ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (86.752223ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (193.898386ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (73.617059ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (81.155433ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (197.350031ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (737.929872ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.809675ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (94.882871ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (2.687739ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.687967ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (65.848385ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (322.078843ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.448333ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (23.107123ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (0.919341ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (36.127689ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (128.744857ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (87.145498ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.409792ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.572224ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.508155ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.428227ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (56.614624ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (54.082125ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (44.075075ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (72.766226ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (637.796866ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.465114ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (5.732941ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.560541ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.717624ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.851678ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (10.954585ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.150362ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.683606ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (3.174145ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (29.628685ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (10.957921ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (7.512049ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.738692ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.659027ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.49639ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1015.739149ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.445536ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (7.003404ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.284995ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.204298ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.291654ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.018174ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.054626ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.134532ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (5.138343ms)
|
||||
✔ tools: listing and search caps hold (13.624938ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.974199ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (6.73413ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.163623ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (2.061201ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.958608ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.387374ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.633358ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.722956ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.477962ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1027.538954ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.499409ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.215807ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.858386ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.214932ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3100.5543
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:332:1
|
||||
✖ the journal: a line with a wrong type refuses with exit 3 and names the field (1.443028ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception: decision: {"at":"2026-10-08T12:00:00.000Z","kind":"dm","decision":7,"outcome":"confirmed","messageId":"1"}
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:361:12)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: undefined,
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (37.422929ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (37.071594ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (21.080584ms)
|
||||
✔ decide prints a declining choice as declining (20.04342ms)
|
||||
✔ an unknown outcome is reported once and never resent (14.804159ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (22.098609ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (18.347239ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (18.919893ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (17.236435ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (17.689522ms)
|
||||
✔ agents and tasks print through the broker (15.65574ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.214317ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (82.349948ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (58.323357ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (61.117009ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (69.683038ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (54.893936ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (86.724411ms)
|
||||
✔ empty views say so (1.085073ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.382591ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.23399ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (970.508565ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (147.889727ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (73.9945ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (120.130908ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (120.81105ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (70.561705ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (139.814459ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (75.086439ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (136.714994ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (22.234726ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.280724ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (201.883187ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (92.117037ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (601.515435ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (28.052979ms)
|
||||
✔ zoned uses the IANA zone across DST (26.821343ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (36.678724ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (27.439441ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.358875ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (23.631759ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (26.362556ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (31.38823ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (25.761153ms)
|
||||
✔ 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 (148.316194ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (60.872804ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (15.323618ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (9.754695ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.769083ms)
|
||||
✔ no Discord id reaches the journal or the log (10.734013ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.221893ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (3.613008ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.698013ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.634529ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.608086ms)
|
||||
✖ the journal: a line with a wrong type refuses with exit 3 and names the field (2.587832ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.407309ms)
|
||||
✔ 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.675645ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.433644ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.475756ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.282334ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.633409ms)
|
||||
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 (405.859855ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (61.851914ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2262.673488ms)
|
||||
✔ busExit and refuseInsideAgent (0.429279ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.418979ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.936612ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.5768ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.530584ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (19.192158ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (7.118732ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (8.146959ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (3.561858ms)
|
||||
✔ authorize: open channel, listed user (1.809982ms)
|
||||
✔ authorize: wrong guild (0.194538ms)
|
||||
✔ authorize: no guild (DM) (0.178467ms)
|
||||
✔ authorize: unlisted channel (0.181147ms)
|
||||
✔ authorize: unknown channel, no info (0.329193ms)
|
||||
✔ authorize: thread of listed parent (0.193682ms)
|
||||
✔ authorize: thread of unlisted parent (0.159838ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.144147ms)
|
||||
✔ authorize: unlisted user (0.199061ms)
|
||||
✔ authorize: no author (0.543357ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.255464ms)
|
||||
✔ authorize: system author (0.131552ms)
|
||||
✔ authorize: the bot itself (0.101056ms)
|
||||
✔ authorize: webhook (0.098724ms)
|
||||
✔ authorize: mention channel without mention (0.137969ms)
|
||||
✔ authorize: mention channel with bot mention (0.13531ms)
|
||||
✔ authorize: mention channel with @everyone only (0.163329ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.086988ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.08537ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.084608ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.09232ms)
|
||||
✔ authorize: thread in another guild per channel info (0.086332ms)
|
||||
✔ authorize: not an object (0.069381ms)
|
||||
✔ authorize: no id (0.080801ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.077372ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.073449ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.485697ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.154697ms)
|
||||
✔ binding: a complete binding validates and is frozen (3.103639ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.886486ms)
|
||||
✔ binding: empty allowlists refuse (0.455986ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.524388ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.698656ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.462978ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.364085ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.505284ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (115.667508ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (3.155697ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (399.506418ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (182.648547ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (132.281894ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.772792ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.187514ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (19.853958ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (22.563378ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.520334ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.344843ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.665819ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (4.529538ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (34.448457ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (4.821618ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.227141ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.444179ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.244218ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (33.377918ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (12.104754ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (5.519883ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.140638ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (4.799755ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.643622ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.732274ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (3.684933ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (3.521755ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (5.067725ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (3.448702ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (6.310496ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.566141ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.932251ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.881546ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.439167ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.427123ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (3.182141ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (2.184927ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.726063ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.412464ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (57.032018ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (53.417567ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (47.006518ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (360.320218ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (229.716776ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (114.522026ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (225.499766ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (126.961635ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (212.478207ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.178135ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.428943ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.543708ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (428.549809ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (28.379874ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (47.340845ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.649166ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.623281ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.548057ms)
|
||||
✔ gateway: op 9 resumable resumes (0.34792ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.935919ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.567601ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.463194ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (67.664274ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (68.192463ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (131.318957ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.461261ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (98.992052ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (86.410784ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (82.56538ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (191.505094ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (68.717608ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (84.934ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (193.056389ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (744.784228ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.916805ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (92.126751ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.22946ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.505797ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (69.861407ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (342.848681ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.445998ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (21.897438ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (0.982212ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (41.258927ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (127.366567ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (89.381422ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.695283ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.377191ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (4.412125ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.892844ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (57.118544ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (54.352578ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (45.339287ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (86.40136ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (641.518432ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.520334ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (9.428935ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (1.259518ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (3.51936ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.871197ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (6.934274ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.090368ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.636415ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.098729ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (27.439635ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (15.747362ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (7.69182ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.387431ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.80604ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.718797ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1011.014195ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.426891ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (5.968259ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.28499ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.208434ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.371375ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (3.890958ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (3.670054ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (4.777472ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (3.413435ms)
|
||||
✔ tools: listing and search caps hold (10.026553ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.642266ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (5.26989ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.340782ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.329359ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (6.961826ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (4.569406ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (2.056576ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.951236ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.425029ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1031.295405ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.111749ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.233388ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.825682ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.191929ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3159.188008
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:332:1
|
||||
✖ the journal: a line with a wrong type refuses with exit 3 and names the field (2.587832ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception: status: {"at":"2026-10-08T12:00:00.000Z","kind":"dm","decision":"a","outcome":"refused","messageId":null,"status":"403"}
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:361:12)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: undefined,
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (34.604208ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (40.054035ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (21.764354ms)
|
||||
✔ decide prints a declining choice as declining (18.885385ms)
|
||||
✔ an unknown outcome is reported once and never resent (12.460984ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (15.301957ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (14.624948ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (14.11494ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (13.866522ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (28.735889ms)
|
||||
✔ agents and tasks print through the broker (26.540393ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.085676ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (76.076506ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (57.037896ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (63.969567ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (66.097615ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (72.570193ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (82.532869ms)
|
||||
✔ empty views say so (1.224819ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (2.068407ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.254337ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (981.60008ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (157.257907ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (86.390523ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (132.002306ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (126.289403ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (81.164738ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (136.059927ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (69.451464ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (132.250025ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (23.799123ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.306365ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (210.88687ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (86.041721ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (596.152761ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (28.251643ms)
|
||||
✔ zoned uses the IANA zone across DST (30.087786ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (45.403788ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (26.393587ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.466722ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (20.101786ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (22.273066ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (21.487198ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (21.779764ms)
|
||||
✔ 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 (149.889362ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (66.436359ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (17.96293ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (9.987668ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.761583ms)
|
||||
✔ no Discord id reaches the journal or the log (10.380825ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (1.978632ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (3.105246ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.34651ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.511658ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.651439ms)
|
||||
✖ the journal: a line with a wrong type refuses with exit 3 and names the field (2.210922ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.401803ms)
|
||||
✔ 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.517836ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.467409ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.481863ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.290913ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (110.78117ms)
|
||||
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 (404.261782ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (72.775506ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2268.694712ms)
|
||||
✔ busExit and refuseInsideAgent (3.5789ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.540198ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.725294ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.635284ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.491455ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (20.224505ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (6.8625ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (7.525788ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.473542ms)
|
||||
✔ authorize: open channel, listed user (1.820629ms)
|
||||
✔ authorize: wrong guild (0.202127ms)
|
||||
✔ authorize: no guild (DM) (0.168119ms)
|
||||
✔ authorize: unlisted channel (0.222404ms)
|
||||
✔ authorize: unknown channel, no info (0.174177ms)
|
||||
✔ authorize: thread of listed parent (0.196227ms)
|
||||
✔ authorize: thread of unlisted parent (0.183736ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.155604ms)
|
||||
✔ authorize: unlisted user (0.213546ms)
|
||||
✔ authorize: no author (0.942744ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.151721ms)
|
||||
✔ authorize: system author (0.115806ms)
|
||||
✔ authorize: the bot itself (0.076729ms)
|
||||
✔ authorize: webhook (0.094283ms)
|
||||
✔ authorize: mention channel without mention (0.218164ms)
|
||||
✔ authorize: mention channel with bot mention (0.170093ms)
|
||||
✔ authorize: mention channel with @everyone only (0.097306ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.075246ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.13087ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.105726ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.082724ms)
|
||||
✔ authorize: thread in another guild per channel info (0.084219ms)
|
||||
✔ authorize: not an object (0.064336ms)
|
||||
✔ authorize: no id (0.069861ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.386853ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.071995ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.426175ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.147486ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.480336ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.28277ms)
|
||||
✔ binding: empty allowlists refuse (0.391807ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.343858ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (2.091459ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.496759ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (2.598319ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.473508ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (121.074688ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (3.12547ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (460.408864ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (201.545322ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (140.954891ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.972347ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.18554ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (20.895686ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (17.94536ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.709978ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.452756ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.704967ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (3.226804ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (32.267094ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.551428ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (2.093712ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.285814ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.717728ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (33.575061ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (7.80267ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (8.43569ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (8.428094ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (10.332193ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (7.290513ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.968591ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (9.896868ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (2.438948ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (3.944514ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.264162ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (3.860405ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.50394ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.781343ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.848896ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.161653ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.496068ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.448818ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.866369ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.773121ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.485371ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (55.57274ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (51.366865ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (49.824237ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (383.049001ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (255.898307ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (105.220681ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (228.931619ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (125.909235ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (212.316315ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (615.239112ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.391394ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.516072ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (427.235616ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (27.385271ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (46.991947ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.440667ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.602604ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.524766ms)
|
||||
✔ gateway: op 9 resumable resumes (0.301042ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.831424ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.573224ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.510912ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (75.714083ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (72.684299ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (152.536589ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.445177ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (86.357762ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (139.426177ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (96.914383ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (204.70006ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (72.011335ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (95.687033ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (213.624448ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (770.360271ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (6.599884ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (88.898613ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (2.372596ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (11.477013ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (86.475051ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (381.975631ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.440673ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (24.912468ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.058676ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (39.512903ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (148.189775ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (94.252275ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.432961ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (4.87512ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.358779ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (2.505854ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (71.220699ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (48.053437ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (40.728666ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (86.81428ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (719.30105ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.451802ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (6.109889ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.694139ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.755482ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (1.392521ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (8.251677ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (5.351041ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.934493ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (8.265875ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (45.998311ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (11.958637ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (7.131065ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (3.483135ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.545878ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (2.431714ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1010.812178ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.44209ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (6.052799ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.249708ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.205669ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.43342ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (3.517176ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (3.670294ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.015399ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (4.081116ms)
|
||||
✔ tools: listing and search caps hold (11.352476ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.960546ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (5.540842ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.286092ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.754676ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (6.392339ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.992815ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.878643ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.88394ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.271061ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1029.910195ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.099399ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.216198ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.522492ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.213869ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3192.79447
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:332:1
|
||||
✖ the journal: a line with a wrong type refuses with exit 3 and names the field (2.210922ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception: messageId: {"at":"2026-10-08T12:00:00.000Z","kind":"dm","decision":"a","outcome":"confirmed","messageId":null}
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:361:12)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: undefined,
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (34.46118ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (42.718341ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (20.250013ms)
|
||||
✔ decide prints a declining choice as declining (22.384696ms)
|
||||
✔ an unknown outcome is reported once and never resent (17.567777ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (17.172845ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (18.050554ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (16.2269ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (20.88717ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (21.971726ms)
|
||||
✔ agents and tasks print through the broker (18.828716ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.666498ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (80.650649ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (63.900192ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (65.390021ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (59.739908ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (65.909697ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (105.349397ms)
|
||||
✔ empty views say so (1.182027ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.557529ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.300621ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (998.628257ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (147.164935ms)
|
||||
✖ a second host for the same data root refuses with exit 3 while the first runs (122.303686ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (124.107194ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (127.702424ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (72.658936ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (129.777837ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (85.684017ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (135.70576ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (22.4901ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.67338ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (204.802721ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (84.46603ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (593.406958ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (25.651811ms)
|
||||
✔ zoned uses the IANA zone across DST (36.475422ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (37.313587ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (30.440144ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.315972ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (26.519409ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (27.850237ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (31.924286ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (21.392784ms)
|
||||
✔ 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 (159.653661ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (57.455517ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (17.40533ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (14.568724ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (1.069928ms)
|
||||
✔ no Discord id reaches the journal or the log (10.98208ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.975297ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (2.591215ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.807701ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.559725ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.637835ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.080466ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.373419ms)
|
||||
✔ 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.515202ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.397955ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.557113ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.29972ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (110.543221ms)
|
||||
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 (394.176938ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (63.787445ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2296.025495ms)
|
||||
✔ busExit and refuseInsideAgent (0.395528ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.279447ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (2.185478ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (1.004833ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.574398ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (25.971319ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (9.048344ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (8.291885ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.194355ms)
|
||||
✔ authorize: open channel, listed user (2.001367ms)
|
||||
✔ authorize: wrong guild (0.186392ms)
|
||||
✔ authorize: no guild (DM) (0.350043ms)
|
||||
✔ authorize: unlisted channel (0.200012ms)
|
||||
✔ authorize: unknown channel, no info (0.277042ms)
|
||||
✔ authorize: thread of listed parent (0.230679ms)
|
||||
✔ authorize: thread of unlisted parent (0.244729ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.11864ms)
|
||||
✔ authorize: unlisted user (0.451057ms)
|
||||
✔ authorize: no author (0.31611ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.164631ms)
|
||||
✔ authorize: system author (0.132288ms)
|
||||
✔ authorize: the bot itself (0.105121ms)
|
||||
✔ authorize: webhook (0.107934ms)
|
||||
✔ authorize: mention channel without mention (0.144878ms)
|
||||
✔ authorize: mention channel with bot mention (0.14995ms)
|
||||
✔ authorize: mention channel with @everyone only (0.154474ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.083761ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.09336ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.088262ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.078244ms)
|
||||
✔ authorize: thread in another guild per channel info (0.078727ms)
|
||||
✔ authorize: not an object (0.064903ms)
|
||||
✔ authorize: no id (0.069133ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.075616ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.07111ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.484811ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.15209ms)
|
||||
✔ binding: a complete binding validates and is frozen (4.386481ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.332464ms)
|
||||
✔ binding: empty allowlists refuse (0.384017ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.361972ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.611248ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.471599ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.524219ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.491878ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (117.338765ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (1.868015ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (456.667007ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (227.116343ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (134.077829ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.57958ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.033987ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (17.250477ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (16.534602ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.431492ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.092744ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.22347ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.101801ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (33.797309ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (4.029288ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (2.201328ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (2.207303ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.644949ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.442696ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (8.276767ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (8.763162ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.871857ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (7.48732ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (5.637294ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.755381ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (2.301092ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (6.07665ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (4.313828ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (5.279328ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (5.765377ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.645274ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.8501ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.036411ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.431338ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.669957ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (2.7424ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (2.839234ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.693109ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.438378ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (71.171023ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (57.380647ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (63.787084ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (353.115324ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (231.132813ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (106.436515ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (229.172827ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (129.11987ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (212.654554ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (613.640007ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.333944ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.501024ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (432.123477ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (24.496899ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (44.848984ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.394024ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.830966ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.490513ms)
|
||||
✔ gateway: op 9 resumable resumes (0.49192ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.884644ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.577702ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.421364ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (89.329351ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (86.724307ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (158.04282ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.389776ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (98.743587ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (97.876574ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (117.800032ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (206.476086ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (64.613298ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (85.682347ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (205.979349ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (742.056346ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.684078ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (85.196494ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (2.453684ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.724732ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (69.614151ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (412.764472ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.505546ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (25.408259ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (0.959767ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (45.763369ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (150.962069ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (82.927413ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.499479ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.168334ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.529865ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.681754ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (51.558816ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (58.749216ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (60.119927ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (107.654965ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (705.801846ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.448901ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (4.496282ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.72985ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.817955ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (1.200875ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.755088ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.463876ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.681441ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (3.579708ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (28.73342ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (13.579184ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (10.366443ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (6.764733ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.934583ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.420706ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1014.873597ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.410526ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (6.014759ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.22341ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.199103ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.362706ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (3.947093ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.013743ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.087367ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (3.862621ms)
|
||||
✔ tools: listing and search caps hold (12.164265ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.991574ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (6.601733ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.702669ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (2.210471ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (12.407443ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.805637ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (2.287762ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.500807ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.597459ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1033.147564ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.105647ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.217852ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.501422ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.148225ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3224.078743
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/host.test.mjs:191:1
|
||||
✖ a second host for the same data root refuses with exit 3 while the first runs (122.303686ms)
|
||||
AssertionError [ERR_ASSERTION]: The validation function is expected to return "true". Received false
|
||||
|
||||
Caught error:
|
||||
|
||||
CliError: broker refused to start: startup-refused
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:201:3)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: CliError: broker refused to start: startup-refused
|
||||
at startHost (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/src/host.mjs:128:11)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async waitForActual (node:assert:615:5)
|
||||
at async strict.rejects (node:assert:738:25)
|
||||
at async TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:201:3)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7),
|
||||
operator: 'rejects',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (32.785143ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (39.613318ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (20.116322ms)
|
||||
✔ decide prints a declining choice as declining (21.236149ms)
|
||||
✔ an unknown outcome is reported once and never resent (20.2856ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (18.166577ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (14.736747ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (14.823682ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (17.283709ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (17.842765ms)
|
||||
✔ agents and tasks print through the broker (20.70521ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (4.501727ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (72.271384ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (58.171466ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (55.05432ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (59.84891ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (62.183024ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (109.443955ms)
|
||||
✔ empty views say so (1.941581ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.55407ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.284878ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (983.756857ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (143.331175ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (79.253795ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (133.526182ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (130.050972ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (73.790364ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (124.285574ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (72.8748ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (137.061279ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (23.595937ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.620001ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (209.595149ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (84.673077ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (599.034968ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (26.385064ms)
|
||||
✔ zoned uses the IANA zone across DST (29.32015ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (32.093104ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (24.339487ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.380926ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (21.641485ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (24.412687ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (34.384339ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (21.950533ms)
|
||||
✔ 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 (147.176623ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (67.85557ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (17.354393ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (12.705565ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (1.292864ms)
|
||||
✔ no Discord id reaches the journal or the log (13.724635ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.43761ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (2.635875ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.877784ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.988631ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.916525ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (3.689456ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.63676ms)
|
||||
✖ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (1.002906ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.619733ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.766019ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.414363ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.963111ms)
|
||||
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 (400.540416ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (78.133318ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2261.886579ms)
|
||||
✔ busExit and refuseInsideAgent (0.404975ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.38687ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.650397ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.579376ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.436915ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (17.232529ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (7.30509ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (7.549517ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.467827ms)
|
||||
✔ authorize: open channel, listed user (2.12047ms)
|
||||
✔ authorize: wrong guild (0.221031ms)
|
||||
✔ authorize: no guild (DM) (0.320478ms)
|
||||
✔ authorize: unlisted channel (0.245649ms)
|
||||
✔ authorize: unknown channel, no info (0.246528ms)
|
||||
✔ authorize: thread of listed parent (0.236648ms)
|
||||
✔ authorize: thread of unlisted parent (0.172043ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.13838ms)
|
||||
✔ authorize: unlisted user (0.222069ms)
|
||||
✔ authorize: no author (0.295863ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.119327ms)
|
||||
✔ authorize: system author (0.118228ms)
|
||||
✔ authorize: the bot itself (0.081957ms)
|
||||
✔ authorize: webhook (0.106363ms)
|
||||
✔ authorize: mention channel without mention (0.141546ms)
|
||||
✔ authorize: mention channel with bot mention (0.156272ms)
|
||||
✔ authorize: mention channel with @everyone only (0.197264ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.090356ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.084168ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.088968ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.067423ms)
|
||||
✔ authorize: thread in another guild per channel info (0.081961ms)
|
||||
✔ authorize: not an object (0.060095ms)
|
||||
✔ authorize: no id (0.068223ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.076584ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.073325ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.487053ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.14135ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.784197ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.381278ms)
|
||||
✔ binding: empty allowlists refuse (0.314119ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.347398ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.702708ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.440487ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.873649ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.674288ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (117.953952ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (2.82238ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (435.829321ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (194.715216ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (139.446086ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (2.586151ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.722238ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (21.834744ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (22.671535ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.573802ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.415378ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.513706ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.998649ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (34.717283ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.221016ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.201271ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.315174ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.254552ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.185363ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (7.598939ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (4.368063ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (2.122311ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (6.679614ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.043552ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.788093ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (4.640348ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (1.831719ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (5.489307ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.03311ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (5.200112ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (3.011081ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.926233ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.053884ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.282724ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.545135ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.218583ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.785159ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (3.403628ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.477039ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (62.576182ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (48.928465ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (56.275209ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (369.007156ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (230.620366ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (104.775658ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (232.86874ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (131.936448ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (212.379176ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.893284ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.354551ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.517091ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (427.462122ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (24.835689ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (44.920356ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.795155ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (2.042813ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.555113ms)
|
||||
✔ gateway: op 9 resumable resumes (0.604983ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.887955ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.634997ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.506437ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (69.440858ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (59.404187ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (144.905989ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.469977ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (125.120538ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (92.662615ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (87.436344ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (207.118827ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (73.66507ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (98.868478ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (206.555736ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (760.903329ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.182619ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (96.044054ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.281212ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.740846ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (60.70269ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (380.809018ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.433994ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (25.947937ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (0.934064ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (35.156902ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (144.969601ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (90.638653ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.49102ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.69234ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.086177ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.85341ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (63.7668ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (62.610912ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (52.244823ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (104.328488ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (685.874805ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.660008ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (2.537445ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.638554ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.831552ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.896568ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.811109ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.034323ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.601385ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.667596ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (33.318283ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (19.649509ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (9.774997ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (5.003522ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.814474ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.342017ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1011.948849ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.445227ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (6.070159ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.241115ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.193684ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.278055ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.692239ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.624775ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (6.445555ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (4.19918ms)
|
||||
✔ tools: listing and search caps hold (11.090696ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (1.152287ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (7.461367ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.556714ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (2.557182ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (7.360418ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (4.496653ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (2.333522ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.808146ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.519399ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1030.014111ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.207652ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.244652ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (3.013093ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.458801ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3158.269529
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:384:1
|
||||
✖ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (1.002906ms)
|
||||
AssertionError [ERR_ASSERTION]: The validation function is expected to return "true". Received false
|
||||
|
||||
Caught error:
|
||||
|
||||
Error: ENOENT: no such file or directory, mkdir '/mnt/storage/scratch/tmp/mosaic-cli-td6Xqg/notify/demo'
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:389:10)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: Error: ENOENT: no such file or directory, mkdir '/mnt/storage/scratch/tmp/mosaic-cli-td6Xqg/notify/demo'
|
||||
at mkdirSync (node:fs:1734:26)
|
||||
at openJournal (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/src/notifier.mjs:162:5)
|
||||
at file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:389:23
|
||||
at getActual (node:assert:580:5)
|
||||
at strict.throws (node:assert:728:24)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:389:10)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19),
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (25.13219ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (40.329677ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (17.847648ms)
|
||||
✔ decide prints a declining choice as declining (16.236026ms)
|
||||
✔ an unknown outcome is reported once and never resent (14.614685ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (15.96882ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (20.265592ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (17.847024ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (19.0666ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (23.771595ms)
|
||||
✔ agents and tasks print through the broker (22.439025ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (5.615165ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (79.614922ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (50.629582ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (59.956202ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (62.369032ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (72.803213ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (91.434873ms)
|
||||
✔ empty views say so (1.119896ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.39368ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.230495ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (971.408631ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (156.703759ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (82.464757ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (132.155646ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (122.572416ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (93.474394ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (124.413606ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (73.070299ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (136.766699ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (23.679527ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.920199ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (199.4411ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (85.823805ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (598.242788ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (26.796886ms)
|
||||
✔ zoned uses the IANA zone across DST (31.356487ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (25.728704ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (21.23633ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.35172ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (27.227779ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (23.50348ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (25.462406ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (18.183037ms)
|
||||
✔ 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 (164.8161ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (65.126378ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (15.207487ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (10.553097ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.803056ms)
|
||||
✔ no Discord id reaches the journal or the log (9.552876ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (4.143616ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (2.828747ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.722799ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.618835ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.666091ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.135547ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.383772ms)
|
||||
✖ 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.923198ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.497729ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.780238ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.332734ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (110.842936ms)
|
||||
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 (378.485199ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (60.534995ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2279.507696ms)
|
||||
✔ busExit and refuseInsideAgent (0.401143ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.711334ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.445569ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.548543ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.553557ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (17.54013ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (7.876847ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (8.350282ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.337891ms)
|
||||
✔ authorize: open channel, listed user (1.89692ms)
|
||||
✔ authorize: wrong guild (0.196017ms)
|
||||
✔ authorize: no guild (DM) (0.159648ms)
|
||||
✔ authorize: unlisted channel (0.211528ms)
|
||||
✔ authorize: unknown channel, no info (0.17437ms)
|
||||
✔ authorize: thread of listed parent (0.186026ms)
|
||||
✔ authorize: thread of unlisted parent (0.149734ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.146165ms)
|
||||
✔ authorize: unlisted user (0.190422ms)
|
||||
✔ authorize: no author (0.287307ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.146782ms)
|
||||
✔ authorize: system author (0.124802ms)
|
||||
✔ authorize: the bot itself (0.1379ms)
|
||||
✔ authorize: webhook (0.10391ms)
|
||||
✔ authorize: mention channel without mention (0.129963ms)
|
||||
✔ authorize: mention channel with bot mention (0.132393ms)
|
||||
✔ authorize: mention channel with @everyone only (0.11567ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.079587ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.110011ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.105934ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.08689ms)
|
||||
✔ authorize: thread in another guild per channel info (0.100341ms)
|
||||
✔ authorize: not an object (0.069366ms)
|
||||
✔ authorize: no id (0.059022ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.07687ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.065262ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.475774ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.183429ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.480725ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.325315ms)
|
||||
✔ binding: empty allowlists refuse (0.424082ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.312887ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.647852ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.591747ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.48463ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.603943ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (109.395682ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (4.189775ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (423.629847ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (253.14589ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (159.519301ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.672429ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.221942ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (17.249242ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (18.409761ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.784586ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.624831ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.46645ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (3.239191ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (34.115343ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (2.388532ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.369891ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.31863ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (43.058028ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (34.555152ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (8.050443ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (4.059826ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (4.062894ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (7.293209ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.111608ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.96512ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (5.176493ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (8.066122ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (5.899833ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.879016ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (6.270749ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (4.846363ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.827351ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.893573ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.312639ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.522057ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.193669ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.6605ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (3.084023ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.458312ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (67.390331ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (54.367852ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (53.716909ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (372.16094ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (251.624099ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (105.442123ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (229.781561ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (130.480676ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (212.285834ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.337496ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.266264ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.495014ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (426.663698ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (27.427227ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (45.004989ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (4.259634ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (3.447212ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.53773ms)
|
||||
✔ gateway: op 9 resumable resumes (0.345384ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.966945ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.566464ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.501479ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (72.158667ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (65.988141ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (170.563911ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.320126ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (92.563422ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (98.500161ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (128.577265ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (269.647215ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (69.971408ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (91.267221ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (201.812402ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (751.369703ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (3.981124ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (88.744096ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.157419ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.632646ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (76.48799ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (394.958079ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.657331ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (32.218768ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.631053ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (54.600895ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (174.674862ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (89.399144ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.538714ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.288162ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.219281ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.803445ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (52.718397ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (61.85884ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (50.757952ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (81.600707ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (763.284875ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.603613ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (3.566635ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.703504ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.720263ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.808599ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.694852ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.735946ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.587449ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (1.949424ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (29.918762ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (11.929938ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (7.588893ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (6.721566ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.697645ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (4.018695ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1020.639488ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.54979ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (6.54055ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (2.957801ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.228015ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.324914ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.294113ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.812717ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (8.630648ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (6.776269ms)
|
||||
✔ tools: listing and search caps hold (21.511378ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (1.122692ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (6.242925ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.485973ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.648253ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (6.444422ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.11594ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (2.084456ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.536126ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.463118ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1029.148129ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.289141ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.236207ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.675576ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.296962ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3164.258704
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:384:1
|
||||
✖ 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.923198ms)
|
||||
AssertionError [ERR_ASSERTION]: The validation function is expected to return "true". Received false
|
||||
|
||||
Caught error:
|
||||
|
||||
Error: ENOTDIR: not a directory, mkdir '/mnt/storage/scratch/tmp/mosaic-cli-ep4mvm/plain/notify/demo'
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:392:10)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: Error: ENOTDIR: not a directory, mkdir '/mnt/storage/scratch/tmp/mosaic-cli-ep4mvm/plain/notify/demo'
|
||||
at mkdirSync (node:fs:1734:26)
|
||||
at openJournal (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/src/notifier.mjs:162:5)
|
||||
at file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:392:23
|
||||
at getActual (node:assert:580:5)
|
||||
at strict.throws (node:assert:728:24)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:392:10)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19),
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (28.859169ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (43.522891ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (26.19858ms)
|
||||
✔ decide prints a declining choice as declining (18.829143ms)
|
||||
✔ an unknown outcome is reported once and never resent (15.751541ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (16.438474ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (17.051428ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (19.773923ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (17.28548ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (24.900065ms)
|
||||
✔ agents and tasks print through the broker (18.520332ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.497874ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (76.618004ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (72.088781ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (64.855255ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (61.500092ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (61.348789ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (86.301376ms)
|
||||
✔ empty views say so (1.090566ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.393387ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.245999ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (985.42783ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (148.671929ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (80.456483ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (129.760165ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (125.886644ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (79.358909ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (123.89411ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (88.464277ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (135.851968ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (28.571507ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (203.046293ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (209.624246ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (90.134268ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (596.260356ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (27.19919ms)
|
||||
✔ zoned uses the IANA zone across DST (26.692816ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (31.916057ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (32.543329ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.593689ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (20.621592ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (41.181728ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (28.69903ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (16.84485ms)
|
||||
✔ 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 (151.570423ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (61.177872ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (16.092738ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (9.267999ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.772851ms)
|
||||
✔ no Discord id reaches the journal or the log (9.717701ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.12873ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (2.642249ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.748411ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.4724ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.553383ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.824286ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.325175ms)
|
||||
✖ 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.878089ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.377773ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.454861ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.258973ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.361261ms)
|
||||
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 (392.782821ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (53.652805ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2305.145021ms)
|
||||
✔ busExit and refuseInsideAgent (0.408181ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (4.267462ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (3.452639ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.845017ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.701646ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (19.334613ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (6.68051ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (5.912514ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.983616ms)
|
||||
✔ authorize: open channel, listed user (1.817784ms)
|
||||
✔ authorize: wrong guild (0.211759ms)
|
||||
✔ authorize: no guild (DM) (0.18151ms)
|
||||
✔ authorize: unlisted channel (0.186039ms)
|
||||
✔ authorize: unknown channel, no info (0.168586ms)
|
||||
✔ authorize: thread of listed parent (0.220159ms)
|
||||
✔ authorize: thread of unlisted parent (0.158735ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.145594ms)
|
||||
✔ authorize: unlisted user (0.212654ms)
|
||||
✔ authorize: no author (0.289283ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.170478ms)
|
||||
✔ authorize: system author (0.152365ms)
|
||||
✔ authorize: the bot itself (0.10816ms)
|
||||
✔ authorize: webhook (0.100938ms)
|
||||
✔ authorize: mention channel without mention (0.124185ms)
|
||||
✔ authorize: mention channel with bot mention (0.163965ms)
|
||||
✔ authorize: mention channel with @everyone only (0.112685ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.092501ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.097279ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.12121ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.084618ms)
|
||||
✔ authorize: thread in another guild per channel info (0.100982ms)
|
||||
✔ authorize: not an object (0.071546ms)
|
||||
✔ authorize: no id (0.06563ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.072863ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.071345ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.480647ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.158273ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.722269ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.311563ms)
|
||||
✔ binding: empty allowlists refuse (0.389812ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (2.987714ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (3.38944ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (1.789481ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.776621ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.683713ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (133.074131ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (3.216267ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (390.462053ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (211.112392ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (136.493498ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.815175ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.223446ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (17.897595ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (17.709019ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.937378ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.261717ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.43824ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (3.435281ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (34.188662ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (4.190739ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (4.526686ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.355518ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (49.257438ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (31.884449ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (7.372369ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (4.206647ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (2.167803ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (6.271301ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.39736ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (2.014298ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (3.52139ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (4.056645ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (4.894751ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.786506ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (6.124475ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.299552ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.898687ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.753624ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.155638ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.329311ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.329615ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.629937ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.60156ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.412031ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (59.470308ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (65.201222ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (53.296921ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (362.742469ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (235.212599ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (105.379544ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (232.560913ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (125.29101ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (212.676517ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.267154ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.324488ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.511341ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (428.115054ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (24.439637ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (45.434863ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.598401ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.698896ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.558802ms)
|
||||
✔ gateway: op 9 resumable resumes (0.41104ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (1.113347ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.501856ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.539476ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (84.848713ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (66.044753ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (134.425936ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.484123ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (97.198044ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (87.27449ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (98.883283ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (210.094729ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (69.268475ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (90.899035ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (211.208093ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (765.663195ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.21821ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (93.38947ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.324053ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.404865ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (65.740808ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (348.345567ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.572613ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (29.276208ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.82824ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (48.859496ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (143.702874ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (83.318894ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.437678ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (4.53815ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.604759ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.733651ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (57.403721ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (54.868213ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (43.315297ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (91.590305ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (670.090674ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.619633ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (7.246855ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.70481ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.714464ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.799529ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.798929ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.065246ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.741474ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.346906ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (30.265903ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (12.294229ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (7.769052ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.984127ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.687406ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (4.248138ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1014.089592ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.451974ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (5.981803ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.253853ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.194411ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.252605ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.872341ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.271195ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (6.154442ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (4.774478ms)
|
||||
✔ tools: listing and search caps hold (11.623179ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.897446ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (5.505285ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.539266ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.752099ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (6.426594ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.109165ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.784497ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.391923ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (1.726674ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1028.721511ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.249065ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.234037ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.579097ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.489611ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3183.915811
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:384:1
|
||||
✖ 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.878089ms)
|
||||
AssertionError [ERR_ASSERTION]: The validation function is expected to return "true". Received false
|
||||
|
||||
Caught error:
|
||||
|
||||
Error: EISDIR: illegal operation on a directory, open '/mnt/storage/scratch/tmp/mosaic-cli-OmjGzR/notify/acme/sent.jsonl'
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:395:10)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: Error: EISDIR: illegal operation on a directory, open '/mnt/storage/scratch/tmp/mosaic-cli-OmjGzR/notify/acme/sent.jsonl'
|
||||
at openSync (node:fs:779:18)
|
||||
at openJournal (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/src/notifier.mjs:182:10)
|
||||
at file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:395:23
|
||||
at getActual (node:assert:580:5)
|
||||
at strict.throws (node:assert:728:24)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:395:10)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19),
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (34.145901ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (47.775731ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (23.290099ms)
|
||||
✔ decide prints a declining choice as declining (22.078854ms)
|
||||
✔ an unknown outcome is reported once and never resent (15.800453ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (17.512369ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (19.855539ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (17.272377ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (25.308467ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (21.103783ms)
|
||||
✔ agents and tasks print through the broker (19.011515ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (5.051234ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (86.869045ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (63.977185ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (53.610588ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (60.499515ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (64.885946ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (95.45487ms)
|
||||
✔ empty views say so (0.998112ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.25455ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.228409ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (984.058106ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (146.819118ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (76.727453ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (124.990947ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (129.654431ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (73.785161ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (124.690274ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (75.685271ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (139.433887ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (23.684881ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.807303ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (200.875958ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (88.51793ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (602.262755ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (25.852417ms)
|
||||
✔ zoned uses the IANA zone across DST (27.817773ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (35.228068ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (21.845993ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.392654ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (21.091273ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (25.404083ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (29.036092ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (17.295857ms)
|
||||
✔ 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 (149.055471ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (68.583829ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (18.844683ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (11.114662ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.939492ms)
|
||||
✔ no Discord id reaches the journal or the log (11.316488ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.438592ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (3.303883ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.783781ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.594691ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.711371ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.226472ms)
|
||||
✖ the journal: a symlinked directory refuses and says it is a link (0.924893ms)
|
||||
✔ 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.60274ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.415488ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.559483ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.314308ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.553163ms)
|
||||
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 (382.349984ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (65.77689ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2275.536161ms)
|
||||
✔ busExit and refuseInsideAgent (0.396752ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.865358ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.730839ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.645587ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (1.144832ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (16.286897ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (6.785029ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (9.217213ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (3.035382ms)
|
||||
✔ authorize: open channel, listed user (1.743476ms)
|
||||
✔ authorize: wrong guild (0.184716ms)
|
||||
✔ authorize: no guild (DM) (0.296654ms)
|
||||
✔ authorize: unlisted channel (0.173389ms)
|
||||
✔ authorize: unknown channel, no info (0.268458ms)
|
||||
✔ authorize: thread of listed parent (0.193059ms)
|
||||
✔ authorize: thread of unlisted parent (0.236881ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.125466ms)
|
||||
✔ authorize: unlisted user (0.348601ms)
|
||||
✔ authorize: no author (0.353029ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.482526ms)
|
||||
✔ authorize: system author (0.12468ms)
|
||||
✔ authorize: the bot itself (0.089956ms)
|
||||
✔ authorize: webhook (0.089729ms)
|
||||
✔ authorize: mention channel without mention (0.26233ms)
|
||||
✔ authorize: mention channel with bot mention (0.173457ms)
|
||||
✔ authorize: mention channel with @everyone only (0.090435ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.099016ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.072492ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.081666ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.061721ms)
|
||||
✔ authorize: thread in another guild per channel info (0.064458ms)
|
||||
✔ authorize: not an object (0.058966ms)
|
||||
✔ authorize: no id (0.060906ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.06914ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.065672ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.444027ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.151302ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.523579ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.263667ms)
|
||||
✔ binding: empty allowlists refuse (0.401048ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.289347ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.532389ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.478841ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.82097ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.977497ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (118.296279ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (3.397965ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (411.521282ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (194.550815ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (137.779632ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.974567ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.331492ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (19.704601ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (19.381665ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.854148ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.448128ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.676395ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (3.10638ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (32.353066ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (4.258277ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.485906ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.295699ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.441789ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (33.751289ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (8.085765ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (3.86994ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (2.702837ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (7.307418ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.953891ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (1.08887ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (6.630868ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (2.42889ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (5.417568ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (5.335304ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (6.443952ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (3.198098ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.817768ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.091539ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.905905ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.501795ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.130302ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.657428ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.38731ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.426603ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (78.967225ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (52.756224ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (53.36595ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (369.503573ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (232.013615ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (104.532393ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (228.448361ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (125.909456ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (211.311518ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.608651ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.357412ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.50683ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (426.105228ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (23.468989ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (46.380584ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.994664ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (3.166413ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (1.288407ms)
|
||||
✔ gateway: op 9 resumable resumes (0.433182ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.871737ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.533253ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.389817ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (74.178775ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (61.149552ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (139.63557ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.475845ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (104.399929ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (90.164396ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (85.522973ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (196.97209ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (70.762086ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (86.674254ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (206.393074ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (731.229421ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (6.477414ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (89.953235ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.449324ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.198128ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (62.418246ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (337.937949ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.458647ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (22.278618ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (0.969196ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (37.649373ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (144.799697ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (87.006079ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.484099ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.313965ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.115607ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.821127ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (57.596185ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (59.831781ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (47.880019ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (84.500653ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (647.531044ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.486496ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (5.502554ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.670847ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.795543ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.918446ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (6.375599ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.438184ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (2.351938ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (3.312775ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (29.677544ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (12.871377ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (8.053667ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (5.467615ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.691971ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.509863ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1013.970826ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.440192ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (6.486217ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.23785ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.20178ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.824088ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (3.713981ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (5.211396ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.380258ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (3.914022ms)
|
||||
✔ tools: listing and search caps hold (11.492148ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.929013ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (6.212559ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.362676ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.405731ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (6.365431ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.934561ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.869419ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.863278ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (3.948662ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1032.943649ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.171229ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.232197ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.681056ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.873492ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3143.804027
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:373:1
|
||||
✖ the journal: a symlinked directory refuses and says it is a link (0.924893ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception.
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:380:10)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: undefined,
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (28.737033ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (35.163479ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (21.102508ms)
|
||||
✔ decide prints a declining choice as declining (25.358311ms)
|
||||
✔ an unknown outcome is reported once and never resent (19.13216ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (16.11116ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (18.68501ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (16.604187ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (17.389564ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (18.438398ms)
|
||||
✔ agents and tasks print through the broker (17.364042ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.823383ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (79.301366ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (58.327636ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (56.690964ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (58.286999ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (74.637089ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (85.803721ms)
|
||||
✔ empty views say so (1.012118ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.271847ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.278744ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (966.100787ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (140.450345ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (78.156839ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (133.865834ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (123.350771ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (77.326819ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (122.516647ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (86.335457ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (136.683399ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (25.080292ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.356572ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (223.727265ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (96.900998ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (604.558178ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (26.650399ms)
|
||||
✔ zoned uses the IANA zone across DST (30.182663ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (24.704189ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (22.481546ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.320562ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (23.793991ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (31.135777ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (30.905443ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (17.455431ms)
|
||||
✔ 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 (155.226466ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (58.182106ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (15.722414ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (9.65291ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.713996ms)
|
||||
✔ no Discord id reaches the journal or the log (9.632608ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (1.994709ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (2.566077ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.628868ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.494324ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.632544ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.218066ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.367964ms)
|
||||
✔ 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.513813ms)
|
||||
✖ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.868388ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.499484ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.271897ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (110.946353ms)
|
||||
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 (363.490563ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (54.836406ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2251.792796ms)
|
||||
✔ busExit and refuseInsideAgent (0.415315ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (2.997865ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.333382ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.473973ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.431141ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (19.162547ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (6.879811ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (10.405ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.102995ms)
|
||||
✔ authorize: open channel, listed user (1.735923ms)
|
||||
✔ authorize: wrong guild (0.180339ms)
|
||||
✔ authorize: no guild (DM) (0.286307ms)
|
||||
✔ authorize: unlisted channel (0.20056ms)
|
||||
✔ authorize: unknown channel, no info (0.170004ms)
|
||||
✔ authorize: thread of listed parent (0.18518ms)
|
||||
✔ authorize: thread of unlisted parent (0.146331ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.151178ms)
|
||||
✔ authorize: unlisted user (0.282805ms)
|
||||
✔ authorize: no author (0.27851ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.178332ms)
|
||||
✔ authorize: system author (0.462125ms)
|
||||
✔ authorize: the bot itself (0.09571ms)
|
||||
✔ authorize: webhook (0.094039ms)
|
||||
✔ authorize: mention channel without mention (0.205392ms)
|
||||
✔ authorize: mention channel with bot mention (0.13218ms)
|
||||
✔ authorize: mention channel with @everyone only (0.220732ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.072694ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.109539ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.0936ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.098945ms)
|
||||
✔ authorize: thread in another guild per channel info (0.094707ms)
|
||||
✔ authorize: not an object (0.078398ms)
|
||||
✔ authorize: no id (0.082876ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.07022ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.068912ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.460012ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.136121ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.674386ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.306358ms)
|
||||
✔ binding: empty allowlists refuse (0.360495ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.236208ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.721003ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.462088ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.920643ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.372535ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (105.709582ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (2.49337ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (400.329376ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (191.864176ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (124.815718ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.901816ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.160395ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (19.079564ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (17.176383ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.959322ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.154588ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.416762ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (3.548332ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (35.099227ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (4.910759ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (0.978617ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.817652ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.228979ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (35.631341ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (6.862583ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (6.208461ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (4.056737ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (9.961835ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (5.850278ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (1.221581ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (2.848345ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (3.72145ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (4.967844ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.705275ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (6.680507ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.463619ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.758355ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.726393ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.126448ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.36912ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.085982ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.635253ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.586546ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.480372ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (56.372209ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (55.646961ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (60.189032ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (360.407805ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (229.179405ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (104.027644ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (229.755165ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (125.50392ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (212.795487ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (615.116051ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.339152ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.504355ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (430.36146ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (31.30779ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (49.029622ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (1.998981ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.888521ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.513941ms)
|
||||
✔ gateway: op 9 resumable resumes (0.353007ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.882529ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.49968ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.350207ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (76.166016ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (71.065158ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (159.994097ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (1.188851ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (93.420476ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (85.835549ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (82.56582ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (196.144674ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (64.371408ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (85.736535ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (203.13621ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (759.727611ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.345198ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (79.066423ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (0.927796ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (3.741239ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (54.643126ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (342.081483ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.393045ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (25.478182ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.064208ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (40.02632ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (123.791284ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (83.04366ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.48889ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.290246ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.762358ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.595804ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (57.614945ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (59.971798ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (50.345009ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (91.724962ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (624.974757ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.103167ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (2.356734ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.632049ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.725331ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.816187ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (6.5774ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.826071ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (2.267287ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (3.038848ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (32.762066ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (14.048738ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (11.981418ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.886959ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.669385ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.270776ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1016.355023ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.420149ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (6.038896ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.430564ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.207487ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.632782ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.666314ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.070401ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.73592ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (3.959728ms)
|
||||
✔ tools: listing and search caps hold (12.331689ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (1.146949ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (6.950845ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.209795ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.404605ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (8.08911ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.140938ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (2.597675ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.836394ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.278616ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1035.670196ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.288103ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.55435ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.729683ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.248629ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3181.332561
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:398:1
|
||||
✖ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.868388ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception.
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:406:10)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: undefined,
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (29.794649ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (38.94218ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (36.490099ms)
|
||||
✔ decide prints a declining choice as declining (38.340314ms)
|
||||
✔ an unknown outcome is reported once and never resent (24.278352ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (16.469313ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (21.948304ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (21.275067ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (16.125688ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (23.9681ms)
|
||||
✔ agents and tasks print through the broker (15.874847ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (3.526583ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (95.999475ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (76.139065ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (68.224936ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (56.183516ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (59.35911ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (95.446003ms)
|
||||
✔ empty views say so (1.218516ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (2.365312ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.263557ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (1021.348478ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (156.932518ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (83.638475ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (139.574988ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (139.856077ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (72.008017ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (131.170086ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (71.932431ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (143.292727ms)
|
||||
✖ watchChildren reports a child that died before it was called, and one that dies later (26.659105ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.753341ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (217.069241ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (117.770349ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (627.610933ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (27.963314ms)
|
||||
✔ zoned uses the IANA zone across DST (27.268366ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (40.291171ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (23.386255ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.313306ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (27.950556ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (60.035804ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (27.619225ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (18.393879ms)
|
||||
✔ 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 (155.418551ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (69.101062ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (21.171124ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (12.111513ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (1.065946ms)
|
||||
✔ no Discord id reaches the journal or the log (11.034042ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.385735ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (2.873808ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.398396ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.770734ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.791724ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (3.349674ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.658124ms)
|
||||
✔ 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.799959ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.60515ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.752058ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.474371ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.732754ms)
|
||||
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 (406.738827ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (68.919135ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2272.81185ms)
|
||||
✔ busExit and refuseInsideAgent (0.396368ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.115426ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.864066ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.639613ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.476948ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (23.164584ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (6.803285ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (8.567729ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (3.874416ms)
|
||||
✔ authorize: open channel, listed user (2.374951ms)
|
||||
✔ authorize: wrong guild (0.203186ms)
|
||||
✔ authorize: no guild (DM) (0.314521ms)
|
||||
✔ authorize: unlisted channel (0.184651ms)
|
||||
✔ authorize: unknown channel, no info (0.264493ms)
|
||||
✔ authorize: thread of listed parent (0.226501ms)
|
||||
✔ authorize: thread of unlisted parent (0.172718ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.189471ms)
|
||||
✔ authorize: unlisted user (1.168464ms)
|
||||
✔ authorize: no author (0.360857ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.149738ms)
|
||||
✔ authorize: system author (0.119963ms)
|
||||
✔ authorize: the bot itself (0.086895ms)
|
||||
✔ authorize: webhook (0.274611ms)
|
||||
✔ authorize: mention channel without mention (0.730675ms)
|
||||
✔ authorize: mention channel with bot mention (0.130524ms)
|
||||
✔ authorize: mention channel with @everyone only (0.083595ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.09759ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.075999ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.082108ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.091275ms)
|
||||
✔ authorize: thread in another guild per channel info (0.077608ms)
|
||||
✔ authorize: not an object (0.063892ms)
|
||||
✔ authorize: no id (0.064984ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.06103ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.062682ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.471043ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.151618ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.41465ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.237917ms)
|
||||
✔ binding: empty allowlists refuse (0.631065ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.313639ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.781488ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.786813ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.559581ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.901167ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (112.038118ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (4.594124ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (459.944523ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (200.449425ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (131.496934ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.731461ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.230436ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (16.861301ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (31.031611ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.579151ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.643543ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.532429ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (4.598246ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (34.438061ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (4.353558ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.936551ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.123755ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.673073ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (31.606126ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (6.177237ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (5.319998ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.617264ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (6.38716ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (4.348666ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.959916ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (6.743145ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (2.751288ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (4.457921ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.906819ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (5.713777ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.5633ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.867817ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.8518ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.164419ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.43115ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.480447ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.901129ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.348155ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.43701ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (67.985039ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (57.926809ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (72.056593ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (365.551435ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (228.213673ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (104.766667ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (230.964112ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (128.137334ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (213.846242ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.452263ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.329494ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.514545ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (428.338658ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (23.521958ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (52.248775ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.558948ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.757563ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.491835ms)
|
||||
✔ gateway: op 9 resumable resumes (0.384894ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (1.010785ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.50756ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.485292ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (103.893504ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (68.936312ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (118.493364ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.709579ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (107.394682ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (86.981856ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (91.299865ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (196.742625ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (69.575682ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (82.575356ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (221.530799ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (801.329172ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.008682ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (105.008354ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.157561ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.293655ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (65.603737ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (371.422885ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.379133ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (23.900067ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.239444ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (39.851252ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (137.413265ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (85.013967ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.417344ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.485599ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (3.033017ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.850845ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (51.666066ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (54.668767ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (49.548825ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (109.641255ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (676.379003ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.635557ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (2.688728ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.873323ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.679665ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.844333ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.973939ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.542815ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.83329ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.200911ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (27.392124ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (12.169765ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (7.586821ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.356442ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.688455ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (4.149597ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1014.059454ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.438188ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (6.290361ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.198511ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.205713ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.264933ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.490622ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (5.283832ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.407578ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (4.549193ms)
|
||||
✔ tools: listing and search caps hold (13.976203ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.912012ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (5.373079ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.473472ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.267278ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.91166ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (4.837165ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.993099ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.479683ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.898109ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1030.53154ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.812264ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.355601ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (3.014188ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (3.093352ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3330.138768
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/host.test.mjs:314:1
|
||||
✖ watchChildren reports a child that died before it was called, and one that dies later (26.659105ms)
|
||||
AssertionError [ERR_ASSERTION]: a death by signal before the watch is not lost either
|
||||
+ actual - expected
|
||||
|
||||
+ []
|
||||
- [
|
||||
- [
|
||||
- 'broker',
|
||||
- null,
|
||||
- 'SIGKILL'
|
||||
- ]
|
||||
- ]
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:323:10)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: [],
|
||||
expected: [ [ 'broker', null, 'SIGKILL' ] ],
|
||||
operator: 'deepStrictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (30.266926ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (45.66417ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (20.856626ms)
|
||||
✔ decide prints a declining choice as declining (20.024767ms)
|
||||
✔ an unknown outcome is reported once and never resent (14.773851ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (17.412784ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (16.826666ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (14.717837ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (13.214516ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (18.808117ms)
|
||||
✔ agents and tasks print through the broker (21.378996ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (5.916491ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (68.898749ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (71.363969ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (57.75907ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (51.943641ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (57.687842ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (89.099702ms)
|
||||
✔ empty views say so (1.125502ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.538518ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.2228ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (979.821029ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (152.727215ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (79.070305ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (127.687377ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (123.155382ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (73.116208ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (134.247025ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (71.896615ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (137.451352ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (22.554712ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.798101ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (198.402743ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (85.719086ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (595.430698ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (27.652828ms)
|
||||
✔ zoned uses the IANA zone across DST (24.726385ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (29.152423ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (25.104303ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.327336ms)
|
||||
✖ a failed DM is journaled, backs off, and is retried until it lands (35.595976ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (29.550213ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (28.075564ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (18.657591ms)
|
||||
✖ 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 (145.611266ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (60.533301ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (14.616093ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (9.927771ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (1.064229ms)
|
||||
✔ no Discord id reaches the journal or the log (9.423783ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.450495ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (3.148722ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.323057ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.50755ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.563077ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.890067ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.412297ms)
|
||||
✔ 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.471954ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.367338ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.43487ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.282615ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.129836ms)
|
||||
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 (379.12536ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (64.309331ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2261.223759ms)
|
||||
✔ busExit and refuseInsideAgent (0.401724ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.625669ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.826441ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.662925ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.505367ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (16.068818ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (6.204007ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (6.869574ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.944775ms)
|
||||
✔ authorize: open channel, listed user (1.754008ms)
|
||||
✔ authorize: wrong guild (0.332026ms)
|
||||
✔ authorize: no guild (DM) (0.193086ms)
|
||||
✔ authorize: unlisted channel (0.178597ms)
|
||||
✔ authorize: unknown channel, no info (0.173256ms)
|
||||
✔ authorize: thread of listed parent (0.199811ms)
|
||||
✔ authorize: thread of unlisted parent (0.189604ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.148632ms)
|
||||
✔ authorize: unlisted user (0.19263ms)
|
||||
✔ authorize: no author (0.331679ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.203138ms)
|
||||
✔ authorize: system author (0.112449ms)
|
||||
✔ authorize: the bot itself (0.107046ms)
|
||||
✔ authorize: webhook (0.078295ms)
|
||||
✔ authorize: mention channel without mention (0.114163ms)
|
||||
✔ authorize: mention channel with bot mention (0.134884ms)
|
||||
✔ authorize: mention channel with @everyone only (0.124096ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.11984ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.094402ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.072586ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.2045ms)
|
||||
✔ authorize: thread in another guild per channel info (0.061572ms)
|
||||
✔ authorize: not an object (0.056638ms)
|
||||
✔ authorize: no id (0.050415ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.059112ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.053124ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.522411ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.143517ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.50615ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.406254ms)
|
||||
✔ binding: empty allowlists refuse (0.393586ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.314708ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.867263ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (1.536018ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.867824ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.349392ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (113.67623ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (1.98223ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (395.157322ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (201.578655ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (140.794764ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.735771ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.09323ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (20.937731ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (18.751751ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (2.441554ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (2.113564ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.664805ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (3.320447ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (33.950871ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.1345ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.028243ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.016164ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (45.347222ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (35.309874ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (7.466538ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (3.882997ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.167248ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (6.589504ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.571267ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.934983ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (3.191145ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (3.527371ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (6.682676ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (4.58346ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (8.265489ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.591628ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.800393ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.901467ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.220002ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.452872ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.287682ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (3.287543ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.641894ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.471487ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (60.916789ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (59.448834ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (48.312646ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (354.971161ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (233.138971ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (104.037246ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (228.741085ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (129.568859ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (212.499258ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.510265ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.569479ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.532706ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (424.943568ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (26.739074ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (46.351166ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (3.094678ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (2.13718ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.849978ms)
|
||||
✔ gateway: op 9 resumable resumes (0.439768ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.915492ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.673117ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.393606ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (91.065486ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (72.446294ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (124.799928ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.432106ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (90.989316ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (91.183644ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (100.13924ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (212.267433ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (64.430551ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (79.208924ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (204.378212ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (750.14077ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.477235ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (97.71233ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.381396ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.158657ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (60.604439ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (342.045322ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.423641ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (23.41472ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.399657ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (50.239489ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (139.135309ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (86.025792ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.415185ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (4.593522ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (1.867258ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.246599ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (55.201975ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (55.320297ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (39.592411ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (79.198989ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (669.553856ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.326724ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (2.511643ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.596354ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.712157ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (1.149145ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.92729ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (2.952258ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.914605ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.341751ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (28.552894ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (13.513095ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (8.346809ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.58156ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.904232ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (4.244152ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1015.260478ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.405047ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (6.013748ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.301499ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.213923ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.229473ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.08884ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.051327ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.231214ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (4.598406ms)
|
||||
✔ tools: listing and search caps hold (11.303745ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (1.023481ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (5.959723ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.377975ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.383195ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (6.391934ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.697706ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.450399ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.705902ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.274213ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1025.356016ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.295926ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.211448ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.720934ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.211507ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 242
|
||||
ℹ fail 2
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3154.843627
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:90:1
|
||||
✖ a failed DM is journaled, backs off, and is retried until it lands (35.595976ms)
|
||||
AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value:
|
||||
|
||||
assert.ok(s.logs.some((l) => /refused \(HTTP 403\); retry in 1800 s/.test(l)))
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:106:10)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: false,
|
||||
expected: true,
|
||||
operator: '==',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:189:1
|
||||
✖ 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 (145.611266ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||
|
||||
0 !== 4
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:198:10)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 0,
|
||||
expected: 4,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (28.161834ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (34.245716ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (20.035247ms)
|
||||
✔ decide prints a declining choice as declining (19.891429ms)
|
||||
✔ an unknown outcome is reported once and never resent (17.257652ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (20.475501ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (18.291156ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (18.810661ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (26.300885ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (21.769095ms)
|
||||
✔ agents and tasks print through the broker (16.052438ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (4.070747ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (71.132011ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (56.117223ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (67.654898ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (58.652035ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (61.9398ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (90.565117ms)
|
||||
✔ empty views say so (1.050744ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.353949ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.227491ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (985.301532ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (144.077175ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (76.333324ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (131.950258ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (132.857416ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (82.667306ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (122.687567ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (83.674749ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (142.595623ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (26.5632ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (203.026821ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (206.257684ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (90.012691ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (596.03406ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (27.161421ms)
|
||||
✔ zoned uses the IANA zone across DST (25.429466ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (31.691106ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (20.083014ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.319233ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (17.218038ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (24.851202ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (30.574446ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (17.555597ms)
|
||||
✔ 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 (158.019254ms)
|
||||
✖ a restart after the second refusal does not send before that refusal's 30 min are up (67.241172ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (20.292854ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (12.902027ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.789252ms)
|
||||
✔ no Discord id reaches the journal or the log (14.374523ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (3.478343ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (3.574695ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.696529ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.758949ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (1.311343ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (3.666022ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.453856ms)
|
||||
✔ 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.609212ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.436278ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.700138ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.330256ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.126458ms)
|
||||
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 (382.420474ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (55.083236ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2276.076705ms)
|
||||
✔ busExit and refuseInsideAgent (0.39123ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.58026ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.486348ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.782136ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.534211ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (18.814758ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (7.814314ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (8.21186ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.842547ms)
|
||||
✔ authorize: open channel, listed user (2.053887ms)
|
||||
✔ authorize: wrong guild (0.202343ms)
|
||||
✔ authorize: no guild (DM) (0.170271ms)
|
||||
✔ authorize: unlisted channel (0.176836ms)
|
||||
✔ authorize: unknown channel, no info (0.168717ms)
|
||||
✔ authorize: thread of listed parent (0.196506ms)
|
||||
✔ authorize: thread of unlisted parent (0.15738ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.171675ms)
|
||||
✔ authorize: unlisted user (0.178809ms)
|
||||
✔ authorize: no author (0.557715ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.241612ms)
|
||||
✔ authorize: system author (0.147439ms)
|
||||
✔ authorize: the bot itself (0.086306ms)
|
||||
✔ authorize: webhook (0.118423ms)
|
||||
✔ authorize: mention channel without mention (0.132895ms)
|
||||
✔ authorize: mention channel with bot mention (0.143843ms)
|
||||
✔ authorize: mention channel with @everyone only (0.10657ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.077662ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.12351ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.105465ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.086038ms)
|
||||
✔ authorize: thread in another guild per channel info (0.079013ms)
|
||||
✔ authorize: not an object (0.065778ms)
|
||||
✔ authorize: no id (0.066375ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.074059ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.066989ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.565552ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.143183ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.656805ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.389076ms)
|
||||
✔ binding: empty allowlists refuse (0.460087ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.229812ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (2.075655ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (1.256022ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.821351ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.562417ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (112.924765ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (2.210818ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (426.874918ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (191.593308ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (130.090866ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (2.534971ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.354584ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (19.977314ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (15.608763ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.407972ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.0526ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.277071ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (3.142759ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (34.723998ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.595978ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (2.116744ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.196179ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (43.671622ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (37.121541ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (7.730249ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (8.609241ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (4.780897ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (8.261007ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (3.238407ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.880169ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (6.537315ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (3.090148ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (3.805543ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (3.586145ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (5.446411ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.670732ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.808101ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.880131ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.187134ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.839132ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (2.040833ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.81323ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.216171ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.448431ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (52.681258ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (64.340402ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (57.681623ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (356.924501ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (228.644049ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (103.734148ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (227.115891ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (129.923051ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (213.844154ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.259305ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.325464ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.518519ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (425.063998ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (25.407984ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (47.760636ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.422469ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.559985ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.47645ms)
|
||||
✔ gateway: op 9 resumable resumes (0.356614ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.871165ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.526328ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.386158ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (92.075027ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (78.749711ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (125.588179ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.477862ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (116.633883ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (87.294943ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (83.764409ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (203.159841ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (64.374856ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (78.940244ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (202.792825ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (760.407252ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.505567ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (101.330217ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.204087ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.150497ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (63.528632ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (346.854604ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.450419ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (26.765004ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (0.993567ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (38.037028ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (136.745993ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (91.597577ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.407662ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (6.206925ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.98158ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (2.101575ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (52.865116ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (56.187766ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (42.06983ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (84.559484ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (666.190228ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.458366ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (2.953106ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.843553ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.788709ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.967209ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (6.08603ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.294386ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.808292ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (3.003245ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (29.758678ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (12.575322ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (11.292108ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (5.305426ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.917032ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (6.21851ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1011.910582ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.450772ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (5.992484ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.226257ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.24979ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.326927ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.172966ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.869575ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (8.252117ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (4.613627ms)
|
||||
✔ tools: listing and search caps hold (13.011719ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (1.010923ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (5.862506ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.464254ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.237546ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (7.302745ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.697192ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.944694ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.569048ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.387757ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1023.46693ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.438251ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.259496ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.814276ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.295039ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3187.624351
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:201:1
|
||||
✖ a restart after the second refusal does not send before that refusal's 30 min are up (67.241172ms)
|
||||
AssertionError [ERR_ASSERTION]: nothing between the restart and +60 min
|
||||
+ actual - expected
|
||||
|
||||
[
|
||||
+ 31.5
|
||||
- 60
|
||||
]
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:208:10)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: [ 31.5 ],
|
||||
expected: [ 60 ],
|
||||
operator: 'deepStrictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (40.685297ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (37.484026ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (16.605654ms)
|
||||
✔ decide prints a declining choice as declining (19.393014ms)
|
||||
✔ an unknown outcome is reported once and never resent (17.852982ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (15.710979ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (29.266905ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (21.009526ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (24.758986ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (24.953573ms)
|
||||
✔ agents and tasks print through the broker (22.864154ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.258424ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (76.228084ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (58.166251ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (58.550672ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (64.331044ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (75.92909ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (82.458244ms)
|
||||
✔ empty views say so (1.087409ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.359589ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.225154ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (993.897545ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (152.938597ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (88.126377ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (137.108011ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (123.170596ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (73.85963ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (125.758845ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (75.267818ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (134.516445ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (23.177914ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.983501ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (205.420751ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (88.934962ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (606.927416ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (26.293005ms)
|
||||
✔ zoned uses the IANA zone across DST (25.704583ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (31.460666ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (30.675203ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.650589ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (25.821533ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (22.378617ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (25.083244ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (16.516437ms)
|
||||
✔ 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 (157.893559ms)
|
||||
✖ a restart after the second refusal does not send before that refusal's 30 min are up (63.495042ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (19.204607ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (10.886139ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.814837ms)
|
||||
✔ no Discord id reaches the journal or the log (10.08282ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.04334ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (2.911381ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.771593ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.544469ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.622522ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.143025ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.363422ms)
|
||||
✔ 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.556486ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.400861ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.481441ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.293111ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (110.51501ms)
|
||||
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 (379.245614ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (54.669042ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2285.747948ms)
|
||||
✔ busExit and refuseInsideAgent (0.410368ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.374242ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.83547ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.705412ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.511744ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (18.198846ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (7.5266ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (9.967294ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.589031ms)
|
||||
✔ authorize: open channel, listed user (1.93302ms)
|
||||
✔ authorize: wrong guild (0.178537ms)
|
||||
✔ authorize: no guild (DM) (0.292093ms)
|
||||
✔ authorize: unlisted channel (0.179326ms)
|
||||
✔ authorize: unknown channel, no info (0.28383ms)
|
||||
✔ authorize: thread of listed parent (0.195098ms)
|
||||
✔ authorize: thread of unlisted parent (0.241817ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.13626ms)
|
||||
✔ authorize: unlisted user (0.84987ms)
|
||||
✔ authorize: no author (0.770423ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.147142ms)
|
||||
✔ authorize: system author (0.13532ms)
|
||||
✔ authorize: the bot itself (0.103191ms)
|
||||
✔ authorize: webhook (0.113308ms)
|
||||
✔ authorize: mention channel without mention (0.146621ms)
|
||||
✔ authorize: mention channel with bot mention (1.211073ms)
|
||||
✔ authorize: mention channel with @everyone only (0.099513ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.071029ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.06969ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.079615ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.057973ms)
|
||||
✔ authorize: thread in another guild per channel info (0.065292ms)
|
||||
✔ authorize: not an object (0.058305ms)
|
||||
✔ authorize: no id (0.059287ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.067696ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.066163ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.462736ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.148095ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.679076ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (2.750459ms)
|
||||
✔ binding: empty allowlists refuse (0.397568ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.42358ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.835916ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.838789ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.768289ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.242621ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (100.203101ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (2.042691ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (416.198477ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (188.38447ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (136.668768ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.702683ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.180138ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (20.584103ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (18.902147ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.485136ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.29353ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.377209ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (3.244496ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (34.28341ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (2.977577ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.155398ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.51738ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.464805ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (33.459075ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (8.694062ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (6.111067ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.492061ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (7.839407ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (4.100361ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.836243ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (5.161106ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (2.17244ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (4.895196ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (4.386984ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (4.567214ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.789429ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.943608ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.051884ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.284424ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.523534ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.574205ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.897148ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.447591ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.425901ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (53.517271ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (54.764108ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (60.690738ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (369.555677ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (228.567122ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (103.590577ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (232.506668ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (130.452014ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (213.77209ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (613.605397ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.379673ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.557131ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (428.116525ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (26.089845ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (44.887003ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.560753ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.638311ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.500406ms)
|
||||
✔ gateway: op 9 resumable resumes (0.384574ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.85435ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.552787ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.540151ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (71.620654ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (83.665217ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (143.858301ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.442071ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (93.475596ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (85.861118ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (86.371812ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (195.730915ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (72.457254ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (87.279071ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (213.658166ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (768.286621ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.724785ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (92.605153ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.117381ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (5.133155ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (74.213903ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (359.688011ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.400101ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (27.700856ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.682573ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (40.085875ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (146.934264ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (88.516909ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.444021ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.260902ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.702818ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.979157ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (51.549499ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (75.472143ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (50.592644ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (80.598114ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (651.842835ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.571622ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (5.380362ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (1.115207ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.693799ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (1.060569ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (7.078616ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.181245ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.833106ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.346432ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (28.150731ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (16.56649ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (8.789814ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (7.204342ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.880292ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (4.025895ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1013.141084ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.545979ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (7.007947ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.314484ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.215368ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.479213ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.058811ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.207036ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.443608ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (7.736511ms)
|
||||
✔ tools: listing and search caps hold (13.678244ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.959099ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (6.77679ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.373321ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.317981ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (6.166706ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.209866ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.906211ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.477696ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.197139ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1024.553116ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.372419ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.266113ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.705228ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.225191ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3198.419333
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:201:1
|
||||
✖ a restart after the second refusal does not send before that refusal's 30 min are up (63.495042ms)
|
||||
AssertionError [ERR_ASSERTION]: nothing between the restart and +60 min
|
||||
+ actual - expected
|
||||
|
||||
[
|
||||
+ 31.5
|
||||
- 60
|
||||
]
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:208:10)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: [ 31.5 ],
|
||||
expected: [ 60 ],
|
||||
operator: 'deepStrictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (36.439821ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (33.925039ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (21.158075ms)
|
||||
✔ decide prints a declining choice as declining (18.952008ms)
|
||||
✔ an unknown outcome is reported once and never resent (16.952476ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (20.117146ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (16.377237ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (17.785936ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (16.701711ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (17.493924ms)
|
||||
✔ agents and tasks print through the broker (17.802635ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (3.969558ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (65.153084ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (65.019891ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (63.315796ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (57.643913ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (79.072851ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (100.614075ms)
|
||||
✔ empty views say so (1.321977ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.642474ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.24347ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (979.002116ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (149.546048ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (75.917239ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (128.077629ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (125.479723ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (74.096919ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (128.383414ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (75.365589ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (149.996979ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (24.844573ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.886459ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (209.271349ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (89.833801ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (596.240557ms)
|
||||
✖ bus-service.sh renders the unit and installs it into a given directory (13.023386ms)
|
||||
✔ zoned uses the IANA zone across DST (25.081139ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (33.087017ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (21.832137ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.313202ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (20.727213ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (26.646629ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (26.996695ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (17.003301ms)
|
||||
✔ 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 (154.842007ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (73.169397ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (20.197995ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (12.140882ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.826196ms)
|
||||
✔ no Discord id reaches the journal or the log (12.640011ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.577376ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (2.291009ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.274937ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.572193ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.647559ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (3.528525ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.371869ms)
|
||||
✔ 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.540285ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.444668ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.517872ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.282252ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.600047ms)
|
||||
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 (371.436455ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (64.122569ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2279.073027ms)
|
||||
✔ busExit and refuseInsideAgent (0.386801ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (5.051726ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.451345ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.679177ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.505543ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (16.759513ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (6.847581ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (7.885012ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.733518ms)
|
||||
✔ authorize: open channel, listed user (2.103689ms)
|
||||
✔ authorize: wrong guild (0.204134ms)
|
||||
✔ authorize: no guild (DM) (0.250642ms)
|
||||
✔ authorize: unlisted channel (0.179972ms)
|
||||
✔ authorize: unknown channel, no info (0.260484ms)
|
||||
✔ authorize: thread of listed parent (0.444085ms)
|
||||
✔ authorize: thread of unlisted parent (0.296317ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.141873ms)
|
||||
✔ authorize: unlisted user (1.233958ms)
|
||||
✔ authorize: no author (0.509405ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.144304ms)
|
||||
✔ authorize: system author (0.325929ms)
|
||||
✔ authorize: the bot itself (0.098491ms)
|
||||
✔ authorize: webhook (0.66558ms)
|
||||
✔ authorize: mention channel without mention (0.157236ms)
|
||||
✔ authorize: mention channel with bot mention (0.138165ms)
|
||||
✔ authorize: mention channel with @everyone only (0.089143ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.085157ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.083237ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.094689ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.266387ms)
|
||||
✔ authorize: thread in another guild per channel info (0.083028ms)
|
||||
✔ authorize: not an object (0.07959ms)
|
||||
✔ authorize: no id (0.070516ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.084489ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.068505ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.465465ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.138041ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.574002ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.281521ms)
|
||||
✔ binding: empty allowlists refuse (0.325095ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.384415ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.623987ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (1.200867ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.649798ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (2.365167ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (112.716415ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (2.641304ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (435.544546ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (202.966592ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (151.624562ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.8529ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.386448ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (22.641577ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (16.801329ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (2.489508ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.553887ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.715878ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.415618ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (34.013139ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.915224ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.294216ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.052537ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (44.144941ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (33.552931ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (7.922673ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (7.891341ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.13703ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (5.04555ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (3.47815ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (1.089163ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (8.646223ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (3.375862ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (4.101761ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (4.56979ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (8.744825ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.499542ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.776987ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.883161ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.241348ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.405812ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (0.999705ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.576183ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.819668ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.461278ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (55.109589ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (50.2841ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (48.874702ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (358.597213ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (234.327562ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (115.980983ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (232.999465ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (127.109157ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (212.630669ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.344473ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.354236ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.500428ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (425.782047ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (24.521955ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (45.954091ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.575865ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.640094ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.494401ms)
|
||||
✔ gateway: op 9 resumable resumes (0.680377ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.950533ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.5513ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.412734ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (79.181806ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (67.333024ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (163.8268ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.339034ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (96.931881ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (86.40052ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (89.861594ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (213.438671ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (79.995959ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (91.684346ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (198.939395ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (760.532022ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.518779ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (94.136648ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.321714ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (5.010352ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (84.932333ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (371.359177ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.953502ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (25.87851ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.08047ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (42.520075ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (162.423161ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (90.616142ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.478887ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.466472ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.244007ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.967992ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (55.135659ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (68.081432ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (52.742578ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (92.661558ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (685.712976ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.555387ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (4.983774ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.512018ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.572608ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.682831ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.690252ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.4364ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.873619ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.484239ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (26.401741ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (13.780187ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (9.506372ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.523326ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.868572ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.473946ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1017.080749ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.417568ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (6.467204ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.171844ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.198172ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.465489ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.187242ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.395016ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (6.590029ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (3.883006ms)
|
||||
✔ tools: listing and search caps hold (13.764368ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.895509ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (6.850724ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.464872ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.848609ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (6.002398ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.244872ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.849368ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.865861ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.229826ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1032.551897ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.2694ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.235987ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.621124ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.199604ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3152.644118
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/host.test.mjs:396:1
|
||||
✖ bus-service.sh renders the unit and installs it into a given directory (13.023386ms)
|
||||
AssertionError [ERR_ASSERTION]: The input did not match the regular expression /^ {2}mkdir -m 0700 -p <dataRoot>\/notify\/<business> +the notifier refuses a looser directory$/m. Input:
|
||||
|
||||
'written: /mnt/storage/scratch/tmp/mosaic-cli-L14dvV/[email protected]\n' +
|
||||
'next, for one business (one per data root):\n' +
|
||||
' write <dataRoot>/notify/<business>/notify.json, mode 0600:\n' +
|
||||
' {"notifyVersion": 1, "binding": "<discord binding>"} or "binding": null for no DMs\n' +
|
||||
' systemctl --user enable --now mosaic-bus@<business> start now and at login\n' +
|
||||
' systemctl --user status mosaic-bus@<business>\n' +
|
||||
" journalctl --user -u mosaic-bus@<business> -f the host's log\n" +
|
||||
' systemctl --user stop mosaic-bus@<business> SIGTERM; restartable\n' +
|
||||
'survive logout and reboot only with lingering on: loginctl enable-linger jwoltje\n'
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:409:10)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: `written: /mnt/storage/scratch/tmp/mosaic-cli-L14dvV/[email protected]\nnext, for one business (one per data root):\n write <dataRoot>/notify/<business>/notify.json, mode 0600:\n {"notifyVersion": 1, "binding": "<discord binding>"} or "binding": null for no DMs\n systemctl --user enable --now mosaic-bus@<business> start now and at login\n systemctl --user status mosaic-bus@<business>\n journalctl --user -u mosaic-bus@<business> -f the host's log\n systemctl --user stop mosaic-bus@<business> SIGTERM; restartable\nsurvive logout and reboot only with lingering on: loginctl enable-linger jwoltje\n`,
|
||||
expected: /^ {2}mkdir -m 0700 -p <dataRoot>\/notify\/<business> +the notifier refuses a looser directory$/m,
|
||||
operator: 'match',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (42.695821ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (50.900968ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (19.526315ms)
|
||||
✔ decide prints a declining choice as declining (27.0715ms)
|
||||
✔ an unknown outcome is reported once and never resent (17.942055ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (21.456887ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (20.232816ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (28.167704ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (23.232404ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (21.716825ms)
|
||||
✔ agents and tasks print through the broker (26.685493ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.448765ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (101.758457ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (64.589268ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (60.806348ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (64.399373ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (75.658057ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (94.792104ms)
|
||||
✔ empty views say so (1.146799ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.41943ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.361908ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (1019.227828ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (152.084167ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (79.194062ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (128.995735ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (127.01056ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (76.219523ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (122.025954ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (72.468138ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (138.063567ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (23.990777ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.881279ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (206.23692ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (83.701998ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (594.58956ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (27.364129ms)
|
||||
✔ zoned uses the IANA zone across DST (26.624668ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (47.126452ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (35.118213ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.335013ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (31.814163ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (31.270483ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (29.440396ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (23.556561ms)
|
||||
✔ 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 (170.013966ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (56.029087ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (15.519714ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (9.167494ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.705928ms)
|
||||
✔ no Discord id reaches the journal or the log (10.038691ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.377141ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (2.279316ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.191204ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.485391ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.550256ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.966907ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.567178ms)
|
||||
✔ 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.476903ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.377081ms)
|
||||
✖ the journal: a directory it cannot write or create refuses with exit 3 and names the path (1.505115ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.457248ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.351089ms)
|
||||
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 (406.719655ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (82.627037ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2314.22678ms)
|
||||
✔ busExit and refuseInsideAgent (0.387445ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (4.949848ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.72578ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.627445ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.547957ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (32.452945ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (7.780424ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (15.756427ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.716303ms)
|
||||
✔ authorize: open channel, listed user (2.116809ms)
|
||||
✔ authorize: wrong guild (0.218948ms)
|
||||
✔ authorize: no guild (DM) (0.178971ms)
|
||||
✔ authorize: unlisted channel (0.220612ms)
|
||||
✔ authorize: unknown channel, no info (0.226098ms)
|
||||
✔ authorize: thread of listed parent (0.216643ms)
|
||||
✔ authorize: thread of unlisted parent (0.206414ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.129743ms)
|
||||
✔ authorize: unlisted user (0.399689ms)
|
||||
✔ authorize: no author (0.257888ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.158293ms)
|
||||
✔ authorize: system author (0.113693ms)
|
||||
✔ authorize: the bot itself (0.105945ms)
|
||||
✔ authorize: webhook (0.088276ms)
|
||||
✔ authorize: mention channel without mention (0.133082ms)
|
||||
✔ authorize: mention channel with bot mention (0.171777ms)
|
||||
✔ authorize: mention channel with @everyone only (0.118598ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.102448ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.188459ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.108392ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.086117ms)
|
||||
✔ authorize: thread in another guild per channel info (0.095182ms)
|
||||
✔ authorize: not an object (0.060444ms)
|
||||
✔ authorize: no id (0.064663ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.087816ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.065254ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (1.91215ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (1.146256ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.615019ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.337072ms)
|
||||
✔ binding: empty allowlists refuse (0.895847ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (2.916197ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (4.098608ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.827967ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (2.909621ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (2.370774ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (131.540131ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (3.987938ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (416.545817ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (204.124078ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (172.458112ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (2.076345ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.564452ms)
|
||||
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (24.99628ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (20.642317ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.767332ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.357443ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.610001ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (3.67124ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (34.36697ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.821187ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.667123ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.204315ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (44.147074ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.723872ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (7.999848ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (4.400002ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.421125ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (6.035732ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (4.577778ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (1.049663ms)
|
||||
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (4.606455ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (1.945793ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (5.902214ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (1.995553ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (5.633274ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (8.128078ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (1.086506ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.245325ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.225153ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (2.571003ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.986458ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.730167ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.59808ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.477007ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (70.434554ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (59.656196ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (54.286738ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (378.18188ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (235.133027ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (105.242619ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (236.697114ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (124.481269ms)
|
||||
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (214.036359ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.793151ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.392011ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.514755ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (426.246807ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (25.181641ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (45.58512ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (4.210163ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (2.021905ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.565785ms)
|
||||
✔ gateway: op 9 resumable resumes (0.947653ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (1.704494ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.58659ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.970406ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (79.348559ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (63.850933ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (162.955784ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.305959ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (86.971312ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (94.214462ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (90.400645ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (235.813734ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (73.935437ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (96.956611ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (198.660681ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (752.56004ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (6.081677ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (108.168434ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.226863ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (5.647633ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (76.813084ms)
|
||||
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (342.479082ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.380568ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (27.340548ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (0.986595ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (46.034157ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (181.634629ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (93.79815ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.419578ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.511894ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.573585ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.750235ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (52.481055ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (61.120525ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (59.098891ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (86.863926ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (716.13454ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.642214ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (2.228517ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.589473ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.660656ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.849316ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (7.93533ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.559885ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.871351ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (3.039388ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (31.865221ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (13.500828ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (9.361846ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (5.723068ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.617638ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.405558ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1013.946075ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.473246ms)
|
||||
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (6.532815ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.239307ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.20801ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.714297ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.036644ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (6.208638ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (7.991555ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (4.727937ms)
|
||||
✔ tools: listing and search caps hold (11.975375ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.948319ms)
|
||||
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (6.128059ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.324429ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.347562ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.563725ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.24243ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.660983ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.329986ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (1.890631ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1028.336229ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.273426ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.244886ms)
|
||||
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.652888ms)
|
||||
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.40933ms)
|
||||
ℹ tests 244
|
||||
ℹ suites 0
|
||||
ℹ pass 243
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3187.126372
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:410:1
|
||||
✖ the journal: a directory it cannot write or create refuses with exit 3 and names the path (1.505115ms)
|
||||
AssertionError [ERR_ASSERTION]: The validation function is expected to return "true". Received false
|
||||
|
||||
Caught error:
|
||||
|
||||
Error: EACCES: permission denied, open '/mnt/storage/scratch/tmp/mosaic-cli-tXUsQU/notify/demo/sent.jsonl'
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:423:10)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: Error: EACCES: permission denied, open '/mnt/storage/scratch/tmp/mosaic-cli-tXUsQU/notify/demo/sent.jsonl'
|
||||
at openSync (node:fs:779:18)
|
||||
at openJournal (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/src/notifier.mjs:182:10)
|
||||
at file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:423:23
|
||||
at getActual (node:assert:580:5)
|
||||
at strict.throws (node:assert:728:24)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:423:10)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19),
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
3.85 2.71 2.46 3/12539 879606
|
||||
2026-10-09T14:08:50Z
|
||||
@@ -0,0 +1,2 @@
|
||||
2.44 1.79 2.16 2/12979 779337
|
||||
2026-10-09T14:05:56Z
|
||||
@@ -0,0 +1,31 @@
|
||||
N2 killed (fail 1, cancelled 0) ✖ the journal: a symlinked directory refuses and says it is a link;
|
||||
N4 killed (fail 1, cancelled 0) ✖ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it;
|
||||
N5 killed (fail 1, cancelled 0) ✖ watchChildren reports a child that died before it was called, and one that dies later;
|
||||
M28 killed (fail 1, cancelled 0) ✖ a second host for the same data root refuses with exit 3 while the first runs;
|
||||
G150 killed (fail 1, cancelled 0) ✖ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker;
|
||||
G144 killed (fail 1, cancelled 0) ✖ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker;
|
||||
F2a killed (fail 2, cancelled 0) ✖ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count;✖ 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;
|
||||
F2b killed (fail 1, cancelled 0) ✖ 429s, 5xx-style unknowns and refusals without a status never count toward the limit;
|
||||
F2c killed (fail 2, cancelled 0) ✖ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing;✖ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count;
|
||||
F2d killed (fail 1, cancelled 0) ✖ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing;
|
||||
F2e killed (fail 1, cancelled 0) ✖ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count;
|
||||
F2f killed (fail 2, cancelled 0) ✖ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count;✖ 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;
|
||||
J4a killed (fail 1, cancelled 0) ✖ the journal: a line with a wrong type refuses with exit 3 and names the field;
|
||||
J4b killed (fail 1, cancelled 0) ✖ the journal: a line with a wrong type refuses with exit 3 and names the field;
|
||||
J4c killed (fail 1, cancelled 0) ✖ the journal: a line with a wrong type refuses with exit 3 and names the field;
|
||||
J4d killed (fail 1, cancelled 0) ✖ the journal: a line with a wrong type refuses with exit 3 and names the field;
|
||||
E1 killed (fail 1, cancelled 0) ✖ the journal: a directory it cannot write or create refuses with exit 3 and names the path;
|
||||
E2 killed (fail 1, cancelled 0) ✖ the journal: a symlinked directory refuses and says it is a link;
|
||||
G144cb killed (fail 1, cancelled 0) ✖ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error;
|
||||
G150cb killed (fail 1, cancelled 0) ✖ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3;
|
||||
G184 killed (fail 1, cancelled 0) ✖ close() whose stop and close sends fail with EPIPE still finishes, with exit 1;
|
||||
G188 killed (fail 1, cancelled 0) ✖ close() whose stop and close sends fail with EPIPE still finishes, with exit 1;
|
||||
R2a killed (fail 2, cancelled 0) ✖ a failed DM is journaled, backs off, and is retried until it lands;✖ 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;
|
||||
R2b killed (fail 1, cancelled 0) ✖ a restart after the second refusal does not send before that refusal's 30 min are up;
|
||||
R2c killed (fail 1, cancelled 0) ✖ a restart after the second refusal does not send before that refusal's 30 min are up;
|
||||
X9 killed (fail 1, cancelled 0) ✖ the journal: a directory it cannot write or create refuses with exit 3 and names the path;
|
||||
X14 killed (fail 1, cancelled 0) ✖ bus-service.sh renders the unit and installs it into a given directory;
|
||||
D3 killed (fail 1, cancelled 0) ✖ the journal: a line with a wrong type refuses with exit 3 and names the field;
|
||||
N1a killed (fail 1, cancelled 0) ✖ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3;
|
||||
N1b killed (fail 1, cancelled 0) ✖ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3;
|
||||
N1c killed (fail 1, cancelled 0) ✖ 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,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,23 @@
|
||||
On branch refactor
|
||||
Your branch is up to date with 'origin/refactor'.
|
||||
|
||||
nothing to commit, working tree clean
|
||||
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: 745170 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 148
|
||||
ℹ pass 148
|
||||
ℹ fail 0
|
||||
OK node --test packages/queue/tests/
|
||||
OK scripts/mosaic queue help
|
||||
skip queue verify and render --check: this checkout (/mnt/storage/scratch/rocko-r45/tree) is not the queue's canonical root (/mnt/storage/src/mosaic-stack)
|
||||
|
||||
queue suite: 27 passed, 0 failed
|
||||
@@ -0,0 +1,7 @@
|
||||
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
|
||||
skip state-machine cases (docker daemon unavailable)
|
||||
|
||||
selftest: 4 passed, 0 failed
|
||||
@@ -0,0 +1,33 @@
|
||||
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
|
||||
FAIL user recall run succeeds (exit 1)
|
||||
FAIL recalled user name (response: )
|
||||
OK no agent identity on headless run
|
||||
|
||||
selftest: 26 passed, 2 failed
|
||||
@@ -1,16 +1,20 @@
|
||||
fd7c9a28364d61669889180bce1482449ab0a583228ebb9e0e8fe5075cbe5eb0 BUILD.md
|
||||
c88b20e4bd185682c82a4ce1733cd5b5a23a5375a10e9de55f2af5f8af7474a9 base.txt
|
||||
4ed9ff61b94b4c9d2426e5703c2e6c0dbcd6bc0c64a88d59347a8b47f4933408 build.patch
|
||||
b329fdcbdf8cb62659f239359570dfa9771559e3112b743df55429244769ed14 candidate-manifest.sha256
|
||||
ff1af113dc9e41699cd697b39cc2f66a89b8cf3febbede8fb87365faa4cd4fb9 BUILD.md
|
||||
b084be74fff27adc1b15158404edbbfee3efe3ac5a2288efa63ebc1a76bebd0d base.txt
|
||||
c8cec070da60d8297f1ee5b006a1099ab9376fd3abf32fa4461f967750ad6756 build.patch
|
||||
5b067a9dad645c31d99e1ea02575cd2fc1ba547ce011ea81c56b17ae29da0d0e candidate-manifest.sha256
|
||||
8d35f99580981064d08725cf8e3dd67885bff77603446b0022701d4357727273 files.txt
|
||||
bbe45c2a578b35cc2559972a4ee8900bd01ffb69e72bef6472dfc7e147e8916f mutants.sh
|
||||
ecf3b48d64d2c73dda8fff357aabc9e8e719920a517d7172a02fce3c240d3756 mutants.sh
|
||||
1102fd4f14df908f0498d31d209023d7903ba090c6c3bf3e3338e3f52158d95d out/base-node-conversation.txt
|
||||
8df77c49bf82833efa56d42401a4a06f20931b7a76dd5eed3a73fdec60e92e79 out/base-suite-task.txt
|
||||
795771fa8a721586343c05dac647306752fad14e06d738bad72af5bd388e3d59 out/gate-r2-end.txt
|
||||
70f96042dfa6380b6d505a50401ecf16ad87f8daf653a5b295bd43fde6d753fd out/gate-r2-exits.txt
|
||||
3a0e4de0a31a7fc5ecff3979aadfd495d0c12599e361323aca6101d78a964522 out/gate-r2-start.txt
|
||||
2fc28669728c05f1f3f2c61e3c3a8fd88e7d13233856739428e772992bfc280b out/load-end-r2.txt
|
||||
729a1baad006dde35b0b214386114ae6a0406ce302891a6e6aab32af52d025a5 out/load-end.txt
|
||||
a0f662486a414610599959e6ea14dc9a80ec8ac266b0d4a98f5f26ec16b95396 out/load-start-r2.txt
|
||||
a78af172868215584d99e036989cb88475fd5eecf8b2ac1775fd68aeb61af76e out/load-start.txt
|
||||
e1bd7a5920b70115d2251ae3289799bb446445703b9da51c55c668f9cedfa461 out/manifest-check-mut.txt
|
||||
e1bd7a5920b70115d2251ae3289799bb446445703b9da51c55c668f9cedfa461 out/manifest-check-tree-r2.txt
|
||||
e1bd7a5920b70115d2251ae3289799bb446445703b9da51c55c668f9cedfa461 out/manifest-check-tree.txt
|
||||
95bdcddfb56d633c72c0b8c18948cffd42e6c1b2aaa3b78800783eb2d1452e35 out/mut-E1.txt
|
||||
1e13a94767e411e0eccf09d8b362dba095e33b6403f3d9bc5cde3e7cdfe62315 out/mut-E2.txt
|
||||
@@ -31,26 +35,82 @@ e75b166d3aa8f605d394ed5449c20a11ed884c910c756230697d4f387e13cd50 out/mut-J4a.tx
|
||||
a6dc80efac48f9de98748433fcac165b4cc3bf000de2b4c04d9107c9b3dfd342 out/mut-N4.txt
|
||||
a0df618c85c05350971ed56203a4d8f37e8a6cc0fa2ba267de0ba34fd197523f out/mut-N5.txt
|
||||
1225000966c8c8c2231ff7091665950f17a6d9d756a46f48b4ae3a8400aa635d out/mutants.txt
|
||||
e0476e93931dc2fea8ce2d77174ce3ddedc30592281d4f0234c1d2e12049e693 out/node-bus-r2.txt
|
||||
f14fe1ee4239ae63dfb1bea143d1d450905387a34abbcb9e4c6b09104b75aefa out/node-bus.txt
|
||||
cc54ca08e481c76ebe389fd8e4686c7f1cd912a7cecb017adfaf91e23d040354 out/node-business-r2.txt
|
||||
1a88ea66c4b4e6cbf23a420628b53082a770828fb10ddf3ae49cd5b65f4855d0 out/node-business.txt
|
||||
4bf6f80ff71513010f9e8bcb17924ca7d346ea7f0fe0362404a0c94a4cc35e80 out/node-cli-r2.txt
|
||||
099d2019d935d5c49cecff107316b040a93959ea832375cc008cb8c991004174 out/node-cli.txt
|
||||
8e1a41b889b71cdcaee93c3ed4bc14e9c065048f750488069343b82e1178e7bb out/node-control-board-r2.txt
|
||||
9d2ef4cf44c714e9d18a2162b1c34aad8867ca631d66253cf08c11c96dd6f472 out/node-control-board.txt
|
||||
254a01cfe6a9516172de1368f6d701e5cfb42929174b9ed3424196666eadac7f out/node-conversation-r2.txt
|
||||
bb4541854d7f2ced39d032f84f36088be1961ec7cf7c591c1196c94157c61858 out/node-conversation.txt
|
||||
6db90f6b20cf0b50eca8d95b4488f999b4f53ba044bacc47a054c15c372e691a out/node-discord-r2.txt
|
||||
9643c28e1490621836a1c64e2b62fad8f44033965cfc68df49187ff5e7c62c33 out/node-discord.txt
|
||||
609bc17c7693d64f49120761b7f60712fbce191f5bbdb26fbb1e56b201f4477f out/node-ledger-r2.txt
|
||||
c5fdadd964fdccc88182f91fa2b724ec7b69d1246b972c25536cc398800127dd out/node-ledger.txt
|
||||
01bbb89694e16d8b0af8f6da010b931fbf202ae73a97b220afbf675d849cc4e3 out/node-mosaic-r2.txt
|
||||
fc0d61d1a3695c44bc5e698523bd790e68bfde3785f38526c601bee852ad3784 out/node-mosaic.txt
|
||||
8b86072a96cc9526d57cbb04f2812fa1b0b884272439538788154d42f6393169 out/node-queue-r2.txt
|
||||
e2e08d74f8cb590e75d50bc011ca5875d8d49c1470e7f9ee9173cba39946eec9 out/node-queue.txt
|
||||
33da17d4998d61f2628648ed5ab04ddd8aff65e231267676ae983a2eac45814c out/node-seat-r2.txt
|
||||
74dbaa3099f9e3e88219cdc2ae4b83f21fc18ddafe060b299c1dcc5eacf95528 out/node-seat.txt
|
||||
cdef1d30ea8e68e2701bc950d233a0f36fdd41613306f55198b1a3822bd72d2c out/node-tasks-r2.txt
|
||||
34e473be5528b430fb28d9e78b59092e4bedf8660306590b24b6083ca844a697 out/node-tasks.txt
|
||||
2f2e8dd3922e2d6c737ae636e0eefc02678aaf0d0e37e17f0c5b15cb007ccadf out/node-webui-r2.txt
|
||||
a911209250eef9dbdbb7c762655b29811b42198cf439c363c601993ac30e1a3c out/node-webui.txt
|
||||
e1bd7a5920b70115d2251ae3289799bb446445703b9da51c55c668f9cedfa461 out/r2/manifest-check-mut.txt
|
||||
71bc7a5b9376362707c79fa0c7e0037058d22f54de5394f448e390dc6e2faa4a out/r2/mut-D3.txt
|
||||
b3dea3c963be86ee51c91282cea5540bff7ab49f4f8031d99c089aae7e47d628 out/r2/mut-E1.txt
|
||||
10f2e9db7f01fa8940bb9eaa25caac515efc31b0d4097585818eacb5b7898f87 out/r2/mut-E2.txt
|
||||
4abaeec9660967e5e3ffa15bcfccdda878614ad6b11ee3884626d6a7f9aee985 out/r2/mut-F2a.txt
|
||||
1e3f85e428b630abc810a76843f1ca9bb9ad10c357553892c764782a7d591b1d out/r2/mut-F2b.txt
|
||||
891f2876e90ca1feceb284f7dbceb47c4f6dc9048acca4eaa0eaa579a1eeaada out/r2/mut-F2c.txt
|
||||
c4f702a00f877a1ab60001e76ae88af54c1f54d4b38b9bfe8d2332516f108d47 out/r2/mut-F2d.txt
|
||||
eb114215eca32c40514f3bae0cc060e19364d41507da306f25b498a01ae09a85 out/r2/mut-F2e.txt
|
||||
ae7a3521ae600758fa00561ee94f666491ea334accf6c587b536a58a4db81392 out/r2/mut-F2f.txt
|
||||
2e347ed278b09249815aff36c2d6918dac66e5abb1be5472c3c2296e63d9d28c out/r2/mut-G144.txt
|
||||
9e17773ef5226ee5be34b779a5fbd2836a6afb40dd46d3835e7ff9f47330f351 out/r2/mut-G144cb.txt
|
||||
ca18292aebbb750e23f6868f83af114042939ea027e9bfb218217733bf221d50 out/r2/mut-G150.txt
|
||||
6a33017f5886c876e1294a7edba9d2dc77632bc1c743ce617be11476d7d3d9a7 out/r2/mut-G150cb.txt
|
||||
44012b9779bfec8d8eeb5b63a8cf0779d40c807e0549df0154254f8a70f39412 out/r2/mut-G184.txt
|
||||
0231a1718a60e7113b00a85d4283204a9f141a0261f28340e804defa77aee97a out/r2/mut-G188.txt
|
||||
c841dab84d16c56f9ddeb6a8b6596d470a95560960a0c244857069bd41f254b0 out/r2/mut-J4a.txt
|
||||
7bdb2636cc19b882475c537359eb4c491746938eb8ac77e6c973755ad810ae2f out/r2/mut-J4b.txt
|
||||
e0b821077d8f5ac301d2a9575ad74c9ec3cd8c5024c8a58963bcf5d4076280f2 out/r2/mut-J4c.txt
|
||||
ad759eeb23e69bb390b309efc8e753383de2a6183a9468c6d75bad5e6e292b2f out/r2/mut-J4d.txt
|
||||
742e9d4485c22a7e87c1f52baccf4eba0f7d3191ec43e8b4f19533b41b757503 out/r2/mut-M28.txt
|
||||
8c6e9dcdf5205c8cf5955cd4c54dbe858d72184396ba4a9a8eda479096900d58 out/r2/mut-N1a.txt
|
||||
4600580bfd983aaff48d3b052f70e3822e5f47855b141d83d5122479a7d73d86 out/r2/mut-N1b.txt
|
||||
9649c89a838906e57644719fa3a9ce7b4d476ce32f1b7d60a69f728f17d44198 out/r2/mut-N1c.txt
|
||||
5eee699126b0efa5cd73d2b7ab898a28f01eea27d9258feb6ee92da1ad4ec594 out/r2/mut-N2.txt
|
||||
0ec34dae8a5ce26d319a57af50c188451fcac7790fefc06e46069ca293c8e5c5 out/r2/mut-N4.txt
|
||||
c79e920c1af4c40cc78651c7f50e9d1934303b577798771513f241c7dbd7f228 out/r2/mut-N5.txt
|
||||
c599f60479601056d702137aa632aa01689ec2b2fbc6c300bd7fa4c0478c14c9 out/r2/mut-R2a.txt
|
||||
047ae41126b9f0fb47918bcf4c57e8d75c3be1cfae82ad4a04fbc5243c513f22 out/r2/mut-R2b.txt
|
||||
1c808e2f92791046aa30f12f5cc5c38cfd7713b30196d056a9cbca3ffcb53bdb out/r2/mut-R2c.txt
|
||||
b5dd327fef18a3d27a8b6a3235e5cda327fa4ba3323fc639220fe9b5e6fab0d1 out/r2/mut-X14.txt
|
||||
44d018a5d7d6d3a9a87f6af426b8813f1f222a6c5ff14728e14f0a562fe3a677 out/r2/mut-X9.txt
|
||||
d3683280d195b7476b72ff0481d1c874c3252b7da5fd75a7233bc78e282a0643 out/r2/mut-end.txt
|
||||
174f543b5500166f6ff4a11330e8fb514148f9cf8a2a88479c9251daf6b552df out/r2/mut-start.txt
|
||||
065fec22c61273a215108713edcace910193705697e91248b1783efbf4546c25 out/r2/mutants.txt
|
||||
8eaa5212718b37938ab792b00210ad00c7b793259ae9e0befc6f7b0316c1881e out/run.txt
|
||||
f91fc95a173339137f981e24fbf630acfde957e32c3cd71e28304532083c6a77 out/suite-auth-r2.txt
|
||||
f91fc95a173339137f981e24fbf630acfde957e32c3cd71e28304532083c6a77 out/suite-auth.txt
|
||||
e1206366f07f3d9029530dcdcb55c0282d3f052adc60fc7fd51642d5d4ec3600 out/suite-conductor-r2.txt
|
||||
e1206366f07f3d9029530dcdcb55c0282d3f052adc60fc7fd51642d5d4ec3600 out/suite-conductor.txt
|
||||
52d211444489ea729dafc3c9745d4474d9dfd50f41055cbad792424197f04073 out/suite-config-r2.txt
|
||||
52d211444489ea729dafc3c9745d4474d9dfd50f41055cbad792424197f04073 out/suite-config.txt
|
||||
7d5016ded1994048642f8b9ae845e52e8af4a069ba05c056c8f580a4589bacbe out/suite-discord-r2.txt
|
||||
7d5016ded1994048642f8b9ae845e52e8af4a069ba05c056c8f580a4589bacbe out/suite-discord.txt
|
||||
4f840b0038586f0227c7f03914fdcb98bf9271068e3830183a877e5ac427d0d0 out/suite-extension-package-r2.txt
|
||||
e4c762a4d4225b2f3eab0e37cba1d36126e67465557501196b91c2ffdd576e0e out/suite-extension-package.txt
|
||||
83dfaa21c4e4ad941088efab3f69e87ebd2f6bd3cc72ede938e21dc06da8a34f out/suite-foundation-r2.txt
|
||||
83dfaa21c4e4ad941088efab3f69e87ebd2f6bd3cc72ede938e21dc06da8a34f out/suite-foundation.txt
|
||||
bf5db498b82546af2d78a218a1d6446da993de629f4819fffeecf690ef93a314 out/suite-queue-r2.txt
|
||||
bf5db498b82546af2d78a218a1d6446da993de629f4819fffeecf690ef93a314 out/suite-queue.txt
|
||||
6183e6b9b05edb497e92ffd09a19bba49c6f656c750ecb2acc283021113d1a98 out/suite-release-r2.txt
|
||||
6183e6b9b05edb497e92ffd09a19bba49c6f656c750ecb2acc283021113d1a98 out/suite-release.txt
|
||||
8df77c49bf82833efa56d42401a4a06f20931b7a76dd5eed3a73fdec60e92e79 out/suite-task-r2.txt
|
||||
8df77c49bf82833efa56d42401a4a06f20931b7a76dd5eed3a73fdec60e92e79 out/suite-task.txt
|
||||
50ceb870e3f56cac0ea04c7f7995a5eb5ec4e66f9c09a92a9fc7705e1df896cf run.sh
|
||||
|
||||
Reference in New Issue
Block a user