docs(cli): row 45 S4 follow-up candidate packet (rocko)
Packet for #1527, base 521597bb: build.patch, candidate manifest
b329fdcb, packet manifest, BUILD.md, gate and mutant receipts. The
candidate itself is not committed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
# Row 45 (#1527): S4 follow-up, candidate packet
|
||||
|
||||
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
|
||||
uncommitted. There are no commits, pushes or Gitea calls, and no token or
|
||||
private binding was read.
|
||||
|
||||
## 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 |
|
||||
| `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/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`).
|
||||
|
||||
## What changed
|
||||
|
||||
### 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.
|
||||
- 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.
|
||||
- The fifth definite refusal appends one line,
|
||||
`{kind:"dm", decision, outcome:"gave-up", messageId:null}`, and logs one
|
||||
line: `notify: dm <id> refused 5 times; gave up, not retried`. It logs no
|
||||
retry line and drops the backoff entry.
|
||||
- A crash between the fifth refusal and its gave-up line: the next tick sees
|
||||
the count at the limit, appends the gave-up line and sends nothing.
|
||||
- `tick` skips a decision that already has a gave-up line. The digest marks
|
||||
it `[blocking, DM refused, not retried]`. The mark order is sent, then
|
||||
gave up, then pending.
|
||||
|
||||
### J4: the journal type check
|
||||
|
||||
The brief says confirmed lines. I type-check **every** complete line on open,
|
||||
because a bad `refused` or `gave-up` line now feeds the F2 count as well. A
|
||||
bad line refuses with a CliError, exit 3:
|
||||
`notify journal line N is malformed (<field>): <file>`. `<field>` is `json`
|
||||
or the first field that fails:
|
||||
|
||||
- `record`: not a plain object.
|
||||
- `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.
|
||||
- `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.
|
||||
|
||||
### Error messages (the brief's error items)
|
||||
|
||||
- mkdir fails with EACCES, EPERM or EROFS:
|
||||
`notify journal directory cannot be created (CODE): <dir>`.
|
||||
- Directory is a symlink (checked with lstat):
|
||||
`notify journal directory must not be a symlink: <dir>`. The 0700 check
|
||||
follows it as before.
|
||||
- Directory not writable (`accessSync` W_OK|X_OK):
|
||||
`notify journal directory is not writable (CODE): <dir>`.
|
||||
- File not writable on open: `notify journal is not writable (CODE): <file>`.
|
||||
|
||||
Every case is a CliError with exit 3 and names the path. bus-service.sh's
|
||||
install message adds `mkdir -m 0700 -p <dataRoot>/notify/<business>`.
|
||||
`docs/TOOLS.md` is **unchanged**: it does not quote the install output and
|
||||
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
|
||||
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.
|
||||
|
||||
### Tests
|
||||
|
||||
- **Reader-capability exposure:** a `node:diagnostics_channel` spy on
|
||||
`child_process` traps the `send` setter and captures the `{op:"start",
|
||||
cap}` message the host sends the notifier. host.mjs is not modified. The
|
||||
test asserts that both `launch.cap` and the reader cap are absent from
|
||||
`/proc/<pid>/cmdline` and `environ` for both children and from the state
|
||||
file.
|
||||
- **M28:** a second host for the same data root refuses with exit 3,
|
||||
`a bus host already runs for acme (pid …)`.
|
||||
- **N5:** `watchChildren` reports a child killed by SIGKILL before the call
|
||||
as `["broker", null, "SIGKILL"]`.
|
||||
- **N2 and N4:** a symlinked directory refuses. An append after the file is
|
||||
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.
|
||||
- **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.
|
||||
- **`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.
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
## Gate
|
||||
|
||||
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 |
|
||||
|
||||
## Hashes
|
||||
|
||||
- `build.patch`: `4ed9ff61b94b4c9d2426e5703c2e6c0dbcd6bc0c64a88d59347a8b47f4933408`
|
||||
- `candidate-manifest.sha256`: `b329fdcbdf8cb62659f239359570dfa9771559e3112b743df55429244769ed14`
|
||||
- `packet-manifest.sha256`: a sorted sha256 list of every packet file except itself.
|
||||
@@ -0,0 +1 @@
|
||||
521597bbe0cf51a0a1aa3b7a16f13fc7b59c98c6
|
||||
@@ -0,0 +1,846 @@
|
||||
diff --git a/packages/cli/README.md b/packages/cli/README.md
|
||||
index 589078c3..67dc6618 100644
|
||||
--- a/packages/cli/README.md
|
||||
+++ b/packages/cli/README.md
|
||||
@@ -145,8 +145,8 @@ 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.
|
||||
|
||||
The notifier only reads the bus. Its memory is the journal
|
||||
@@ -165,16 +165,29 @@ one line per send attempt:
|
||||
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).
|
||||
- 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
|
||||
syncs it. It logs both steps. A crash between the two leaves the torn
|
||||
tail in place, and the next open repairs it with a second copy. The next
|
||||
append therefore starts on a line of its own.
|
||||
-- A malformed complete line refuses with exit 3 and changes nothing.
|
||||
-- The journal refuses with exit 3 when its directory is looser than 0700 or
|
||||
- not yours, when `sent.jsonl` is a symlink, or when the file is not a
|
||||
- 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`,
|
||||
+ `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.
|
||||
- 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
|
||||
--- 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
|
||||
} catch (e) {
|
||||
notify.kill("SIGTERM");
|
||||
await ended(notify, 5000);
|
||||
- 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" });
|
||||
await ended(broker, CLOSE_TIMEOUT_MS);
|
||||
throw new CliError(`notifier refused to start: ${typeof ok?.error === "string" ? ok.error : "notifier-refused"}`, 3);
|
||||
}
|
||||
diff --git a/packages/cli/src/notifier.mjs b/packages/cli/src/notifier.mjs
|
||||
index a4bb78d2..0610e1f5 100644
|
||||
--- a/packages/cli/src/notifier.mjs
|
||||
+++ b/packages/cli/src/notifier.mjs
|
||||
@@ -4,17 +4,22 @@
|
||||
// bus; its memory is the journal `<dataRoot>/notify/<business>/sent.jsonl`
|
||||
// (0600, in a 0700 directory), one line per send attempt:
|
||||
//
|
||||
-// {at, kind: "dm"|"digest", decision, day?, outcome: "confirmed"|"refused"|"unknown", messageId, status?}
|
||||
+// {at, kind: "dm"|"digest", decision, day?, outcome: "confirmed"|"refused"|"unknown"|"gave-up", messageId, status?}
|
||||
//
|
||||
// A decision counts as sent once it has a confirmed line; a day's digest
|
||||
// likewise. A refused or unknown send is retried with backoff (30 s
|
||||
// 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.
|
||||
// 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.
|
||||
|
||||
import { createHash } from "node:crypto";
|
||||
-import { closeSync, constants, fstatSync, fsyncSync, ftruncateSync, lstatSync, mkdirSync, openSync, readFileSync, writeSync } from "node:fs";
|
||||
+import { accessSync, closeSync, constants, fstatSync, fsyncSync, ftruncateSync, lstatSync, mkdirSync, openSync, readFileSync, writeSync } from "node:fs";
|
||||
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;
|
||||
const BACKOFF_MS = 30000;
|
||||
const BACKOFF_MAX_MS = 30 * 60 * 1000;
|
||||
const LIMIT = 2000;
|
||||
+export const REFUSAL_LIMIT = 5;
|
||||
|
||||
export const journalPath = (dataRoot, business) => join(dataRoot, "notify", business, "sent.jsonl");
|
||||
|
||||
@@ -62,14 +68,14 @@ export function dmContent(business, d) {
|
||||
return clip(lines.join("\n"), LIMIT);
|
||||
}
|
||||
|
||||
-export function digestContent(business, day, inbox, dmSent) {
|
||||
+export function digestContent(business, day, inbox, dmSent, dmGaveUp = () => false) {
|
||||
if (inbox.length === 0) return `Mosaic digest (${business}, ${day}): your inbox is empty.`;
|
||||
const head = `Mosaic digest (${business}, ${day}): ${inbox.length} open decision(s).`;
|
||||
const tail = "Run mosaic inbox for the full list.";
|
||||
const lines = [head];
|
||||
let shown = 0;
|
||||
for (const d of inbox) {
|
||||
- const mark = d.blocking ? (dmSent(d.id) ? "[blocking, DM sent] " : "[blocking, DM pending] ") : "";
|
||||
+ const mark = d.blocking ? (dmSent(d.id) ? "[blocking, DM sent] " : dmGaveUp(d.id) ? "[blocking, DM refused, not retried] " : "[blocking, DM pending] ") : "";
|
||||
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) {
|
||||
}
|
||||
}
|
||||
|
||||
+const OUTCOMES = ["confirmed", "refused", "unknown", "gave-up"];
|
||||
+const DAY = /^\d{4}-\d{2}-\d{2}$/;
|
||||
+const UNWRITABLE = ["EACCES", "EPERM", "EROFS"];
|
||||
+
|
||||
+// The first field of a journal record that has the wrong type, or null
|
||||
+// (lead decision 72, J4). A confirmed line carries the message id.
|
||||
+function badField(r) {
|
||||
+ if (!r || typeof r !== "object" || Array.isArray(r)) return "record";
|
||||
+ 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 (!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";
|
||||
+ return null;
|
||||
+}
|
||||
+
|
||||
+// A DM refused with an HTTP 4xx other than 429.
|
||||
+const definite = (r) => r.kind === "dm" && r.outcome === "refused" && Number.isInteger(r.status) && r.status >= 400 && r.status < 500 && r.status !== 429;
|
||||
+
|
||||
// Opens (creating if needed) the journal. The directory must be 0700 or
|
||||
-// tighter and the file 0600, both owned by this user; a symlinked journal
|
||||
-// refuses. Every complete line must parse, or the open refuses with exit 3.
|
||||
+// tighter, writable and not a symlink, and the file 0600, both owned by
|
||||
+// this user; a symlinked journal refuses. Every complete line must parse
|
||||
+// and type-check, or the open refuses with exit 3.
|
||||
// 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) {
|
||||
// both; the second copy is harmless.
|
||||
export function openJournal(file, { log = () => {}, now = () => new Date() } = {}) {
|
||||
const dir = dirname(file);
|
||||
- mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||
+ 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);
|
||||
+ if (e.code !== "EEXIST") throw e;
|
||||
+ }
|
||||
const ds = lstatSync(dir);
|
||||
+ if (ds.isSymbolicLink()) throw new CliError(`notify journal directory must not be a symlink: ${dir}`, 3);
|
||||
if (!ds.isDirectory() || ds.uid !== process.getuid() || (ds.mode & 0o077) !== 0) {
|
||||
throw new CliError(`notify journal directory must be mode 0700 and owned by this user: ${dir}`, 3);
|
||||
}
|
||||
+ try {
|
||||
+ accessSync(dir, constants.W_OK | constants.X_OK);
|
||||
+ } catch (e) {
|
||||
+ if (UNWRITABLE.includes(e.code)) throw new CliError(`notify journal directory is not writable (${e.code}): ${dir}`, 3);
|
||||
+ throw e;
|
||||
+ }
|
||||
let fd;
|
||||
try {
|
||||
fd = openSync(file, O_RDWR | O_APPEND | O_CREAT | O_NOFOLLOW, 0o600);
|
||||
} 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);
|
||||
throw e;
|
||||
}
|
||||
const sent = new Set();
|
||||
const days = new Set();
|
||||
+ const refusals = new Map();
|
||||
+ const gaveUp = new Set();
|
||||
+ 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 (r.outcome !== "confirmed") return;
|
||||
+ if (r.kind === "dm") sent.add(r.decision);
|
||||
+ else days.add(r.day);
|
||||
+ };
|
||||
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() } = {
|
||||
} catch {
|
||||
r = null;
|
||||
}
|
||||
- if (!r || typeof r !== "object" || !["dm", "digest"].includes(r.kind)) {
|
||||
- throw new CliError(`notify journal line ${i + 1} is malformed: ${file}`, 3);
|
||||
- }
|
||||
- if (r.outcome !== "confirmed") return;
|
||||
- if (r.kind === "dm") sent.add(r.decision);
|
||||
- else days.add(r.day);
|
||||
+ const bad = r === null ? "json" : badField(r);
|
||||
+ if (bad) throw new CliError(`notify journal line ${i + 1} is malformed (${bad}): ${file}`, 3);
|
||||
+ count(r);
|
||||
});
|
||||
if (end < bytes.length) {
|
||||
const name = copyTorn(dir, bytes.subarray(end), now());
|
||||
@@ -176,6 +223,8 @@ export function openJournal(file, { log = () => {}, now = () => new Date() } = {
|
||||
return {
|
||||
sent,
|
||||
days,
|
||||
+ refusals,
|
||||
+ gaveUp,
|
||||
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() } = {
|
||||
} finally {
|
||||
closeSync(afd);
|
||||
}
|
||||
- if (record.outcome !== "confirmed") return;
|
||||
- if (record.kind === "dm") sent.add(record.decision);
|
||||
- else days.add(record.day);
|
||||
+ count(record);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -202,6 +249,13 @@ export function createNotifier({ business, dataRoot, inbox, direct, now = () =>
|
||||
return b !== undefined && t < b.next;
|
||||
}
|
||||
|
||||
+ // One `gave-up` line and one log line; the decision is never sent again.
|
||||
+ function giveUp(decision, t) {
|
||||
+ journal.append({ at: new Date(t).toISOString(), kind: "dm", decision, outcome: "gave-up", messageId: null });
|
||||
+ backoff.delete(`dm:${decision}`);
|
||||
+ log(`notify: dm ${shortId(decision)} refused ${journal.refusals.get(decision)} times; gave up, not retried`);
|
||||
+ }
|
||||
+
|
||||
async function attempt(key, record, message) {
|
||||
const t = now().getTime();
|
||||
try {
|
||||
@@ -215,6 +269,10 @@ export function createNotifier({ business, dataRoot, inbox, direct, now = () =>
|
||||
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 } : {}) });
|
||||
+ 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`);
|
||||
return false;
|
||||
}
|
||||
@@ -232,7 +290,12 @@ export function createNotifier({ business, dataRoot, inbox, direct, now = () =>
|
||||
}
|
||||
const t = now();
|
||||
for (const d of list) {
|
||||
- if (!d.blocking || journal.sent.has(d.id)) continue;
|
||||
+ if (!d.blocking || journal.sent.has(d.id) || journal.gaveUp.has(d.id)) continue;
|
||||
+ // A crash between the fifth refusal and its gave-up line.
|
||||
+ if ((journal.refusals.get(d.id) ?? 0) >= REFUSAL_LIMIT) {
|
||||
+ giveUp(d.id, t.getTime());
|
||||
+ 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 = () =>
|
||||
const { day, hour } = zoned(t, zone);
|
||||
const key = `digest:${day}`;
|
||||
if (hour >= DIGEST_HOUR && !journal.days.has(day) && !waiting(key, t.getTime())) {
|
||||
- const content = digestContent(business, day, list, (id) => journal.sent.has(id));
|
||||
+ const content = digestContent(business, day, list, (id) => journal.sent.has(id), (id) => journal.gaveUp.has(id));
|
||||
if (await attempt(key, { kind: "digest", decision: null, day }, { content, nonce: digestNonce(business, day) })) done.digest = true;
|
||||
else done.failed++;
|
||||
}
|
||||
diff --git a/packages/cli/tests/host.test.mjs b/packages/cli/tests/host.test.mjs
|
||||
index 8d20b47f..649031ae 100644
|
||||
--- a/packages/cli/tests/host.test.mjs
|
||||
+++ b/packages/cli/tests/host.test.mjs
|
||||
@@ -1,6 +1,7 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
+import { subscribe, unsubscribe } from "node:diagnostics_channel";
|
||||
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) {
|
||||
throw new Error("timed out waiting");
|
||||
}
|
||||
|
||||
+// Records every IPC message this process sends to a child it creates while
|
||||
+// the spy is on, by trapping the `send` that node installs on a new child.
|
||||
+// The host never exposes the notifier's reader capability; this is how a
|
||||
+// test sees it.
|
||||
+function spySends(t, onSend = () => {}) {
|
||||
+ const sent = [];
|
||||
+ const onChild = ({ process: child }) => {
|
||||
+ let send;
|
||||
+ Object.defineProperty(child, "send", {
|
||||
+ configurable: true,
|
||||
+ get: () => send,
|
||||
+ set(fn) {
|
||||
+ send = function (m, ...rest) {
|
||||
+ sent.push(m);
|
||||
+ onSend(m);
|
||||
+ return fn.call(this, m, ...rest);
|
||||
+ };
|
||||
+ },
|
||||
+ });
|
||||
+ };
|
||||
+ subscribe("child_process", onChild);
|
||||
+ t.after(() => unsubscribe("child_process", onChild));
|
||||
+ return sent;
|
||||
+}
|
||||
+
|
||||
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
|
||||
const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env });
|
||||
assert.equal("trackers" in boot, false);
|
||||
const logs = [];
|
||||
+ const sends = spySends(t);
|
||||
const host = await startHost({ boot, business: "acme", notifier: { binding: "test-seat", base: discord.base, pollMs: 100 }, log: (l) => logs.push(l) });
|
||||
t.after(() => host.close(0));
|
||||
+ const start = sends.find((m) => m?.op === "start");
|
||||
+ assert.equal(typeof start?.cap, "string", "the spy saw the notifier's start message");
|
||||
+ assert.ok(start.cap.length >= 16);
|
||||
|
||||
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
|
||||
assert.ok(discord.requests.every((r) => r.authorized));
|
||||
assert.match(discord.dms()[0].body.content, new RegExp(`mosaic decide ${d.id.slice(0, 8)}`));
|
||||
|
||||
- // No capability in a child's argv or environment, or in the state file.
|
||||
- for (const pid of Object.values(host.pids)) {
|
||||
- assert.ok(!procText(pid, "cmdline").includes(launch.cap));
|
||||
- assert.ok(!procText(pid, "environ").includes(launch.cap));
|
||||
+ // No capability, the launch's or the notifier's reader, in a child's argv
|
||||
+ // or environment, or in the state file.
|
||||
+ for (const cap of [launch.cap, start.cap]) {
|
||||
+ for (const pid of Object.values(host.pids)) {
|
||||
+ assert.notEqual(procText(pid, "cmdline"), "", `pid ${pid} is readable`);
|
||||
+ assert.ok(!procText(pid, "cmdline").includes(cap));
|
||||
+ assert.ok(!procText(pid, "environ").includes(cap));
|
||||
+ }
|
||||
+ assert.ok(!readFileSync(hostFile(f.dataRoot), "utf8").includes(cap));
|
||||
}
|
||||
- assert.ok(!readFileSync(hostFile(f.dataRoot), "utf8").includes(launch.cap));
|
||||
|
||||
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
|
||||
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) });
|
||||
+ t.after(() => host.close(0));
|
||||
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
|
||||
assert.equal(existsSync(hostFile(f.dataRoot)), false);
|
||||
});
|
||||
|
||||
+test("a second host for the same data root refuses with exit 3 while the first runs", 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 });
|
||||
+ const host = await startHost({ boot, business: "acme", log: () => {} });
|
||||
+ t.after(() => host.close(0));
|
||||
+ const second = startHost({ boot, business: "acme", log: () => {} });
|
||||
+ // If the refusal regresses and a second host starts, close it too.
|
||||
+ t.after(async () => (await second.catch(() => null))?.close(0));
|
||||
+ await assert.rejects(second, (e) => e.exitCode === 3 && e.message === `a bus host already runs for acme (pid ${process.pid})`);
|
||||
+ assert.equal(hostStatus(f.dataRoot).host.live, true, "the first host still runs");
|
||||
+ assert.equal(await host.close(0), 0);
|
||||
+});
|
||||
+
|
||||
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"
|
||||
assert.equal(existsSync(hostFile(f.dataRoot)), false);
|
||||
});
|
||||
|
||||
+test("a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker", 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 });
|
||||
+ const children = [];
|
||||
+ const onChild = ({ process: child }) => children.push(child);
|
||||
+ subscribe("child_process", onChild);
|
||||
+ t.after(() => unsubscribe("child_process", onChild));
|
||||
+ const errors = [];
|
||||
+ // The broker dies as the host sends the notifier its start message.
|
||||
+ const sends = spySends(t, (m) => {
|
||||
+ if (m?.op !== "start") return;
|
||||
+ children[0].on("error", (e) => errors.push(e.code));
|
||||
+ children[0].kill("SIGKILL");
|
||||
+ });
|
||||
+ 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 && /no dmRecipient/.test(e.message));
|
||||
+ await new Promise((r) => setImmediate(r));
|
||||
+ assert.deepEqual(errors, [], "no close was sent over the closed channel");
|
||||
+ assert.equal(sends.filter((m) => m?.op === "close").length, 0);
|
||||
+});
|
||||
+
|
||||
+test("a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker", 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 });
|
||||
+ const children = [];
|
||||
+ const onChild = ({ process: child }) => children.push(child);
|
||||
+ subscribe("child_process", onChild);
|
||||
+ t.after(() => unsubscribe("child_process", onChild));
|
||||
+ const errors = [];
|
||||
+ // The broker dies as the host sends the start message. The notifier is
|
||||
+ // stopped so it cannot reply, and killed once the host has seen the broker
|
||||
+ // disconnect: the host takes the no-reply path, not the refusal path.
|
||||
+ const sends = spySends(t, (m) => {
|
||||
+ if (m?.op !== "start") return;
|
||||
+ children[0].on("error", (e) => errors.push(e.code));
|
||||
+ children[1].kill("SIGSTOP");
|
||||
+ children[0].once("disconnect", () => children[1].kill("SIGKILL"));
|
||||
+ children[0].kill("SIGKILL");
|
||||
+ });
|
||||
+ 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));
|
||||
+ await new Promise((r) => setImmediate(r));
|
||||
+ assert.deepEqual(errors, [], "no close was sent over the closed channel");
|
||||
+ assert.equal(sends.filter((m) => m?.op === "close").length, 0);
|
||||
+});
|
||||
+
|
||||
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" });
|
||||
await once(early, "exit");
|
||||
+ const killed = spawn(process.execPath, ["-e", "setTimeout(() => {}, 60000)"], { stdio: "ignore" });
|
||||
+ await once(killed, "spawn");
|
||||
+ killed.kill("SIGKILL");
|
||||
+ await once(killed, "exit");
|
||||
+ const signalled = [];
|
||||
+ watchChildren({ broker: killed }, (...d) => signalled.push(d));
|
||||
+ assert.deepEqual(signalled, [["broker", null, "SIGKILL"]], "a death by signal before the watch is not lost either");
|
||||
const late = spawn(process.execPath, ["-e", "setTimeout(() => {}, 60000)"], { stdio: "ignore" });
|
||||
t.after(() => late.kill("SIGKILL"));
|
||||
await once(late, "spawn");
|
||||
diff --git a/packages/cli/tests/notifier.test.mjs b/packages/cli/tests/notifier.test.mjs
|
||||
index 99127662..aa81ac69 100644
|
||||
--- a/packages/cli/tests/notifier.test.mjs
|
||||
+++ b/packages/cli/tests/notifier.test.mjs
|
||||
@@ -1,12 +1,15 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
-import { appendFileSync, chmodSync, mkdirSync, readdirSync, readFileSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
+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 { RestOutcome } from "../../discord/src/rest.mjs";
|
||||
import { broker, tmp } from "./helpers.mjs";
|
||||
|
||||
+const AT = "2026-10-08T12:00:00.000Z";
|
||||
+
|
||||
// Discord-side fake: records sends, answers from a script (default: ok).
|
||||
+// An entry is "ok", "refused" (HTTP 403), "unknown", or {kind, status}.
|
||||
function fakeDirect(script = []) {
|
||||
const sends = [];
|
||||
let n = 0;
|
||||
@@ -16,7 +19,8 @@ function fakeDirect(script = []) {
|
||||
sends.push(m);
|
||||
const next = script.shift() ?? "ok";
|
||||
if (next === "ok") return { messageId: `30000000000000${String(++n).padStart(4, "0")}` };
|
||||
- throw new RestOutcome(next, `dm: ${next}`, { status: next === "refused" ? 403 : null });
|
||||
+ const { kind, status } = typeof next === "string" ? { kind: next, status: next === "refused" ? 403 : null } : next;
|
||||
+ throw new RestOutcome(kind, `dm: ${kind}`, { status });
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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)));
|
||||
});
|
||||
|
||||
+// 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;
|
||||
+
|
||||
+test("five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count", async (t) => {
|
||||
+ assert.equal(REFUSAL_LIMIT, 5);
|
||||
+ const s = setup(t, MIDNIGHT, ["refused", "refused", "refused", { kind: "refused", status: 404 }, { kind: "refused", status: 400 }]);
|
||||
+ const d = s.raise("git.push.protected", { target: "refactor", blocking: true, task_ref: "vikunja:1/7" });
|
||||
+ for (let i = 0; i < 3; i++) {
|
||||
+ assert.equal((await s.notifier.tick()).failed, 1);
|
||||
+ s.time.advance(PAST_BACKOFF);
|
||||
+ }
|
||||
+ const after = s.make();
|
||||
+ for (let i = 0; i < 2; i++) {
|
||||
+ assert.equal((await after.tick()).failed, 1, "the restart did not reset the count");
|
||||
+ s.time.advance(PAST_BACKOFF);
|
||||
+ }
|
||||
+ assert.equal(s.direct.sends.length, 5);
|
||||
+ assert.deepEqual(s.journal().map((r) => r.outcome), ["refused", "refused", "refused", "refused", "refused", "gave-up"]);
|
||||
+ assert.deepEqual(s.journal().at(-1), { at: s.journal().at(-1).at, kind: "dm", decision: d.id, outcome: "gave-up", messageId: null });
|
||||
+ assert.deepEqual(await after.tick(), { dms: 0, digest: false, failed: 0 });
|
||||
+ assert.deepEqual(await s.make().tick(), { dms: 0, digest: false, failed: 0 });
|
||||
+ assert.equal(s.direct.sends.length, 5, "never sent again, before or after a restart");
|
||||
+ assert.equal(s.journal().length, 6);
|
||||
+ const gave = s.logs.filter((l) => /gave up/.test(l));
|
||||
+ assert.deepEqual(gave, [`notify: dm ${d.id.slice(0, 8)} refused 5 times; gave up, not retried`]);
|
||||
+ assert.equal(s.logs.filter((l) => /retry in/.test(l)).length, 4, "the fifth refusal logs the give-up, not a retry");
|
||||
+ s.time.clock.t = new Date("2026-10-08T13:00:00Z");
|
||||
+ assert.equal((await s.make().tick()).digest, true);
|
||||
+ assert.match(s.direct.sends.at(-1).content, new RegExp(`\\[blocking, DM refused, not retried\\] ${d.id.slice(0, 8)} git\\.push\\.protected`));
|
||||
+});
|
||||
+
|
||||
+test("429s, 5xx-style unknowns and refusals without a status never count toward the limit", async (t) => {
|
||||
+ const script = [
|
||||
+ ...Array(6).fill({ kind: "refused", status: 429 }),
|
||||
+ ...Array(3).fill("unknown"),
|
||||
+ { kind: "refused", status: null },
|
||||
+ ...Array(REFUSAL_LIMIT - 1).fill("refused"),
|
||||
+ "ok",
|
||||
+ ];
|
||||
+ const s = setup(t, MIDNIGHT, [...script]);
|
||||
+ s.raise("git.push.protected", { target: "refactor", blocking: true, task_ref: "vikunja:1/7" });
|
||||
+ for (let i = 0; i < script.length; i++) {
|
||||
+ await s.notifier.tick();
|
||||
+ s.time.advance(PAST_BACKOFF);
|
||||
+ }
|
||||
+ assert.equal(s.direct.sends.length, script.length);
|
||||
+ assert.equal(s.journal().at(-1).outcome, "confirmed");
|
||||
+ assert.ok(!s.journal().some((r) => r.outcome === "gave-up"));
|
||||
+ assert.ok(!s.logs.some((l) => /gave up/.test(l)));
|
||||
+});
|
||||
+
|
||||
+test("a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing", async (t) => {
|
||||
+ const s = setup(t, MIDNIGHT);
|
||||
+ const d = s.raise("git.push.protected", { target: "refactor", blocking: true, task_ref: "vikunja:1/7" });
|
||||
+ const file = journalPath(s.dataRoot, "demo");
|
||||
+ const line = { at: AT, kind: "dm", decision: d.id, outcome: "refused", messageId: null, status: 403 };
|
||||
+ appendFileSync(file, `${JSON.stringify(line)}\n`.repeat(REFUSAL_LIMIT));
|
||||
+ assert.deepEqual(await s.make().tick(), { dms: 0, digest: false, failed: 0 });
|
||||
+ assert.equal(s.direct.sends.length, 0);
|
||||
+ assert.equal(s.journal().at(-1).outcome, "gave-up");
|
||||
+ assert.equal(s.logs.filter((l) => /gave up/.test(l)).length, 1);
|
||||
+ await s.make().tick();
|
||||
+ assert.equal(s.journal().length, REFUSAL_LIMIT + 1, "one gave-up line only");
|
||||
+});
|
||||
+
|
||||
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';
|
||||
|
||||
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");
|
||||
- openJournal(file).append({ at: "x", kind: "dm", decision: "a", outcome: "confirmed", messageId: "1" });
|
||||
+ openJournal(file).append({ at: AT, kind: "dm", decision: "a", outcome: "confirmed", messageId: "1" });
|
||||
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
|
||||
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/);
|
||||
- j.append({ at: "y", kind: "dm", decision: "b", outcome: "confirmed", messageId: "2" });
|
||||
+ j.append({ at: AT, kind: "dm", decision: "b", outcome: "confirmed", messageId: "2" });
|
||||
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
|
||||
|
||||
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");
|
||||
- openJournal(file).append({ at: "x", kind: "dm", decision: "a", outcome: "confirmed", messageId: "1" });
|
||||
+ openJournal(file).append({ at: AT, kind: "dm", decision: "a", outcome: "confirmed", messageId: "1" });
|
||||
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
|
||||
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);
|
||||
- j.append({ at: "y", kind: "dm", decision: "b", outcome: "confirmed", messageId: "2" });
|
||||
+ j.append({ at: AT, kind: "dm", decision: "b", outcome: "confirmed", messageId: "2" });
|
||||
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
|
||||
assert.throws(() => openJournal(linked), (e) => e.exitCode === 3 && /must not be a symlink/.test(e.message));
|
||||
});
|
||||
|
||||
+test("the journal: a line with a wrong type refuses with exit 3 and names the field", (t) => {
|
||||
+ const file = journalPath(tmp(t), "demo");
|
||||
+ openJournal(file);
|
||||
+ const dm = { at: AT, kind: "dm", decision: "a", outcome: "confirmed", messageId: "1" };
|
||||
+ const digest = { at: AT, kind: "digest", decision: null, day: "2026-10-08", outcome: "confirmed", messageId: "2" };
|
||||
+ const bad = [
|
||||
+ ["record", [1]],
|
||||
+ ["at", { ...dm, at: "x" }],
|
||||
+ ["at", { ...dm, at: "2026-10-08" }],
|
||||
+ ["at", { ...dm, at: 1 }],
|
||||
+ ["kind", { ...dm, kind: "dg" }],
|
||||
+ ["decision", { ...dm, decision: 7 }],
|
||||
+ ["decision", { ...dm, decision: "" }],
|
||||
+ ["decision", { ...digest, decision: "a" }],
|
||||
+ ["day", { ...digest, day: undefined }],
|
||||
+ ["day", { ...digest, day: "2026-10-8" }],
|
||||
+ ["day", { ...dm, day: 20261008 }],
|
||||
+ ["outcome", { ...dm, outcome: "sent" }],
|
||||
+ ["outcome", { ...digest, outcome: "gave-up", messageId: null }],
|
||||
+ ["messageId", { ...dm, messageId: null }],
|
||||
+ ["messageId", { ...dm, outcome: "refused", messageId: 1 }],
|
||||
+ ["status", { ...dm, outcome: "refused", messageId: null, status: "403" }],
|
||||
+ ["status", { ...dm, outcome: "refused", messageId: null, status: 403.5 }],
|
||||
+ ];
|
||||
+ for (const [field, r] of bad) {
|
||||
+ const text = `${JSON.stringify(dm)}\n${JSON.stringify(r)}\n`;
|
||||
+ writeFileSync(file, text);
|
||||
+ assert.throws(() => openJournal(file), (e) => e.exitCode === 3 && e.message === `notify journal line 2 is malformed (${field}): ${file}`, `${field}: ${JSON.stringify(r)}`);
|
||||
+ assert.equal(readFileSync(file, "utf8"), text, "a refusal changes nothing");
|
||||
+ }
|
||||
+ const good = [dm, digest, { ...dm, outcome: "refused", messageId: null, status: 403 }, { ...dm, outcome: "gave-up", messageId: null }, { ...digest, outcome: "unknown", messageId: null }];
|
||||
+ writeFileSync(file, good.map((r) => `${JSON.stringify(r)}\n`).join(""));
|
||||
+ const j = openJournal(file);
|
||||
+ assert.deepEqual([...j.sent], ["a"]);
|
||||
+ assert.deepEqual([...j.days], ["2026-10-08"]);
|
||||
+ assert.deepEqual([...j.gaveUp], ["a"]);
|
||||
+ assert.deepEqual([...j.refusals], [["a", 1]]);
|
||||
+});
|
||||
+
|
||||
+test("the journal: a symlinked directory refuses and says it is a link", (t) => {
|
||||
+ const root = tmp(t);
|
||||
+ const real = join(root, "real");
|
||||
+ mkdirSync(real, { mode: 0o700 });
|
||||
+ const file = journalPath(root, "demo");
|
||||
+ mkdirSync(dirname(dirname(file)), { mode: 0o700 });
|
||||
+ symlinkSync(real, dirname(file));
|
||||
+ assert.throws(() => openJournal(file), (e) => e.exitCode === 3 && e.message === `notify journal directory must not be a symlink: ${dirname(file)}`);
|
||||
+ assert.deepEqual(readdirSync(real), [], "nothing was created through the link");
|
||||
+});
|
||||
+
|
||||
+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");
|
||||
+ const j = openJournal(file);
|
||||
+ const other = join(root, "elsewhere.jsonl");
|
||||
+ writeFileSync(other, "", { mode: 0o600 });
|
||||
+ rmSync(file);
|
||||
+ symlinkSync(other, file);
|
||||
+ assert.throws(() => j.append({ at: AT, kind: "dm", decision: "a", outcome: "confirmed", messageId: "1" }), (e) => e.code === "ELOOP");
|
||||
+ assert.equal(readFileSync(other, "utf8"), "");
|
||||
+});
|
||||
+
|
||||
+test("the journal: a directory it cannot write or create refuses with exit 3 and names the path", { skip: process.getuid() === 0 && "root ignores the modes" }, (t) => {
|
||||
+ const root = tmp(t);
|
||||
+ const file = journalPath(root, "demo");
|
||||
+ mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
||||
+ // Modes are restored in finally: tmp()'s cleanup runs first among the after hooks.
|
||||
+ chmodSync(dirname(file), 0o500);
|
||||
+ try {
|
||||
+ assert.throws(() => openJournal(file), (e) => e.exitCode === 3 && e.message === `notify journal directory is not writable (EACCES): ${dirname(file)}`);
|
||||
+ } finally {
|
||||
+ chmodSync(dirname(file), 0o700);
|
||||
+ }
|
||||
+ const parent = join(root, "notify");
|
||||
+ const other = journalPath(root, "acme");
|
||||
+ chmodSync(parent, 0o500);
|
||||
+ try {
|
||||
+ assert.throws(() => openJournal(other), (e) => e.exitCode === 3 && e.message === `notify journal directory cannot be created (EACCES): ${dirname(other)}`);
|
||||
+ } finally {
|
||||
+ chmodSync(parent, 0o700);
|
||||
+ }
|
||||
+});
|
||||
+
|
||||
test("digest content stays within Discord's 2000 characters", () => {
|
||||
const inbox = Array.from({ length: 60 }, (_, i) => ({ id: `${String(i).padStart(8, "0")}-x`, action: "deploy", question: "q".repeat(300), blocking: i % 2 === 0 }));
|
||||
const text = digestContent("demo", "2026-10-08", inbox, () => true);
|
||||
diff --git a/packages/cli/tests/trackers-boot.test.mjs b/packages/cli/tests/trackers-boot.test.mjs
|
||||
new file mode 100644
|
||||
index 00000000..b7710013
|
||||
--- /dev/null
|
||||
+++ b/packages/cli/tests/trackers-boot.test.mjs
|
||||
@@ -0,0 +1,66 @@
|
||||
+// Sage's row 39 gate check, not part of the candidate: the S4 host boots the
|
||||
+// S3 adapter through the real process.mjs when bootConfig emits trackers.
|
||||
+import { test } from "node:test";
|
||||
+import assert from "node:assert/strict";
|
||||
+import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
+import { join } from "node:path";
|
||||
+import { Client } from "../../bus/src/client.mjs";
|
||||
+import { bootConfig, loadSystem } from "../src/config.mjs";
|
||||
+import { startHost, startTimeOf } from "../src/host.mjs";
|
||||
+import { writeJson } from "../../business/tests/helpers.mjs";
|
||||
+import { FakeVikunja } from "../../tasks/src/fake.mjs";
|
||||
+import { SCOPES } from "../../tasks/tests/world.mjs";
|
||||
+import { fixture, tmp } from "./helpers.mjs";
|
||||
+
|
||||
+test("bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja", async (t) => {
|
||||
+ const root = tmp(t);
|
||||
+ const fake = new FakeVikunja();
|
||||
+ const srv = await fake.listen();
|
||||
+ t.after(() => srv.close());
|
||||
+ const owner = fake.user("svc-acme");
|
||||
+ const bots = {};
|
||||
+ for (const r of ["sync", "pm", "cto", "coder", "reviewer"]) bots[r] = fake.user(`bot-acme-${r}`, { owner });
|
||||
+ const project = fake.project("Acme", owner);
|
||||
+ for (const r of ["pm", "cto", "coder", "reviewer"]) fake.share(project, bots[r], 1);
|
||||
+ fake.share(project, bots.sync, 0);
|
||||
+ fake.install(project);
|
||||
+
|
||||
+ const f = fixture(root, "acme", (doc) => {
|
||||
+ doc.vars["tracker.baseUrl"] = srv.url;
|
||||
+ doc.tracker.sync.botId = bots.sync;
|
||||
+ for (const r of Object.keys(doc.roles)) doc.roles[r].tracker.botId = bots[r];
|
||||
+ return doc;
|
||||
+ });
|
||||
+ const put = (file, token) => {
|
||||
+ writeFileSync(file, token + "\n");
|
||||
+ chmodSync(file, 0o600);
|
||||
+ };
|
||||
+ put(f.doc.tracker.sync.credentials.vikunja.file, fake.token(bots.sync, SCOPES.sync));
|
||||
+ for (const r of Object.keys(f.doc.roles)) put(f.doc.roles[r].credentials.vikunja.file, fake.token(bots[r], r === "pm" ? SCOPES.pm : SCOPES.worker));
|
||||
+ writeJson(join(root, "project", ".mosaic", "project.json"), { projectVersion: 1, id: "stack", vars: { "tracker.project": project } });
|
||||
+
|
||||
+ mkdirSync(f.dataRoot, { recursive: true, mode: 0o700 });
|
||||
+ const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env });
|
||||
+ assert.deepEqual(boot.trackers, { acme: { baseUrl: srv.url, project, pollSeconds: 60, reconcileMinutes: 60 } });
|
||||
+ const host = await startHost({ boot, business: "acme", log: () => {} });
|
||||
+ t.after(() => host.close(0));
|
||||
+
|
||||
+ const launch = await host.bindLaunch({ business: "acme", role: "pm", run: "pm-run", harness: "pi", pid: process.pid, startTime: startTimeOf(process.pid) });
|
||||
+ const pm = new Client({ path: host.path, cap: launch.cap });
|
||||
+ await pm.call("role.claim");
|
||||
+ let code;
|
||||
+ const end = Date.now() + 15000;
|
||||
+ do {
|
||||
+ code = await pm.call("task.close", {}).then(() => "ok", (e) => e.code);
|
||||
+ if (code !== "tracker-starting") break;
|
||||
+ await new Promise((r) => setTimeout(r, 100));
|
||||
+ } while (Date.now() < end);
|
||||
+ console.log(`task.close {} answered: ${code}; fake saw ${fake.requests.length} requests, first ${fake.requests.slice(0, 3).map((r) => `${r.method} ${r.path} ${r.status}`).join(", ")}`);
|
||||
+ assert.ok(fake.requests.some((r) => r.path === "/info"), "the adapter called the fake");
|
||||
+ assert.doesNotMatch(code, /^(tracker-|credential-|scope-too-broad)/, "the adapter is ready, so the verb fails on its own arguments");
|
||||
+ const before = fake.requests.length;
|
||||
+ const missing = await pm.call("task.close", { task_ref: `vikunja:${project}/999`, verdict: "gate check" }).then(() => "ok", (e) => e.code);
|
||||
+ console.log(`task.close on a missing task answered: ${missing}; it made ${fake.requests.slice(before).map((r) => `${r.method} ${r.path} ${r.status}`).join(", ")}`);
|
||||
+ assert.ok(fake.requests.length > before, "a well-formed verb reached Vikunja through the adapter");
|
||||
+ assert.equal(await host.close(0), 0);
|
||||
+});
|
||||
diff --git a/packages/discord/tests/journal.test.mjs b/packages/discord/tests/journal.test.mjs
|
||||
index e3443510..681d8b83 100644
|
||||
--- a/packages/discord/tests/journal.test.mjs
|
||||
+++ b/packages/discord/tests/journal.test.mjs
|
||||
@@ -17,6 +17,15 @@ function journal() {
|
||||
return dir;
|
||||
}
|
||||
|
||||
+// A pid that is provably dead: a child spawned and reaped here, then
|
||||
+// confirmed gone with kill(pid, 0). A fixed number can belong to a live process.
|
||||
+function deadPid() {
|
||||
+ for (;;) {
|
||||
+ const { pid } = spawnSync(process.execPath, ["-e", ""], { stdio: "ignore" });
|
||||
+ if (Number.isSafeInteger(pid) && !pidAlive(pid)) return pid;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
function publish(dir, rec) {
|
||||
mkdirSync(lockPath(dir), { recursive: true });
|
||||
writeFileSync(ownerPath(dir), JSON.stringify(rec) + "\n", { mode: 0o600 });
|
||||
@@ -76,7 +85,7 @@ test("lock: the claim is exclusive; a second start against a live owner refuses"
|
||||
|
||||
test("lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it", () => {
|
||||
const dir = journal();
|
||||
- const dead = { pid: 2 ** 22 - 7, start: "1", boot: bootId() };
|
||||
+ const dead = { pid: deadPid(), start: "1", boot: bootId() };
|
||||
publish(dir, dead);
|
||||
assert.equal(stopTarget(dir), null);
|
||||
assert.throws(() => writePid(dir, process.pid), (err) => err instanceof DiscordError && /which is gone/.test(err.message) && /unlock/.test(err.message));
|
||||
@@ -116,7 +125,7 @@ test("lock: a stale lock (dead owner, reused pid, or record without start) refus
|
||||
assert.throws(() => unlock(dir), /cannot be verified; nothing removed/);
|
||||
rmSync(stopPath(dir));
|
||||
rmSync(lockPath(dir), { recursive: true });
|
||||
- publish(dir, { pid: 2 ** 22 - 7, start: "1" });
|
||||
+ publish(dir, { pid: deadPid(), start: "1" });
|
||||
assert.equal(ownerState(readPid(dir)), "dead");
|
||||
unlock(dir);
|
||||
rmSync(stopPath(dir));
|
||||
@@ -227,8 +236,9 @@ test("lock: identity syntax; only canonical unsigned decimal start ticks and low
|
||||
|
||||
test("lock: a process whose start marker or boot id cannot be read refuses to claim", () => {
|
||||
const dir = journal();
|
||||
- assert.equal(processStart(2 ** 22 - 7), null);
|
||||
- assert.throws(() => writePid(dir, 2 ** 22 - 7), (err) => err instanceof DiscordError && /start time or the boot id/.test(err.message));
|
||||
+ const gone = deadPid();
|
||||
+ assert.equal(processStart(gone), null);
|
||||
+ assert.throws(() => writePid(dir, gone), (err) => err instanceof DiscordError && /start time or the boot id/.test(err.message));
|
||||
assert.equal(existsSync(lockPath(dir)), false, "nothing was left behind");
|
||||
const noBoot = (pid) => ({ start: processStart(pid), boot: null });
|
||||
assert.throws(() => writePid(dir, process.pid, { identity: noBoot }), /start time or the boot id/);
|
||||
@@ -276,7 +286,7 @@ test("lock: four processes racing for the same binding; exactly one claims it an
|
||||
|
||||
test("lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner", async () => {
|
||||
const dir = journal();
|
||||
- const stale = { pid: 2 ** 22 - 7, start: "1", boot: bootId() };
|
||||
+ const stale = { pid: deadPid(), start: "1", boot: bootId() };
|
||||
publish(dir, stale);
|
||||
// Several starts race over the stale lock: none may reclaim it.
|
||||
const r1 = await race(dir, 4, "stale");
|
||||
@@ -315,7 +325,7 @@ test("lock: four-party schedule; claims landing inside an unlock's gap never sur
|
||||
const worker = fileURLToPath(new URL("../fixtures/claim-worker.mjs", import.meta.url));
|
||||
const go = join(dir, "go");
|
||||
writeFileSync(go, "");
|
||||
- const stale = { pid: 2 ** 22 - 7, start: "1", boot: bootId() };
|
||||
+ const stale = { pid: deadPid(), start: "1", boot: bootId() };
|
||||
publish(dir, stale);
|
||||
const claims = [];
|
||||
const claim = (tag) => {
|
||||
diff --git a/scripts/bus-service.sh b/scripts/bus-service.sh
|
||||
index 4d8c4b95..a60b3610 100755
|
||||
--- a/scripts/bus-service.sh
|
||||
+++ b/scripts/bus-service.sh
|
||||
@@ -68,6 +68,7 @@ install_unit() {
|
||||
fi
|
||||
cat <<MSG
|
||||
next, for one business (one per data root):
|
||||
+ mkdir -m 0700 -p <dataRoot>/notify/<business> the notifier refuses a looser directory
|
||||
write <dataRoot>/notify/<business>/notify.json, mode 0600:
|
||||
{"notifyVersion": 1, "binding": "<discord binding>"} or "binding": null for no DMs
|
||||
systemctl --user enable --now mosaic-bus@<business> start now and at login
|
||||
@@ -0,0 +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
|
||||
f015ef5ed6ae2fa6ec40b2b0d5148baf23436917f71a7fa5521249864fdde0c4 packages/cli/tests/trackers-boot.test.mjs
|
||||
219924be715db3cbfc6030c4eafd57b27189d6d971b6da26f719b2f86599d37a packages/discord/tests/journal.test.mjs
|
||||
bc7abfb98e0ffa8c0d1111069c05ac19d9cc9be3b0ed4d457c4f135f97d08bb1 scripts/bus-service.sh
|
||||
@@ -0,0 +1,8 @@
|
||||
packages/cli/README.md
|
||||
packages/cli/src/host.mjs
|
||||
packages/cli/src/notifier.mjs
|
||||
packages/cli/tests/host.test.mjs
|
||||
packages/cli/tests/notifier.test.mjs
|
||||
packages/cli/tests/trackers-boot.test.mjs
|
||||
packages/discord/tests/journal.test.mjs
|
||||
scripts/bus-service.sh
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/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
|
||||
# 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.
|
||||
# Usage: mutants.sh <tree> <outdir>
|
||||
set -u
|
||||
T="$1"; O="$2"; cd "$T"
|
||||
run() {
|
||||
local id="$1" file="$2" expr="$3"
|
||||
cp "$file" "$file.orig"
|
||||
perl -0pi -e "$expr" "$file"
|
||||
if cmp -s "$file" "$file.orig"; then echo "$id NOT-APPLIED $file"; mv "$file.orig" "$file"; return; fi
|
||||
env -u NODE_TEST_CONTEXT timeout 900 node --test 'packages/cli/tests/*.test.mjs' 'packages/discord/tests/*.test.mjs' > "$O/mut-$id.txt" 2>&1
|
||||
local f c; f=$(grep -E '^ℹ fail ' "$O/mut-$id.txt" | awk '{print $3}'); c=$(grep -E '^ℹ cancelled ' "$O/mut-$id.txt" | awk '{print $3}')
|
||||
local which; which=$(sed -n '/^✖ failing tests:/,$p' "$O/mut-$id.txt" | grep -E '^✖ ' | grep -v 'failing tests' | sed 's/ ([0-9.]*ms)$//' | sort -u | tr '\n' ';')
|
||||
if [ "${f:-?}" = 0 ]; then echo "$id SURVIVED (cancelled ${c:-?})"; else echo "$id killed (fail $f, cancelled ${c:-?}) $which"; fi
|
||||
mv "$file.orig" "$file"
|
||||
}
|
||||
NT=packages/cli/src/notifier.mjs
|
||||
HS=packages/cli/src/host.mjs
|
||||
run N2 $NT 's/lstatSync, mkdirSync/lstatSync, statSync, mkdirSync/; s/const ds = lstatSync\(dir\);/const ds = statSync(dir);/'
|
||||
run N4 $NT 's/O_WRONLY \| O_APPEND \| O_NOFOLLOW\)/O_WRONLY | O_APPEND)/'
|
||||
run N5 $HS 's/if \(child\.exitCode !== null \|\| child\.signalCode !== null\) onDeath/if (child.exitCode !== null) onDeath/'
|
||||
run M28 $HS 's/ if \(prior\?\.live\) throw new CliError\([^\n]*\n//'
|
||||
run G150 $HS 's/(if \(ok\?\.ok !== true\) \{\n\s*await ended\(notify, 5000\);\n\s*)if \(broker\.connected\) /$1/'
|
||||
run G144 $HS 's/(notify\.kill\("SIGTERM"\);\n\s*await ended\(notify, 5000\);\n\s*)if \(broker\.connected\) /$1/'
|
||||
run F2a $NT 's/export const REFUSAL_LIMIT = 5;/export const REFUSAL_LIMIT = 6;/'
|
||||
run F2b $NT 's/ && r\.status !== 429;/;/'
|
||||
run F2c $NT 's/ \|\| journal\.gaveUp\.has\(d\.id\)\) continue;/) continue;/'
|
||||
run F2d $NT 's/if \(\(journal\.refusals\.get\(d\.id\) \?\? 0\) >= REFUSAL_LIMIT\) \{/if (false) {/'
|
||||
run F2e $NT 's/ : dmGaveUp\(d\.id\) \? "\[blocking, DM refused, not retried\] "//'
|
||||
run F2f $NT 's/ if \(record\.kind === "dm" && \(journal\.refusals\.get\(record\.decision\) \?\? 0\) >= REFUSAL_LIMIT\) \{/ if (false) {/'
|
||||
run J4a $NT 's/ \|\| new Date\(r\.at\)\.toISOString\(\) !== r\.at\) return "at";/) return "at";/'
|
||||
run J4b $NT 's/typeof r\.decision !== "string" \|\| r\.decision === ""/false/'
|
||||
run J4c $NT 's/if \(r\.status !== undefined && !Number\.isInteger\(r\.status\)\) return "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//'
|
||||
@@ -0,0 +1,190 @@
|
||||
✔ W1: two processes acquire the same pair at once; exactly one claim (153.746759ms)
|
||||
✔ W1: two writers publish the same revision at once: one wins, the other gets null, the winner's record stays (7.784605ms)
|
||||
✔ W1: a revision name appears only after its bytes are synced; before that, only a temp file exists (4.144815ms)
|
||||
✔ W2: acquire while a claim is reserved or active refuses already-active (166.426621ms)
|
||||
✔ W3: acquire while stopping, uncertain, or stopped without proof refuses unsafe-replacement (241.738789ms)
|
||||
✔ W4: same session with another seat tuple, and the reverse, both refuse; a loser on the seat key closes it no-unit (175.405896ms)
|
||||
✔ W4: a hard link of one session under another seat is the same session: the second controller refuses already-active and launches nothing (19.158132ms)
|
||||
✔ W4: a copy of one session under another seat is the same session: the second controller refuses already-active and launches nothing (20.470794ms)
|
||||
✔ W4: a session header ID that changes after construction refuses target; nothing is claimed or launched (2.73781ms)
|
||||
✔ W5: SIGKILL between every publication barrier of acquire and transition; restart never finds two holders or a lost claim (9837.331291ms)
|
||||
✔ W5: SIGKILL between every publication barrier of release; restart finishes or holds the release (40771.630758ms)
|
||||
✔ W6: controller killed mid-turn while the engine lives; restart is uncertain, no launch, prompts refuse (167.467634ms)
|
||||
✔ W12: a live owner paused with SIGSTOP; a second controller refuses already-active and changes nothing (104.420259ms)
|
||||
✔ W13: crash after the engine spawns, before active; restart finds the live unit: uncertain, no second spawn, force stop only (260.70778ms)
|
||||
✔ W14: crash after reservation, before the spawn marker: stopped with a no-unit observation; the pair is free (278.246273ms)
|
||||
✔ W20: crash after the spawn marker, scope collected; uncertain in both runs, the marker is copied, no launch until a boot proof (279.982807ms)
|
||||
✔ W15: crash between the two keys during release; restart finishes it under the same claim ID (44.582363ms)
|
||||
✔ W7: recorded boot ID differs on the same machine: stopped with a boot proof; open tool calls become uncertain (124.210757ms)
|
||||
✔ W8: resume after a proven stop with the same pins: new claim ID, generation +1, same conversation, branch and leaf (39.21486ms)
|
||||
✔ W9: resume with a changed binary, argv digest, branch or leaf is refused and the claim is unchanged (118.478531ms)
|
||||
✔ W11: the controller writes no session file; only the fake engine's own appends appear (28.512816ms)
|
||||
✔ W16: a highest revision that won't parse holds the pair uncertain; the older stopped revision is not reused (66.257461ms)
|
||||
✔ W17: a claim root copied from another host refuses foreign-host and promotes nothing (63.37671ms)
|
||||
✔ G1: a session path or claim root under .pi/state, ~/.claude, the data root or a registration refuses at construction (4.451994ms)
|
||||
✔ G2: a symlink inside the fixture root to a live session file is refused by the real-path check (1.822083ms)
|
||||
✔ G3: a fixture path swapped for a live path after construction is refused at bind (2.533541ms)
|
||||
✖ K1: force stop kills a tool child that called setsid; stopped with a verified proof (2453.596253ms)
|
||||
✔ K2: K1 on the process-group fallback ends uncertain, never stopped (133.461009ms)
|
||||
✖ K3: SIGTERM acknowledged while a member lives: stopping until the kill phase, never stopped from TERM (2169.732929ms)
|
||||
✔ K4: two engines; force stop one; the other survives by independent observation (4753.51382ms)
|
||||
✔ K5: a stop during a tool call leaves the effect uncertain, and it is shown (3015.867804ms)
|
||||
✔ K12: a member forking in a loop: the freeze stops it, enumeration is complete, populated 0 after cgroup.kill (2821.477795ms)
|
||||
✔ K13: a member writing its pid into another cgroup is refused by the namespace; the kill is complete (2560.990513ms)
|
||||
✔ K15: the shim gone, engine/cgroup.events unreadable, or the engine cgroup missing: evidence unavailable, not empty; uncertain (5307.957066ms)
|
||||
✔ K10: controller killed between the TERM and kill phases: restart checks the invocation ID and re-runs from TERM for the same stop (1690.955029ms)
|
||||
✔ K11: controller killed after the confirmation is recorded, before TERM: restart checks the invocation ID and re-runs from TERM for the same stop (1331.178006ms)
|
||||
✔ K14: a unit with the recorded name but another invocation ID: evidence unavailable, no signals, uncertain (2289.358054ms)
|
||||
✔ K6: recover without proof, without confirmation, or with changed pins is refused (443.243493ms)
|
||||
✔ K7: recover after proof, then launch: new claim and execution, generation +1, same leaf; the cancelled prompt is not replayed (175.516822ms)
|
||||
✔ K8: an engine that loads another leaf on resume is refused before admission; it stays claimed until a proven stop (254.589459ms)
|
||||
✔ K9: an interrupt that never settles stays uncertain; force stop stays available; takeover is refused while fenced (3114.38952ms)
|
||||
✔ K16: a claim from another machine ID refuses foreign-host; no boot proof is issued (52.688797ms)
|
||||
✔ K17: two launcher calls with one eligibility record: one launch, the other refuses, no second engine (164.698278ms)
|
||||
✔ K18: the leaf changes after eligibility: launch refused; the reservation stays until released with proof (93.468012ms)
|
||||
✔ S1: `/goal x`, with leading spaces or a tab, refuses text-policy at admission; zero engine bytes (40.783091ms)
|
||||
✔ S2: every prefix pinned Pi interprets is refused, from the list the code uses; the rest reach the engine exactly (32.851607ms)
|
||||
✔ S3: `/goal` on the second line is pinned from the source: Pi checks only index 0, so it is admitted and sent exactly (25.042487ms)
|
||||
✔ S4: a `/` left in the composer is cleared when control transfers and returns; the next submit sends only the new text (47.449941ms)
|
||||
✔ S5: an observer terminal gets a paste then Enter, as send-message.sh does: not admitted: controller, nothing sent (21.23609ms)
|
||||
✔ S6: a mediated-shaped registration (no tmux) passed to the board's replyToRow: 409 no tmux session; exec never runs (0.790056ms)
|
||||
✔ S7: ESC, bracketed-paste markers and U+2028/U+2029 travel as one JSON string; the engine receives the exact text in one record (22.513453ms)
|
||||
✔ P3: a Pi confirm, select, input or editor dialog is shown disabled with a reason and never answered (128.184526ms)
|
||||
✔ E1: send, ack, user, toolCall, toolResult, final answer: shown once, no refresh, draft and reading position kept (41.237234ms)
|
||||
✔ E2: U+2028, U+2029 inside JSON strings and CRLF line ends each parse as one record, on the splitter and through the controller (23.996737ms)
|
||||
✔ E3: a multipart final, two blocks, null request correlation and duplicate delivery (37.896102ms)
|
||||
✔ 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 (34.642719ms)
|
||||
✔ E4: a gap or a new epoch also reconciles; nothing is concatenated across a gap (11.875435ms)
|
||||
✔ E5: an unknown native event gives no client event; evidence records its type and bytes; the terminal count goes up (33.784407ms)
|
||||
✔ E6: a tool result delayed across a pause and a reconnect is reconciled without a manual refresh (48.64611ms)
|
||||
✔ E7: the terminal renders the same stream as the library client, as observer and then as controller, and submits only as controller (41.304836ms)
|
||||
✔ terminal: engine control characters are made visible; a lost connection refuses submit (27.388046ms)
|
||||
✔ terminal: outcome unknown is shown as such, with no resend offer, and nothing is resent (0.610859ms)
|
||||
✔ terminal: text after Enter in the same input chunk starts the next message; it never joins the one submitted (0.442332ms)
|
||||
✔ terminal: a paste-start marker split right after its ESC still opens the paste; the Enter inside it never submits (0.509431ms)
|
||||
✔ terminal: invisible and bidi characters are made visible; head, status and notice lines stay one line (0.185304ms)
|
||||
✔ every record these fixtures produced is a valid CHAT-01 record (E5: no record fails the schema) (378.84344ms)
|
||||
✔ H1: two takeovers with the same expected generation: one wins, +1; the other refuses generation (60.668832ms)
|
||||
✔ H2: the old controller's prompt after a takeover commits is refused with zero engine bytes (88.718645ms)
|
||||
✔ H3: a takeover while a prompt holds the dispatch lock: written under the old actor, or refused; never both (154.442717ms)
|
||||
✔ H4: self-takeover is refused (25.295334ms)
|
||||
✔ H9: Interrupt racing a prompt's dispatch: before the write, dispatch-refused and no-turn; after, §3 rules (97.648608ms)
|
||||
✔ H10: Interrupt and force stop together: one stop chain, force stop supersedes (93.134482ms)
|
||||
✔ H10: an overlap during the pause before the abort: no abort, the stop ends uncertain (27.283589ms)
|
||||
✔ H10: a no-turn Interrupt lifts only its own fence; admission stays closed under force stop, overlap or revocation (90.391453ms)
|
||||
✔ H11: the controller disconnects mid-turn: work continues, the claim is unchanged, control stays put (132.006689ms)
|
||||
✔ H12: an exact retry after reconnecting to the same incarnation returns the same receipt; one dispatch (18.680476ms)
|
||||
✔ H13: a retry with the same request ID and different text is refused (14.630305ms)
|
||||
✔ H14: late stdout from the old engine after a replacement is dropped by incarnation, counted, never rendered (137.933419ms)
|
||||
✔ H15: a revoked connection's command is refused; the revocation fence holds (69.503201ms)
|
||||
✔ H16: a second controller for the same session refuses already-active; the first is untouched (18.488925ms)
|
||||
✔ H10: a second force stop while the first escalation runs refuses fenced; one escalation, and the claim records only the first stop's phases (55.801538ms)
|
||||
✔ H17: a confirmation reused, answered from another connection, or used after the stop changed is refused (52.666046ms)
|
||||
✔ H18: two prompts before any native output: the second refuses busy; one engine write (12.758511ms)
|
||||
✔ H19: the pipe fails mid-line under a large prompt: delivery-unknown transport-unknown, poisoned, no later write (117.520914ms)
|
||||
✔ H19: the link itself never writes again after an unknown outcome, whoever calls it (0.740774ms)
|
||||
✔ H19: the controller dies mid-write of a large line: after restart the outcome is unknown and nothing is resent (464.817062ms)
|
||||
✔ H20: the line is written but the ack is lost when the controller dies: orphan, outcome unknown, nothing resent (355.552569ms)
|
||||
✔ H21: a retry of the exact request with the old token after a crash is stale-incarnation; no second write (333.392946ms)
|
||||
✔ H22: after H21 and a valid recovery, a new request with the new token is admitted (2430.079532ms)
|
||||
✔ H23: requests pending at a restart are not resent; each shows outcome unknown (476.626319ms)
|
||||
✔ a plain conversation: catalogue row, one page, CHAT-01 records (7.29351ms)
|
||||
✔ native entries map to blocks: tools, thinking, bash, notices, ids that do not fit (1.823709ms)
|
||||
✔ F1: a malformed line is an unavailable part at its position, and reading continues (1.891417ms)
|
||||
✔ F1: a missing parent stops the history with a notice that names the unreadable lines (2.959212ms)
|
||||
✔ F1: an unreadable fork is never merged into another branch's history (2.409827ms)
|
||||
✔ F1: a follow stays on its branch when the next entry's parent is unreadable (2.598438ms)
|
||||
✔ F1: a file whose entries are all unreadable shows a notice per line (1.010206ms)
|
||||
✔ F2: a truncated trailing line marks the view incomplete, not an error (2.780127ms)
|
||||
✔ pagination: 100 parts, then the rest; parts concatenate to the whole branch (5.610015ms)
|
||||
✔ F3: a replaced file (new inode) refuses old cursors with reconcile (5.643614ms)
|
||||
✔ F4: a same-inode rewrite of the prefix refuses old cursors with reconcile (7.546061ms)
|
||||
✔ F5: growth between pages keeps the epoch and the page stops at the pinned length (5.701031ms)
|
||||
✔ F6: unknown, foreign and expired cursors refuse and leave the cursor usable (8.76656ms)
|
||||
✔ F7: a symlinked file and a symlinked directory component are refused, never opened (9.291219ms)
|
||||
✔ F8: a file swapped for a symlink after the catalogue is refused (2.393268ms)
|
||||
✔ F9: registrations never add or redirect a root (1.560785ms)
|
||||
✔ F10: a header cwd naming another project is refused (3.395293ms)
|
||||
✔ F11: parentSession renders with a marker and the parent is never opened (0.833621ms)
|
||||
✔ F12: two leaves: the default leaf is shown and the other branch reads alone (4.757559ms)
|
||||
✔ F12: a follow refuses when an appended duplicate id changes the branch's earlier parts (2.902579ms)
|
||||
✔ F12: a second root (Pi's resetLeaf) starts its own branch (1.509379ms)
|
||||
✔ F13: compaction is a marker in place, then the retained content (0.873772ms)
|
||||
✔ F14: long strings split into fragments and parts, reassemble exactly, and pages respect the byte cap (764.977066ms)
|
||||
✔ fragments never cut a surrogate pair and keep an empty string (5.227965ms)
|
||||
✔ F15: a Claude seat is an unsupported-harness placeholder whose directory is never read (1.887197ms)
|
||||
✔ unknown conversations, empty files and non-Pi files refuse (2.608025ms)
|
||||
✔ an unreadable file or root inside the roots is refused per row, not a failed catalogue (1.284438ms)
|
||||
✔ a seat directory without search permission refuses that root, not the catalogue (2.300259ms)
|
||||
✔ every page and cursor is a valid CHAT-01 record (876.06977ms)
|
||||
✔ the engine pin holds for the installed package (2.75492ms)
|
||||
✔ pinned Pi, sealed and without credentials, answers the controller's commands with the shapes the fake models (381.62069ms)
|
||||
✔ pinned Pi appends thinking_level_change at start when the branch lacks one, so the leaf moves (K8 then fails closed) (372.059171ms)
|
||||
✔ N25: ordinary Interrupt reconciles; a non-empty queue_update in the window is O5 (76.35646ms)
|
||||
✔ N1: an extension's follow-up queued after the fence is cleared before any abort; O5, Unknown (52.808455ms)
|
||||
✔ N1: a follow-up queued before the fence is O5 at once; the Interrupt refuses fenced (29.596354ms)
|
||||
✔ N2: with abort first, the fake runs the external item (the ordering guard has teeth) (22.978671ms)
|
||||
✔ N3: the fence lands in preflight, preflight errors, no run: failed, No run, uncertain (40.685824ms)
|
||||
✔ N4: the ack arrives after the first abort and a run starts: clear and abort again; Interrupted (26.470834ms)
|
||||
✔ N5: an input handler takes the prompt: ack, no run, delivery-unknown handled-without-run (118.815266ms)
|
||||
✔ N6: an extension queues between clear_queue and abort: O5 and O6, Unknown (57.556349ms)
|
||||
✔ N7: clear_queue times out: no abort, nativeQueue unknown, force stop still ends it (1530.983348ms)
|
||||
✔ N7: clear_queue answers an error: no abort, nativeQueue unknown, the link not poisoned (14.788022ms)
|
||||
✔ N8: an extension prompt starts a run during Mosaic preflight; the losing settle is O3 (69.682312ms)
|
||||
✔ N9: a run that started before the fence and ends aborted: failed interrupted, Interrupted (14.206497ms)
|
||||
✔ N9: decision 34: a run that ends aborted with no stop in progress: aborted-without-stop, uncertain, outcome unknown (15.455575ms)
|
||||
✔ N9: an aborted that lands after the fence but before any abort is written: aborted-without-stop, Unknown (31.017307ms)
|
||||
✔ N10: fake conformance (32.810275ms)
|
||||
✔ N11: the run fails before any user message_start: delivery-unknown ack-without-start, never failed (26.776932ms)
|
||||
✔ N12: input that starts a run after the final empty clear is O1 and not part of the stop's proof (18.993357ms)
|
||||
✔ N13: agent_start with no slot held is O1; a later prompt refuses with zero engine bytes (65.733731ms)
|
||||
✔ N14: the run completes while clear_queue is in flight: finished, Completed first, uncertain (29.100256ms)
|
||||
✔ N14: the run completes after the abort is written, before Pi applies it: finished, never relabelled (29.428952ms)
|
||||
✔ N15: the fence lands in preflight, then an input handler takes it: handled-without-run, No run (21.553505ms)
|
||||
✔ N16: Interrupt with no slot and no run refuses no-turn: no stop, no bytes, admission open (12.809566ms)
|
||||
✔ N17: the run fails on its own during the exchange: failed, Failed on its own (27.483881ms)
|
||||
✔ N18: no final assistant message_end, or a lost line: working stays working; before working, transport-unknown (105.434227ms)
|
||||
✔ N19: a losing extension prompt settles inside the Mosaic run before its user message: O3, run-overlap (138.972954ms)
|
||||
✔ N20: an extension triggerTurn during Mosaic preflight starts first; while streaming it queues with no signal (85.341949ms)
|
||||
✔ N21: a losing settle after the receipt settled finished is O2; the receipt stays finished (15.696829ms)
|
||||
✔ N22: an agent-level custom message is dropped by the clear with no signal; evidence names the seal (16.038262ms)
|
||||
✔ N23: a nextTurn message survives clear and abort and attaches to the next prompt, with no signal (16.215135ms)
|
||||
✔ 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 (49.811955ms)
|
||||
ℹ tests 152
|
||||
ℹ suites 0
|
||||
ℹ pass 150
|
||||
ℹ fail 2
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 55022.350456
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/conversation/tests/cohort.test.mjs:139:1
|
||||
✖ K1: force stop kills a tool child that called setsid; stopped with a verified proof (2453.596253ms)
|
||||
AssertionError [ERR_ASSERTION]: the escaped child is a listed member
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/base/packages/conversation/tests/cohort.test.mjs:151:12)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: false,
|
||||
expected: true,
|
||||
operator: '==',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/conversation/tests/cohort.test.mjs:176:1
|
||||
✖ K3: SIGTERM acknowledged while a member lives: stopping until the kill phase, never stopped from TERM (2169.732929ms)
|
||||
AssertionError [ERR_ASSERTION]: the member ignored TERM
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/base/packages/conversation/tests/cohort.test.mjs:190:12)
|
||||
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: '==',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
08:14:31 up 33 days, 9:49, 5 users, load average: 5.38, 6.86, 6.38
|
||||
@@ -0,0 +1 @@
|
||||
08:07:46 up 33 days, 9:42, 5 users, load average: 5.65, 7.22, 5.62
|
||||
@@ -0,0 +1 @@
|
||||
08:13:31 up 33 days, 9:48, 5 users, load average: 3.98, 7.07, 6.41
|
||||
@@ -0,0 +1 @@
|
||||
08:04:03 up 33 days, 9:38, 5 users, load average: 4.34, 9.58, 5.81
|
||||
@@ -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,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,281 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (37.582072ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (39.118859ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (22.39365ms)
|
||||
✔ decide prints a declining choice as declining (21.884386ms)
|
||||
✔ an unknown outcome is reported once and never resent (19.138442ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (25.194489ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (25.069966ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (20.658963ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (19.02862ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (25.159957ms)
|
||||
✔ agents and tasks print through the broker (27.433998ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (4.647518ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (104.468131ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (63.473238ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (65.078595ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (82.569083ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (77.189096ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (78.66824ms)
|
||||
✔ empty views say so (1.31889ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.717368ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.292069ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (1036.100057ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (166.880964ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (86.069782ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (153.466029ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (143.924424ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (93.953635ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (27.070795ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.934948ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (247.777929ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (106.794138ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (654.225946ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (43.704745ms)
|
||||
✔ zoned uses the IANA zone across DST (30.321559ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (46.793145ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (22.381489ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.464757ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (21.915219ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (41.429847ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (33.327933ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (24.011326ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (29.201518ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (25.877083ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (2.21007ms)
|
||||
✔ no Discord id reaches the journal or the log (30.195966ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (5.572269ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (12.550013ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (4.188259ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.041871ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (3.52701ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (4.248863ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.981486ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.80728ms)
|
||||
✖ the journal: a directory it cannot write or create refuses with exit 3 and names the path (5.32643ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.630322ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.170117ms)
|
||||
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 (395.852096ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (66.719116ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2295.924683ms)
|
||||
✔ busExit and refuseInsideAgent (0.417178ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.371621ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.726195ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.872159ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.53128ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (18.372834ms)
|
||||
✔ 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.78667ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (8.838891ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.790716ms)
|
||||
✔ authorize: open channel, listed user (2.272538ms)
|
||||
✔ authorize: wrong guild (0.219111ms)
|
||||
✔ authorize: no guild (DM) (0.181056ms)
|
||||
✔ authorize: unlisted channel (0.298149ms)
|
||||
✔ authorize: unknown channel, no info (0.283628ms)
|
||||
✔ authorize: thread of listed parent (0.211795ms)
|
||||
✔ authorize: thread of unlisted parent (0.163882ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.140675ms)
|
||||
✔ authorize: unlisted user (0.194174ms)
|
||||
✔ authorize: no author (0.500087ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.260496ms)
|
||||
✔ authorize: system author (0.121045ms)
|
||||
✔ authorize: the bot itself (0.113225ms)
|
||||
✔ authorize: webhook (0.099137ms)
|
||||
✔ authorize: mention channel without mention (0.220813ms)
|
||||
✔ authorize: mention channel with bot mention (0.156671ms)
|
||||
✔ authorize: mention channel with @everyone only (0.262392ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.104753ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.12819ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.1304ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.078928ms)
|
||||
✔ authorize: thread in another guild per channel info (0.077933ms)
|
||||
✔ authorize: not an object (0.074428ms)
|
||||
✔ authorize: no id (0.085666ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.09731ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.072026ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.52564ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.155784ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.861489ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.559364ms)
|
||||
✔ binding: empty allowlists refuse (0.413276ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.352546ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.669688ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.505364ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.574307ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.38879ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (127.969866ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (3.107523ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (430.884057ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (239.109273ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (150.671822ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.985085ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.447798ms)
|
||||
✔ 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.086686ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (20.647621ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.578622ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.306951ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (2.031887ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (3.303037ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (34.446552ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (2.884563ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.724558ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.155942ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.14996ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (34.052853ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (10.694575ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (18.477773ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (5.934981ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (5.779415ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (4.883219ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (1.915952ms)
|
||||
✔ 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.000171ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (2.346679ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (6.259981ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.831411ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (5.266317ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.658781ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.836759ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.187727ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.442673ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (5.044197ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (2.937936ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (1.341962ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (4.23219ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.478842ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (54.147454ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (53.219298ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (58.694747ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (373.472186ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (242.817582ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (116.016435ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (236.779213ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (135.164216ms)
|
||||
✔ 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 (215.983172ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.602581ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (3.146486ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.980092ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (436.042214ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (24.229871ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (47.386364ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.602238ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.889263ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.531979ms)
|
||||
✔ gateway: op 9 resumable resumes (0.389936ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.901757ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.789676ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.44579ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (75.330882ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (78.665549ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (162.34658ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.441751ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (92.079401ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (92.201168ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (116.714187ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (233.963473ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (80.43671ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (111.80945ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (245.815162ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (911.710307ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.181231ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (104.569189ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.242238ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (5.676ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (105.347867ms)
|
||||
✔ 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 (364.975437ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.543512ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (26.301787ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.15153ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (48.036606ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (168.668103ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (104.652705ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.570491ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.617857ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.913509ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (2.242077ms)
|
||||
✔ 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.439649ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (86.148184ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (44.686208ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (77.201967ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (774.129613ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (3.406666ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (5.405756ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.742561ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (1.801065ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (1.392577ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.928723ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.658379ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.924081ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.634231ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (28.864676ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (14.28349ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (8.545977ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.493661ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (1.115488ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.564605ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1016.715809ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.608316ms)
|
||||
✔ 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.902751ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.375939ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.23034ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (3.444858ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (7.594811ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (8.462805ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.762502ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (4.381281ms)
|
||||
✔ tools: listing and search caps hold (11.574267ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.816758ms)
|
||||
✔ 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.871471ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.441468ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.50274ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (6.743042ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.631808ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.74157ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.469516ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.346862ms)
|
||||
✔ 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.168036ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (5.460302ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.263948ms)
|
||||
✔ 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.006669ms)
|
||||
✔ 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.613053ms)
|
||||
ℹ tests 238
|
||||
ℹ suites 0
|
||||
ℹ pass 237
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3105.930867
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:357:1
|
||||
✖ the journal: a directory it cannot write or create refuses with exit 3 and names the path (5.32643ms)
|
||||
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-kdoUwM/notify/demo/sent.jsonl
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:364: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-kdoUwM/notify/demo/sent.jsonl
|
||||
at openJournal (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/src/notifier.mjs:179:44)
|
||||
at file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:364: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:364: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,281 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (30.040857ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (44.031844ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (25.521798ms)
|
||||
✔ decide prints a declining choice as declining (18.448352ms)
|
||||
✔ an unknown outcome is reported once and never resent (19.594957ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (18.498298ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (21.6223ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (17.060265ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (23.980572ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (25.821891ms)
|
||||
✔ agents and tasks print through the broker (21.302803ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (5.191776ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (86.432511ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (65.359911ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (62.688322ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (58.818616ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (61.473301ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (81.14929ms)
|
||||
✔ empty views say so (1.247009ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.495542ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.252556ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (978.938242ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (175.857201ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (83.350544ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (142.446188ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (148.415892ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (69.750004ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (25.56054ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.706707ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (237.955053ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (93.775195ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (619.406985ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (25.733736ms)
|
||||
✔ zoned uses the IANA zone across DST (25.931799ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (52.297888ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (27.607911ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (1.437047ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (25.287988ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (31.694473ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (30.367633ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (19.583974ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (21.871506ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (20.358967ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (1.345764ms)
|
||||
✔ no Discord id reaches the journal or the log (27.328615ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (4.772294ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (8.660812ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (2.692467ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.630703ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (1.347217ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (4.655013ms)
|
||||
✖ the journal: a symlinked directory refuses and says it is a link (1.518457ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (1.097035ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.8063ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.495779ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (113.191003ms)
|
||||
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 (381.731938ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (58.744272ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2266.469496ms)
|
||||
✔ busExit and refuseInsideAgent (0.428836ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.259311ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.849222ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.618066ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.635753ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (22.434533ms)
|
||||
✔ 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.766907ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (7.904736ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (6.516886ms)
|
||||
✔ authorize: open channel, listed user (2.078307ms)
|
||||
✔ authorize: wrong guild (0.207962ms)
|
||||
✔ authorize: no guild (DM) (0.309551ms)
|
||||
✔ authorize: unlisted channel (0.206069ms)
|
||||
✔ authorize: unknown channel, no info (0.283278ms)
|
||||
✔ authorize: thread of listed parent (0.260015ms)
|
||||
✔ authorize: thread of unlisted parent (0.27857ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.146677ms)
|
||||
✔ authorize: unlisted user (1.206967ms)
|
||||
✔ authorize: no author (0.301948ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.155986ms)
|
||||
✔ authorize: system author (0.136158ms)
|
||||
✔ authorize: the bot itself (0.099989ms)
|
||||
✔ authorize: webhook (0.116965ms)
|
||||
✔ authorize: mention channel without mention (0.166508ms)
|
||||
✔ authorize: mention channel with bot mention (1.722734ms)
|
||||
✔ authorize: mention channel with @everyone only (0.280333ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.102797ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.101319ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.092686ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.068996ms)
|
||||
✔ authorize: thread in another guild per channel info (0.077879ms)
|
||||
✔ authorize: not an object (0.069214ms)
|
||||
✔ authorize: no id (0.070198ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.092155ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.058919ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.473043ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.156411ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.716913ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.329452ms)
|
||||
✔ binding: empty allowlists refuse (0.406112ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.391167ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.731766ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (1.380989ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.482254ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.457412ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (137.894892ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (4.897938ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (478.829084ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (297.114053ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (142.164503ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (2.53397ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.340864ms)
|
||||
✔ 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.242352ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (19.510822ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.686991ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.395439ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.790842ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (3.229263ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (34.655754ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (2.887962ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.247204ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.379891ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.319519ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.93649ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (10.159228ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (8.608831ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (4.095543ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (6.053165ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (3.464877ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.875045ms)
|
||||
✔ 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.084866ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (3.587149ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (4.713606ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (3.131098ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (6.748559ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.955557ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (1.171814ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.089446ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.314552ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.626794ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.643684ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.921142ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.695221ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.447412ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (76.43499ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (52.610502ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (51.791561ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (367.908634ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (257.043191ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (106.3616ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (236.974167ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (135.449764ms)
|
||||
✔ 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.242274ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (615.199395ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.267142ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.468445ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (426.956703ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (25.933239ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (46.632083ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.514287ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.746903ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.698553ms)
|
||||
✔ gateway: op 9 resumable resumes (0.397925ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.955243ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.561457ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.530669ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (88.851159ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (81.5538ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (121.234753ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.360911ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (98.478995ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (163.324706ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (179.213143ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (256.717195ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (74.587149ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (94.916669ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (212.22541ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (866.156154ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.389196ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (97.291287ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.174036ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.641479ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (75.114768ms)
|
||||
✔ 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 (462.716435ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.610411ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (35.104544ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (2.03876ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (56.205868ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (168.553258ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (94.54537ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.477242ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.300582ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.153649ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.750578ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (58.506297ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (58.30982ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (42.801788ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (87.093836ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (862.339269ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.673374ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (5.708752ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.588565ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.819016ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.884038ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (6.451236ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.230881ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.618372ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.44822ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (31.133985ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (12.622951ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (11.151142ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (5.592303ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.764418ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.973262ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1011.801258ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.476568ms)
|
||||
✔ 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.827655ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.691844ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.28847ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.795542ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.491307ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (5.500309ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.524577ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (3.60283ms)
|
||||
✔ tools: listing and search caps hold (11.17589ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.995235ms)
|
||||
✔ 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.603592ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.290775ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (0.929431ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.040271ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.42525ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.294143ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (2.713924ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (1.971639ms)
|
||||
✔ 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.312801ms)
|
||||
✔ 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.802749ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.25962ms)
|
||||
✔ 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.086818ms)
|
||||
✔ 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.530428ms)
|
||||
ℹ tests 238
|
||||
ℹ suites 0
|
||||
ℹ pass 237
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 2979.66389
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:334:1
|
||||
✖ the journal: a symlinked directory refuses and says it is a link (1.518457ms)
|
||||
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-Ewejwg/notify/demo
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:341: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-Ewejwg/notify/demo
|
||||
at openJournal (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/src/notifier.mjs:165:11)
|
||||
at file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:341: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:341: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,271 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (36.911794ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (57.937729ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (23.655211ms)
|
||||
✔ decide prints a declining choice as declining (33.19939ms)
|
||||
✔ an unknown outcome is reported once and never resent (32.211558ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (31.136881ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (27.922318ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (21.547786ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (31.081489ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (25.566108ms)
|
||||
✔ agents and tasks print through the broker (22.631474ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.48699ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (93.546525ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (80.966735ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (95.392118ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (76.417851ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (80.815482ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (89.590341ms)
|
||||
✔ empty views say so (1.305944ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.654443ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.289135ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (1055.533952ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (187.340685ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (88.441926ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (168.276578ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (145.187007ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (86.230168ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (24.286542ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (203.358588ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (209.401777ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (88.349059ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (617.111033ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (31.387856ms)
|
||||
✔ zoned uses the IANA zone across DST (32.531664ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (50.702559ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (28.455924ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.367868ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (29.450186ms)
|
||||
✖ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (1.976255ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (46.493102ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (39.432366ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (32.486372ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (36.76367ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (1.254918ms)
|
||||
✔ no Discord id reaches the journal or the log (31.964943ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (5.083637ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (7.725657ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (3.859531ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.965699ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (1.091272ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (11.853754ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.905926ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.984103ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.806167ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.498272ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (110.593231ms)
|
||||
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 (415.241747ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (65.157853ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2349.186301ms)
|
||||
✔ busExit and refuseInsideAgent (0.414805ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.373852ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.791594ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.885937ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.530178ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (21.436135ms)
|
||||
✔ 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.365948ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (12.680451ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (3.994212ms)
|
||||
✔ authorize: open channel, listed user (2.23961ms)
|
||||
✔ authorize: wrong guild (0.209057ms)
|
||||
✔ authorize: no guild (DM) (0.382297ms)
|
||||
✔ authorize: unlisted channel (0.219137ms)
|
||||
✔ authorize: unknown channel, no info (0.292619ms)
|
||||
✔ authorize: thread of listed parent (0.221267ms)
|
||||
✔ authorize: thread of unlisted parent (0.259672ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.150093ms)
|
||||
✔ authorize: unlisted user (1.235171ms)
|
||||
✔ authorize: no author (0.71837ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.164811ms)
|
||||
✔ authorize: system author (0.140085ms)
|
||||
✔ authorize: the bot itself (0.085204ms)
|
||||
✔ authorize: webhook (1.06688ms)
|
||||
✔ authorize: mention channel without mention (0.161976ms)
|
||||
✔ authorize: mention channel with bot mention (0.149601ms)
|
||||
✔ authorize: mention channel with @everyone only (0.100979ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.083815ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.152626ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.100779ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.088825ms)
|
||||
✔ authorize: thread in another guild per channel info (0.088174ms)
|
||||
✔ authorize: not an object (0.075104ms)
|
||||
✔ authorize: no id (0.072568ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.335419ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.073336ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.530368ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.181539ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.904808ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.474454ms)
|
||||
✔ binding: empty allowlists refuse (0.605646ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.956274ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (2.641474ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.546399ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (2.12013ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (2.11447ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (135.385153ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (8.060178ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (505.679926ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (225.331933ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (155.270779ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.858684ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.822429ms)
|
||||
✔ 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.274165ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (21.330294ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (2.376416ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.372856ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.704453ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (4.154256ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (34.420027ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (4.917313ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.248843ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.425933ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.27971ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (34.665103ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (6.888441ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (8.189922ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (2.186713ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (23.907764ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.148968ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.715317ms)
|
||||
✔ 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.300563ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (5.81848ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (6.810434ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (1.625989ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (3.982479ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.753379ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.947245ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.898456ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.519206ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.536119ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (4.015856ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.911121ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (3.892234ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.502148ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (69.790925ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (74.456856ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (78.339128ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (385.3505ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (236.101743ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (106.920189ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (236.830202ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (131.151186ms)
|
||||
✔ 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 (215.154748ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.74027ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.355778ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.658974ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (426.165924ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (28.02058ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (50.521862ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.74531ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.747047ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (1.185579ms)
|
||||
✔ gateway: op 9 resumable resumes (0.409868ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (1.147814ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.602938ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.674425ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (106.123908ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (122.250287ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (142.508777ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.368561ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (93.711512ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (117.209843ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (99.909988ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (238.677675ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (78.756038ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (118.81021ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (233.264274ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (783.738236ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.736652ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (134.930699ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.344481ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (5.042473ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (67.420248ms)
|
||||
✔ 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 (417.402785ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (3.377878ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (32.841526ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.369784ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (44.364495ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (172.797147ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (109.178136ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.673673ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (6.910093ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (3.36967ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (2.094509ms)
|
||||
✔ 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.732603ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (51.468633ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (54.039627ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (95.036228ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (781.067793ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.551514ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (9.116821ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.979913ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (3.58181ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.990891ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (6.546731ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.82862ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.881906ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.182022ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (32.554137ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (14.852215ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (11.824641ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.690814ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.809129ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.800737ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1017.88746ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.540209ms)
|
||||
✔ 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 (10.808732ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (2.009845ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.279141ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (3.71708ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (5.292333ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.401566ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (4.591586ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (4.120305ms)
|
||||
✔ tools: listing and search caps hold (10.789113ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (1.219337ms)
|
||||
✔ 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.99206ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.204852ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.064784ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.470824ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.873215ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.945415ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.634841ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.380527ms)
|
||||
✔ 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.293578ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (5.164799ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.429563ms)
|
||||
✔ 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.391993ms)
|
||||
✔ 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.85178ms)
|
||||
ℹ tests 238
|
||||
ℹ suites 0
|
||||
ℹ pass 237
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3046.188974
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:112:1
|
||||
✖ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (1.976255ms)
|
||||
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:113: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'
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (35.809338ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (61.516163ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (22.093249ms)
|
||||
✔ decide prints a declining choice as declining (21.836263ms)
|
||||
✔ an unknown outcome is reported once and never resent (17.950709ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (28.103713ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (22.398173ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (26.537612ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (22.128019ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (30.447864ms)
|
||||
✔ agents and tasks print through the broker (22.834215ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (3.827223ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (82.245241ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (80.081523ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (71.091522ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (73.932272ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (63.300201ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (96.995535ms)
|
||||
✔ empty views say so (2.34839ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.719817ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.336067ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (1036.568564ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (157.540774ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (73.222992ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (134.688555ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (143.460777ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (76.603607ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (25.440968ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (203.202963ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (213.697478ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (101.117934ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (605.898068ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (29.833009ms)
|
||||
✔ zoned uses the IANA zone across DST (26.918303ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (28.091629ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (36.349978ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (1.255753ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (41.990812ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (41.187076ms)
|
||||
✖ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (39.405817ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (27.647569ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (26.280346ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (21.870495ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (1.606275ms)
|
||||
✔ no Discord id reaches the journal or the log (18.196793ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (8.537083ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (8.940279ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.995992ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.128762ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (1.294086ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (4.346849ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.898255ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (1.129457ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.708252ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.481499ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.293002ms)
|
||||
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 (417.533285ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (62.725716ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2326.218148ms)
|
||||
✔ busExit and refuseInsideAgent (0.402309ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (6.68008ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (3.382952ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.82823ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.537315ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (26.493252ms)
|
||||
✔ 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.869087ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (8.470197ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.828975ms)
|
||||
✔ authorize: open channel, listed user (1.852092ms)
|
||||
✔ authorize: wrong guild (0.219828ms)
|
||||
✔ authorize: no guild (DM) (0.170371ms)
|
||||
✔ authorize: unlisted channel (0.191907ms)
|
||||
✔ authorize: unknown channel, no info (0.329577ms)
|
||||
✔ authorize: thread of listed parent (0.236919ms)
|
||||
✔ authorize: thread of unlisted parent (0.153708ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.136076ms)
|
||||
✔ authorize: unlisted user (0.850401ms)
|
||||
✔ authorize: no author (0.300577ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.16224ms)
|
||||
✔ authorize: system author (0.185399ms)
|
||||
✔ authorize: the bot itself (0.114483ms)
|
||||
✔ authorize: webhook (0.10755ms)
|
||||
✔ authorize: mention channel without mention (0.147574ms)
|
||||
✔ authorize: mention channel with bot mention (0.148882ms)
|
||||
✔ authorize: mention channel with @everyone only (0.111457ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.120736ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.091221ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.086075ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.078538ms)
|
||||
✔ authorize: thread in another guild per channel info (0.082719ms)
|
||||
✔ authorize: not an object (0.069518ms)
|
||||
✔ authorize: no id (0.075474ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.11407ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.073174ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.518002ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.150507ms)
|
||||
✔ binding: a complete binding validates and is frozen (3.266078ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.50948ms)
|
||||
✔ binding: empty allowlists refuse (0.436742ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (2.346238ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (2.13474ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.501447ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.782175ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.534295ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (145.325726ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (5.949024ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (415.47176ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (237.908348ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (137.827867ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (2.790162ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.809129ms)
|
||||
✔ 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.462271ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (29.202632ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.618021ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.386614ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.199014ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (3.321777ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (36.327279ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (12.997027ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (2.406104ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.311505ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (43.323567ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (34.567235ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (7.523511ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (6.21761ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (2.999215ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (5.36129ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (3.162488ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (1.373165ms)
|
||||
✔ 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.801223ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (1.830329ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (10.969654ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (4.213571ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (4.662434ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.693549ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.831632ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.85741ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.2215ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.560786ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.531935ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (1.008434ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.50528ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.46555ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (61.777537ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (62.513581ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (58.557306ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (372.02923ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (243.499589ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (104.528611ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (232.918914ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (131.353378ms)
|
||||
✔ 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.401472ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.892512ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.478111ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.575785ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (427.724455ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (24.836549ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (45.327148ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (3.904539ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (2.658181ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.535838ms)
|
||||
✔ gateway: op 9 resumable resumes (0.423511ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.956917ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.654482ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.494117ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (93.939318ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (81.740151ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (152.45589ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.470672ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (94.563316ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (98.876016ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (108.789089ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (224.211268ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (71.894818ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (99.773727ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (206.215453ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (772.64153ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.133856ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (97.07907ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.182929ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.170957ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (66.585825ms)
|
||||
✔ 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 (383.009997ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.638379ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (30.271734ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.242261ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (51.268989ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (159.733108ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (102.293769ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.608209ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (6.054023ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.553149ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.878016ms)
|
||||
✔ 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.721283ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (63.370947ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (51.644202ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (86.009972ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (742.853774ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.673686ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (3.260468ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.77875ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.858328ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (1.133484ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (11.086093ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.460315ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.934726ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.383464ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (29.542656ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (14.819021ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (9.742775ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (5.089911ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (1.138734ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (4.430962ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1017.382604ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.394604ms)
|
||||
✔ 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.801686ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.189262ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.18607ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.265707ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (5.935879ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.35616ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.768507ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (3.807816ms)
|
||||
✔ tools: listing and search caps hold (16.130833ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.956079ms)
|
||||
✔ 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.297231ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.488625ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.351752ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.727236ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.432798ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (2.233255ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.411247ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.553255ms)
|
||||
✔ 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.090468ms)
|
||||
✔ 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.827732ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.25188ms)
|
||||
✔ 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.881377ms)
|
||||
✔ 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.459233ms)
|
||||
ℹ tests 238
|
||||
ℹ suites 0
|
||||
ℹ pass 237
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 2951.550138
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:140:1
|
||||
✖ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (39.405817ms)
|
||||
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:154: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,284 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (30.974017ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (47.663494ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (36.338423ms)
|
||||
✔ decide prints a declining choice as declining (28.430287ms)
|
||||
✔ an unknown outcome is reported once and never resent (17.526559ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (21.037159ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (25.891335ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (26.095097ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (27.444466ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (19.017214ms)
|
||||
✔ agents and tasks print through the broker (19.416438ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.984936ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (91.041944ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (64.674911ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (77.032346ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (63.612501ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (70.53448ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (87.558803ms)
|
||||
✔ empty views say so (1.168964ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.41271ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.230356ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (1029.46395ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (158.643559ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (77.316736ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (141.05337ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (125.780681ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (81.88852ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (24.86455ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.893787ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (221.639141ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (98.465056ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (657.432996ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (26.679685ms)
|
||||
✔ zoned uses the IANA zone across DST (28.191215ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (28.9936ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (26.055041ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.336008ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (32.889789ms)
|
||||
✖ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (28.87508ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (31.206205ms)
|
||||
✖ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (20.2688ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (32.1436ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (29.141993ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (1.466298ms)
|
||||
✔ no Discord id reaches the journal or the log (24.078396ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (8.393743ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (4.938105ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (2.790225ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.770636ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (1.338326ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (5.250981ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.667573ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.777427ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.831372ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.47603ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (114.479628ms)
|
||||
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 (427.232373ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (63.671553ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2294.658955ms)
|
||||
✔ busExit and refuseInsideAgent (0.475121ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (4.66743ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.888526ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.666086ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.485259ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (17.680969ms)
|
||||
✔ 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.403166ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (7.738244ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (3.024914ms)
|
||||
✔ authorize: open channel, listed user (2.152101ms)
|
||||
✔ authorize: wrong guild (0.220743ms)
|
||||
✔ authorize: no guild (DM) (0.323828ms)
|
||||
✔ authorize: unlisted channel (0.215051ms)
|
||||
✔ authorize: unknown channel, no info (0.202818ms)
|
||||
✔ authorize: thread of listed parent (0.196809ms)
|
||||
✔ authorize: thread of unlisted parent (0.144665ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.180461ms)
|
||||
✔ authorize: unlisted user (0.207932ms)
|
||||
✔ authorize: no author (0.290736ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.679777ms)
|
||||
✔ authorize: system author (0.156024ms)
|
||||
✔ authorize: the bot itself (0.085854ms)
|
||||
✔ authorize: webhook (0.098312ms)
|
||||
✔ authorize: mention channel without mention (0.146701ms)
|
||||
✔ authorize: mention channel with bot mention (0.158809ms)
|
||||
✔ authorize: mention channel with @everyone only (0.106023ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.091997ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.079981ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.090015ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.073924ms)
|
||||
✔ authorize: thread in another guild per channel info (0.080136ms)
|
||||
✔ authorize: not an object (0.071873ms)
|
||||
✔ authorize: no id (0.48855ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.09263ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.074734ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.503124ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (1.982524ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.91697ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.588867ms)
|
||||
✔ binding: empty allowlists refuse (0.449432ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.299445ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.978791ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (1.178874ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.793977ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.429862ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (137.48477ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (2.559555ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (460.430143ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (212.969236ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (132.105195ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (3.473401ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.851739ms)
|
||||
✔ 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.697522ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (17.065998ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.582824ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (2.274563ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (2.566846ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (3.597846ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (33.869207ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (2.985712ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.038249ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (2.230859ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (43.86252ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (33.108402ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (8.570541ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (4.679177ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (4.043586ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (8.063775ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.499981ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (1.128651ms)
|
||||
✔ 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.487359ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (4.637193ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (4.673208ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (1.899018ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (5.428591ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.733965ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.87423ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.983748ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.306382ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.814922ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.630332ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.711466ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.735731ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.451324ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (77.611761ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (59.396429ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (57.024048ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (360.833471ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (251.873516ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (104.181243ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (230.463603ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (130.659273ms)
|
||||
✔ 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.909563ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.736363ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.556728ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.547357ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (426.907371ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (24.323751ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (47.55764ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (5.83175ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (3.473517ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.537842ms)
|
||||
✔ gateway: op 9 resumable resumes (0.352223ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.933225ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.537514ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.531703ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (106.762097ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (71.596605ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (136.543143ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.477211ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (108.728602ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (114.889784ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (103.987593ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (214.996313ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (69.59293ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (99.661481ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (218.079079ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (779.851627ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.799182ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (102.500565ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.295987ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (5.156095ms)
|
||||
✔ 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.370567ms)
|
||||
✔ 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 (429.858471ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.368809ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (20.849977ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.087212ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (39.050887ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (147.299626ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (93.604522ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.482982ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.794072ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.871795ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.886799ms)
|
||||
✔ 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.94343ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (53.367112ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (52.595637ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (88.375853ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (711.639235ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.856221ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (2.503901ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.665927ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.763035ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.98033ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.875834ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.307469ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.817896ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.547941ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (32.294581ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (13.564114ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (9.449436ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.157989ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.64574ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.198621ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1016.269954ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.454039ms)
|
||||
✔ 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.600835ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.237469ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.197411ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.30572ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.395895ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.12301ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.989673ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (5.827967ms)
|
||||
✔ tools: listing and search caps hold (13.795991ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.984657ms)
|
||||
✔ 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.522234ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.468531ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.467316ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (6.528786ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.476266ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.705889ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (4.193562ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.777193ms)
|
||||
✔ 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.406849ms)
|
||||
✔ 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.638672ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.280727ms)
|
||||
✔ 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.035503ms)
|
||||
✔ 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.599244ms)
|
||||
ℹ tests 238
|
||||
ℹ suites 0
|
||||
ℹ pass 236
|
||||
ℹ fail 2
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 2988.347758
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:112:1
|
||||
✖ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (28.87508ms)
|
||||
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:131: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:160:1
|
||||
✖ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (20.2688ms)
|
||||
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:171: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,273 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (54.320311ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (76.108892ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (24.96531ms)
|
||||
✔ decide prints a declining choice as declining (28.089663ms)
|
||||
✔ an unknown outcome is reported once and never resent (17.865686ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (24.161673ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (22.168394ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (23.551558ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (21.030428ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (22.652467ms)
|
||||
✔ agents and tasks print through the broker (19.192188ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.619957ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (107.141849ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (103.062755ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (62.364747ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (71.304866ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (71.797192ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (104.772783ms)
|
||||
✔ empty views say so (1.295013ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.684853ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.277462ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (1062.01507ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (179.079686ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (97.752942ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (148.822575ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (133.36864ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (75.400752ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (24.039172ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (203.147036ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (236.9255ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (88.842674ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (626.692718ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (27.760703ms)
|
||||
✔ zoned uses the IANA zone across DST (27.955296ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (46.595479ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (38.440988ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.77874ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (52.880854ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (45.669256ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (34.314952ms)
|
||||
✖ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (20.619416ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (27.380309ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (24.772794ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (1.434255ms)
|
||||
✔ no Discord id reaches the journal or the log (20.733487ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (3.371998ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (4.56623ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (2.792887ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.026224ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (1.053653ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (4.72282ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.949791ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (1.356153ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.95355ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.484189ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (116.27952ms)
|
||||
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 (456.532629ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (93.023065ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2325.935429ms)
|
||||
✔ busExit and refuseInsideAgent (0.465376ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (5.042031ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.674027ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.706293ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.513267ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (39.071539ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (16.756572ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (12.179075ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (4.253596ms)
|
||||
✔ authorize: open channel, listed user (1.772288ms)
|
||||
✔ authorize: wrong guild (0.221109ms)
|
||||
✔ authorize: no guild (DM) (0.192267ms)
|
||||
✔ authorize: unlisted channel (0.192242ms)
|
||||
✔ authorize: unknown channel, no info (0.296866ms)
|
||||
✔ authorize: thread of listed parent (0.196836ms)
|
||||
✔ authorize: thread of unlisted parent (0.285431ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.139592ms)
|
||||
✔ authorize: unlisted user (1.499097ms)
|
||||
✔ authorize: no author (0.434796ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.141252ms)
|
||||
✔ authorize: system author (0.131924ms)
|
||||
✔ authorize: the bot itself (0.105212ms)
|
||||
✔ authorize: webhook (0.101465ms)
|
||||
✔ authorize: mention channel without mention (1.561995ms)
|
||||
✔ authorize: mention channel with bot mention (1.11634ms)
|
||||
✔ authorize: mention channel with @everyone only (0.534757ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.868437ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.255449ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.438112ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.557388ms)
|
||||
✔ authorize: thread in another guild per channel info (0.262378ms)
|
||||
✔ authorize: not an object (0.107849ms)
|
||||
✔ authorize: no id (0.321704ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.307594ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.342669ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (2.064896ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.297446ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.821451ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.510268ms)
|
||||
✔ binding: empty allowlists refuse (0.640011ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.411625ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (2.133729ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (1.469192ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (2.794814ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (3.337076ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (170.035675ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (5.36089ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (502.61482ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (232.10205ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (161.880379ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (2.355099ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.426985ms)
|
||||
✔ 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 (28.933249ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (29.137392ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.626706ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.427654ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (3.70041ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (15.169828ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (35.205869ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.871187ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (2.776459ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (6.233773ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (44.375131ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (33.809311ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (9.010319ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (5.415791ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.504976ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (6.772268ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.939113ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.961824ms)
|
||||
✔ 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.593519ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (4.313891ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (5.810403ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.848556ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (5.836732ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (5.191637ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.882983ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.197826ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (2.065002ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.602633ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.330284ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.775292ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (3.275132ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.540355ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (94.312495ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (72.872213ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (59.695856ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (366.29491ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (235.905323ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (105.074427ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (243.08647ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (131.705299ms)
|
||||
✔ 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.341407ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.733899ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.308705ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.49694ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (429.390175ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (28.39639ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (58.584345ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.802122ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.780056ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.710052ms)
|
||||
✔ gateway: op 9 resumable resumes (0.401096ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (1.126929ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.634918ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.590188ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (130.859679ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (70.19481ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (134.21238ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.414483ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (111.33558ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (140.946792ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (116.916303ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (231.28888ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (90.25353ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (101.894764ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (236.298187ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (787.036234ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (15.953705ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (138.00148ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.549813ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.317924ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (59.645496ms)
|
||||
✔ 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 (433.762427ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.401483ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (36.332977ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.198167ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (48.018321ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (160.045571ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (107.880971ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.716367ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (6.192777ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (3.133871ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (2.190418ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (59.670856ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (53.451698ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (43.603413ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (92.710409ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (809.523438ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (3.010859ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (3.053414ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.650854ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.922361ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.962364ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (6.494812ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.562222ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (2.480774ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.243768ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (30.757408ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (13.038443ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (9.505882ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.434462ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.670388ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.407246ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1010.231573ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.491376ms)
|
||||
✔ 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.576902ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.409451ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.261697ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.842112ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.498104ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.070708ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (7.126815ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (3.425643ms)
|
||||
✔ tools: listing and search caps hold (10.163717ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.679338ms)
|
||||
✔ 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.043314ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.40505ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.317516ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.777171ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.572825ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.528494ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.078616ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (1.791486ms)
|
||||
✔ 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.740878ms)
|
||||
✔ 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.934933ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.237789ms)
|
||||
✔ 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.41001ms)
|
||||
✔ 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.540569ms)
|
||||
ℹ tests 238
|
||||
ℹ suites 0
|
||||
ℹ pass 237
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3086.089989
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:160:1
|
||||
✖ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (20.619416ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
|
||||
{
|
||||
digest: false,
|
||||
+ dms: 1,
|
||||
- dms: 0,
|
||||
failed: 0
|
||||
}
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:166: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: { dms: 1, digest: false, failed: 0 },
|
||||
expected: { dms: 0, digest: false, failed: 0 },
|
||||
operator: 'deepStrictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (38.155082ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (44.658463ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (20.893936ms)
|
||||
✔ decide prints a declining choice as declining (22.159064ms)
|
||||
✔ an unknown outcome is reported once and never resent (15.433145ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (16.965102ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (18.663693ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (16.255728ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (16.159005ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (19.358806ms)
|
||||
✔ agents and tasks print through the broker (25.695876ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.820287ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (94.399555ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (59.560636ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (64.177886ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (62.264953ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (61.81234ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (93.633666ms)
|
||||
✔ empty views say so (1.315865ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.665843ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.353599ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (1000.427317ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (181.301472ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (93.27176ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (137.414953ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (141.848657ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (95.505329ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (26.925453ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.555971ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (237.486632ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (117.330067ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (658.019765ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (35.529035ms)
|
||||
✔ zoned uses the IANA zone across DST (29.550095ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (43.805187ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (29.977568ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.46787ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (22.827627ms)
|
||||
✖ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (29.034824ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (29.965794ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (19.730528ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (21.150931ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (15.760328ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (1.46266ms)
|
||||
✔ no Discord id reaches the journal or the log (16.677846ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (5.032213ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (5.220692ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (4.010719ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.153268ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (1.2048ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (4.977804ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.773011ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.992776ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.670579ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.42966ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (113.257589ms)
|
||||
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 (395.361001ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (82.855011ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2278.373709ms)
|
||||
✔ busExit and refuseInsideAgent (0.431799ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.664189ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.812611ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.811908ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.541292ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (24.612386ms)
|
||||
✔ 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.106236ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (7.980753ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.724449ms)
|
||||
✔ authorize: open channel, listed user (2.730321ms)
|
||||
✔ authorize: wrong guild (0.216213ms)
|
||||
✔ authorize: no guild (DM) (0.30558ms)
|
||||
✔ authorize: unlisted channel (0.232323ms)
|
||||
✔ authorize: unknown channel, no info (0.277862ms)
|
||||
✔ authorize: thread of listed parent (0.209019ms)
|
||||
✔ authorize: thread of unlisted parent (0.199105ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.164142ms)
|
||||
✔ authorize: unlisted user (1.982512ms)
|
||||
✔ authorize: no author (0.581787ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.269499ms)
|
||||
✔ authorize: system author (0.124357ms)
|
||||
✔ authorize: the bot itself (0.120655ms)
|
||||
✔ authorize: webhook (1.019816ms)
|
||||
✔ authorize: mention channel without mention (0.155318ms)
|
||||
✔ authorize: mention channel with bot mention (0.146654ms)
|
||||
✔ authorize: mention channel with @everyone only (0.299638ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.097446ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.123116ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.100199ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.084209ms)
|
||||
✔ authorize: thread in another guild per channel info (0.087858ms)
|
||||
✔ authorize: not an object (0.07003ms)
|
||||
✔ authorize: no id (0.068622ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.093507ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.072008ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.504625ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.161233ms)
|
||||
✔ binding: a complete binding validates and is frozen (4.718139ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.736694ms)
|
||||
✔ binding: empty allowlists refuse (0.454343ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.285306ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.740505ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.474791ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.657245ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.771458ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (116.598655ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (4.336163ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (405.471247ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (206.800948ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (140.951322ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (3.143363ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (2.171132ms)
|
||||
✔ 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.650482ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (18.18108ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.563245ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.210636ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.306985ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.384482ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (34.504141ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.497559ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.90795ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.485682ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (43.606985ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (35.303339ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (8.798928ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (4.650383ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (2.113333ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (7.392769ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (3.735746ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.850048ms)
|
||||
✔ 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.147189ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (1.452023ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (4.25487ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.183121ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (4.683411ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (3.880807ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.875234ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (4.461524ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.464798ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.737679ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.50789ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (1.045646ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.712047ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.464769ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (69.480024ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (59.333179ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (50.422428ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (367.399397ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (230.353265ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (105.506805ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (236.750334ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (132.087259ms)
|
||||
✔ 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.503661ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (613.96446ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.511114ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.552981ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (431.767537ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (32.687689ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (52.548432ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.883786ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (2.857212ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.656876ms)
|
||||
✔ gateway: op 9 resumable resumes (0.426298ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.924719ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.583905ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (1.182635ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (79.062868ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (58.187339ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (123.704201ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.480361ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (100.611461ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (101.429319ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (90.962052ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (216.314936ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (77.197405ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (104.525865ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (255.526939ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (894.867161ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.488961ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (95.21334ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.52049ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.448612ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (58.081326ms)
|
||||
✔ 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 (365.796007ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.611577ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (27.196841ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.060337ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (41.85408ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (139.995819ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (88.801792ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.650585ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.454528ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.161356ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.718151ms)
|
||||
✔ 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.47922ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (53.100037ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (46.706101ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (82.879066ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (723.110751ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.655778ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (2.362339ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.581319ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.812976ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.786227ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.925145ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (8.075518ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.900435ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.544844ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (33.824092ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (11.595441ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (7.842071ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.431921ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.5717ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.042893ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1010.701121ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.493497ms)
|
||||
✔ 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.172599ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.263049ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.196583ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.28015ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.78376ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (3.840836ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (4.213843ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (3.161054ms)
|
||||
✔ tools: listing and search caps hold (8.135886ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.837185ms)
|
||||
✔ 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.30883ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (2.093538ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.866409ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (8.151203ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.524361ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.955522ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (2.976056ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.627809ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1034.133229ms)
|
||||
✔ 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.533691ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.235049ms)
|
||||
✔ 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.873039ms)
|
||||
✔ 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.357248ms)
|
||||
ℹ tests 238
|
||||
ℹ suites 0
|
||||
ℹ pass 237
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3070.23874
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:112:1
|
||||
✖ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (29.034824ms)
|
||||
AssertionError [ERR_ASSERTION]: The input did not match the regular expression /\[blocking, DM refused, not retried\] b84c8033 git\.push\.protected/. Input:
|
||||
|
||||
'Mosaic digest (demo, 2026-10-08): 1 open decision(s).\n' +
|
||||
'- [blocking, DM pending] b84c8033 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:137: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] b84c8033 git.push.protected: Push the release?\nRun mosaic inbox for the full list.',
|
||||
expected: /\[blocking, DM refused, not retried\] b84c8033 git\.push\.protected/,
|
||||
operator: 'match',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (39.399613ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (42.752447ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (23.619661ms)
|
||||
✔ decide prints a declining choice as declining (26.723196ms)
|
||||
✔ an unknown outcome is reported once and never resent (25.837713ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (20.486234ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (17.643031ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (21.580481ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (21.160872ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (18.051751ms)
|
||||
✔ agents and tasks print through the broker (22.166504ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.880253ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (101.606834ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (63.248461ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (75.77414ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (65.864648ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (58.375203ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (107.964014ms)
|
||||
✔ empty views say so (1.30274ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.751646ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.28704ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (1013.596332ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (153.225587ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (72.369039ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (134.955046ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (124.852456ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (83.365093ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (22.83235ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.11194ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (212.680767ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (83.349513ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (611.753218ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (26.88276ms)
|
||||
✔ zoned uses the IANA zone across DST (35.741125ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (42.182898ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (23.477066ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.374022ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (27.804634ms)
|
||||
✖ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (21.215696ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (40.528558ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (19.558887ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (22.683419ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (21.833006ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (1.526351ms)
|
||||
✔ no Discord id reaches the journal or the log (20.676837ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (6.316386ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (7.532213ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (3.915338ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.184655ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (1.268513ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (5.931023ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.789351ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (1.363442ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.929449ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.520921ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.475641ms)
|
||||
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.184074ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (68.610738ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2296.123091ms)
|
||||
✔ busExit and refuseInsideAgent (0.447727ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.712726ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (3.024231ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.61249ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.517578ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (19.36331ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (10.687623ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (10.245876ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (3.024185ms)
|
||||
✔ authorize: open channel, listed user (2.498851ms)
|
||||
✔ authorize: wrong guild (0.236205ms)
|
||||
✔ authorize: no guild (DM) (0.18152ms)
|
||||
✔ authorize: unlisted channel (0.218218ms)
|
||||
✔ authorize: unknown channel, no info (0.217544ms)
|
||||
✔ authorize: thread of listed parent (0.206298ms)
|
||||
✔ authorize: thread of unlisted parent (0.154946ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.135272ms)
|
||||
✔ authorize: unlisted user (0.253115ms)
|
||||
✔ authorize: no author (0.316314ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.189561ms)
|
||||
✔ authorize: system author (0.14359ms)
|
||||
✔ authorize: the bot itself (0.09083ms)
|
||||
✔ authorize: webhook (0.100336ms)
|
||||
✔ authorize: mention channel without mention (0.28ms)
|
||||
✔ authorize: mention channel with bot mention (1.877822ms)
|
||||
✔ authorize: mention channel with @everyone only (0.162538ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.092843ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.086165ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.098071ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.07816ms)
|
||||
✔ authorize: thread in another guild per channel info (0.09546ms)
|
||||
✔ authorize: not an object (0.0621ms)
|
||||
✔ authorize: no id (0.065507ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.095716ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.062381ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (1.92612ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.173319ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.827008ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (2.379902ms)
|
||||
✔ binding: empty allowlists refuse (0.449461ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.468679ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (2.030457ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.530019ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (2.043261ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.756996ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (129.019664ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (5.509946ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (428.99424ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (206.89933ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (132.254684ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.97042ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.165299ms)
|
||||
✔ 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 (25.192081ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (19.348133ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (2.285103ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.294487ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.476894ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.961021ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (35.291646ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (5.320802ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (2.639498ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.959597ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (43.522196ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (33.395495ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (8.938315ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (4.679593ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.619776ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (5.663066ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.320171ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.699162ms)
|
||||
✔ 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.05361ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (2.647527ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (4.347797ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.51634ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (5.020868ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (5.407167ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.956239ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.155157ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (2.18761ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.890901ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.483218ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.870623ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (6.073901ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.711779ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (78.611886ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (60.273514ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (57.052337ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (368.629854ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (233.779864ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (105.194203ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (226.994029ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (129.117617ms)
|
||||
✔ 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.048381ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.518495ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.368631ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.502018ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (427.354452ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (24.416984ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (44.291976ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.587568ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.793598ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.540418ms)
|
||||
✔ gateway: op 9 resumable resumes (0.398196ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (1.031682ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.630766ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.709722ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (94.847126ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (60.644235ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (135.026696ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.521044ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (118.249376ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (81.782602ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (89.371359ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (210.831428ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (71.549629ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (88.737171ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (204.420644ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (782.658523ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (6.122199ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (104.690417ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.630128ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (6.226466ms)
|
||||
✔ 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.836983ms)
|
||||
✔ 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 (347.707446ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.451723ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (23.9286ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (0.985477ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (39.836014ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (155.830596ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (84.268254ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.428955ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.686483ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.529648ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.844549ms)
|
||||
✔ 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.640262ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (53.967422ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (46.827641ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (99.282963ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (662.247545ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (3.019352ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (3.575787ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.810262ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.786533ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (1.013248ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (6.114627ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.291963ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (3.750646ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (3.733421ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (29.454966ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (12.599113ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (8.654825ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (5.353869ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.720818ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (8.54527ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1014.157679ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.472066ms)
|
||||
✔ 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.088956ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.451914ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.225798ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.410104ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.411157ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (5.303281ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (7.81989ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (5.608088ms)
|
||||
✔ tools: listing and search caps hold (13.675057ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.96592ms)
|
||||
✔ 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.459439ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.545129ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.6313ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (7.646125ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.97911ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.789413ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.464866ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.440214ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1038.573157ms)
|
||||
✔ 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.39088ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.231412ms)
|
||||
✔ 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.573875ms)
|
||||
✔ 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.162781ms)
|
||||
ℹ tests 238
|
||||
ℹ suites 0
|
||||
ℹ pass 237
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 2893.997443
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:112:1
|
||||
✖ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (21.215696ms)
|
||||
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:126: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'
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (37.492424ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (46.901314ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (19.320263ms)
|
||||
✔ decide prints a declining choice as declining (20.211541ms)
|
||||
✔ an unknown outcome is reported once and never resent (20.73056ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (20.168847ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (20.833601ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (23.8767ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (32.884637ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (27.089906ms)
|
||||
✔ agents and tasks print through the broker (26.096234ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.94948ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (90.828961ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (63.263936ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (64.080217ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (94.007056ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (76.77176ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (94.104967ms)
|
||||
✔ empty views say so (1.301202ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.50944ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.258581ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (1038.216646ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (167.709144ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (79.033426ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (150.846799ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (142.775643ms)
|
||||
✖ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (88.736957ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (23.979586ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.45962ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (214.384562ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (95.193623ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (629.211493ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (27.050807ms)
|
||||
✔ zoned uses the IANA zone across DST (44.169703ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (42.462522ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (28.879096ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.351178ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (23.5297ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (30.368991ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (35.153659ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (36.770199ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (40.033585ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (27.37375ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (1.45511ms)
|
||||
✔ no Discord id reaches the journal or the log (39.428261ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (6.474543ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (5.76472ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (3.507057ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.077401ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (1.149715ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (3.470898ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.588062ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.671505ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.546066ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.532604ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (110.366907ms)
|
||||
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 (415.897875ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (87.073395ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2293.572745ms)
|
||||
✔ busExit and refuseInsideAgent (0.39016ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.44924ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.58996ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.597253ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.521908ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (19.480035ms)
|
||||
✔ 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.034758ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (6.027036ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (3.179268ms)
|
||||
✔ authorize: open channel, listed user (2.202193ms)
|
||||
✔ authorize: wrong guild (0.223048ms)
|
||||
✔ authorize: no guild (DM) (0.189411ms)
|
||||
✔ authorize: unlisted channel (0.202054ms)
|
||||
✔ authorize: unknown channel, no info (0.463484ms)
|
||||
✔ authorize: thread of listed parent (0.201048ms)
|
||||
✔ authorize: thread of unlisted parent (0.28348ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.155034ms)
|
||||
✔ authorize: unlisted user (1.058267ms)
|
||||
✔ authorize: no author (0.26728ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.15142ms)
|
||||
✔ authorize: system author (0.138447ms)
|
||||
✔ authorize: the bot itself (0.096213ms)
|
||||
✔ authorize: webhook (1.513478ms)
|
||||
✔ authorize: mention channel without mention (0.242797ms)
|
||||
✔ authorize: mention channel with bot mention (0.150003ms)
|
||||
✔ authorize: mention channel with @everyone only (0.24078ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.111664ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.103927ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.093072ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.081848ms)
|
||||
✔ authorize: thread in another guild per channel info (0.078317ms)
|
||||
✔ authorize: not an object (0.066804ms)
|
||||
✔ authorize: no id (0.077435ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.076551ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.065789ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.494237ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.183045ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.52746ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.251136ms)
|
||||
✔ binding: empty allowlists refuse (0.370771ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.286459ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.67299ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.888812ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.569365ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.832579ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (126.952943ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (3.35444ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (467.527415ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (235.435875ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (155.159011ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.627766ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.153784ms)
|
||||
✔ 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.307154ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (19.789273ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.508334ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.526541ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.634357ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (3.310934ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (32.152699ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (4.035026ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.684283ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.232098ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (41.826109ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.744032ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (7.995623ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (4.911303ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.688121ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (6.165624ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (4.258075ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.840034ms)
|
||||
✔ 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.360198ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (9.426972ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (7.513172ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.491409ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (14.163631ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.516788ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.814796ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.962895ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.358474ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.681658ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.209683ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.773092ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (3.329708ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.458423ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (71.282406ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (63.938548ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (65.967788ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (379.136992ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (238.335738ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (105.552931ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (249.662987ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (132.42548ms)
|
||||
✔ 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.052749ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (613.665387ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.436497ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.518866ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (427.003583ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (25.77751ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (50.866344ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (3.048495ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (2.080435ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.692223ms)
|
||||
✔ gateway: op 9 resumable resumes (0.509448ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (1.071569ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.574532ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.580243ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (81.020584ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (96.171888ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (157.089241ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.330816ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (104.691311ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (101.634508ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (104.90505ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (242.523202ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (86.611862ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (92.380528ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (224.989596ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (835.226686ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.370997ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (112.42347ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (5.943609ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (7.244536ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (89.033858ms)
|
||||
✔ 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 (397.490465ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.572835ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (24.2874ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.08916ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (47.098882ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (166.45975ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (98.431987ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.43822ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.40412ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.65507ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.794684ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (85.065322ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (65.083954ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (41.410836ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (91.224724ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (747.8925ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.782205ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (6.82382ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.743234ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.737002ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.87848ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (6.369135ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.146023ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.950274ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.911356ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (33.295712ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (19.570912ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (11.011574ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.565558ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.666641ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.768094ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1014.107302ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.538534ms)
|
||||
✔ 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.276976ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.478296ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.224197ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.389052ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (5.700251ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (6.10373ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (6.198369ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (5.026638ms)
|
||||
✔ tools: listing and search caps hold (10.788783ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.990129ms)
|
||||
✔ 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.201741ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.391125ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.618291ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.737268ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.232904ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.356659ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.397313ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.905481ms)
|
||||
✔ 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.526748ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (5.475888ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.318435ms)
|
||||
✔ 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 (4.184267ms)
|
||||
✔ 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.368008ms)
|
||||
ℹ tests 238
|
||||
ℹ suites 0
|
||||
ℹ pass 237
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3033.968805
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/host.test.mjs:204:1
|
||||
✖ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (88.736957ms)
|
||||
AssertionError [ERR_ASSERTION]: no close was sent over the closed channel
|
||||
+ actual - expected
|
||||
|
||||
+ [
|
||||
+ 'ERR_IPC_CHANNEL_CLOSED'
|
||||
+ ]
|
||||
- []
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:228: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: [ 'ERR_IPC_CHANNEL_CLOSED' ],
|
||||
expected: [],
|
||||
operator: 'deepStrictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (32.228605ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (40.278004ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (21.348264ms)
|
||||
✔ decide prints a declining choice as declining (16.912534ms)
|
||||
✔ an unknown outcome is reported once and never resent (17.481236ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (20.695264ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (22.461546ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (20.587981ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (19.735965ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (20.654305ms)
|
||||
✔ agents and tasks print through the broker (14.565243ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.38852ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (102.567725ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (67.414959ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (57.270434ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (63.527893ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (51.138233ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (75.0068ms)
|
||||
✔ empty views say so (1.345349ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.556933ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.253858ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (964.48508ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (143.652607ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (90.476055ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (134.168937ms)
|
||||
✖ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (138.183722ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (91.643843ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (28.488994ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (203.588328ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (237.543324ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (94.223396ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (611.398545ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (47.266818ms)
|
||||
✔ zoned uses the IANA zone across DST (29.628864ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (38.474233ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (22.620799ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.431182ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (22.575811ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (24.467542ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (26.178389ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (18.870431ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (24.774943ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (20.870719ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (1.936476ms)
|
||||
✔ no Discord id reaches the journal or the log (24.265066ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.880031ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (6.579526ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (5.57158ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.215725ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (1.192825ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (3.848255ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.82527ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.777662ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.631763ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.461135ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.585568ms)
|
||||
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.333244ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (64.934878ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2270.117889ms)
|
||||
✔ busExit and refuseInsideAgent (0.481272ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.274401ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.827351ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.654273ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.504905ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (24.321214ms)
|
||||
✔ 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.187852ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (7.354124ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.168845ms)
|
||||
✔ authorize: open channel, listed user (1.686065ms)
|
||||
✔ authorize: wrong guild (0.179712ms)
|
||||
✔ authorize: no guild (DM) (0.156831ms)
|
||||
✔ authorize: unlisted channel (0.243682ms)
|
||||
✔ authorize: unknown channel, no info (0.168147ms)
|
||||
✔ authorize: thread of listed parent (0.162883ms)
|
||||
✔ authorize: thread of unlisted parent (0.126641ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.121431ms)
|
||||
✔ authorize: unlisted user (0.74653ms)
|
||||
✔ authorize: no author (0.294984ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.133181ms)
|
||||
✔ authorize: system author (0.108172ms)
|
||||
✔ authorize: the bot itself (0.081269ms)
|
||||
✔ authorize: webhook (0.095768ms)
|
||||
✔ authorize: mention channel without mention (0.121743ms)
|
||||
✔ authorize: mention channel with bot mention (0.123365ms)
|
||||
✔ authorize: mention channel with @everyone only (0.130847ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.070893ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.077446ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.074164ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.060388ms)
|
||||
✔ authorize: thread in another guild per channel info (0.060283ms)
|
||||
✔ authorize: not an object (0.060163ms)
|
||||
✔ authorize: no id (0.052814ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.061177ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.069877ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.464962ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.149034ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.758522ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.327057ms)
|
||||
✔ binding: empty allowlists refuse (0.848491ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.393551ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.584087ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.424984ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.383419ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (2.447015ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (116.0478ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (3.616638ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (404.018497ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (205.349169ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (123.877473ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (2.078686ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.022787ms)
|
||||
✔ 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.724052ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (20.17376ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (2.435425ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.085901ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.230549ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.874651ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (32.455002ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.828976ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.020689ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.231898ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.291641ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.385519ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (8.622368ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (8.888178ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.319892ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (6.630106ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (3.22566ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (1.225906ms)
|
||||
✔ 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.130614ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (4.598557ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (5.456602ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.996909ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (5.104301ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.808573ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (1.057801ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.015527ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.297812ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.561517ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (2.538155ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.862153ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.574018ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.428052ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (61.323542ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (55.184989ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (49.635077ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (373.371234ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (232.777044ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (103.903915ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (229.929019ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (128.691271ms)
|
||||
✔ 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.982165ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.151761ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.776884ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.510358ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (430.530041ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (25.03044ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (46.233853ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.280403ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.543335ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.477173ms)
|
||||
✔ gateway: op 9 resumable resumes (0.375039ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.803216ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.67304ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.419021ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (79.408432ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (64.764277ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (137.834111ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (1.076971ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (89.024856ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (79.923571ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (87.595595ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (211.412191ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (69.13527ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (86.016305ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (206.511752ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (887.320392ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.638447ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (91.252181ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.31844ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (7.75022ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (67.283394ms)
|
||||
✔ 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 (321.083241ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.423785ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (27.373497ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.551861ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (43.920996ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (148.081404ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (89.461077ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.447968ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.470555ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.382851ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.813565ms)
|
||||
✔ 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.397766ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (53.875844ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (44.679845ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (77.808878ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (656.511409ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.54486ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (6.155851ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.588238ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.71188ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.786562ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (7.433375ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.542964ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.887947ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.475055ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (27.431777ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (10.641288ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (6.683763ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.122666ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.717083ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (4.322649ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1013.125836ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.426793ms)
|
||||
✔ 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.511687ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.43475ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.244943ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.491524ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (3.979852ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.670757ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (4.959995ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (4.407547ms)
|
||||
✔ tools: listing and search caps hold (10.945615ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.976211ms)
|
||||
✔ 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.975095ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.41625ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.337924ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.398277ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.125383ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.824525ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.548795ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.527893ms)
|
||||
✔ 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.106717ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (5.286511ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.305898ms)
|
||||
✔ 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.048986ms)
|
||||
✔ 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.627593ms)
|
||||
ℹ tests 238
|
||||
ℹ suites 0
|
||||
ℹ pass 237
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 2930.347759
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/host.test.mjs:180:1
|
||||
✖ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (138.183722ms)
|
||||
AssertionError [ERR_ASSERTION]: no close was sent over the closed channel
|
||||
+ actual - expected
|
||||
|
||||
+ [
|
||||
+ 'ERR_IPC_CHANNEL_CLOSED'
|
||||
+ ]
|
||||
- []
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/host.test.mjs:200: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: [ 'ERR_IPC_CHANNEL_CLOSED' ],
|
||||
expected: [],
|
||||
operator: 'deepStrictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (40.178394ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (35.863003ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (24.736188ms)
|
||||
✔ decide prints a declining choice as declining (19.073159ms)
|
||||
✔ an unknown outcome is reported once and never resent (17.085244ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (19.009292ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (15.976373ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (17.073234ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (17.122084ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (23.084738ms)
|
||||
✔ agents and tasks print through the broker (20.525446ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.758998ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (96.537836ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (66.423889ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (57.680444ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (66.148474ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (62.209784ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (81.142717ms)
|
||||
✔ empty views say so (1.35848ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.38045ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.236756ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (971.109222ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (154.493033ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (88.44709ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (137.38753ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (137.585033ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (76.775458ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (21.801903ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.673437ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (220.100444ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (87.478616ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (592.668832ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (26.254166ms)
|
||||
✔ zoned uses the IANA zone across DST (40.883013ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (36.543813ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (24.964737ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.323865ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (19.339417ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (26.701409ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (27.199062ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (20.707002ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (20.970264ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (16.937632ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (1.303016ms)
|
||||
✔ no Discord id reaches the journal or the log (22.113291ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (3.51767ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (6.93237ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (2.649603ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.977266ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (1.656843ms)
|
||||
✖ the journal: a line with a wrong type refuses with exit 3 and names the field (1.942977ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.764619ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (1.867527ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.932467ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.520961ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (112.03963ms)
|
||||
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 (375.121884ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (92.605468ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2283.411553ms)
|
||||
✔ busExit and refuseInsideAgent (0.385315ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (6.699494ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.883197ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.68724ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.525914ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (17.719514ms)
|
||||
✔ 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.305576ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (6.276686ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.346813ms)
|
||||
✔ authorize: open channel, listed user (2.005713ms)
|
||||
✔ authorize: wrong guild (0.232843ms)
|
||||
✔ authorize: no guild (DM) (0.328135ms)
|
||||
✔ authorize: unlisted channel (0.205629ms)
|
||||
✔ authorize: unknown channel, no info (0.282936ms)
|
||||
✔ authorize: thread of listed parent (0.1984ms)
|
||||
✔ authorize: thread of unlisted parent (0.165858ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.133678ms)
|
||||
✔ authorize: unlisted user (0.437424ms)
|
||||
✔ authorize: no author (0.281711ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.164005ms)
|
||||
✔ authorize: system author (0.136004ms)
|
||||
✔ authorize: the bot itself (0.099494ms)
|
||||
✔ authorize: webhook (0.12051ms)
|
||||
✔ authorize: mention channel without mention (0.163499ms)
|
||||
✔ authorize: mention channel with bot mention (0.163623ms)
|
||||
✔ authorize: mention channel with @everyone only (0.141232ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.096826ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.140084ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.161249ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.086036ms)
|
||||
✔ authorize: thread in another guild per channel info (0.085184ms)
|
||||
✔ authorize: not an object (0.059304ms)
|
||||
✔ authorize: no id (0.064754ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.075987ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.062031ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.481951ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.155196ms)
|
||||
✔ binding: a complete binding validates and is frozen (3.352913ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.346132ms)
|
||||
✔ binding: empty allowlists refuse (0.502913ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.356312ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (2.062133ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (1.539619ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.85753ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.497531ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (116.387843ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (2.214413ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (423.602366ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (189.832448ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (134.828463ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.830854ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.172785ms)
|
||||
✔ 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.045323ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (17.499086ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.569036ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.165007ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.29847ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (3.618609ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (32.571062ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.915062ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.730894ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.34288ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (43.088159ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (33.472076ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (7.540104ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (5.315309ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.302271ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (6.267064ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (3.365998ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.836379ms)
|
||||
✔ 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.038025ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (2.770196ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (6.184327ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.890275ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (4.363724ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.662734ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.795574ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.998211ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.363229ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.977642ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.241732ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.71357ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (5.024172ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (2.056102ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (68.437918ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (57.675462ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (51.164612ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (355.942781ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (231.327046ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (105.060805ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (236.351435ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (130.071807ms)
|
||||
✔ 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.944482ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.861955ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.294732ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.487353ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (432.88905ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (25.475298ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (44.768518ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.321535ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.772038ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.577067ms)
|
||||
✔ gateway: op 9 resumable resumes (0.34586ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.93422ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.704485ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.445703ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (93.797845ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (64.166394ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (128.702975ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.457336ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (100.329641ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (109.242173ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (87.592994ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (202.979136ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (75.785086ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (108.917459ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (211.454357ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (813.086235ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.25015ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (92.71004ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.319005ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.075149ms)
|
||||
✔ 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.99521ms)
|
||||
✔ 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 (354.248055ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.512083ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (26.396756ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (0.997808ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (41.647727ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (137.663271ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (97.432177ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.462093ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.439869ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (1.995341ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.903174ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (48.539121ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (67.503755ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (44.935878ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (77.781249ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (686.325791ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.664458ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (2.55206ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (1.004101ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.917914ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.947217ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (6.166081ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.283228ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.817515ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.375665ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (32.809267ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (13.87844ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (8.242295ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (3.642743ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.918933ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (2.396372ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1009.836205ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.463842ms)
|
||||
✔ 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.047968ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.381771ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.220929ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.616961ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.676761ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.314775ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (4.842744ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (4.136825ms)
|
||||
✔ tools: listing and search caps hold (12.308106ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.928262ms)
|
||||
✔ 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.670569ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.362704ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.267139ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (6.376714ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.11512ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.90241ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.912301ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (1.986547ms)
|
||||
✔ 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.299401ms)
|
||||
✔ 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.237208ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.230824ms)
|
||||
✔ 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.66606ms)
|
||||
✔ 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.241633ms)
|
||||
ℹ tests 238
|
||||
ℹ suites 0
|
||||
ℹ pass 237
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 2848.156584
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:295:1
|
||||
✖ the journal: a line with a wrong type refuses with exit 3 and names the field (1.942977ms)
|
||||
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:322: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,267 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (31.641462ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (35.322452ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (31.007569ms)
|
||||
✔ decide prints a declining choice as declining (21.334099ms)
|
||||
✔ an unknown outcome is reported once and never resent (21.545933ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (16.821501ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (14.401704ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (17.161541ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (16.988672ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (22.531163ms)
|
||||
✔ agents and tasks print through the broker (15.933678ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.287977ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (88.236762ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (61.977269ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (63.082225ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (60.535926ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (55.16602ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (76.783314ms)
|
||||
✔ empty views say so (1.064505ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.308551ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.234359ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (972.034043ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (154.350102ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (74.920176ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (124.347216ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (123.739385ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (76.308466ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (25.124867ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.263685ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (208.216742ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (89.979963ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (592.879248ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (25.965347ms)
|
||||
✔ zoned uses the IANA zone across DST (26.816491ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (33.216071ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (20.697551ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.332262ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (17.676761ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (28.422641ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (29.570644ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (20.535023ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (22.602597ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (17.347517ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (1.200189ms)
|
||||
✔ no Discord id reaches the journal or the log (22.245451ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (3.47286ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (4.677474ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (3.288916ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (3.667165ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (1.338822ms)
|
||||
✖ the journal: a line with a wrong type refuses with exit 3 and names the field (3.152447ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.646042ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.796723ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.755944ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.482202ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.437991ms)
|
||||
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 (364.361766ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (65.680839ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2264.835281ms)
|
||||
✔ busExit and refuseInsideAgent (0.399536ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.393959ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.488072ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.568962ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.535135ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (20.721229ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (10.458788ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (5.96672ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (3.4541ms)
|
||||
✔ authorize: open channel, listed user (1.857656ms)
|
||||
✔ authorize: wrong guild (0.202587ms)
|
||||
✔ authorize: no guild (DM) (0.185227ms)
|
||||
✔ authorize: unlisted channel (0.19674ms)
|
||||
✔ authorize: unknown channel, no info (0.215987ms)
|
||||
✔ authorize: thread of listed parent (0.212618ms)
|
||||
✔ authorize: thread of unlisted parent (0.161533ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.15495ms)
|
||||
✔ authorize: unlisted user (0.200742ms)
|
||||
✔ authorize: no author (0.293668ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.188008ms)
|
||||
✔ authorize: system author (0.118566ms)
|
||||
✔ authorize: the bot itself (0.080874ms)
|
||||
✔ authorize: webhook (0.086295ms)
|
||||
✔ authorize: mention channel without mention (0.205867ms)
|
||||
✔ authorize: mention channel with bot mention (0.157022ms)
|
||||
✔ authorize: mention channel with @everyone only (0.109362ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.08095ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.090266ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.083992ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.071717ms)
|
||||
✔ authorize: thread in another guild per channel info (0.101541ms)
|
||||
✔ authorize: not an object (0.074541ms)
|
||||
✔ authorize: no id (0.068059ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.074999ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.068581ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.488922ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (1.190475ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.674013ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.358442ms)
|
||||
✔ binding: empty allowlists refuse (0.4153ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.315184ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.778353ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.486844ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.869814ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.381587ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (112.588398ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (2.510588ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (406.492464ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (203.097235ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (127.324494ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.569021ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.074049ms)
|
||||
✔ 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.740245ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (20.053806ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.60393ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.201759ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.276822ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.688447ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (33.542332ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (2.567071ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.191369ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.397294ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.975367ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (31.925905ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (7.263204ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (5.741574ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (2.085021ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (6.703092ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (3.005378ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (1.121373ms)
|
||||
✔ 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.4934ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (1.52353ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (5.915792ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (3.909654ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (6.244093ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.484088ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.853441ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.819341ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.347953ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.535876ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.519688ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.623174ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.292646ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.350414ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (56.673591ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (57.934364ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (48.207752ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (353.752922ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (234.938963ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (104.384991ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (228.174488ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (126.644239ms)
|
||||
✔ 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.613099ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.356988ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.271412ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.50658ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (433.700172ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (30.309757ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (47.111341ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.820168ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.662312ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.568208ms)
|
||||
✔ gateway: op 9 resumable resumes (0.485667ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.695931ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.545155ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.511481ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (87.685511ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (68.447224ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (127.464049ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.464149ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (88.244813ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (92.170311ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (99.858961ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (203.808709ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (64.694759ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (77.536586ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (211.623088ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (772.562388ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (6.015982ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (91.888288ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (2.051467ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.694244ms)
|
||||
✔ 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.516666ms)
|
||||
✔ 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 (319.75231ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.445221ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (25.608545ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.265128ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (36.49031ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (133.385729ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (84.369184ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.41669ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.12683ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.021367ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.823894ms)
|
||||
✔ 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.813973ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (50.338857ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (32.858718ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (72.509993ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (651.879335ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (3.520304ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (3.082983ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.693962ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.769472ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.871511ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (6.356801ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.562355ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (3.569983ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.906368ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (30.333769ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (10.978455ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (7.027036ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (3.495905ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.738514ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.441111ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1013.290604ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.457839ms)
|
||||
✔ 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.041447ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.271969ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.199875ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.853766ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.072384ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (3.423642ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (4.821114ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (3.777509ms)
|
||||
✔ tools: listing and search caps hold (11.031892ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.690283ms)
|
||||
✔ 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.937534ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (0.984937ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (0.934924ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (4.405574ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.140078ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.770148ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.524403ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.295062ms)
|
||||
✔ 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.016237ms)
|
||||
✔ 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.900783ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.237087ms)
|
||||
✔ 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.850069ms)
|
||||
✔ 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.257061ms)
|
||||
ℹ tests 238
|
||||
ℹ suites 0
|
||||
ℹ pass 237
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 2801.746065
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:295:1
|
||||
✖ the journal: a line with a wrong type refuses with exit 3 and names the field (3.152447ms)
|
||||
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:322: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,267 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (27.643591ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (35.519263ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (21.51663ms)
|
||||
✔ decide prints a declining choice as declining (17.103225ms)
|
||||
✔ an unknown outcome is reported once and never resent (21.013534ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (15.771698ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (18.664329ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (22.91903ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (16.771711ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (28.335192ms)
|
||||
✔ agents and tasks print through the broker (18.980555ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.40751ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (78.745402ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (55.791893ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (65.608092ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (56.192209ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (63.695722ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (84.188839ms)
|
||||
✔ empty views say so (1.211604ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.614115ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.263092ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (963.651705ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (188.927523ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (86.130295ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (160.578811ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (142.029051ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (81.203624ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (25.75571ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.916303ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (228.047912ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (101.463668ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (651.122298ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (26.989433ms)
|
||||
✔ zoned uses the IANA zone across DST (29.022051ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (26.16599ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (23.329429ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.737087ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (22.42865ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (19.938613ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (34.258521ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (26.749293ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (21.13039ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (23.302223ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (1.334876ms)
|
||||
✔ no Discord id reaches the journal or the log (21.74432ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (9.869971ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (8.396279ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (3.039484ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.652043ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (1.291647ms)
|
||||
✖ the journal: a line with a wrong type refuses with exit 3 and names the field (6.742891ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (1.008499ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.951665ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.904542ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.500688ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (113.599001ms)
|
||||
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.710441ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (60.889333ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2272.007535ms)
|
||||
✔ busExit and refuseInsideAgent (0.93688ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (2.984573ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.649396ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.650683ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.519943ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (15.713554ms)
|
||||
✔ 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.009811ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (7.988703ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (4.054012ms)
|
||||
✔ authorize: open channel, listed user (2.155137ms)
|
||||
✔ authorize: wrong guild (0.231068ms)
|
||||
✔ authorize: no guild (DM) (0.139169ms)
|
||||
✔ authorize: unlisted channel (0.242635ms)
|
||||
✔ authorize: unknown channel, no info (0.197047ms)
|
||||
✔ authorize: thread of listed parent (0.217685ms)
|
||||
✔ authorize: thread of unlisted parent (0.138087ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.143813ms)
|
||||
✔ authorize: unlisted user (0.20649ms)
|
||||
✔ authorize: no author (0.826126ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.147938ms)
|
||||
✔ authorize: system author (0.134168ms)
|
||||
✔ authorize: the bot itself (0.086033ms)
|
||||
✔ authorize: webhook (0.079787ms)
|
||||
✔ authorize: mention channel without mention (0.129088ms)
|
||||
✔ authorize: mention channel with bot mention (0.124423ms)
|
||||
✔ authorize: mention channel with @everyone only (0.102813ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.079671ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.101451ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.089304ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.072609ms)
|
||||
✔ authorize: thread in another guild per channel info (0.069493ms)
|
||||
✔ authorize: not an object (0.064169ms)
|
||||
✔ authorize: no id (0.086729ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.078644ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.064843ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.492882ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.176708ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.701957ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.513197ms)
|
||||
✔ binding: empty allowlists refuse (0.484505ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.372818ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.673879ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.508837ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.759711ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.580571ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (112.96298ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (4.045177ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (386.063224ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (200.269955ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (141.821303ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.85183ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.273087ms)
|
||||
✔ 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.936257ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (18.605297ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.640247ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.160227ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.339179ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.703086ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (34.822871ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.942841ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (2.555991ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.09954ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.017815ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.862744ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (10.393392ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (5.13336ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (4.983138ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (5.31752ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (3.062855ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.790629ms)
|
||||
✔ 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.077898ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (1.489226ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (5.657756ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (3.931782ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (4.347332ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.829283ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.855326ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.966854ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.333221ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (2.05521ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.324838ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.725719ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.633729ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.454609ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (59.129019ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (49.415781ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (51.220156ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (364.961065ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (231.955945ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (116.019705ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (230.352138ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (137.680452ms)
|
||||
✔ 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.351143ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.062479ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.451869ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.511624ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (429.918503ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (22.664666ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (42.859203ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.655333ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.717421ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.475124ms)
|
||||
✔ gateway: op 9 resumable resumes (0.345225ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.883072ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.652498ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.446444ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (75.172652ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (62.074807ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (130.4647ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.472125ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (98.464789ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (82.906024ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (82.873477ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (207.426247ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (74.484382ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (94.567199ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (233.988825ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (920.904415ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.631822ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (88.588384ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (0.938697ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (3.429738ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (66.058687ms)
|
||||
✔ 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.769924ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.433509ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (27.171468ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.020419ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (35.622349ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (145.258571ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (89.71827ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.523241ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (4.955312ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.861113ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.660167ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (48.431183ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (56.072959ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (45.338491ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (77.074753ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (668.03626ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.51816ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (2.520593ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.952685ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.892951ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.901009ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.454531ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.51003ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.742372ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.50826ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (40.658199ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (14.329824ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (9.646494ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (5.33795ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (1.132372ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.363517ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1015.94769ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.5173ms)
|
||||
✔ 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.33134ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.461949ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.245936ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.822686ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (5.007203ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.299205ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.631266ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (6.445715ms)
|
||||
✔ tools: listing and search caps hold (12.040382ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.773293ms)
|
||||
✔ 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.142623ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.015887ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.006631ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.383525ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.708757ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.41607ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.707807ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (1.884602ms)
|
||||
✔ 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.144482ms)
|
||||
✔ 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.910791ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.276005ms)
|
||||
✔ 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.00704ms)
|
||||
✔ 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.699595ms)
|
||||
ℹ tests 238
|
||||
ℹ suites 0
|
||||
ℹ pass 237
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3000.276564
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:295:1
|
||||
✖ the journal: a line with a wrong type refuses with exit 3 and names the field (6.742891ms)
|
||||
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:322: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,267 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (41.966803ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (40.097449ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (23.69498ms)
|
||||
✔ decide prints a declining choice as declining (21.07254ms)
|
||||
✔ an unknown outcome is reported once and never resent (17.391697ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (16.608125ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (17.572481ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (18.891247ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (20.775923ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (28.627978ms)
|
||||
✔ agents and tasks print through the broker (21.777188ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.814134ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (80.074717ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (62.005354ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (54.449612ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (64.644778ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (65.718177ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (82.718237ms)
|
||||
✔ empty views say so (1.262534ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.697088ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.244247ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (971.845944ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (151.88012ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (84.294917ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (134.927199ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (116.886659ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (91.204963ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (21.252358ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.09451ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (227.24473ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (115.300034ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (714.675186ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (35.377467ms)
|
||||
✔ zoned uses the IANA zone across DST (30.869939ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (44.459735ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (24.492749ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.436265ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (22.309484ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (33.524709ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (26.55875ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (17.836076ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (21.282863ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (22.059919ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (2.068053ms)
|
||||
✔ no Discord id reaches the journal or the log (25.647907ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (4.414229ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (4.682502ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (2.791605ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (2.079003ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (1.30314ms)
|
||||
✖ the journal: a line with a wrong type refuses with exit 3 and names the field (4.707985ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.740859ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.766019ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.718589ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.496017ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (116.482105ms)
|
||||
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 (389.492252ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (79.33565ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2270.05979ms)
|
||||
✔ busExit and refuseInsideAgent (0.584772ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.211031ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.53466ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.573472ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.625536ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (20.060861ms)
|
||||
✔ 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.376565ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (6.355832ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (3.240272ms)
|
||||
✔ authorize: open channel, listed user (6.518614ms)
|
||||
✔ authorize: wrong guild (0.26889ms)
|
||||
✔ authorize: no guild (DM) (0.160209ms)
|
||||
✔ authorize: unlisted channel (0.182076ms)
|
||||
✔ authorize: unknown channel, no info (0.464429ms)
|
||||
✔ authorize: thread of listed parent (0.200252ms)
|
||||
✔ authorize: thread of unlisted parent (0.172028ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.124469ms)
|
||||
✔ authorize: unlisted user (0.185508ms)
|
||||
✔ authorize: no author (1.217413ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.124484ms)
|
||||
✔ authorize: system author (0.138957ms)
|
||||
✔ authorize: the bot itself (0.073761ms)
|
||||
✔ authorize: webhook (0.089308ms)
|
||||
✔ authorize: mention channel without mention (0.152009ms)
|
||||
✔ authorize: mention channel with bot mention (0.123676ms)
|
||||
✔ authorize: mention channel with @everyone only (0.092072ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.082584ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.093984ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.07809ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.073531ms)
|
||||
✔ authorize: thread in another guild per channel info (0.065903ms)
|
||||
✔ authorize: not an object (0.05368ms)
|
||||
✔ authorize: no id (0.054333ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.063206ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.055769ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.436373ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.166861ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.847364ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.240996ms)
|
||||
✔ binding: empty allowlists refuse (0.428035ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.336318ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.709714ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.447635ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (2.787058ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.553854ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (115.012865ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (3.270197ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (398.740447ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (209.363159ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (133.32321ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.970279ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.199173ms)
|
||||
✔ 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.818489ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (20.098166ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.659682ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.16753ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.546124ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.975849ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (32.904314ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (2.539357ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.090762ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.016259ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (41.345485ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.236127ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (7.126717ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (5.890159ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (2.169082ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (6.041275ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.763057ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (1.089434ms)
|
||||
✔ 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.471961ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (4.31072ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (6.865553ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.116829ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (5.709207ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.081103ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (1.32134ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.175487ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.316526ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.511178ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.317863ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.88573ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.299989ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.427714ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (58.111779ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (54.180505ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (46.375444ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (365.133612ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (231.267836ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (106.184309ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (236.045554ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (133.059654ms)
|
||||
✔ 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.425752ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.357728ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.820801ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.599095ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (433.072474ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (37.019414ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (56.099169ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.571084ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.968993ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.583697ms)
|
||||
✔ gateway: op 9 resumable resumes (0.389204ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.79605ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.535765ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.395453ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (81.252516ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (60.05454ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (135.547057ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.348609ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (99.611908ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (101.195638ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (84.298307ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (213.017412ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (75.766287ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (85.340594ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (206.585292ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (816.130677ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.775587ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (94.037882ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (3.319636ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.789482ms)
|
||||
✔ 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.039427ms)
|
||||
✔ 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 (338.130452ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.442039ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (23.721803ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.07883ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (62.099782ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (134.551297ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (96.737907ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.416317ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (4.774252ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.022207ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.929507ms)
|
||||
✔ 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.52031ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (52.833927ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (46.815091ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (79.887662ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (679.7069ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.800138ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (5.247609ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.649344ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.851987ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.717194ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (6.681539ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.566101ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.905946ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.709423ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (29.140921ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (13.372659ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (9.859757ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.627391ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.940787ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.012968ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1017.594153ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.433237ms)
|
||||
✔ 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.168368ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.267652ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.200275ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.327958ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.087159ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.190978ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (6.854966ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (4.773636ms)
|
||||
✔ tools: listing and search caps hold (12.570471ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.938293ms)
|
||||
✔ 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.871978ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (0.948924ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.016527ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (4.953793ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.180679ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.766959ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.858203ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.411369ms)
|
||||
✔ 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.884072ms)
|
||||
✔ 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.128329ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.221509ms)
|
||||
✔ 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.872481ms)
|
||||
✔ 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.204624ms)
|
||||
ℹ tests 238
|
||||
ℹ suites 0
|
||||
ℹ pass 237
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3014.938113
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:295:1
|
||||
✖ the journal: a line with a wrong type refuses with exit 3 and names the field (4.707985ms)
|
||||
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:322: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,275 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (31.719722ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (36.824606ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (22.767644ms)
|
||||
✔ decide prints a declining choice as declining (24.668827ms)
|
||||
✔ an unknown outcome is reported once and never resent (18.919231ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (16.463344ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (17.329446ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (18.000094ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (27.498598ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (22.685852ms)
|
||||
✔ agents and tasks print through the broker (20.472104ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.571583ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (66.470735ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (53.998195ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (64.422993ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (66.323652ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (62.833256ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (94.449444ms)
|
||||
✔ empty views say so (1.601906ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.358315ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.252744ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (993.133184ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (139.799718ms)
|
||||
✖ a second host for the same data root refuses with exit 3 while the first runs (111.225187ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (138.505981ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (131.843098ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (74.560577ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (22.393583ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.767033ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (204.631487ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (85.252005ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (596.559579ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (27.181696ms)
|
||||
✔ zoned uses the IANA zone across DST (26.787292ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (28.307289ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (21.680364ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.424977ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (22.569248ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (31.884051ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (34.920697ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (18.706367ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (24.598025ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (16.546414ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (1.643479ms)
|
||||
✔ no Discord id reaches the journal or the log (26.867815ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (5.77937ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (8.132901ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (4.526598ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.115156ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (1.919761ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (7.028019ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.79119ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.869087ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.798291ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.46731ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (113.094878ms)
|
||||
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 (385.74549ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (67.453351ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2276.792937ms)
|
||||
✔ busExit and refuseInsideAgent (0.395811ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.193125ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.590134ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.623601ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.535692ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (15.428031ms)
|
||||
✔ 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.567928ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (7.779783ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (5.858597ms)
|
||||
✔ authorize: open channel, listed user (2.016946ms)
|
||||
✔ authorize: wrong guild (0.29867ms)
|
||||
✔ authorize: no guild (DM) (0.197983ms)
|
||||
✔ authorize: unlisted channel (0.184622ms)
|
||||
✔ authorize: unknown channel, no info (0.163233ms)
|
||||
✔ authorize: thread of listed parent (0.191943ms)
|
||||
✔ authorize: thread of unlisted parent (0.257525ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.192846ms)
|
||||
✔ authorize: unlisted user (0.290214ms)
|
||||
✔ authorize: no author (0.308068ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.219737ms)
|
||||
✔ authorize: system author (0.138269ms)
|
||||
✔ authorize: the bot itself (0.102473ms)
|
||||
✔ authorize: webhook (0.120127ms)
|
||||
✔ authorize: mention channel without mention (0.343041ms)
|
||||
✔ authorize: mention channel with bot mention (0.17213ms)
|
||||
✔ authorize: mention channel with @everyone only (0.099901ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.072357ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.078214ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.083573ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.07436ms)
|
||||
✔ authorize: thread in another guild per channel info (0.074336ms)
|
||||
✔ authorize: not an object (0.068171ms)
|
||||
✔ authorize: no id (0.067967ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.078013ms)
|
||||
✔ authorize: exactly the limit is not oversize (2.234707ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.618829ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.150673ms)
|
||||
✔ binding: a complete binding validates and is frozen (3.825709ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.335037ms)
|
||||
✔ binding: empty allowlists refuse (0.426495ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.459532ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (3.260378ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (1.641519ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.637434ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.330838ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (126.67327ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (3.798325ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (409.620358ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (191.885328ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (135.510858ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.732445ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.217294ms)
|
||||
✔ 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.305485ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (19.38883ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.616295ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.21026ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.352481ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (3.13292ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (33.455619ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.960828ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.220106ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.131612ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (43.27868ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (33.384625ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (8.529987ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (6.924194ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (2.611438ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (4.023079ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (3.361161ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.854582ms)
|
||||
✔ 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.668091ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (1.713151ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (4.843672ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (1.636779ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (4.733655ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (3.299411ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.844904ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.505172ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.500455ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.949362ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.254604ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.830137ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (3.128729ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.459241ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (62.630797ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (50.877733ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (50.957206ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (382.511497ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (229.850841ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (115.436713ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (225.535074ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (123.495707ms)
|
||||
✔ 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.181452ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.412657ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.346262ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.501651ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (427.089234ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (28.512319ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (46.183127ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.899501ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.796397ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.5165ms)
|
||||
✔ gateway: op 9 resumable resumes (0.360281ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (2.607398ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.588405ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.525236ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (78.776187ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (82.312067ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (143.195579ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.344564ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (84.006943ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (88.365912ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (88.685186ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (203.557303ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (70.715091ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (90.189485ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (205.965036ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (750.348411ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.062727ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (93.365592ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.15487ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (5.338151ms)
|
||||
✔ 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.059484ms)
|
||||
✔ 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.510661ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.441441ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (24.468164ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.05492ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (37.2506ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (140.914302ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (93.001566ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.461268ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.125916ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.517775ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (2.349255ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (54.030277ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (76.603912ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (48.691131ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (69.559903ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (677.891149ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.775261ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (2.536385ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.649066ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.9198ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (1.623488ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (8.179491ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.784261ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (2.231102ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.557681ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (30.3176ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (13.543188ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (8.137416ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (5.128703ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.730113ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.363505ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1015.162646ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.435139ms)
|
||||
✔ 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.05067ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.289677ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.213853ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.306527ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.425677ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.137777ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.454504ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (5.649069ms)
|
||||
✔ tools: listing and search caps hold (16.812451ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (1.003642ms)
|
||||
✔ 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 (14.770143ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (2.880692ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.383946ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (6.54899ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.665508ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (2.289948ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.663688ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.410641ms)
|
||||
✔ 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.730376ms)
|
||||
✔ 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.362198ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.237061ms)
|
||||
✔ 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.700723ms)
|
||||
✔ 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.18647ms)
|
||||
ℹ tests 238
|
||||
ℹ suites 0
|
||||
ℹ pass 237
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 2879.095421
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/host.test.mjs:152:1
|
||||
✖ a second host for the same data root refuses with exit 3 while the first runs (111.225187ms)
|
||||
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:162: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:162: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,267 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (32.497516ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (38.490636ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (22.348175ms)
|
||||
✔ decide prints a declining choice as declining (25.112741ms)
|
||||
✔ an unknown outcome is reported once and never resent (16.833861ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (18.720274ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (24.391965ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (20.968235ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (28.704553ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (25.449941ms)
|
||||
✔ agents and tasks print through the broker (19.448441ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (4.766765ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (82.34974ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (62.294674ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (64.754545ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (65.063425ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (65.586983ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (83.279826ms)
|
||||
✔ empty views say so (1.123361ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.401426ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.228532ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (1005.096231ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (170.444587ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (79.564099ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (135.196089ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (135.385998ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (85.752433ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (20.791112ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.761499ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (215.712543ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (97.574356ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (600.989063ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (29.199158ms)
|
||||
✔ zoned uses the IANA zone across DST (29.133529ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (28.221362ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (19.755433ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.38697ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (23.035949ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (27.099832ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (28.639538ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (19.363766ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (26.599492ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (20.656503ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (1.349527ms)
|
||||
✔ no Discord id reaches the journal or the log (23.631117ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (6.13041ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (4.916627ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (4.805046ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.530562ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (1.03679ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (5.514615ms)
|
||||
✖ the journal: a symlinked directory refuses and says it is a link (1.483327ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (1.045902ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.93ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.485367ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (114.590631ms)
|
||||
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 (391.986621ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (61.511217ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2280.842611ms)
|
||||
✔ busExit and refuseInsideAgent (0.412574ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.392105ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.506385ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.552264ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.67579ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (19.072074ms)
|
||||
✔ 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.042445ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (5.538362ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (3.283797ms)
|
||||
✔ authorize: open channel, listed user (1.847162ms)
|
||||
✔ authorize: wrong guild (0.218365ms)
|
||||
✔ authorize: no guild (DM) (0.168432ms)
|
||||
✔ authorize: unlisted channel (0.177956ms)
|
||||
✔ authorize: unknown channel, no info (0.250932ms)
|
||||
✔ authorize: thread of listed parent (0.173952ms)
|
||||
✔ authorize: thread of unlisted parent (0.13666ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.119261ms)
|
||||
✔ authorize: unlisted user (1.081999ms)
|
||||
✔ authorize: no author (0.268925ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.143329ms)
|
||||
✔ authorize: system author (0.11497ms)
|
||||
✔ authorize: the bot itself (0.084262ms)
|
||||
✔ authorize: webhook (0.820073ms)
|
||||
✔ authorize: mention channel without mention (0.228981ms)
|
||||
✔ authorize: mention channel with bot mention (0.177931ms)
|
||||
✔ authorize: mention channel with @everyone only (0.131955ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.094613ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.0935ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.094602ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.073194ms)
|
||||
✔ authorize: thread in another guild per channel info (0.085739ms)
|
||||
✔ authorize: not an object (0.067934ms)
|
||||
✔ authorize: no id (0.061094ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.067229ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.060583ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.472367ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.232552ms)
|
||||
✔ binding: a complete binding validates and is frozen (3.994609ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.312814ms)
|
||||
✔ binding: empty allowlists refuse (0.502973ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (2.147937ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.866292ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.485903ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (2.473028ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.485027ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (121.040848ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (3.351775ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (401.592756ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (215.340496ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (156.249019ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.735866ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.390694ms)
|
||||
✔ 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.052854ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (18.885672ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.695996ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.244777ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.60155ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.759148ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (33.157215ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.739007ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.393344ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.575209ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.946899ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.869704ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (9.898533ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (7.303815ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (2.358421ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (5.81992ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.336127ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.794696ms)
|
||||
✔ 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.816366ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (3.416879ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (6.680609ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (3.563811ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (8.724542ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.568491ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.794968ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.817309ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.237381ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.458214ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.217341ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.846261ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.761161ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.415329ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (60.717944ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (66.591593ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (61.088315ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (373.386515ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (231.358721ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (104.70835ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (229.015204ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (129.701226ms)
|
||||
✔ 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.205773ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (613.943876ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.676499ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.739784ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (431.500734ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (23.789849ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (45.123881ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (3.39225ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.770806ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.526266ms)
|
||||
✔ gateway: op 9 resumable resumes (0.400527ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (1.187042ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.566444ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.389055ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (78.425922ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (73.233119ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (134.921767ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.458224ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (95.532088ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (92.877385ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (94.779166ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (223.826431ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (81.807641ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (97.250815ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (220.327915ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (778.460728ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.025221ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (103.652162ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (2.057759ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.155145ms)
|
||||
✔ 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.549571ms)
|
||||
✔ 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 (356.19483ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.431217ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (25.574638ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.023751ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (47.524864ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (156.33667ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (97.717546ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.468234ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.166868ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.372897ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (2.206314ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (59.414739ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (58.956985ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (45.250912ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (75.615727ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (717.310307ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.464619ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (5.274342ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.695432ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.890491ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.868787ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (6.110535ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.340765ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (2.564966ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.705828ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (33.318628ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (12.70041ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (9.285182ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (5.005852ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.654759ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (4.920918ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1014.795295ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.516392ms)
|
||||
✔ 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.841094ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.316684ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.224161ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.744763ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.75266ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (3.761228ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.252323ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (6.189425ms)
|
||||
✔ tools: listing and search caps hold (13.027554ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.97548ms)
|
||||
✔ 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 (15.022162ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.21086ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.150493ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.336441ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.766637ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.310026ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (2.594078ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (1.551847ms)
|
||||
✔ 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.874141ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (5.101597ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.273801ms)
|
||||
✔ 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.786524ms)
|
||||
✔ 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.41871ms)
|
||||
ℹ tests 238
|
||||
ℹ suites 0
|
||||
ℹ pass 237
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 2907.988628
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:334:1
|
||||
✖ the journal: a symlinked directory refuses and says it is a link (1.483327ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception.
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:341: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,267 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (37.62348ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (36.330508ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (21.591937ms)
|
||||
✔ decide prints a declining choice as declining (23.299535ms)
|
||||
✔ an unknown outcome is reported once and never resent (18.967804ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (16.245994ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (20.615078ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (15.437281ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (17.523778ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (22.848108ms)
|
||||
✔ agents and tasks print through the broker (19.133473ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.943469ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (96.799246ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (63.956337ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (54.801908ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (58.600581ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (59.696237ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (79.030136ms)
|
||||
✔ empty views say so (1.11215ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (2.916989ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.265947ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (976.619208ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (151.092001ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (77.927265ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (133.724603ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (123.744114ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (73.12753ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (22.420373ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.241047ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (211.416291ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (84.459427ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (594.299415ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (27.094002ms)
|
||||
✔ zoned uses the IANA zone across DST (36.577938ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (27.687007ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (18.826189ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.310685ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (22.970691ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (25.109136ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (29.60365ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (18.134701ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (23.996036ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (18.358179ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (1.689308ms)
|
||||
✔ no Discord id reaches the journal or the log (23.621239ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (4.47762ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (5.047736ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (2.882032ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.096862ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (1.014103ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (4.215527ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.71468ms)
|
||||
✖ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (1.537015ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.864733ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.502465ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (112.589099ms)
|
||||
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.374374ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (77.358525ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2263.519215ms)
|
||||
✔ busExit and refuseInsideAgent (0.410452ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.516223ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.806412ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.678347ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.497458ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (20.764413ms)
|
||||
✔ 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.697163ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (7.590229ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.884584ms)
|
||||
✔ authorize: open channel, listed user (2.061523ms)
|
||||
✔ authorize: wrong guild (0.225839ms)
|
||||
✔ authorize: no guild (DM) (0.201114ms)
|
||||
✔ authorize: unlisted channel (0.206065ms)
|
||||
✔ authorize: unknown channel, no info (0.371693ms)
|
||||
✔ authorize: thread of listed parent (0.235539ms)
|
||||
✔ authorize: thread of unlisted parent (0.170268ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.173029ms)
|
||||
✔ authorize: unlisted user (1.087572ms)
|
||||
✔ authorize: no author (0.281563ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.142261ms)
|
||||
✔ authorize: system author (0.129558ms)
|
||||
✔ authorize: the bot itself (0.096026ms)
|
||||
✔ authorize: webhook (0.097146ms)
|
||||
✔ authorize: mention channel without mention (0.151605ms)
|
||||
✔ authorize: mention channel with bot mention (1.567799ms)
|
||||
✔ authorize: mention channel with @everyone only (0.264562ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.095975ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.09995ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.087655ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.079765ms)
|
||||
✔ authorize: thread in another guild per channel info (0.081549ms)
|
||||
✔ authorize: not an object (0.11792ms)
|
||||
✔ authorize: no id (0.083436ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.079163ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.109768ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.782771ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.179764ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.7305ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.487718ms)
|
||||
✔ binding: empty allowlists refuse (0.401183ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (2.039909ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.860033ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.903012ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.799112ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.566052ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (118.862501ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (3.728597ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (423.35948ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (193.752673ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (139.745577ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.700128ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.444438ms)
|
||||
✔ 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.494236ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (17.034495ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.618282ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.302365ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.375626ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.988243ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (31.984818ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (8.399466ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.533545ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.150002ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.352814ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.989386ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (6.480094ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (4.810415ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.110791ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (6.953426ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.886858ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (1.297211ms)
|
||||
✔ 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.774057ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (2.92225ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (5.115612ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (1.700801ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (4.222401ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.6176ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.907311ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.03687ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.248315ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.437978ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.250263ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.70542ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.513656ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.414881ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (58.452827ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (46.986646ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (56.202887ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (353.391748ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (240.490058ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (103.348945ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (226.629586ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (128.826032ms)
|
||||
✔ 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.677517ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.7834ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.26268ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.477141ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (428.808613ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (27.746039ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (47.038759ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.606578ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.484505ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.528046ms)
|
||||
✔ gateway: op 9 resumable resumes (0.410331ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.817155ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.59244ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.502721ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (80.155593ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (65.030371ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (133.600924ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.297167ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (83.166698ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (122.379633ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (101.207743ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (205.456961ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (67.527591ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (84.631693ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (201.859418ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (742.544703ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.80267ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (105.394238ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.116147ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (3.894657ms)
|
||||
✔ 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.339535ms)
|
||||
✔ 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 (366.206588ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.456522ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (29.822214ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.018322ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (38.724707ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (147.26956ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (91.53557ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.835869ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (6.027111ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.16864ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (2.055091ms)
|
||||
✔ 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.906163ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (53.68158ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (43.311643ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (77.98091ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (690.211689ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.520586ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (2.255302ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (1.225499ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.666303ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.750255ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.891019ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.554894ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.889594ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.771522ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (27.753054ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (13.601938ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (7.59235ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.046066ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.683679ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.245253ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1011.611258ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.490322ms)
|
||||
✔ 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.494853ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.27141ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.197583ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.292227ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (5.39002ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (3.380898ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (4.672722ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (3.541939ms)
|
||||
✔ tools: listing and search caps hold (11.574441ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.744493ms)
|
||||
✔ 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.327012ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.465082ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.355536ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.911487ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.034439ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.851761ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.402914ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.2859ms)
|
||||
✔ 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.116536ms)
|
||||
✔ 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.091156ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.21751ms)
|
||||
✔ 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.574708ms)
|
||||
✔ 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.640424ms)
|
||||
ℹ tests 238
|
||||
ℹ suites 0
|
||||
ℹ pass 237
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 2821.667216
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/notifier.test.mjs:345:1
|
||||
✖ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (1.537015ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception.
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/cli/tests/notifier.test.mjs:353: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,276 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (33.698397ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (36.841202ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (30.223846ms)
|
||||
✔ decide prints a declining choice as declining (18.760355ms)
|
||||
✔ an unknown outcome is reported once and never resent (17.149784ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (15.405241ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (16.059021ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (14.957888ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (18.658204ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (24.951316ms)
|
||||
✔ agents and tasks print through the broker (14.561643ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.996325ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (78.049724ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (75.145974ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (58.085297ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (56.810686ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (58.3762ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (72.918546ms)
|
||||
✔ empty views say so (1.855533ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (3.044737ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.264543ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (971.936253ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (151.966076ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (77.504917ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (127.321816ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (128.421661ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (79.102493ms)
|
||||
✖ watchChildren reports a child that died before it was called, and one that dies later (24.138792ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (203.202369ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (209.502057ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (89.201267ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (597.229223ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (26.007096ms)
|
||||
✔ zoned uses the IANA zone across DST (32.200346ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (37.885759ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (20.714665ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.349474ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (19.344421ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (23.512152ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (33.230831ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (20.072905ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (21.590726ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (16.781686ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (1.204984ms)
|
||||
✔ no Discord id reaches the journal or the log (24.213213ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (6.514027ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (3.648473ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (6.572512ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (2.266636ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (1.08847ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (4.51419ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.624272ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.697156ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.712743ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.539437ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.889547ms)
|
||||
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.03397ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (63.072985ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2249.669472ms)
|
||||
✔ busExit and refuseInsideAgent (0.524671ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.496627ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.638591ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.605402ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.497791ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (16.786467ms)
|
||||
✔ 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.169753ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (6.224176ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.737755ms)
|
||||
✔ authorize: open channel, listed user (2.570321ms)
|
||||
✔ authorize: wrong guild (0.202796ms)
|
||||
✔ authorize: no guild (DM) (0.16124ms)
|
||||
✔ authorize: unlisted channel (0.17353ms)
|
||||
✔ authorize: unknown channel, no info (0.15263ms)
|
||||
✔ authorize: thread of listed parent (0.174787ms)
|
||||
✔ authorize: thread of unlisted parent (0.139129ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.157015ms)
|
||||
✔ authorize: unlisted user (0.305344ms)
|
||||
✔ authorize: no author (0.293006ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.174231ms)
|
||||
✔ authorize: system author (0.103008ms)
|
||||
✔ authorize: the bot itself (0.105989ms)
|
||||
✔ authorize: webhook (0.091704ms)
|
||||
✔ authorize: mention channel without mention (0.148552ms)
|
||||
✔ authorize: mention channel with bot mention (0.1454ms)
|
||||
✔ authorize: mention channel with @everyone only (0.095023ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.072051ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.081847ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.126752ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.05011ms)
|
||||
✔ authorize: thread in another guild per channel info (0.069016ms)
|
||||
✔ authorize: not an object (0.086579ms)
|
||||
✔ authorize: no id (0.076186ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.093252ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.08954ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.522513ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.160767ms)
|
||||
✔ binding: a complete binding validates and is frozen (3.141303ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.372644ms)
|
||||
✔ binding: empty allowlists refuse (0.38767ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.348349ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.628404ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.492059ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.485084ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.374793ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (113.203348ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (2.493414ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (372.512ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (190.096457ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (130.711156ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.873882ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.239069ms)
|
||||
✔ 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.10929ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (19.270969ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.516459ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.354129ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.370236ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.613709ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (33.117218ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (4.306551ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.258267ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.604637ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.000634ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.68049ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (6.532455ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (4.705783ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.892775ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (5.957931ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.168583ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.739148ms)
|
||||
✔ 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.068217ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (7.71617ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (5.561091ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.812384ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (5.16763ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (5.604324ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.887618ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.745169ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.173496ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.532875ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.349165ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.69471ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.791861ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.453761ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (63.66679ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (63.472529ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (42.928123ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (354.43756ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (231.045881ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (114.797967ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (228.330023ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (128.011125ms)
|
||||
✔ 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.44806ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.749173ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.34552ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.500854ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (429.023654ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (24.483632ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (44.478498ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.989541ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.748031ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.570047ms)
|
||||
✔ gateway: op 9 resumable resumes (0.388531ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.819944ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.523874ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.498819ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (75.23357ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (62.835869ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (134.439208ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.446574ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (92.543917ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (84.475504ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (84.667593ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (194.07553ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (69.012642ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (88.604739ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (209.101826ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (765.044736ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.389801ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (89.907033ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.33035ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (3.600608ms)
|
||||
✔ 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.296931ms)
|
||||
✔ 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 (320.311854ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.37093ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (23.153703ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.033232ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (37.561655ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (129.223703ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (87.15181ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.441976ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.782946ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.4048ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.788619ms)
|
||||
✔ 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.919227ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (52.521144ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (46.435694ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (78.720187ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (664.423592ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.696425ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (4.969241ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.746828ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (1.286921ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.907063ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.505028ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (2.840352ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.61937ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.090374ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (28.921152ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (11.595973ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (7.326657ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (3.873523ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.671494ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.574564ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1016.194318ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.44458ms)
|
||||
✔ 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.085035ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.277589ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.195026ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.700804ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (3.72362ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.663037ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.9241ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (4.293996ms)
|
||||
✔ tools: listing and search caps hold (10.530074ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.895669ms)
|
||||
✔ 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.489273ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.284852ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.737902ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.967897ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.318822ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.885589ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (4.192573ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.367844ms)
|
||||
✔ 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.512721ms)
|
||||
✔ 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.213528ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.242007ms)
|
||||
✔ 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.720933ms)
|
||||
✔ 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.410981ms)
|
||||
ℹ tests 238
|
||||
ℹ suites 0
|
||||
ℹ pass 237
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 2832.895398
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/host.test.mjs:232:1
|
||||
✖ watchChildren reports a child that died before it was called, and one that dies later (24.138792ms)
|
||||
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:241: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,18 @@
|
||||
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 1, cancelled 0) ✖ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count;
|
||||
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 1, cancelled 0) ✖ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count;
|
||||
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;
|
||||
@@ -0,0 +1,75 @@
|
||||
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (26.846644ms)
|
||||
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (24.410589ms)
|
||||
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (27.415485ms)
|
||||
✔ launch events require a human CLI capability; generic emit cannot forge authority events (17.322612ms)
|
||||
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (15.214254ms)
|
||||
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (14.265276ms)
|
||||
✔ task action subjects and linked decision trail are complete and ordered (19.069327ms)
|
||||
✔ launch binding is durable and reconnecting requires the identical trusted record (7.77888ms)
|
||||
✔ business isolation includes inherited object names and cross-business message references (14.751071ms)
|
||||
✔ authority never transfers between action, run, target, unresolved or replaced role holder (18.821953ms)
|
||||
✔ task projection uses schema current view, skipping earlier and equal-start polls (9.51298ms)
|
||||
✔ revocation permanently bars the old run from reclaiming first, including after broker restart (16.665953ms)
|
||||
✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (10.381636ms)
|
||||
✔ both arbiters require human resolution when their cross-role route is themselves (16.060798ms)
|
||||
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (2.260303ms)
|
||||
✔ only validated broker references load; returned data and exceptions cannot expose a known token (4.662176ms)
|
||||
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (2.274841ms)
|
||||
✔ expiry refuses use and env references never become client data (0.846181ms)
|
||||
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.886071ms)
|
||||
✔ opaque tokens shorter than 16 characters refuse before use (0.380255ms)
|
||||
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (3.142096ms)
|
||||
✔ process reader gets own kernel identity without exposing environment values (0.587722ms)
|
||||
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (0.90547ms)
|
||||
✔ real pid 1 remains inspectable when its environment is protected (0.332779ms)
|
||||
✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (26.991803ms)
|
||||
✔ missing and foreign-business citations refuse and roll back message and grant (17.104311ms)
|
||||
✔ cross-role sends still need a matching resolved decision and consume it once (24.686477ms)
|
||||
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (71.175386ms)
|
||||
✔ startup token refusal returns safe code without value or partial listening broker (40.729917ms)
|
||||
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (54.183712ms)
|
||||
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (78.772711ms)
|
||||
✔ trusted host registers later launches; socket clients never have a registration verb (45.867398ms)
|
||||
✔ runtime excludes declared project roots even when host supplies no repoRoots (32.232364ms)
|
||||
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (48.104217ms)
|
||||
✔ v3b prototype refusals, views and append-only mutations (81.348764ms)
|
||||
✔ gated approval authorizes once, survives store reopen, and fresh approval works (29.934609ms)
|
||||
✔ another run cannot consume an approval; a failed check leaves it usable (20.555999ms)
|
||||
✔ two scheduled callers have exactly one grant and one consumed refusal (15.996108ms)
|
||||
✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (28.92796ms)
|
||||
✔ class drift gated to cross-role refuses before consumption (18.925789ms)
|
||||
✔ class drift cross-role to gated refuses before consumption (15.067399ms)
|
||||
✔ class drift gated to within-role refuses before consumption (17.375127ms)
|
||||
✔ class drift cross-role to within-role refuses before consumption (15.892017ms)
|
||||
✔ class drift within-role to gated refuses before consumption (14.067652ms)
|
||||
✔ class drift within-role to cross-role refuses before consumption (14.605399ms)
|
||||
✔ message.send consumes approval and prevents a later send or authorize (16.732989ms)
|
||||
✔ role.revoke consumes approval and prevents a later revoke or authorize (17.644261ms)
|
||||
✔ creates private WAL store and excludes a second writer until explicit close (16.034473ms)
|
||||
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (17.001735ms)
|
||||
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (16.692012ms)
|
||||
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (45.916215ms)
|
||||
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (9.472638ms)
|
||||
✔ async transactions refuse before invoking their function (6.980829ms)
|
||||
✔ recordTask keeps sync reads and a role write apart (20.710101ms)
|
||||
✔ read_at must be one canonical UTC format, so the projection compares strings safely (10.109562ms)
|
||||
✔ a bad entry refuses the whole record (10.378294ms)
|
||||
✔ taskView reads the projection for one business (13.335375ms)
|
||||
✔ requestTask hands only a holder and a task verb to the handler, and records refusals (22.935167ms)
|
||||
✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (275.637672ms)
|
||||
✔ without an adapter the server refuses every task verb (16.580658ms)
|
||||
✔ the runtime refuses an invalid adapter and closes a valid one (15.966457ms)
|
||||
✔ the process loads the S3 adapter from plain-data trackers (93.982671ms)
|
||||
✔ socket capability stamps launch identity; shared views use wire, no SQL client (27.361245ms)
|
||||
✔ two wire claims serialize; a lost reply never automatically retries (18.975306ms)
|
||||
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (11.699018ms)
|
||||
✔ client preserves UTF-8 when a response divides a multibyte character (10.659184ms)
|
||||
✔ committed mutation followed by dropped reply reports unknown and is never retried (14.223015ms)
|
||||
ℹ tests 67
|
||||
ℹ suites 0
|
||||
ℹ pass 67
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 589.325424
|
||||
@@ -0,0 +1,68 @@
|
||||
✔ config directory and file path follow MOSAIC_CONFIG (2.060554ms)
|
||||
✔ the fixture business validates and comes back frozen (8.891121ms)
|
||||
✔ two instances may share a definition (1.615478ms)
|
||||
✔ top-level refusals (10.211662ms)
|
||||
✔ arbiters and projects (6.388944ms)
|
||||
✔ role instances (6.610536ms)
|
||||
✔ Vikunja bots (5.670745ms)
|
||||
✔ a role without Vikunja takes no tracker block (1.943047ms)
|
||||
✔ credential references match the definition's services (2.256351ms)
|
||||
✔ launch (6.688948ms)
|
||||
✔ loadBusiness: file checks (1.677377ms)
|
||||
✔ loadBusiness: not a regular file (40.685368ms)
|
||||
✔ loading writes nothing (1.331501ms)
|
||||
✔ names that are Object.prototype properties don't count as declared (2.783854ms)
|
||||
✔ the shipped example refuses as written and validates once filled in (0.624939ms)
|
||||
✔ usage errors exit 4 (295.59984ms)
|
||||
✔ validate: a good business exits 0 and prints instance digests (67.837139ms)
|
||||
✔ validate: project files (318.727398ms)
|
||||
✔ validate: missing files and a broken system config (233.964049ms)
|
||||
✔ validate: credential reference problems exit 2 and name each one (69.456587ms)
|
||||
✔ validate: a token file inside the repository is refused (72.059109ms)
|
||||
✔ validate: role definitions come from MOSAIC_ROLES_DIR (202.693628ms)
|
||||
✔ resolve: prints one instance's record (193.75122ms)
|
||||
✔ resolve: refusals (413.820251ms)
|
||||
✔ parse: exactly one of file or env, plus the service's date (3.146733ms)
|
||||
✔ check: a good file has no problems (5.026882ms)
|
||||
✔ check never opens the file: a write-only token passes (0.429986ms)
|
||||
✔ check: file problems (1.106131ms)
|
||||
✔ check: token files can't live in the repository or dataRoot, even through a linked directory (1.067741ms)
|
||||
✔ check: dates and environment references (0.512385ms)
|
||||
✔ path and load (3.014255ms)
|
||||
✔ refusals (1.475493ms)
|
||||
✔ systemVars flattens the validated config (2.391181ms)
|
||||
✔ precedence: system, business, project, project role, agent (11.420701ms)
|
||||
✔ limits narrow the definition and never widen it (3.142722ms)
|
||||
✔ role.launch stays within-role only for the instance the launch block names (8.557034ms)
|
||||
✔ limits.authority without role.launch leaves the launcher with no launch block (1.83253ms)
|
||||
✔ limits.authority narrows cross-role actions too (0.973509ms)
|
||||
✔ classify (1.081899ms)
|
||||
✔ the record carries what the broker and launcher need (0.856808ms)
|
||||
✔ digest: key order doesn't matter, any value change does (5.508374ms)
|
||||
✔ refusals (2.123402ms)
|
||||
✔ the four shipped version 2 roles load (2.591051ms)
|
||||
✔ shipped role scopes match addendum B section 2 and the SR runbook (0.880223ms)
|
||||
✔ shipped authority follows the note's table (0.41691ms)
|
||||
✔ version 1 files keep loading with no authority (0.815806ms)
|
||||
✔ the conductor policy isn't a role (0.203812ms)
|
||||
✔ a missing role file is exit 4, a symbolic link too (0.343785ms)
|
||||
✔ version 2 refusals (1.111426ms)
|
||||
✔ authority: closed vocabulary, no gated-only action, no overlap (2.487968ms)
|
||||
✔ credentials: Gitea scopes (1.060952ms)
|
||||
✔ credentials: Vikunja scopes are a group-to-verbs map from the grantable list (1.140756ms)
|
||||
✔ credentials: services (4.276744ms)
|
||||
✔ contract: a non-empty regular Markdown file beside the role file (0.628106ms)
|
||||
✔ every key names known layers and a merge rule (0.751364ms)
|
||||
✔ unknown keys and wrong layers refuse (0.685442ms)
|
||||
✔ types (2.207498ms)
|
||||
✔ merge: defaults, then the most specific layer wins (0.320241ms)
|
||||
✔ merge: limits only narrow, and provenance lists each source (0.442349ms)
|
||||
✔ merge doesn't change its inputs (0.191325ms)
|
||||
ℹ tests 60
|
||||
ℹ suites 0
|
||||
ℹ pass 60
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 1939.00582
|
||||
@@ -0,0 +1,70 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (15.333744ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (19.761043ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (12.070932ms)
|
||||
✔ decide prints a declining choice as declining (14.791719ms)
|
||||
✔ an unknown outcome is reported once and never resent (9.838594ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (11.487185ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (10.776028ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (10.706493ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (10.87443ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (10.053197ms)
|
||||
✔ agents and tasks print through the broker (9.736823ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.266942ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (38.056701ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (37.348149ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (33.621208ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (35.945482ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (38.794152ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (59.97719ms)
|
||||
✔ empty views say so (0.668769ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.059245ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.196323ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (836.097185ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (143.632917ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (74.009683ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (133.931155ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (129.1185ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (71.509915ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (20.94978ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (203.12217ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (202.899358ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (94.924396ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (602.666947ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (35.723488ms)
|
||||
✔ zoned uses the IANA zone across DST (14.603844ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (16.983817ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (12.118336ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.180211ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (10.154915ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (15.424671ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (17.149256ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (11.014788ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (12.696482ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (10.612958ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.807809ms)
|
||||
✔ no Discord id reaches the journal or the log (14.019126ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.32609ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (3.073151ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.609898ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.619298ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.627086ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.528865ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.877939ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.490916ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.491892ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.261386ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (112.038558ms)
|
||||
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 (256.738911ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (30.628962ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2158.215151ms)
|
||||
✔ busExit and refuseInsideAgent (0.383363ms)
|
||||
ℹ tests 60
|
||||
ℹ suites 0
|
||||
ℹ pass 60
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 2648.404994
|
||||
@@ -0,0 +1,132 @@
|
||||
✔ explicit request, Seen, ordinary completion and a new request have distinct attention states (3.457901ms)
|
||||
✔ attention convention ignores reasoning/quoted examples and permits leading blank lines (0.210965ms)
|
||||
✔ completed smoke replies and ordinary questions are idle, not human blockers (1.090015ms)
|
||||
✔ only an explicit first-line input request makes a finished reply waiting (0.20662ms)
|
||||
✔ tool activity, user text, errors and unfinished turns override attention text (0.128362ms)
|
||||
✔ STOP access failure is unknown, not absence, under a non-root identity (34.802356ms)
|
||||
✔ connector Task never inherits Discord routing envelopes; ordinary Task still uses user text (3.350334ms)
|
||||
✔ connector discovery keeps only safe identity; rejects modes, mismatches, links and traversal (1.532551ms)
|
||||
✔ canonical owner identity and STOP are independent; no tmux fallback or forged registration (6.321534ms)
|
||||
✔ connector reply refusal precedes forged live tmux registration; ordinary agent still sends (0.520501ms)
|
||||
✔ server rescans connector discovery and refuses HTTP reply without transport (32.830631ms)
|
||||
✔ connector session links and linked directories are not read (1.109034ms)
|
||||
✔ newer live matching launch marks old activity, preserves history/attention/attribution, then clears on new activity (3.017821ms)
|
||||
✔ CLI print uses the relaunch notice instead of old current preview (54.849283ms)
|
||||
✔ connector owner and fixed task never inherit a native relaunch notice (2.375021ms)
|
||||
✔ equality, stale/unknown/offline, mismatched registration and unknown activity do not assert relaunch (1.041907ms)
|
||||
✔ loadConfig: missing file throws ConfigError (1.40259ms)
|
||||
✔ loadConfig: invalid JSON throws ConfigError (0.273634ms)
|
||||
✔ loadConfig: missing dataRoot throws ConfigError (0.208746ms)
|
||||
✔ loadConfig: relative dataRoot throws ConfigError (0.194581ms)
|
||||
✔ loadConfig: valid config returns dataRoot (0.616688ms)
|
||||
✔ findNewestSession: picks the newest by mtime among two files (0.4533ms)
|
||||
✔ findNewestSession: finds files in nested subdirectories (0.294144ms)
|
||||
✔ findNewestSession: returns null for a missing dir (0.117402ms)
|
||||
✔ readSession: extracts fields, collapses/truncates text, counts a truncated final line (0.616183ms)
|
||||
✔ readSession: model and provider follow the latest model_change entry or assistant turn; null when the log names neither; scanAgent carries them (1.143359ms)
|
||||
✔ readSession: lastError carries the assistant errorMessage only when the last assistant turn errored (0.445338ms)
|
||||
✔ findNewestSession/scan: never read sibling auth or secrets next to a sessions dir (1.023221ms)
|
||||
✔ deriveState: full state table (0.161249ms)
|
||||
✔ rule: newest entry is an assistant message with a tool call, after a question-looking text, is working (0.287196ms)
|
||||
✔ rule: newest entry is a tool result with no assistant text after it is working (0.26169ms)
|
||||
✔ rule: a finished ordinary turn is idle, even if it says your move (0.243287ms)
|
||||
✔ task: the first user message of the session, from text blocks (0.239048ms)
|
||||
✔ task: a plain-string user content is accepted, whitespace collapsed and long text capped (0.227896ms)
|
||||
✔ task: no user message in the log means null (shown as unknown), never a guess (0.250213ms)
|
||||
✔ workspace: the live tmux pane path wins; the session cwd is the fallback; neither means null (0.346582ms)
|
||||
✔ activeProject: basename of the nearest .git directory or .git file above the workspace; none means null (0.465724ms)
|
||||
✔ scan: the written record carries task, workspace and activeProject (0.43714ms)
|
||||
✔ registration: overrides task, project and workspace; every source says registration; registered carries the launch fields; the grouping column is untouched (0.538162ms)
|
||||
✔ registration: empty task and null project/workspace leave the derived values in place; registered is still non-null (0.366495ms)
|
||||
✔ 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.92943ms)
|
||||
✔ registration: no registration leaves the Gate A fields exactly as before, and registered is null (0.384102ms)
|
||||
✔ loadRegistrations: a missing seatsDir gives empty lists (0.157807ms)
|
||||
✔ loadRegistrations: one good record, one malformed JSON, one with an unknown field; a stray file under seatsDir is ignored (0.676201ms)
|
||||
✔ 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.301293ms)
|
||||
✔ scan: writes the registration override to disk; index.json carries registered and registrationErrors (0.64436ms)
|
||||
✔ scan: a relative seatsDir throws ConfigError; an omitted seatsDir behaves as before (0.767931ms)
|
||||
✔ scanAgent: waitingOnYou is true for waiting/error and false otherwise (0.501275ms)
|
||||
✔ scanAgent: ageSeconds is computed from the injected now (0.252702ms)
|
||||
✔ scanAgent: sessionFile null and state idle when sessions dir is empty but alive (0.145448ms)
|
||||
✔ discoverRepoAgents: finds agents with a sessions dir, skips those without, sorted by name (0.315535ms)
|
||||
✔ discoverFleetAgents: finds agents with a sessions dir, sorted by name, fleet tmux fields (0.501566ms)
|
||||
✔ scan: writes per-agent files and index.json, rerun overwrites, no leftover tmp files (0.895728ms)
|
||||
✔ scan: relative boardDir throws ConfigError (0.092901ms)
|
||||
✔ CLI: scan with assume-alive liveness exits 0, prints board summary, writes board files (53.690492ms)
|
||||
✔ CLI: missing config exits 2 with a refused: message (51.39965ms)
|
||||
✔ CLI: unknown command exits 2 (49.844823ms)
|
||||
✔ CLI: unknown --liveness value exits 2 (47.375608ms)
|
||||
✔ panesRunPi: true when any trimmed line equals 'pi' (0.179214ms)
|
||||
✔ panesRunPi: false for bash-only, claude, empty, or node-pi-style lines (0.063738ms)
|
||||
✔ tmuxIsAlive: a pane running pi is alive (0.156185ms)
|
||||
✔ tmuxIsAlive: session exists but pi has exited is not alive (0.108197ms)
|
||||
✔ tmuxIsAlive: no such tmux session is not alive (0.052732ms)
|
||||
✔ tmuxIsAlive: tmux could not be run at all is unknown (null), never assumed alive (0.060265ms)
|
||||
✔ tmuxIsAlive: passes -L <socket> only when a socket is given (0.089151ms)
|
||||
✔ parsePanes: one pane per line, command and optional tab-separated path (0.071838ms)
|
||||
✔ tmuxInspect: reports the path of the pane running pi, not of a shell pane (0.07376ms)
|
||||
✔ tmuxInspect: no pi pane, no session, or no tmux gives no workspace and the matching liveness (0.089511ms)
|
||||
✔ loadSeen: missing file returns {} (0.12806ms)
|
||||
✔ loadSeen: invalid JSON throws ConfigError (0.171159ms)
|
||||
✔ loadSeen: a JSON array throws ConfigError (0.14116ms)
|
||||
✔ loadSeen: a non-string value throws ConfigError (0.150134ms)
|
||||
✔ markSeen: seen true adds the key and writes seen.json mode 0600, no leftover tmp files (0.318986ms)
|
||||
✔ markSeen: seen false deletes the key (0.241303ms)
|
||||
✔ markSeen: missing, empty, or non-string fields throw ConfigError (0.184977ms)
|
||||
✔ markSeen: project containing '/' throws ConfigError (0.118837ms)
|
||||
✔ markSeen: non-boolean seen throws ConfigError (0.114032ms)
|
||||
✔ scanAgent: a seen mark matching the waiting session's lastTimestamp clears waitingOnYou (0.268492ms)
|
||||
✔ scanAgent: a stale mark (agent wrote something newer) is not seen and waitingOnYou is true (0.214726ms)
|
||||
✔ scanAgent: a working session with a matching mark is not seen (marks only apply to waiting/error) (0.218063ms)
|
||||
✔ scanAgent: an error-state session with a matching mark is seen (0.195255ms)
|
||||
✔ scan: index.seen and waitingOnYou reflect seen.json, which scan never rewrites or deletes (0.486331ms)
|
||||
✔ scan: a corrupt seen.json makes scan throw ConfigError (fail closed) (0.172043ms)
|
||||
✔ 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.591856ms)
|
||||
✔ 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.534497ms)
|
||||
✔ 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 (0.941446ms)
|
||||
✔ isLoopbackHost: recognizes loopback hosts (1.157069ms)
|
||||
✔ isLoopbackHost: rejects non-loopback hosts (4.22175ms)
|
||||
✔ startServer: refuses a non-loopback host with ConfigError, never opens a socket (2.882772ms)
|
||||
✔ startServer: serves page, healthz, and a rescanning /api/board (33.329392ms)
|
||||
✔ startServer: a seatsDir registration overrides the row and index.registered reflects it (6.003934ms)
|
||||
✔ startServer: /api/board returns 500 JSON with an error field when scan throws (2.17292ms)
|
||||
✔ CLI: serve refuses a non-loopback host with exit 2 and a refused: message (51.945036ms)
|
||||
✔ CLI: serve rejects a non-numeric --port with exit 2 (47.723159ms)
|
||||
✔ CLI: scan still works after the async cli refactor (51.151701ms)
|
||||
✔ CLI: live serve prints its URL and answers /healthz (59.918321ms)
|
||||
✔ page.html: esc() escapes every HTML-significant character (0.585672ms)
|
||||
✔ POST /api/seen marks a row; GET /api/board still shows it seen; seen:false clears it (7.934282ms)
|
||||
✔ POST /api/seen without a JSON content-type returns 400 and does not write a mark (2.183499ms)
|
||||
✔ POST /api/seen with invalid JSON returns 400 (2.802761ms)
|
||||
✔ POST /api/seen with a body over 4096 bytes returns 400 (or resets the connection) and writes no mark (1.992493ms)
|
||||
✔ POST /api/seen with a missing agent returns 400 (1.324636ms)
|
||||
✔ POST /api/board returns 405; PUT /api/seen returns 405 (1.617933ms)
|
||||
✔ CLI: scan --print marks a seen row with 's' and the summary line ends with 'N seen)' (50.366821ms)
|
||||
✔ page.html: seenControl() escapes rec.project/agent/lastActivity, and the POST uses a JSON content-type (0.302553ms)
|
||||
✔ page.html: has a collapsed Seen section that lists seen rows with the shared row builder (0.261579ms)
|
||||
✔ page.html: each project has a Hide seen checkbox (default on) beside Hide offline, with a hidden-count note (0.159021ms)
|
||||
✔ page.html: a project header reads "N of N" only while a checkbox hides rows (0.125061ms)
|
||||
✔ page.html: every row shows Task and Active project, derived or the word unknown, with the workspace in the detail (0.315393ms)
|
||||
✔ 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.590488ms)
|
||||
✔ 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 (29.211381ms)
|
||||
✔ POST /api/reply: a registration with a tmux socket adds -L <socket> (24.744096ms)
|
||||
✔ POST /api/reply: a non-zero tool exit is a 200 with delivered false, the exit code and the stderr verbatim (25.396115ms)
|
||||
✔ 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 (13.805133ms)
|
||||
✔ POST /api/reply: a missing agent-send.sh is a 500 with the path in the error, not a crash (4.223438ms)
|
||||
✔ replyToRow: DEFAULT_AGENT_SEND is the repository's tools/tmux/agent-send.sh and it is executable (0.151448ms)
|
||||
✔ 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 (1.01866ms)
|
||||
✔ startServer: /api/board carries taskSetBy from a live registration and null for the derived rows (2.373716ms)
|
||||
✔ 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.306642ms)
|
||||
✔ 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.947275ms)
|
||||
✔ Host/Origin guard: loopback names on this port are accepted, with or without a same-origin Origin (28.619647ms)
|
||||
✔ 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.50907ms)
|
||||
✔ every refusal code the reader can raise has an HTTP status (0.90892ms)
|
||||
✔ conversation routes: catalogue, first page, next page and follow over HTTP; refusals map to 4xx with their code; nothing is written (47.93621ms)
|
||||
ℹ tests 124
|
||||
ℹ suites 0
|
||||
ℹ pass 124
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 607.404619
|
||||
@@ -0,0 +1,205 @@
|
||||
✔ W1: two processes acquire the same pair at once; exactly one claim (123.886324ms)
|
||||
✔ W1: two writers publish the same revision at once: one wins, the other gets null, the winner's record stays (7.755379ms)
|
||||
✔ W1: a revision name appears only after its bytes are synced; before that, only a temp file exists (3.65401ms)
|
||||
✔ W2: acquire while a claim is reserved or active refuses already-active (162.486357ms)
|
||||
✔ W3: acquire while stopping, uncertain, or stopped without proof refuses unsafe-replacement (272.008644ms)
|
||||
✔ W4: same session with another seat tuple, and the reverse, both refuse; a loser on the seat key closes it no-unit (194.387604ms)
|
||||
✔ W4: a hard link of one session under another seat is the same session: the second controller refuses already-active and launches nothing (20.97984ms)
|
||||
✔ W4: a copy of one session under another seat is the same session: the second controller refuses already-active and launches nothing (20.016183ms)
|
||||
✔ W4: a session header ID that changes after construction refuses target; nothing is claimed or launched (2.839286ms)
|
||||
✔ W5: SIGKILL between every publication barrier of acquire and transition; restart never finds two holders or a lost claim (5534.169545ms)
|
||||
✔ W5: SIGKILL between every publication barrier of release; restart finishes or holds the release (21929.381212ms)
|
||||
✔ W6: controller killed mid-turn while the engine lives; restart is uncertain, no launch, prompts refuse (154.511521ms)
|
||||
✔ W12: a live owner paused with SIGSTOP; a second controller refuses already-active and changes nothing (91.583066ms)
|
||||
✔ W13: crash after the engine spawns, before active; restart finds the live unit: uncertain, no second spawn, force stop only (220.322777ms)
|
||||
✔ W14: crash after reservation, before the spawn marker: stopped with a no-unit observation; the pair is free (192.230518ms)
|
||||
✔ W20: crash after the spawn marker, scope collected; uncertain in both runs, the marker is copied, no launch until a boot proof (212.053095ms)
|
||||
✔ W15: crash between the two keys during release; restart finishes it under the same claim ID (33.238494ms)
|
||||
✔ W7: recorded boot ID differs on the same machine: stopped with a boot proof; open tool calls become uncertain (96.007412ms)
|
||||
✔ W8: resume after a proven stop with the same pins: new claim ID, generation +1, same conversation, branch and leaf (32.189988ms)
|
||||
✔ W9: resume with a changed binary, argv digest, branch or leaf is refused and the claim is unchanged (88.899598ms)
|
||||
✔ W11: the controller writes no session file; only the fake engine's own appends appear (23.44816ms)
|
||||
✔ W16: a highest revision that won't parse holds the pair uncertain; the older stopped revision is not reused (57.959617ms)
|
||||
✔ W17: a claim root copied from another host refuses foreign-host and promotes nothing (51.394722ms)
|
||||
✔ G1: a session path or claim root under .pi/state, ~/.claude, the data root or a registration refuses at construction (3.469485ms)
|
||||
✔ G2: a symlink inside the fixture root to a live session file is refused by the real-path check (1.099651ms)
|
||||
✔ G3: a fixture path swapped for a live path after construction is refused at bind (1.759782ms)
|
||||
✖ K1: force stop kills a tool child that called setsid; stopped with a verified proof (2638.995605ms)
|
||||
✔ K2: K1 on the process-group fallback ends uncertain, never stopped (120.815989ms)
|
||||
✖ K3: SIGTERM acknowledged while a member lives: stopping until the kill phase, never stopped from TERM (2145.378907ms)
|
||||
✔ K4: two engines; force stop one; the other survives by independent observation (4276.305903ms)
|
||||
✔ K5: a stop during a tool call leaves the effect uncertain, and it is shown (2150.599749ms)
|
||||
✔ K12: a member forking in a loop: the freeze stops it, enumeration is complete, populated 0 after cgroup.kill (2238.71035ms)
|
||||
✔ K13: a member writing its pid into another cgroup is refused by the namespace; the kill is complete (2197.403591ms)
|
||||
✔ K15: the shim gone, engine/cgroup.events unreadable, or the engine cgroup missing: evidence unavailable, not empty; uncertain (4389.273083ms)
|
||||
✖ K10: controller killed between the TERM and kill phases: restart checks the invocation ID and re-runs from TERM for the same stop (220.95379ms)
|
||||
✔ K11: controller killed after the confirmation is recorded, before TERM: restart checks the invocation ID and re-runs from TERM for the same stop (305.662874ms)
|
||||
✔ K14: a unit with the recorded name but another invocation ID: evidence unavailable, no signals, uncertain (282.798305ms)
|
||||
✔ K6: recover without proof, without confirmation, or with changed pins is refused (64.911446ms)
|
||||
✔ K7: recover after proof, then launch: new claim and execution, generation +1, same leaf; the cancelled prompt is not replayed (30.698505ms)
|
||||
✔ K8: an engine that loads another leaf on resume is refused before admission; it stays claimed until a proven stop (41.472205ms)
|
||||
✔ K9: an interrupt that never settles stays uncertain; force stop stays available; takeover is refused while fenced (3026.575238ms)
|
||||
✔ K16: a claim from another machine ID refuses foreign-host; no boot proof is issued (5.079035ms)
|
||||
✔ K17: two launcher calls with one eligibility record: one launch, the other refuses, no second engine (29.75158ms)
|
||||
✔ K18: the leaf changes after eligibility: launch refused; the reservation stays until released with proof (25.798198ms)
|
||||
✔ S1: `/goal x`, with leading spaces or a tab, refuses text-policy at admission; zero engine bytes (35.971066ms)
|
||||
✔ S2: every prefix pinned Pi interprets is refused, from the list the code uses; the rest reach the engine exactly (29.491638ms)
|
||||
✔ S3: `/goal` on the second line is pinned from the source: Pi checks only index 0, so it is admitted and sent exactly (23.236826ms)
|
||||
✔ S4: a `/` left in the composer is cleared when control transfers and returns; the next submit sends only the new text (42.901852ms)
|
||||
✔ S5: an observer terminal gets a paste then Enter, as send-message.sh does: not admitted: controller, nothing sent (14.268623ms)
|
||||
✔ S6: a mediated-shaped registration (no tmux) passed to the board's replyToRow: 409 no tmux session; exec never runs (0.429962ms)
|
||||
✔ S7: ESC, bracketed-paste markers and U+2028/U+2029 travel as one JSON string; the engine receives the exact text in one record (17.207644ms)
|
||||
✔ P3: a Pi confirm, select, input or editor dialog is shown disabled with a reason and never answered (128.639854ms)
|
||||
✔ E1: send, ack, user, toolCall, toolResult, final answer: shown once, no refresh, draft and reading position kept (42.234223ms)
|
||||
✔ E2: U+2028, U+2029 inside JSON strings and CRLF line ends each parse as one record, on the splitter and through the controller (28.958148ms)
|
||||
✔ E3: a multipart final, two blocks, null request correlation and duplicate delivery (37.213287ms)
|
||||
✔ 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 (33.531476ms)
|
||||
✔ E4: a gap or a new epoch also reconciles; nothing is concatenated across a gap (11.822207ms)
|
||||
✔ E5: an unknown native event gives no client event; evidence records its type and bytes; the terminal count goes up (30.508636ms)
|
||||
✔ E6: a tool result delayed across a pause and a reconnect is reconciled without a manual refresh (49.795234ms)
|
||||
✔ E7: the terminal renders the same stream as the library client, as observer and then as controller, and submits only as controller (44.891328ms)
|
||||
✔ terminal: engine control characters are made visible; a lost connection refuses submit (27.506297ms)
|
||||
✔ terminal: outcome unknown is shown as such, with no resend offer, and nothing is resent (0.70488ms)
|
||||
✔ terminal: text after Enter in the same input chunk starts the next message; it never joins the one submitted (0.472935ms)
|
||||
✔ terminal: a paste-start marker split right after its ESC still opens the paste; the Enter inside it never submits (0.535934ms)
|
||||
✔ terminal: invisible and bidi characters are made visible; head, status and notice lines stay one line (0.205422ms)
|
||||
✔ every record these fixtures produced is a valid CHAT-01 record (E5: no record fails the schema) (357.924335ms)
|
||||
✔ H1: two takeovers with the same expected generation: one wins, +1; the other refuses generation (49.509042ms)
|
||||
✔ H2: the old controller's prompt after a takeover commits is refused with zero engine bytes (88.63243ms)
|
||||
✔ H3: a takeover while a prompt holds the dispatch lock: written under the old actor, or refused; never both (147.250773ms)
|
||||
✔ H4: self-takeover is refused (26.662033ms)
|
||||
✔ H9: Interrupt racing a prompt's dispatch: before the write, dispatch-refused and no-turn; after, §3 rules (114.174961ms)
|
||||
✔ H10: Interrupt and force stop together: one stop chain, force stop supersedes (97.942599ms)
|
||||
✔ H10: an overlap during the pause before the abort: no abort, the stop ends uncertain (27.642411ms)
|
||||
✔ H10: a no-turn Interrupt lifts only its own fence; admission stays closed under force stop, overlap or revocation (85.725198ms)
|
||||
✔ H11: the controller disconnects mid-turn: work continues, the claim is unchanged, control stays put (132.18795ms)
|
||||
✔ H12: an exact retry after reconnecting to the same incarnation returns the same receipt; one dispatch (18.297678ms)
|
||||
✔ H13: a retry with the same request ID and different text is refused (16.898281ms)
|
||||
✔ H14: late stdout from the old engine after a replacement is dropped by incarnation, counted, never rendered (136.257726ms)
|
||||
✔ H15: a revoked connection's command is refused; the revocation fence holds (68.025803ms)
|
||||
✔ H16: a second controller for the same session refuses already-active; the first is untouched (21.620776ms)
|
||||
✔ H10: a second force stop while the first escalation runs refuses fenced; one escalation, and the claim records only the first stop's phases (51.721388ms)
|
||||
✔ H17: a confirmation reused, answered from another connection, or used after the stop changed is refused (52.616189ms)
|
||||
✔ H18: two prompts before any native output: the second refuses busy; one engine write (13.050317ms)
|
||||
✔ H19: the pipe fails mid-line under a large prompt: delivery-unknown transport-unknown, poisoned, no later write (118.05931ms)
|
||||
✔ H19: the link itself never writes again after an unknown outcome, whoever calls it (0.661981ms)
|
||||
✔ H19: the controller dies mid-write of a large line: after restart the outcome is unknown and nothing is resent (476.876348ms)
|
||||
✔ H20: the line is written but the ack is lost when the controller dies: orphan, outcome unknown, nothing resent (376.160543ms)
|
||||
✔ H21: a retry of the exact request with the old token after a crash is stale-incarnation; no second write (521.879678ms)
|
||||
✔ H22: after H21 and a valid recovery, a new request with the new token is admitted (2407.944298ms)
|
||||
✔ H23: requests pending at a restart are not resent; each shows outcome unknown (488.034247ms)
|
||||
✔ a plain conversation: catalogue row, one page, CHAT-01 records (6.053202ms)
|
||||
✔ native entries map to blocks: tools, thinking, bash, notices, ids that do not fit (1.692911ms)
|
||||
✔ F1: a malformed line is an unavailable part at its position, and reading continues (1.637046ms)
|
||||
✔ F1: a missing parent stops the history with a notice that names the unreadable lines (2.520464ms)
|
||||
✔ F1: an unreadable fork is never merged into another branch's history (1.732369ms)
|
||||
✔ F1: a follow stays on its branch when the next entry's parent is unreadable (1.923983ms)
|
||||
✔ F1: a file whose entries are all unreadable shows a notice per line (0.781628ms)
|
||||
✔ F2: a truncated trailing line marks the view incomplete, not an error (1.230053ms)
|
||||
✔ pagination: 100 parts, then the rest; parts concatenate to the whole branch (3.406218ms)
|
||||
✔ F3: a replaced file (new inode) refuses old cursors with reconcile (4.175191ms)
|
||||
✔ F4: a same-inode rewrite of the prefix refuses old cursors with reconcile (5.089202ms)
|
||||
✔ F5: growth between pages keeps the epoch and the page stops at the pinned length (4.765007ms)
|
||||
✔ F6: unknown, foreign and expired cursors refuse and leave the cursor usable (8.003776ms)
|
||||
✔ F7: a symlinked file and a symlinked directory component are refused, never opened (8.186944ms)
|
||||
✔ F8: a file swapped for a symlink after the catalogue is refused (2.051135ms)
|
||||
✔ F9: registrations never add or redirect a root (1.51796ms)
|
||||
✔ F10: a header cwd naming another project is refused (3.391099ms)
|
||||
✔ F11: parentSession renders with a marker and the parent is never opened (0.749559ms)
|
||||
✔ F12: two leaves: the default leaf is shown and the other branch reads alone (6.036891ms)
|
||||
✔ F12: a follow refuses when an appended duplicate id changes the branch's earlier parts (2.771538ms)
|
||||
✔ F12: a second root (Pi's resetLeaf) starts its own branch (1.429035ms)
|
||||
✔ F13: compaction is a marker in place, then the retained content (0.789613ms)
|
||||
✔ F14: long strings split into fragments and parts, reassemble exactly, and pages respect the byte cap (720.732626ms)
|
||||
✔ fragments never cut a surrogate pair and keep an empty string (8.322828ms)
|
||||
✔ F15: a Claude seat is an unsupported-harness placeholder whose directory is never read (2.705224ms)
|
||||
✔ unknown conversations, empty files and non-Pi files refuse (3.996185ms)
|
||||
✔ an unreadable file or root inside the roots is refused per row, not a failed catalogue (1.771588ms)
|
||||
✔ a seat directory without search permission refuses that root, not the catalogue (3.562476ms)
|
||||
✔ every page and cursor is a valid CHAT-01 record (860.131884ms)
|
||||
✔ the engine pin holds for the installed package (1.755515ms)
|
||||
✔ pinned Pi, sealed and without credentials, answers the controller's commands with the shapes the fake models (310.30598ms)
|
||||
✔ pinned Pi appends thinking_level_change at start when the branch lacks one, so the leaf moves (K8 then fails closed) (388.783611ms)
|
||||
✔ N25: ordinary Interrupt reconciles; a non-empty queue_update in the window is O5 (72.457016ms)
|
||||
✔ N1: an extension's follow-up queued after the fence is cleared before any abort; O5, Unknown (58.388647ms)
|
||||
✔ N1: a follow-up queued before the fence is O5 at once; the Interrupt refuses fenced (19.920269ms)
|
||||
✔ N2: with abort first, the fake runs the external item (the ordering guard has teeth) (21.000044ms)
|
||||
✔ N3: the fence lands in preflight, preflight errors, no run: failed, No run, uncertain (35.20039ms)
|
||||
✔ N4: the ack arrives after the first abort and a run starts: clear and abort again; Interrupted (22.838822ms)
|
||||
✔ N5: an input handler takes the prompt: ack, no run, delivery-unknown handled-without-run (117.180697ms)
|
||||
✔ N6: an extension queues between clear_queue and abort: O5 and O6, Unknown (64.482426ms)
|
||||
✔ N7: clear_queue times out: no abort, nativeQueue unknown, force stop still ends it (1548.239144ms)
|
||||
✔ N7: clear_queue answers an error: no abort, nativeQueue unknown, the link not poisoned (15.635725ms)
|
||||
✔ N8: an extension prompt starts a run during Mosaic preflight; the losing settle is O3 (69.01426ms)
|
||||
✔ N9: a run that started before the fence and ends aborted: failed interrupted, Interrupted (14.559371ms)
|
||||
✔ N9: decision 34: a run that ends aborted with no stop in progress: aborted-without-stop, uncertain, outcome unknown (17.497072ms)
|
||||
✔ N9: an aborted that lands after the fence but before any abort is written: aborted-without-stop, Unknown (31.712737ms)
|
||||
✔ N10: fake conformance (32.449068ms)
|
||||
✔ N11: the run fails before any user message_start: delivery-unknown ack-without-start, never failed (28.329983ms)
|
||||
✔ N12: input that starts a run after the final empty clear is O1 and not part of the stop's proof (19.351161ms)
|
||||
✔ N13: agent_start with no slot held is O1; a later prompt refuses with zero engine bytes (64.655033ms)
|
||||
✔ N14: the run completes while clear_queue is in flight: finished, Completed first, uncertain (31.417017ms)
|
||||
✔ N14: the run completes after the abort is written, before Pi applies it: finished, never relabelled (28.416734ms)
|
||||
✔ N15: the fence lands in preflight, then an input handler takes it: handled-without-run, No run (23.332338ms)
|
||||
✔ N16: Interrupt with no slot and no run refuses no-turn: no stop, no bytes, admission open (13.235099ms)
|
||||
✔ N17: the run fails on its own during the exchange: failed, Failed on its own (30.043722ms)
|
||||
✔ N18: no final assistant message_end, or a lost line: working stays working; before working, transport-unknown (109.473027ms)
|
||||
✔ N19: a losing extension prompt settles inside the Mosaic run before its user message: O3, run-overlap (133.702413ms)
|
||||
✔ N20: an extension triggerTurn during Mosaic preflight starts first; while streaming it queues with no signal (82.943002ms)
|
||||
✔ N21: a losing settle after the receipt settled finished is O2; the receipt stays finished (15.065652ms)
|
||||
✔ N22: an agent-level custom message is dropped by the clear with no signal; evidence names the seal (13.360129ms)
|
||||
✔ N23: a nextTurn message survives clear and abort and attaches to the next prompt, with no signal (18.561592ms)
|
||||
✔ 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.711816ms)
|
||||
ℹ tests 152
|
||||
ℹ suites 0
|
||||
ℹ pass 149
|
||||
ℹ fail 3
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 31544.713442
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/conversation/tests/cohort.test.mjs:139:1
|
||||
✖ K1: force stop kills a tool child that called setsid; stopped with a verified proof (2638.995605ms)
|
||||
AssertionError [ERR_ASSERTION]: the escaped child is a listed member
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/conversation/tests/cohort.test.mjs:151:12)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: false,
|
||||
expected: true,
|
||||
operator: '==',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/conversation/tests/cohort.test.mjs:176:1
|
||||
✖ K3: SIGTERM acknowledged while a member lives: stopping until the kill phase, never stopped from TERM (2145.378907ms)
|
||||
AssertionError [ERR_ASSERTION]: the member ignored TERM
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/conversation/tests/cohort.test.mjs:190:12)
|
||||
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: '==',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/conversation/tests/cohort.test.mjs:426:3
|
||||
✖ K10: controller killed between the TERM and kill phases: restart checks the invocation ID and re-runs from TERM for the same stop (220.95379ms)
|
||||
AssertionError [ERR_ASSERTION]: the member is alive across the crash
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r45/tree/packages/conversation/tests/cohort.test.mjs:431:12)
|
||||
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: false,
|
||||
expected: true,
|
||||
operator: '==',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.048365ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.509589ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.567279ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.427021ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (16.461407ms)
|
||||
✔ 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.718488ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (7.792264ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.974669ms)
|
||||
✔ authorize: open channel, listed user (2.583069ms)
|
||||
✔ authorize: wrong guild (0.195464ms)
|
||||
✔ authorize: no guild (DM) (0.335835ms)
|
||||
✔ authorize: unlisted channel (0.194541ms)
|
||||
✔ authorize: unknown channel, no info (0.295776ms)
|
||||
✔ authorize: thread of listed parent (0.187446ms)
|
||||
✔ authorize: thread of unlisted parent (0.249749ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.14337ms)
|
||||
✔ authorize: unlisted user (1.063207ms)
|
||||
✔ authorize: no author (0.607158ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.141104ms)
|
||||
✔ authorize: system author (0.129682ms)
|
||||
✔ authorize: the bot itself (0.090794ms)
|
||||
✔ authorize: webhook (0.109088ms)
|
||||
✔ authorize: mention channel without mention (0.133929ms)
|
||||
✔ authorize: mention channel with bot mention (1.156736ms)
|
||||
✔ authorize: mention channel with @everyone only (0.219088ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.086046ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.080592ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.08324ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.072543ms)
|
||||
✔ authorize: thread in another guild per channel info (0.076743ms)
|
||||
✔ authorize: not an object (0.066971ms)
|
||||
✔ authorize: no id (0.065534ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.341154ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.078782ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.427402ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.152876ms)
|
||||
✔ binding: a complete binding validates and is frozen (2.703261ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.404758ms)
|
||||
✔ binding: empty allowlists refuse (0.468185ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.290011ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.931725ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (1.586816ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (1.532893ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.291292ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (90.132991ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (1.903695ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (343.905667ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (200.174989ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (171.523856ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.922018ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.616284ms)
|
||||
✔ 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.768047ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (16.490975ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.391857ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.038882ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.14398ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.227966ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (33.317895ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.045825ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (0.673464ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (0.694267ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.512094ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (31.427935ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (5.74349ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (3.786994ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (2.391427ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (4.308482ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (1.08345ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.414452ms)
|
||||
✔ 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.374678ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (1.843676ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (3.576255ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.252481ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (3.857941ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.417438ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.739374ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.981426ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.829847ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.451565ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.197922ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (1.134524ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.709508ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.407249ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (60.73066ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (49.279288ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (34.617313ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (332.337368ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (233.340952ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (106.772282ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (235.888971ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (136.711037ms)
|
||||
✔ 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.888838ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.022055ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.320587ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.474722ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (429.384543ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (25.15694ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (50.872219ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.496157ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.757068ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.531936ms)
|
||||
✔ gateway: op 9 resumable resumes (0.373098ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (1.013184ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.569434ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.409186ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (64.596811ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (40.976755ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (67.83755ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.314968ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (93.755182ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (96.656828ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (98.248538ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (203.898863ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (84.702824ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (123.474492ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (242.222469ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (771.123817ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.311239ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (80.233294ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (0.761684ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (2.369784ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (41.448098ms)
|
||||
✔ 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 (326.999184ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.47773ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (25.131511ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.01056ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (48.78091ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (141.323211ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (117.941621ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.692034ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (6.770257ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.314538ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.875652ms)
|
||||
✔ 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.13958ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (39.725064ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (28.447422ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (81.715611ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (719.569643ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.551388ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (3.685105ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.967341ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (1.022929ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (1.053617ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.837344ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (2.756482ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.507394ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (1.922931ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (22.553003ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (9.298935ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (6.55584ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (3.329623ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.450254ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (1.960974ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1008.326549ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (1.159338ms)
|
||||
✔ 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.878604ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.688281ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.227088ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.412164ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (2.77565ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (3.588564ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (3.425507ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (2.566279ms)
|
||||
✔ tools: listing and search caps hold (7.60629ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.574731ms)
|
||||
✔ 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.162789ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (0.912937ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (0.964881ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (3.955945ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.44194ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.42934ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.423414ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.386179ms)
|
||||
✔ 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.578187ms)
|
||||
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (5.487335ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (1.15617ms)
|
||||
✔ 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.56656ms)
|
||||
✔ 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.706348ms)
|
||||
ℹ tests 178
|
||||
ℹ suites 0
|
||||
ℹ pass 178
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 2647.039322
|
||||
@@ -0,0 +1,86 @@
|
||||
✔ a raw token file, with or without one trailing newline, reaches curl only through the config stream (165.924047ms)
|
||||
✔ the raw path accepts nothing else, and refuses before curl runs (406.055497ms)
|
||||
✔ the file checks still apply on the raw path: mode, symlink, missing, directory (359.792039ms)
|
||||
✔ the raw path base URL has no override (61.084155ms)
|
||||
✔ the JSON path is unchanged, and JSON never falls through to the raw path (276.479897ms)
|
||||
✔ a file that changes between the two reads refuses before curl runs, with or without a body (929.515573ms)
|
||||
✔ the token reaches no child environment, even with an inherited CFG or SHELLOPTS=allexport (892.631999ms)
|
||||
✔ real helper GET HTTP 200 preserves exit 0 without credentials (17.119021ms)
|
||||
✔ real helper POST HTTP 201 preserves exit 0 without credentials (14.717875ms)
|
||||
✔ real helper GET HTTP 403 preserves exit 1 without credentials (10.164851ms)
|
||||
✔ fixture git subjects only, follow-ups and three session kinds (136.108668ms)
|
||||
✔ text and JSON carry same numbers, open and truncated title (199.370787ms)
|
||||
✔ missing credentials exit 2, no-issues never calls API and shows unknown (154.34927ms)
|
||||
✔ empty range gives no rows and zero totals (100.922076ms)
|
||||
✔ inclusive UTC dates, first-line preamble only, role and seat boundaries (135.623258ms)
|
||||
✔ close-only issue included, even median, missing metadata stays unknown (125.027146ms)
|
||||
✔ unique commits but per-issue links count multiple tags once each (113.056961ms)
|
||||
✔ page cap refuses rather than silently undercounting (106.228218ms)
|
||||
✔ bad API payload not JSON refuses (97.205569ms)
|
||||
✔ bad API payload {} refuses (100.975217ms)
|
||||
✔ bad API payload [{"number":1}] refuses (107.560107ms)
|
||||
✔ partial or malformed session log refuses with location, not content (117.155111ms)
|
||||
✔ a U+2028 or U+2029 inside a session string is one line, not a malformed record (132.521872ms)
|
||||
✔ no sessions is an empty table; symlink source refuses (228.293901ms)
|
||||
✔ reads only refactor even when another branch is checked out (145.248816ms)
|
||||
✔ invalid dates, reverse dates and duplicate options refuse (86.724784ms)
|
||||
✔ T3 agent assignments do not count as human in Table 2 (131.423095ms)
|
||||
✔ preamble parsing and issue number boundaries (0.587999ms)
|
||||
✔ T3 header: agent, or board from control-board; anything short of the full header is human (0.183559ms)
|
||||
✔ no closed issues with human messages means undefined ratio, not invented zero (0.231127ms)
|
||||
✔ T3: seat, archived, unmapped and Researcher threads count; imported, deleted and other-project threads do not (264.621647ms)
|
||||
✔ T3: the default path is read from HOME and prints no path line; --no-t3 says so (537.303375ms)
|
||||
✔ T3: a HOME with no database exits 1 and names --no-t3 (136.196572ms)
|
||||
✔ T3: a file that is not a database exits 1 and names --no-t3 (163.770735ms)
|
||||
✔ T3: a seat thread renamed to another seat exits 1 naming thread, title and roles (156.237364ms)
|
||||
✔ T3: an unmapped thread addressed as a seat exits 1 (198.157112ms)
|
||||
✔ T3: a header to another thread id is not cross-checked (141.948768ms)
|
||||
✔ T3: no project, or two, for this root exits 1 (338.133774ms)
|
||||
✔ T3: a removed column exits 1 and names it (126.881768ms)
|
||||
✔ T3: a missing table exits 1 and names it (120.390088ms)
|
||||
✔ T3: a counted row with an unknown role exits 1 without its text (147.573714ms)
|
||||
✔ T3: a counted row with non-text content exits 1 without its text (129.34913ms)
|
||||
✔ T3: a counted row with an unparseable created_at exits 1 without its text (123.687867ms)
|
||||
✔ T3: a missing orchestration_events makes the diagnostic unknown and keeps the counts (245.917864ms)
|
||||
✔ T3: a human message with no event counts in humanWithoutEvent (146.529273ms)
|
||||
✔ T3: an unparseable event makes the diagnostic unknown and keeps the counts (229.767827ms)
|
||||
✔ T3: an event with no string messageId makes the diagnostic unknown and keeps the counts (239.156876ms)
|
||||
✔ T3: an error that is not from SQLite is rethrown, not reported as a database failure (80.39157ms)
|
||||
✔ T3: a symlink at ~/.t3 exits 1 (96.097141ms)
|
||||
✔ T3: a symlink at ~/.t3/userdata exits 1 (96.814724ms)
|
||||
✔ T3: a symlink at ~/.t3/userdata/state.sqlite exits 1 (98.58124ms)
|
||||
✔ T3: with --t3-db, a symlinked file or directory exits 1 (232.307406ms)
|
||||
✔ T3 WAL: the newest message only in -wal, writer attached, is counted (158.189271ms)
|
||||
✔ T3 WAL: stopped cleanly, counts are correct and the main file is unchanged (144.472439ms)
|
||||
✔ T3 WAL: -wal without -shm in a writable directory is read (157.355895ms)
|
||||
✔ T3 WAL: -wal without -shm in a read-only directory exits 1 (171.762565ms)
|
||||
✔ T3 WAL: stopped cleanly in a read-only directory exits 1 (149.387801ms)
|
||||
✔ T3: a lock held past the 5 s busy timeout exits 1 and names --no-t3 (5121.802284ms)
|
||||
✔ a done row whose closing issue is open is a violation; a row that is not done is not (1.909114ms)
|
||||
✔ an issue several rows close is expected closed only once all of them are done (0.508786ms)
|
||||
✔ closure needs positive evidence: unknown is undecided, and so is a skipped or short issue check (0.348898ms)
|
||||
✔ each owner of an in-progress or in-review row gets one liveness class (8.123876ms)
|
||||
✔ a required row not done after 14 days is a violation; a legacy row uses genesis as its lower bound (0.416449ms)
|
||||
✔ an ISO requiredSince, as `set required` writes it, ages from its UTC day; one that does not parse is a violation (0.24282ms)
|
||||
✔ the text section always ends in a count and a result, and never prints a full pass (0.356876ms)
|
||||
✔ pidAlive: a running pid is present, an exited one is gone, and EPERM still means present (31.89034ms)
|
||||
✔ issue states: open list first, then the metric page, then at most 10 lookups (333.535581ms)
|
||||
✔ a full open list: lookups settle what it leaves out, and only an unsettled issue keeps it undecided (434.294609ms)
|
||||
✔ the open list refuses on a failed call or a bad record, and never echoes the helper (208.448436ms)
|
||||
✔ a helper call past the deadline is killed with its child, and the call reports it (2012.242172ms)
|
||||
✔ readQueue loads queue.json through the queue validator and refuses anything else (135.29559ms)
|
||||
✔ protected changes list every in-range entry that changes a required or parked row (413.044318ms)
|
||||
✔ the CLI prints the queue section above the weekly table and under a queue key in --json (588.933257ms)
|
||||
✔ a queue with nothing wrong prints 0 violations and a reduced pass, never a full pass (241.511182ms)
|
||||
✔ --no-issues makes no call and leaves the issue checks undecided; --no-queue skips the section (308.831682ms)
|
||||
✔ the CLI refuses a bad queue before any call, and a failed open list with exit 2 (252.500929ms)
|
||||
✔ --unsupported-runtime repeats once per seat and takes a seat name (380.580852ms)
|
||||
✔ an unreadable config makes every owner invalid instead of passing them (134.874241ms)
|
||||
ℹ tests 78
|
||||
ℹ suites 0
|
||||
ℹ pass 78
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 12227.76449
|
||||
@@ -0,0 +1,77 @@
|
||||
✔ pure resolution selects current default or explicit enrolled account (2.488438ms)
|
||||
✔ scope is explicit, bounded and never inferred (1.856965ms)
|
||||
✔ fork pin is preserved against default change, override, missing account and revocation (0.842962ms)
|
||||
✔ unenrolled account/provider, missing harness, model expansion and native model ceiling refuse (0.871202ms)
|
||||
✔ only explicit synthetic credential forms and internal fixture stores admitted (6.83248ms)
|
||||
✔ two concurrent workspaces of the same agent publish distinct complete private generations (64.479537ms)
|
||||
✔ same execution ID is exclusively claimed and cannot overwrite a published generation (41.084097ms)
|
||||
✔ failed generation after-auth preserves prior files, records failure and refuses blind same-ID retry (50.804275ms)
|
||||
✔ failed generation before-publish preserves prior files, records failure and refuses blind same-ID retry (61.293179ms)
|
||||
✔ credential lock contention refuses without duplicate side effects (12.718151ms)
|
||||
✔ symlinked pre-existing final target is refused and never followed (35.009186ms)
|
||||
✔ invalid registry cannot resolve; no fallback to supplied partial entries (0.347623ms)
|
||||
✔ post-publication failure records uncertainty, preserves complete generation and prevents replay (29.526903ms)
|
||||
✔ expired credentials refresh under transaction and subsequent generation reuses rotation (110.59713ms)
|
||||
✔ refresh failure retains prior generation and store state (77.146993ms)
|
||||
✔ refresh timeout retains prior generation and store state (153.356722ms)
|
||||
✔ refresh malformed retains prior generation and store state (66.800371ms)
|
||||
✔ concurrent refresh on same account refuses contention while unrelated account proceeds (216.019093ms)
|
||||
✔ invalid refresh options refuse before burning claim (33.899759ms)
|
||||
✔ fixed fake process rotates both OAuth fields without mutating caller input (43.57806ms)
|
||||
✔ concurrent isolated processes preserve separate provider credentials (35.326457ms)
|
||||
✔ fake failure is refused with fixed diagnostics (26.933496ms)
|
||||
✔ fake malformed is refused with fixed diagnostics (34.527734ms)
|
||||
✔ fake timeout is refused with fixed diagnostics (105.207471ms)
|
||||
✔ fake unchanged is refused with fixed diagnostics (29.924808ms)
|
||||
✔ caller executable/environment injection is rejected before spawning (0.404432ms)
|
||||
✔ valid fixture tree validates and lists without secrets (77.023191ms)
|
||||
✔ unknown-field refuses (0.415288ms)
|
||||
✔ invalid-id refuses uppercase and traversal shapes (0.392754ms)
|
||||
✔ plain-http baseUrl requires allowInsecureTransport (0.400835ms)
|
||||
✔ native provider rejects allowInsecureTransport (0.173921ms)
|
||||
✔ unsupported credential type and kind refuse (0.204613ms)
|
||||
✔ account provider-path mismatch refuses (0.145764ms)
|
||||
✔ profile account refs must be provider/account shaped (0.276049ms)
|
||||
✔ seat selection accepts fork pin field, validates account refs (0.263953ms)
|
||||
✔ harness manifest id must equal executable (gate 1) (0.823761ms)
|
||||
✔ CLI validate: duplicate provider id across files refuses (32.637734ms)
|
||||
✔ CLI validate: missing referenced provider/account refuse (44.381341ms)
|
||||
✔ CLI validate: broken JSON refuses without secret echo (34.151247ms)
|
||||
✔ CLI usage errors exit 2 (60.973031ms)
|
||||
✔ credential.json sibling presence does not break validation and is never read (78.431077ms)
|
||||
✔ D1 missing, empty and structurally empty roots refuse, no list projection (215.152309ms)
|
||||
✔ D1 required directory auth cannot be absent (66.284061ms)
|
||||
✔ D1 required directory auth/providers cannot be absent (69.149537ms)
|
||||
✔ D1 required directory auth/accounts cannot be absent (77.579276ms)
|
||||
✔ D1 required directory auth/settings cannot be absent (81.001525ms)
|
||||
✔ D1 required directory harnesses cannot be absent (67.7753ms)
|
||||
✔ D1 root file and unreadable metadata refuse (116.45675ms)
|
||||
✔ D2 no symlink traversal at auth/providers/openai-codex.json (59.550832ms)
|
||||
✔ D2 no symlink traversal at auth/accounts/openai-codex/homelab-openai (73.008484ms)
|
||||
✔ D2 no symlink traversal at auth/providers (70.691711ms)
|
||||
✔ D2 no symlink traversal at auth (59.78201ms)
|
||||
✔ D2 root and ancestor symlinks and lexical traversal refuse (190.390235ms)
|
||||
✔ private filesystem modes enforced for root (60.425069ms)
|
||||
✔ private filesystem modes enforced for auth (65.249523ms)
|
||||
✔ private filesystem modes enforced for auth/providers/openai-codex.json (73.927662ms)
|
||||
✔ private filesystem modes enforced for auth/accounts/openai-codex/homelab-openai/account.json (76.003834ms)
|
||||
✔ D3 numeric version 1 only across all record kinds (1.616278ms)
|
||||
✔ D4 nested unknown keys and missing per-kind required fields refuse (74.668603ms)
|
||||
✔ D5 unenrolled default refuses even when account exists (75.477908ms)
|
||||
✔ D6 provider/account credential type must match (71.53665ms)
|
||||
✔ D7 every harness endpoint enforces HTTP opt-in and shape (0.556494ms)
|
||||
✔ D8 URLs reject embedded credentials and unsupported protocols without echo (195.246658ms)
|
||||
✔ D9 malformed JSON diagnostics contain no content excerpt (74.979807ms)
|
||||
✔ D10 missing metadata is missing-path, not invalid-json (69.179398ms)
|
||||
✔ D10 library returns no partial entries on any invalid record (75.675783ms)
|
||||
✔ null/scalar/array metadata refuses without stack or echo (265.801703ms)
|
||||
✔ credential sibling is never opened, even when an unreadable symlink (32.740301ms)
|
||||
✔ oversized metadata refuses before parsing (67.809426ms)
|
||||
ℹ tests 69
|
||||
ℹ suites 0
|
||||
ℹ pass 69
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 2488.4192
|
||||
@@ -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 (1267.472445ms)
|
||||
✔ F1: a plain `commit -e` whose guard ran before update-ref fails at its own HEAD update (1177.783651ms)
|
||||
ℹ 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 (1073.585632ms)
|
||||
ℹ 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 (1159.263234ms)
|
||||
✔ F1: HEAD moving after the step-7 guard check and before update-ref: refused, nothing published (1127.159868ms)
|
||||
✔ F1: H is recorded before the canary, so HEAD moving during the step-1 canary is refused at step 7 (1167.392219ms)
|
||||
✔ F1: a queue commit landing after H is recorded: step 1 says HEAD moved, not the guard (1544.416864ms)
|
||||
✔ F1: a queue commit landing between the HEAD check and the canary: the failed clean run is reported as HEAD moved (1087.312895ms)
|
||||
✔ F1: a shared-index change during the procedure is not committed (1174.36203ms)
|
||||
✔ F1: a queue path staged after update-ref: step 8 stops and touches nothing (1188.320928ms)
|
||||
✔ F1: a missing or a different hook refuses (779.267672ms)
|
||||
✔ F1: same bytes without the exec bit, a symlinked hook, and core.hooksPath in the local or global scope each refuse before update-ref (1290.921047ms)
|
||||
✔ F1: the canary refuses a hook that git would not run (688.256011ms)
|
||||
✔ F1: the guard deactivated after step 1 is refused at the step-7 recheck (1165.557182ms)
|
||||
✔ bootstrap: implementation-only HEAD, the guard, genesis, the --genesis commit, then an extending commit (899.651184ms)
|
||||
✔ bootstrap: --genesis with a base present, no base without --genesis, an op before the first commit, a changed map, another branch (911.176517ms)
|
||||
✔ bootstrap: the archived tests and validator run outside any repository (889.140742ms)
|
||||
✔ general: an unrelated staged file stays staged, and the committed blobs are the snapshot bytes (908.359983ms)
|
||||
✔ general: a queue write after the snapshot is not committed (1350.190221ms)
|
||||
✔ general: a snapshot whose log does not extend the base refuses (1006.207946ms)
|
||||
✔ general: install-hook privilege, repair of a same-bytes hook, and its refusals (196.755697ms)
|
||||
✔ general: environment overrides, a linked worktree and usage (582.084464ms)
|
||||
✔ general: a queue path staged before the run refuses at step 1 (689.057933ms)
|
||||
✔ general: HEAD's queue tests failing in the archive refuse (985.695073ms)
|
||||
✔ genesis document serializes deterministically and replays (4.67164ms)
|
||||
✔ a hand edit that stays valid JSON fails replay; a formatting-only edit fails re-serialization (2.63623ms)
|
||||
✔ a tampered result, receipt or viewSha fails replay (2.897793ms)
|
||||
✔ op ids: 8 to 72 characters for callers, 80 in the log for .outcome entries (0.201786ms)
|
||||
✔ add: defaults for an ordinary seat, privileged extras, refusals (5.031203ms)
|
||||
✔ matrix: queued→briefed privileged; briefed→in-progress owner with after satisfied (7.046382ms)
|
||||
✔ matrix: release, review round, changes requested and waiting-on-jason (13.550117ms)
|
||||
✔ matrix J5: in-review→done by the gate owner with evidence naming the current round (8.710419ms)
|
||||
✔ review issue, lead decision 23: none refuses, one is used, several need --issue, later rounds keep it (19.262517ms)
|
||||
✔ the row schema refuses a round with a null issue, and the A1 review shape (P2) (2.40439ms)
|
||||
✔ matrix R1: every state × target × actor class matches 8.7, gate owner jason or not, required or not (1607.188383ms)
|
||||
✔ matrix: blocked keeps the claim and returns only to previousState (4.309894ms)
|
||||
✔ matrix J4: parking is Jason's, refused while required; unpark returns to queued (2.996456ms)
|
||||
✔ field edits: who may change what (5.549845ms)
|
||||
✔ set issues keeps a logged narrowing of closes (N10) (2.919138ms)
|
||||
✔ text the table shows refuses \ and <, everywhere it enters (N8) (2.0547ms)
|
||||
✔ every accepted text renders to nine cells on every row (N8) (24.715138ms)
|
||||
✔ genesis: the map refuses an owner among its row's reviewers; replay doesn't (2026-10-04) (0.585192ms)
|
||||
✔ times and dates must be calendar values, not just the shape (2026-10-04) (1.821812ms)
|
||||
✔ replay holds every op id to the caller's rule (N11) (5.613134ms)
|
||||
✔ note: owner, listed reviewer or privileged; empty clears (1.215475ms)
|
||||
✔ assign moves the claim with the owner; done clears it (2.73492ms)
|
||||
✔ render is byte-stable and escapes pipes (0.545087ms)
|
||||
✔ view classification: current, genuine stale, edited stale marker, changed current body, markers (0.741249ms)
|
||||
✔ next: resume, then review, then start, then wait, then nothing; lowest id first (20.61101ms)
|
||||
✔ canonical args make a retry's identity independent of list order (0.329425ms)
|
||||
✔ manifests, headings and blob ids (0.488475ms)
|
||||
✔ the migration map: one queue-map block, exact keys (0.630741ms)
|
||||
✔ every call but `queue` reaches the seat CLI exactly as before A2 (665.163411ms)
|
||||
✔ `queue` reaches the queue CLI with the rest of the arguments (216.543764ms)
|
||||
✔ the pre-A2 fixture is the script A2 changed (0.283762ms)
|
||||
✔ acquire publishes the record by link; release removes only its own lock (7.848872ms)
|
||||
✔ a kill between the temp write and the link leaves no lock (47.824153ms)
|
||||
✔ a short or failed temp write refuses and leaves no lock and no temp (2.247722ms)
|
||||
✔ a link error other than EEXIST refuses (1.275562ms)
|
||||
✔ an error after the link releases the lock: unreadable gate, failing temp stat (3.22735ms)
|
||||
✔ a release that fails on a gate path is reported, never a stack trace (P1) (5.585558ms)
|
||||
✔ a paused holder: another writer waits 10 s, then refuses naming it live (10068.407105ms)
|
||||
✔ two concurrent unlockers: the second refuses on the gate (26.175642ms)
|
||||
✔ a writer publishing during an unlock, lock first: unlock sees it live and refuses (1.709441ms)
|
||||
✔ a writer publishing during an unlock, gate first: the writer releases and refuses (1.681053ms)
|
||||
✔ a gate swapped while held is left in place and reported, on success and on refusal (N1) (2.046227ms)
|
||||
✔ a reused pid within one boot is mismatch; unlock removes the lock and never signals the process (3.38893ms)
|
||||
✔ the same pid and start on a different boot is mismatch (0.694945ms)
|
||||
✔ a foreign host is unknown whatever the local pid says; unlock refuses (50.608594ms)
|
||||
✔ unreadable /proc: classification is unknown and acquire refuses (0.687618ms)
|
||||
✔ invalid records: empty, unparsable, wrong keys, bad start or boot (0.207121ms)
|
||||
✔ a stale gate blocks writers; --check-gate says mismatch for a reused pid (3.797254ms)
|
||||
✔ a delayed release by a dead owner, after unlock and a new owner: the inode check keeps the new lock (2.367947ms)
|
||||
✔ release checks the inode too: a byte-identical lock file with a new inode is left in place (1.67287ms)
|
||||
✔ unlock refuses a live, unknown or invalid lock, and does nothing without one (5.433797ms)
|
||||
✔ the migration map validates and renders the golden genesis table (3.864429ms)
|
||||
✔ the marked QUEUE.md holds every row and parked item between its markers (1.678386ms)
|
||||
✔ map-check reports each kind of drift (5.968189ms)
|
||||
✔ a request posts once as the requester; a retry sends nothing (608.748996ms)
|
||||
✔ each transport answer maps to posted, failed or uncertain (8.9 step 3) (3553.150687ms)
|
||||
✔ the pre-send checks: GET user must name the requester, under the deadline (1053.192883ms)
|
||||
✔ the lead's request refuses a token for login sage (590.274225ms)
|
||||
✔ the credential file: the seat's own, 0600, no symlink, never the shared default (509.216864ms)
|
||||
✔ an unresolved request blocks a new request, a new round, waiting-on-jason and done (1753.612336ms)
|
||||
✔ a same-op retry after a kill sends nothing, even with a stale view (2671.507409ms)
|
||||
✔ a held lock at the outcome exits 3 and names what the transport said (616.752838ms)
|
||||
✔ late outcomes: after an abandon, and after a resolve with the same or another id (1735.746071ms)
|
||||
✔ resolve checks the comment: issue, markers, round, candidate and author (1684.588505ms)
|
||||
✔ the lead resolves a seat's request: the comment's author is the requester, fetched with the lead's token (670.185663ms)
|
||||
✔ validateRow checks a request round's shape, which every replayed entry must keep (446.136054ms)
|
||||
✔ request, changes, a new candidate, approval: every round pinned; no review files (1453.335613ms)
|
||||
✔ a row with no reviewers opens a round that posts nothing (860.567174ms)
|
||||
✔ verify-commit: a prospective tree must hold exactly the candidate's paths (1222.264465ms)
|
||||
✔ semantics: v1 entries replay as before; review entries need v2 (369.019506ms)
|
||||
✔ set reviewers refuses the row's owner (2026-09-28) (302.37542ms)
|
||||
✔ the owner records no verdict, even as a listed reviewer (372.180331ms)
|
||||
✔ a request comment over the length limit is not sent (458.430535ms)
|
||||
✔ a late POST on a closed row leaves a conflict nothing can resolve, and resolve asks nothing (423.543333ms)
|
||||
✔ a Jason-gated row reaches waiting-on-jason only on every reviewer's approval (1936.775462ms)
|
||||
✔ genesis: refusals before anything is written (446.203952ms)
|
||||
✔ genesis: the map must be committed, well formed, with committed briefs and seats (648.378342ms)
|
||||
✔ genesis: markers, a stray witness, once only; a retry returns the receipt (625.787436ms)
|
||||
✔ genesis: a file holding genesis alone with no witness is confirmed by sync or a retry (329.164259ms)
|
||||
✔ canonical checks: worktree, second clone, detached HEAD, wrong branch, GIT_DIR, foreign code; a symlink works (699.498238ms)
|
||||
✔ --by that differs from MOSAIC_AGENT_NAME warns on stderr and logs nothing more (N12) (605.937281ms)
|
||||
✔ op ids: missing, too long, reserved; a retry answers; another payload refuses (708.605587ms)
|
||||
✔ a retried add returns the id it first allocated, after reassignment and after done (664.910717ms)
|
||||
✔ Rocko's S4 schedule: a lost result, another writer, then the retry opens no second round (502.017358ms)
|
||||
✔ the review issue and the evidence round through the CLI (lead decision 23, 8.7) (940.318816ms)
|
||||
✔ claims and add defaults through the CLI; candidates are manifests or reachable commits (690.881078ms)
|
||||
✔ add, set reviewers and assign refuse the row's owner as a reviewer (455.505712ms)
|
||||
✔ the working-brief check: a changed working copy refuses the start and flags next (662.413569ms)
|
||||
✔ next: resume first, then nothing for an idle seat; needs a seat (321.302931ms)
|
||||
✔ view stale: new ops and verify refuse naming the unshown op; retries answer; reads warn; render fixes (674.737431ms)
|
||||
✔ view unknown: a hand edit, an old marker over an edited body, missing or duplicate markers (1200.927341ms)
|
||||
✔ a hand edit to queue.json refuses every verb, reads included (610.012598ms)
|
||||
✔ verify and render --check leave bytes and mtimes unchanged (457.244762ms)
|
||||
✔ render is byte-stable across runs and repositories (279.672708ms)
|
||||
✔ snapshot and verify --snapshot (830.80842ms)
|
||||
✔ usage errors exit 4 (618.590269ms)
|
||||
✔ a short write, ENOSPC or a file fsync failure: nothing visible, temp removed (180.944534ms)
|
||||
✔ a rename failure: nothing visible, temp removed (133.754187ms)
|
||||
✔ a directory fsync failure: uncertain, exit 3, no receipt; the tail refuses new ops; a retry confirms (167.147416ms)
|
||||
✔ a directory fsync failure, then sync names the op (320.250141ms)
|
||||
✔ a witness write failure: uncertain, durable, exit 3; the view is untouched (151.888162ms)
|
||||
✔ the .git fsync after the witness rename fails: uncertain, exit 3, the witness says so (133.487295ms)
|
||||
✔ confirming a tail fsyncs queue.json and docs/plans before the witness; either failure changes nothing (164.768232ms)
|
||||
✔ the docs/plans fsync after the view rename fails: the op stands, the view is written, a warning says so (149.736024ms)
|
||||
✔ a lock swapped while held is left in place and reported, on a receipt and on a refusal (189.205094ms)
|
||||
✔ a lock that cannot be released after an op is reported, on a receipt and on a refusal (153.039097ms)
|
||||
✔ unlock prints a swapped gate's warning on stderr, the result on stdout (117.886054ms)
|
||||
✔ a view write that fails keeps the op and reports a stale view (121.523798ms)
|
||||
✔ SIGKILL before the rename: nothing recorded; the retry removes the leftover temp (597.292669ms)
|
||||
✔ SIGKILL after the rename, before the witness: the tail refuses new ops and sync names the op (647.495398ms)
|
||||
✔ SIGKILL after the witness, before the view: the stale refusal names the op (629.675381ms)
|
||||
✔ SIGKILL after the view, before the receipt: the retry returns the receipt (605.569834ms)
|
||||
✔ git checkout between steps 1 and 7: step 7 refuses and nothing is written (141.574196ms)
|
||||
✔ git stash restoring an older valid pair: history lost; accept-history needs privilege, a reason and --yes (923.684788ms)
|
||||
✔ a deleted witness: refused after the locked recheck; accept-history records it absent (198.053439ms)
|
||||
✔ a header edit during a write: the op stands, the view write is skipped with a warning (127.634229ms)
|
||||
✔ a reader paused between the witness and the file while a writer finishes: no lost-history report (125.11438ms)
|
||||
✔ file-then-witness order forced by a hook: the locked recheck prevents a false report (149.132546ms)
|
||||
✔ a writer paused before and after the witness rename: readers see a tail, then a match (163.237756ms)
|
||||
✔ a true rollback is reported only after the locked recheck; a held lock names its holder instead (526.95369ms)
|
||||
✔ an accept-history in progress: an unlocked reader waits on the lock and never reports lost history (488.501315ms)
|
||||
✔ the platform check refuses other filesystems (129.376413ms)
|
||||
✔ tmpfs passes only a test layer that allows it (N5) (188.65551ms)
|
||||
✔ unlock keeps a multi-line lock record on stdout (P3) (190.345833ms)
|
||||
ℹ tests 148
|
||||
ℹ suites 0
|
||||
ℹ pass 148
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 24374.869593
|
||||
@@ -0,0 +1,27 @@
|
||||
✔ resolveSeat: by name under --repo resolves the repo layout (1.614406ms)
|
||||
✔ resolveSeat: by path resolves the fleet layout (0.438607ms)
|
||||
✔ resolveSeat: refusals for missing dir, missing launch.sh, non-executable launch.sh, invalid name, and unknown layout (0.927604ms)
|
||||
✔ tmuxContext: outside tmux, default socket, custom socket, and exec failure (0.834961ms)
|
||||
✔ makeRegistration produces a record that validates; each shape violation throws SeatError (1.381157ms)
|
||||
✔ writeRegistration/readRegistration: round trip, permissions, absence, and malformed records (2.263772ms)
|
||||
✔ updateTask: changes task, taskSetBy and updatedAt only, and refuses appropriately (2.387902ms)
|
||||
✔ CLI launch: registers, execs the fake launch script, and passes args through (39.06825ms)
|
||||
✔ CLI launch: --harness lands in the record (36.375336ms)
|
||||
✔ CLI launch: the launch script's own exit code passes through (35.303199ms)
|
||||
✔ CLI launch: relaunching a seat rewrites the one registration record (69.126645ms)
|
||||
✔ CLI launch: omitting --task records an empty string, not null (39.089153ms)
|
||||
✔ CLI seat task: updates only the task after a launch, and refuses on an unlaunched seat (110.908399ms)
|
||||
✔ CLI refusals: no args, unknown flag, missing config, already-registered env, and exec failure (183.525014ms)
|
||||
✔ samePath: equal paths, symlinked dirs, distinct dirs, and non-strings (0.530911ms)
|
||||
✔ resolveSetBy: explicit --by wins over the environment; absent or empty environment gives unknown; invalid explicit or environment values refuse with exit 4 (0.73824ms)
|
||||
✔ 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.987699ms)
|
||||
✔ 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 (14.887655ms)
|
||||
✔ 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 (329.05029ms)
|
||||
ℹ tests 19
|
||||
ℹ suites 0
|
||||
ℹ pass 19
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 947.479624
|
||||
@@ -0,0 +1,59 @@
|
||||
✔ the boot config is checked before anything starts (15.05903ms)
|
||||
✔ a business with no tracker entry refuses task verbs (13.206278ms)
|
||||
✔ credential.expiring and .expired are recorded once per instance (31.54058ms)
|
||||
✔ a token file that changes on disk records credential.changed (12.932978ms)
|
||||
✔ autostart polls, reconciles and retries a startup the tracker was down for (36.134415ms)
|
||||
✔ a refusal a restart must clear is not retried by the poll (10.023738ms)
|
||||
✔ a poll that fires while two are queued is dropped (13.172448ms)
|
||||
✔ close waits for a running verb and refuses one that has not started (43.815995ms)
|
||||
✔ the bundled Vikunja is the pinned upstream image the runbook names (0.841445ms)
|
||||
✔ every published port is on 127.0.0.1, and no secret is in the file (0.323511ms)
|
||||
✔ the fake answers each route with the statuses and shapes Vikunja v2.7.0 sent (99.216958ms)
|
||||
✔ the recorded task bodies pass the checks S3 applies to every read (0.703623ms)
|
||||
✔ the client works against the fake over real HTTP with the platform fetch (109.227397ms)
|
||||
✔ a correct install starts, and the first reconcile records tasks that already exist (27.003598ms)
|
||||
✔ verbs refuse while a business is starting and after startup refused it (18.409758ms)
|
||||
✔ startup refuses a token that can do more than its role needs (37.708403ms)
|
||||
✔ startup refuses an unsupported version and flags an untested one (27.75872ms)
|
||||
✔ startup refuses a board that the runbook did not install (35.737928ms)
|
||||
✔ startup refuses a project the sync bot cannot read (7.660232ms)
|
||||
✔ startup refuses a configured label the pm bot cannot see (8.774459ms)
|
||||
✔ startup refuses an expired credential and a missing sync credential (17.994987ms)
|
||||
✔ an unreachable tracker refuses with tracker-unavailable (8.207959ms)
|
||||
✔ an edit in the UI is recorded once, with the fields that changed (84.516069ms)
|
||||
✔ a move between open buckets is seen on the board, though updated does not change (40.375056ms)
|
||||
✔ a person's comment is counted and a bot's is not (71.753693ms)
|
||||
✔ the hourly reconcile catches a comment through comment_count (79.835253ms)
|
||||
✔ a task closed in the UI leaves the open view with its done bucket (110.806868ms)
|
||||
✔ a task that leaves the board is recorded as deleted, moved or out of reach (61.66387ms)
|
||||
✔ a poll that read before a verb wrote does not overwrite the verb (32.839804ms)
|
||||
✔ a tracker fault during a tick is reported and the next tick catches up (34.990017ms)
|
||||
✔ a malformed answer refuses the tick with tracker-shape (11.377332ms)
|
||||
✔ no token value reaches the database, the log or a refusal (44.700715ms)
|
||||
✔ the first look at a task counts only comments inside the window (35.105901ms)
|
||||
✔ task.create needs a recorded human request and a requirement id (46.207399ms)
|
||||
✔ only labels named in the business file can be written (26.968361ms)
|
||||
✔ task.schedule sets and clears a due date and relations (33.946077ms)
|
||||
✔ assign and reassign move the role bots and record task.assigned (56.375338ms)
|
||||
✔ task.update.assigned is for the assignee and records task.state (57.693568ms)
|
||||
✔ a wrong expected digest records task.conflict and writes nothing (19.007855ms)
|
||||
✔ a cross-role verb needs a resolved decision, used once (20.518054ms)
|
||||
✔ task.close needs a verdict; after it every verb refuses with task-done (20.669219ms)
|
||||
✔ a lost answer is settled by a re-read and never retried (22.291008ms)
|
||||
✔ a create whose answer is lost is reported uncertain, and the poll finds the task (34.736066ms)
|
||||
✔ a task the sync bot cannot read refuses and records nothing (14.308492ms)
|
||||
✔ verbs and polls for one business run one at a time (84.331414ms)
|
||||
✔ a due date with milliseconds is written to the second (60.706832ms)
|
||||
✔ every write landed and the final read failed: the verb succeeds and records what it wrote (36.641238ms)
|
||||
✔ some writes landed and the final read failed: write-uncertain, and nothing is recorded (15.172674ms)
|
||||
✔ a create whose final read fails succeeds and records task.created (33.280859ms)
|
||||
✔ an edit between the last write and the final read shows as external on the next poll (36.807608ms)
|
||||
✔ task.created is recorded when a later label write fails (12.149566ms)
|
||||
ℹ tests 51
|
||||
ℹ suites 0
|
||||
ℹ pass 51
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 726.078114
|
||||
@@ -0,0 +1,23 @@
|
||||
✔ browser edge states: loading, empty, malformed, stale, hostile/long values, in-flight reply and appearance fallback (2231.885573ms)
|
||||
Rendered contrast: {"failures":[],"count":330,"lowest":4.504658476260286}
|
||||
✔ served Console browser: real board fixtures, keyboard, drafts, receipts, themes, 320px and failures (2515.444403ms)
|
||||
✔ conversation view: full history, collapsed tools, hidden thinking, inert hostile content, malformed and reconcile markers (2135.844177ms)
|
||||
✔ conversation view: a fork keeps the open branch, says so, and opens the new one on request (1394.840864ms)
|
||||
✔ conversation view: a newer session with no readable history keeps the marker (921.250471ms)
|
||||
✔ conversation view: seats without history say so and offer no reply (534.275461ms)
|
||||
✔ Discord row through real board/WebUI: independent brake/liveness, no Reply, literal content (2292.765977ms)
|
||||
✔ return flow through the conversation view: send, tool call, delayed result, peer message, exact long answers, relaunch (53489.49899ms)
|
||||
✔ both presentations replace old activity with relaunch notice, label retained history, then resume after new activity (2266.991606ms)
|
||||
✔ reported return flow and relative Age: reply sent from the inspector, then the new answer appears there without manual refresh (21865.873619ms)
|
||||
✔ loopback host and board origin fail closed (7.278431ms)
|
||||
✔ real board fixture passes through WebUI; assets and isolated seen/reply work (83.201298ms)
|
||||
✔ proxy preserves exact request bytes, status and receipt, rejects forms and malformed JSON, never follows redirect (55.32886ms)
|
||||
✔ unreachable board reports URL; CLI rejects unsupported options (1031.529212ms)
|
||||
ℹ tests 14
|
||||
ℹ suites 0
|
||||
ℹ pass 14
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 53784.421
|
||||
@@ -0,0 +1,21 @@
|
||||
node-business exit 0
|
||||
node-bus exit 0
|
||||
node-cli exit 0
|
||||
node-control-board exit 0
|
||||
node-conversation exit 1
|
||||
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,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: 3722098 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
|
||||
@@ -0,0 +1,56 @@
|
||||
fd7c9a28364d61669889180bce1482449ab0a583228ebb9e0e8fe5075cbe5eb0 BUILD.md
|
||||
c88b20e4bd185682c82a4ce1733cd5b5a23a5375a10e9de55f2af5f8af7474a9 base.txt
|
||||
4ed9ff61b94b4c9d2426e5703c2e6c0dbcd6bc0c64a88d59347a8b47f4933408 build.patch
|
||||
b329fdcbdf8cb62659f239359570dfa9771559e3112b743df55429244769ed14 candidate-manifest.sha256
|
||||
8d35f99580981064d08725cf8e3dd67885bff77603446b0022701d4357727273 files.txt
|
||||
bbe45c2a578b35cc2559972a4ee8900bd01ffb69e72bef6472dfc7e147e8916f mutants.sh
|
||||
1102fd4f14df908f0498d31d209023d7903ba090c6c3bf3e3338e3f52158d95d out/base-node-conversation.txt
|
||||
8df77c49bf82833efa56d42401a4a06f20931b7a76dd5eed3a73fdec60e92e79 out/base-suite-task.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.txt
|
||||
95bdcddfb56d633c72c0b8c18948cffd42e6c1b2aaa3b78800783eb2d1452e35 out/mut-E1.txt
|
||||
1e13a94767e411e0eccf09d8b362dba095e33b6403f3d9bc5cde3e7cdfe62315 out/mut-E2.txt
|
||||
792f305fd87032806ebeba719008f927a8dd11358be89cd73e3e89abe721d5f0 out/mut-F2a.txt
|
||||
fff65df80a7e894292c7784d86cd1430e46d176e3f0435b6cfa9d9bb2422f9c4 out/mut-F2b.txt
|
||||
0aa3076b1eace7751bd5b0737c7ae7fd396c6c7a91a4641326a1e43be252ba57 out/mut-F2c.txt
|
||||
f3822b75aef0317fefd384f5260c0a4f0301240d72ea9f1cff9c0472acec81aa out/mut-F2d.txt
|
||||
d107089b12645ec903983207985135a5a71ac472eef3465104c3615b78b18e4a out/mut-F2e.txt
|
||||
d28c92f2defb187c1834b4ba63fb337cc656c1aaf7bf23b83cd80f235be836a3 out/mut-F2f.txt
|
||||
3aa5bdcbe2cba59ef458b7ad7b524d7b995168c81462a52c638cd3eabb9c5e7e out/mut-G144.txt
|
||||
05de6f2ae139f346e8f0bc4c1d56bb59bfe83228551f7833c6e9343c8322b303 out/mut-G150.txt
|
||||
e75b166d3aa8f605d394ed5449c20a11ed884c910c756230697d4f387e13cd50 out/mut-J4a.txt
|
||||
593f97f881967eeda72a8dab6027a9b58282ce73c490a690114953247af18c2c out/mut-J4b.txt
|
||||
00aba46c8254eb28a4b5f33d0e5fe73b43fc2c55f9e1b1411096987656f8d45d out/mut-J4c.txt
|
||||
7bd781744b5a42cf5a21aba340c8e40ae39871ef50c7de7a3913f0532ee9f985 out/mut-J4d.txt
|
||||
8f72f99fd972c1d9a6a2e8ca391d88317299c56511de8e8dd0402986421fbb03 out/mut-M28.txt
|
||||
2316655ada74ac0d000ffbb3d27d234a838d036d8ad4266ec147373c841ee9d3 out/mut-N2.txt
|
||||
a6dc80efac48f9de98748433fcac165b4cc3bf000de2b4c04d9107c9b3dfd342 out/mut-N4.txt
|
||||
a0df618c85c05350971ed56203a4d8f37e8a6cc0fa2ba267de0ba34fd197523f out/mut-N5.txt
|
||||
1225000966c8c8c2231ff7091665950f17a6d9d756a46f48b4ae3a8400aa635d out/mutants.txt
|
||||
f14fe1ee4239ae63dfb1bea143d1d450905387a34abbcb9e4c6b09104b75aefa out/node-bus.txt
|
||||
1a88ea66c4b4e6cbf23a420628b53082a770828fb10ddf3ae49cd5b65f4855d0 out/node-business.txt
|
||||
099d2019d935d5c49cecff107316b040a93959ea832375cc008cb8c991004174 out/node-cli.txt
|
||||
9d2ef4cf44c714e9d18a2162b1c34aad8867ca631d66253cf08c11c96dd6f472 out/node-control-board.txt
|
||||
bb4541854d7f2ced39d032f84f36088be1961ec7cf7c591c1196c94157c61858 out/node-conversation.txt
|
||||
9643c28e1490621836a1c64e2b62fad8f44033965cfc68df49187ff5e7c62c33 out/node-discord.txt
|
||||
c5fdadd964fdccc88182f91fa2b724ec7b69d1246b972c25536cc398800127dd out/node-ledger.txt
|
||||
fc0d61d1a3695c44bc5e698523bd790e68bfde3785f38526c601bee852ad3784 out/node-mosaic.txt
|
||||
e2e08d74f8cb590e75d50bc011ca5875d8d49c1470e7f9ee9173cba39946eec9 out/node-queue.txt
|
||||
74dbaa3099f9e3e88219cdc2ae4b83f21fc18ddafe060b299c1dcc5eacf95528 out/node-seat.txt
|
||||
34e473be5528b430fb28d9e78b59092e4bedf8660306590b24b6083ca844a697 out/node-tasks.txt
|
||||
a911209250eef9dbdbb7c762655b29811b42198cf439c363c601993ac30e1a3c out/node-webui.txt
|
||||
8eaa5212718b37938ab792b00210ad00c7b793259ae9e0befc6f7b0316c1881e out/run.txt
|
||||
f91fc95a173339137f981e24fbf630acfde957e32c3cd71e28304532083c6a77 out/suite-auth.txt
|
||||
e1206366f07f3d9029530dcdcb55c0282d3f052adc60fc7fd51642d5d4ec3600 out/suite-conductor.txt
|
||||
52d211444489ea729dafc3c9745d4474d9dfd50f41055cbad792424197f04073 out/suite-config.txt
|
||||
7d5016ded1994048642f8b9ae845e52e8af4a069ba05c056c8f580a4589bacbe out/suite-discord.txt
|
||||
e4c762a4d4225b2f3eab0e37cba1d36126e67465557501196b91c2ffdd576e0e out/suite-extension-package.txt
|
||||
83dfaa21c4e4ad941088efab3f69e87ebd2f6bd3cc72ede938e21dc06da8a34f out/suite-foundation.txt
|
||||
bf5db498b82546af2d78a218a1d6446da993de629f4819fffeecf690ef93a314 out/suite-queue.txt
|
||||
6183e6b9b05edb497e92ffd09a19bba49c6f656c750ecb2acc283021113d1a98 out/suite-release.txt
|
||||
8df77c49bf82833efa56d42401a4a06f20931b7a76dd5eed3a73fdec60e92e79 out/suite-task.txt
|
||||
50ceb870e3f56cac0ea04c7f7995a5eb5ec4e66f9c09a92a9fc7705e1df896cf run.sh
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sequential gate in the candidate worktree; each output kept.
|
||||
# Usage: run.sh <tree> <outdir> [suffix]
|
||||
T="$1"; O="$2"; X="${3:-}"; cd "$T"
|
||||
for d in packages/*/tests; do
|
||||
p=$(basename "$(dirname "$d")")
|
||||
ls "$d"/*.test.mjs >/dev/null 2>&1 || continue
|
||||
env -u NODE_TEST_CONTEXT timeout 1200 node --test "packages/$p/tests/*.test.mjs" > "$O/node-$p$X.txt" 2>&1; echo "node-$p exit $?"
|
||||
done
|
||||
for s in scripts/test-*.sh; do
|
||||
n=$(basename "$s" .sh); n=${n#test-}
|
||||
if [ "$n" = task ] || [ "$n" = release ]; then
|
||||
env -u NODE_TEST_CONTEXT DOCKER_HOST=unix:///nonexistent-rocko-r45-docker.sock timeout 900 bash "$s" > "$O/suite-$n$X.txt" 2>&1; echo "suite-$n exit $?"
|
||||
else
|
||||
env -u NODE_TEST_CONTEXT timeout 900 bash "$s" > "$O/suite-$n$X.txt" 2>&1; echo "suite-$n exit $?"
|
||||
fi
|
||||
done
|
||||
Reference in New Issue
Block a user