fix all seven failing tests — every one was a stale assertion, not a live bug

The suite has been carried at "1038 pass / 7 fail, same as baseline" for long
enough that the count became the thing being checked instead of the tests. All
seven turned out to pin a contract that changed on 2026-08-13, when a batch of
features were switched off pending plugin extraction. None was a defect in
running code; two were tests that could no longer detect what they were written
to detect.

── Five in registry.test.ts, one root cause ──

Switching a feature off comments out its `api:` line but keeps the permission
(the frontend route still needs gating, and existing role grants have to keep
meaning something). Eleven permissions therefore declare no API prefix now.

  • `REAL_WS` still listed 'desktop', which server.tsx stopped serving. The
    file's own header says it mirrors that map by hand and that this is "the
    drift this file keeps catching" — it caught it, and the failure was read as
    baseline for five days.
  • 'refuses a claim on a router that no longer exists' withdrew /gitea, which
    claims nothing any more, so nothing threw. The test could not fail for the
    right reason OR pass. Re-pointed at /tasks, which is core and mounted.
  • Two path→permission tests asserted /api/gitea and /api/caldav resolve.
    Re-pointed at /tasks and at /user vs /users — the latter is a live overlap
    AND a real privilege boundary (resolving /api/users to 'account' would hand
    the ordinary-account permission the user-administration API), where the old
    /dav vs /caldav pair mapped to one permission and never showed the property.
  • 'every permission declares at least one api prefix' stopped being true by
    design. Replaced with what it was protecting — a permission must gate
    SOMETHING reachable, api or route, since one gating nothing can be granted
    and revoked to no effect while reading as a control. `notify` is named as
    the one genuine exception rather than silently tolerated: no API, and no
    screen by design.

── plugins/music/cliamp: a real ordering fix, pushed separately ──

openPlayer checked for the cliamp binary BEFORE validating the path, so a path
escaping the owner's home answered "cliamp not found on host" on any machine
without it. Fixed there (3f4ee82): validate the request, then inspect the host.

── The pty timeout: an env var that had done nothing for five days ──

The test pinned `HOME_DIR: '/tmp'`. `HOME_DIR` was removed from the platform on
2026-08-13 when the install root became derived, and sessions.mjs resolves `~`
with node's `homedir()` — which reads `$HOME`. So the shell opened in the real
home, `pwd` never printed /tmp, and the test spent its full 15s timeout waiting
for output that could not arrive. One character class of a rename, five days
ago, costing 15s of every suite run since.

