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:
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user