chore: version packages

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-07-20 17:35:40 +02:00
parent 2fdf7dabac
commit 6c8d4ca72f
12 changed files with 593 additions and 17 deletions
+55
View File
@@ -145,3 +145,58 @@ describe('admin API', () => {
}
});
});
describe('admin case-folder API', () => {
const kim = { Authorization: basic('kim', 'kimpass1234') };
it('lists a user case folders (admin only, hidden dirs excluded)', async () => {
// Fresh regular user with a known password (joe's was reset above).
const created = await fetch(url('/api/admin/users'), {
method: 'POST',
headers: admin,
body: JSON.stringify({ username: 'kim', role: 'user', password: 'kimpass1234' }),
});
expect(created.status).toBe(200);
await fs.mkdir(path.join(spacesDir, 'kim', 'cases', 'proj1'), { recursive: true });
await fs.mkdir(path.join(spacesDir, 'kim', 'cases', '.hidden'), { recursive: true });
const forbidden = await fetch(url('/api/admin/users/kim/cases'), { headers: kim });
expect(forbidden.status).toBe(403);
const res = await fetch(url('/api/admin/users/kim/cases'), { headers: adminNoBody });
expect(res.status).toBe(200);
const { data } = await res.json();
expect(data.cases.map((c: { name: string }) => c.name)).toEqual(['proj1']);
expect(data.cases[0].liveSessions).toBe(0);
});
it('404s for an unknown user', async () => {
const res = await fetch(url('/api/admin/users/ghost/cases'), { headers: adminNoBody });
expect(res.status).toBe(404);
});
it('deletes a case folder, refusing unsafe names and symlinks', async () => {
// Traversal-shaped name: rejected before any filesystem access.
const bad = await fetch(url('/api/admin/users/kim/cases/..%2Fescape'), {
method: 'DELETE',
headers: adminNoBody,
});
expect([400, 404]).toContain(bad.status);
// A symlinked "case" is refused, never followed.
await fs.mkdir(path.join(spacesDir, 'outside'), { recursive: true });
await fs.symlink(path.join(spacesDir, 'outside'), path.join(spacesDir, 'kim', 'cases', 'link'));
const sl = await fetch(url('/api/admin/users/kim/cases/link'), { method: 'DELETE', headers: adminNoBody });
expect(sl.status).toBe(400);
await expect(fs.stat(path.join(spacesDir, 'outside'))).resolves.toBeTruthy();
// A real folder is deleted.
const del = await fetch(url('/api/admin/users/kim/cases/proj1'), { method: 'DELETE', headers: adminNoBody });
expect(del.status).toBe(200);
await expect(fs.stat(path.join(spacesDir, 'kim', 'cases', 'proj1'))).rejects.toBeTruthy();
// Deleting it again 404s.
const gone = await fetch(url('/api/admin/users/kim/cases/proj1'), { method: 'DELETE', headers: adminNoBody });
expect(gone.status).toBe(404);
});
});
+65
View File
@@ -27,6 +27,7 @@ function resp(status: number, body: unknown) {
async function bootWith(me: Record<string, unknown>) {
const dom = new JSDOM(
`<!doctype html><body>
<button id="adminPanelBtn" class="btn-admin-panel btn-admin-panel--hidden"></button>
<div class="modal" id="appSettingsModal"><div class="modal-tabs"></div><div class="modal-body"></div></div>
</body>`,
{ url: 'http://localhost/', runScripts: 'outside-only' }
@@ -70,6 +71,65 @@ describe('admin-ui boot', () => {
// Forced: the cancel button is hidden.
expect((modal!.querySelector('#cpCancel') as HTMLElement).style.display).toBe('none');
});
it('reveals the header Admin Panel button for a multi-user admin only', async () => {
const hidden = (w: Window) =>
w.document.getElementById('adminPanelBtn')!.classList.contains('btn-admin-panel--hidden');
const a = await bootWith({ username: 'root', role: 'admin', multiUser: true, mustChangePassword: false });
expect(hidden(a.win)).toBe(false);
const b = await bootWith({ username: 'joe', role: 'user', multiUser: true, mustChangePassword: false });
expect(hidden(b.win)).toBe(true);
const c = await bootWith({ username: 'admin', role: 'admin', multiUser: false, mustChangePassword: false });
expect(hidden(c.win)).toBe(true);
});
});
describe('admin panel modal', () => {
it('opens for an admin, renders users, and shows the case-folder drawer', async () => {
const { win } = await bootWith({ username: 'root', role: 'admin', multiUser: true, mustChangePassword: false });
win.fetch = (async (path: string) => {
if (path === '/api/admin/users')
return resp(200, {
success: true,
data: [
{
username: 'root',
role: 'admin',
disabled: false,
mustChangePassword: false,
canBypassPermissions: true,
createdAt: 1,
lastLoginAt: 2,
stats: { liveSessions: 1, activeSessions: 2, caseCount: 1 },
},
],
});
if (path === '/api/admin/users/root/cases')
return resp(200, {
success: true,
data: { dir: '/tmp/spaces/root/cases', cases: [{ name: 'proj1', modifiedAt: 3, liveSessions: 0 }] },
});
return resp(200, { success: true });
}) as unknown as typeof fetch;
(win as unknown as { codemanAdmin: { openAdminPanel: () => void } }).codemanAdmin.openAdminPanel();
for (let i = 0; i < 6; i++) await new Promise((r) => setTimeout(r, 0));
const modal = win.document.getElementById('adminPanelModal') as HTMLElement;
expect(modal).toBeTruthy();
expect(modal.style.display).toBe('flex');
expect(modal.querySelector('#apTable')!.textContent).toContain('root');
(modal.querySelector('button[data-act="cases"]') as HTMLButtonElement).click();
for (let i = 0; i < 6; i++) await new Promise((r) => setTimeout(r, 0));
expect(modal.textContent).toContain('proj1');
expect(modal.textContent).toContain('/tmp/spaces/root/cases');
});
it('does NOT open for a regular user', async () => {
const { win } = await bootWith({ username: 'joe', role: 'user', multiUser: true, mustChangePassword: false });
(win as unknown as { codemanAdmin: { openAdminPanel: () => void } }).codemanAdmin.openAdminPanel();
expect(win.document.getElementById('adminPanelModal')).toBeFalsy();
});
});
describe('index.html wiring', () => {
@@ -80,4 +140,9 @@ describe('index.html wiring', () => {
expect(admin).toBeGreaterThan(settings);
expect(session).toBeGreaterThan(admin);
});
it('ships the header Admin Panel button hidden by default', () => {
expect(INDEX_HTML).toContain('id="adminPanelBtn"');
expect(INDEX_HTML).toContain('btn-admin-panel--hidden');
});
});