1044 pass, 0 fail, 85 files (unchanged). Suite is ~15s quicker for the obvious
reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 13:51:16 +00:00
co-authored by Claude Opus 5
parent f6a1194905
commit 5e4f3fe29f
2 changed files with 53 additions and 12 deletions
+46 -10
View File
@@ -18,7 +18,11 @@ import { assertPermissionTotality, isExemptApiPath } from './totality';
// `cliamp` and `cliamp-audio` left on 2026-08-15 with `plugins/music/cliamp/` — see below. `vault` left
// on 2026-08-18 with `plugins/vault/`, taking its `/vault` api prefix and its ws provider with it: a
// plugin's routes are mounted behind the account gate like any other, so neither needs an exemption now.
const REAL_WS = ['terminal', 'chat', 'task-runner', 'pipeline', 'desktop', 'sidecar'];
// `desktop` is NOT here: server.tsx's handlers map has it commented out with the rest of the plugin,
// switched off 2026-08-13. It was listed here until 2026-08-18 and made this file's own totality tests
// fail for five days — the mirror claiming a provider the server does not serve, which is precisely the
// drift the comment above warns about, caught by the check and then ignored because it was "the baseline".
const REAL_WS = ['terminal', 'chat', 'task-runner', 'pipeline', 'sidecar'];
const realApi = () => [...new Set(PERMISSIONS.flatMap((c) => c.api))];
const surface = () => ({
apiPrefixes: [...realApi(), '/auth', '/landing-page-data', '/waitlist', '/sidecar'],
@@ -44,10 +48,13 @@ describe('totality', () => {
expect(() => assertPermissionTotality(s)).toThrow(/websocket provider 'newsocket' is served/);
});
// Used `/gitea`, which claims no API prefix any more (unmounted 2026-08-13), so removing it from the
// served list produced no error and the test could not detect what it was written to detect. `/tasks`
// is core, mounted, and claimed — so withdrawing it is a real "a permission points at nothing" case.
test('refuses a claim on a router that no longer exists', () => {
const s = surface();
s.apiPrefixes = s.apiPrefixes.filter((p) => p !== '/gitea');
expect(() => assertPermissionTotality(s)).toThrow(/claims \/api\/gitea, which nothing mounts/);
s.apiPrefixes = s.apiPrefixes.filter((p) => p !== '/tasks');
expect(() => assertPermissionTotality(s)).toThrow(/claims \/api\/tasks, which nothing mounts/);
});
test('refuses a claim on a socket that is not served', () => {
@@ -74,9 +81,14 @@ describe('totality', () => {
});
describe('path → permission', () => {
// These used to read `/api/gitea`, which stopped resolving on 2026-08-13 when gitea's router was
// unmounted pending its own extraction — the permission still exists and still gates the FRONTEND route,
// but `permissionForApiPath` answers about mounted API prefixes and there is no longer one. Re-pointed
// at `/tasks`, which is core and mounted, so the test exercises the resolver rather than the state of
// whichever feature happens to be mid-extraction.
test('resolves a prefix and its descendants', () => {
expect(permissionForApiPath('/api/gitea')?.key).toBe('gitea');
expect(permissionForApiPath('/api/gitea/repos/a/b')?.key).toBe('gitea');
expect(permissionForApiPath('/api/tasks')?.key).toBe('tasks');
expect(permissionForApiPath('/api/tasks/pipeline/ws')?.key).toBe('tasks');
});
test('does not match a prefix that is merely a string prefix', () => {
@@ -85,9 +97,15 @@ describe('path → permission', () => {
expect(permissionForApiPath('/api/username')).toBeNull();
});
test('longest prefix wins, so /dav and /caldav do not fight', () => {
expect(permissionForApiPath('/api/caldav/x')?.key).toBe('calendar');
expect(permissionForApiPath('/api/dav/x')?.key).toBe('calendar');
// Was `/dav` vs `/caldav`, both of which are unmounted now — and both claimed by the same permission,
// so it never demonstrated the property anyway. `/user` (account) against `/users` (user-admin) is the
// live overlap AND a real privilege boundary: resolving `/api/users` to `account` would hand the
// ordinary-account permission the user-administration API.
test('longest prefix wins, so /user and /users do not fight', () => {
expect(permissionForApiPath('/api/user')?.key).toBe('account');
expect(permissionForApiPath('/api/user/settings')?.key).toBe('account');
expect(permissionForApiPath('/api/users')?.key).toBe('user-admin');
expect(permissionForApiPath('/api/users/3/permissions')?.key).toBe('user-admin');
});
test('an unclaimed path resolves to nothing rather than to something permissive', () => {
@@ -236,7 +254,25 @@ describe('kinds', () => {
expect(GRANTABLE_PERMISSIONS.some((c) => c.key === 'gitea')).toBe(true);
});
test('every permission declares at least one api prefix', () => {
for (const permission of PERMISSIONS) expect(permission.api.length).toBeGreaterThan(0);
/*
Was `every permission declares at least one api prefix`, and that stopped being true on 2026-08-13
rather than becoming wrong: eleven permissions now declare none, because their routers were unmounted
pending plugin extraction while the permission stayed to keep the frontend route gated and existing
role grants meaningful. The test went red and was carried as "baseline" for five days.
What it was actually protecting is still worth protecting — a permission that gates NOTHING can be
granted and revoked to no effect, which is worse than not existing because it reads as a control. So
the invariant becomes "gates something a client can reach": an API prefix, or a frontend route.
*/
test('every permission gates something a client can reach', () => {
// `notify` is the one that genuinely gates nothing today: no API (unmounted with its plugin) and no
// screen of its own by design — it produces notifications FOR other features. Named here rather than
// silently tolerated, so it is a known state and not a hole the test stopped being able to see.
const DORMANT = new Set(['notify']);
for (const permission of PERMISSIONS) {
if (DORMANT.has(permission.key)) continue;
const gates = permission.api.length + (permission.routes?.length ?? 0);
expect(gates).toBeGreaterThan(0);
}
});
});
+7 -2
View File
@@ -117,7 +117,12 @@ describe('pty sidecar', () => {
// The sidecar chooses the shell and the home itself, so pin both rather than spawning the owner's
// interactive zsh, which would read their rc files and their history.
SHELL: '/bin/sh',
HOME_DIR: '/tmp',
// `HOME`, not `HOME_DIR`. This said `HOME_DIR: '/tmp'` until 2026-08-18 and had silently done
// nothing since 2026-08-13, when that variable was removed from the platform entirely (the install
// root is derived now — see `data-path.ts`). `sessions.mjs` resolves `~` with node's `homedir()`,
// which on POSIX reads `$HOME`, so the shell opened in the OWNER's real home and `pwd` never
// printed `/tmp`. The test then waited the full 15s for output that could not arrive.
HOME: '/tmp',
ENV: '/dev/null',
},
stdout: 'ignore',
@@ -132,7 +137,7 @@ describe('pty sidecar', () => {
const a = client(wsBase, `sessionId=${sessionId}&cwd=~&cols=80&rows=24`);
await a.open();
// `~` is resolved by the sidecar against its own HOME_DIR.
// `~` is resolved by the sidecar against its own home — `homedir()` in sessions.mjs, pinned above.
a.send({ type: 'input', data: 'pwd\n' });
await a.await(sawOutput('/tmp'), 'pwd output');