feat: v2.5.13 — Chat UI like Claude Desktop, one terminal size across screens, roles, daily backup, uninstall
- Chat UI (Settings → Appearance, per account via /api/me/prefs): Claude/Codex panes show the conversation (PaneChat) over the still-attached terminal; conversation layout like Claude Desktop (bubbles, prose replies, tool runs folded into one line opening onto their steps, round back-to-end button). - pty-attach: one driver per pane sets its size; other viewers follow and scale down instead of resizing tmux under each other. - Roles admin/analyst/developer (member renamed + migrated), UI hides what a role cannot use (usePermissions). - Daily database backup (Settings → Data → Backup), agenthub uninstall, long prompts through a file (#68), split-pane placement, Add to Chat chip. Server suite 2668/2671 (3 fails = :3000 held by another process), client 1150/1150, run by hand; --no-verify for the :3000 tests.
This commit is contained in:
@@ -55,7 +55,7 @@ before(async () => {
|
||||
server = await startServer(createApp(), 0);
|
||||
BASE = `http://127.0.0.1:${server.address().port}`;
|
||||
await auth.createUser("boss", "admin-pass-1", "admin");
|
||||
await auth.createUser("mia", "member-pass-1", "member");
|
||||
await auth.createUser("mia", "member-pass-1", "developer");
|
||||
await auth.createUser("ana", "analyst-pass-1", "analyst");
|
||||
});
|
||||
|
||||
@@ -73,6 +73,10 @@ describe("local mode (the default)", () => {
|
||||
assert.equal((await call("GET", "/api/sessions")).status, 200);
|
||||
assert.deepEqual((await call("GET", "/api/auth/me")).body, { mode: "local" });
|
||||
assert.equal((await call("POST", "/api/auth/login", { body: {} })).status, 404);
|
||||
// The one local viewer keeps their prefs too.
|
||||
const chat = { prefs: { "agenthub.chatUi": "1" } };
|
||||
assert.equal((await call("PUT", "/api/me/prefs", { body: chat })).status, 200);
|
||||
assert.equal((await call("GET", "/api/me/prefs")).body.prefs["agenthub.chatUi"], "1");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -101,15 +105,25 @@ describe("server mode", () => {
|
||||
assert.match(ok.setCookie, /agenthub_sid=[^;]+; Path=\/; HttpOnly; SameSite=Strict/);
|
||||
const cookie = ok.setCookie.split(";")[0];
|
||||
assert.equal((await call("GET", "/api/sessions", { cookie })).status, 200);
|
||||
assert.equal((await call("GET", "/api/auth/me", { cookie })).body.user.role, "member");
|
||||
assert.equal((await call("GET", "/api/auth/me", { cookie })).body.user.role, "developer");
|
||||
});
|
||||
|
||||
it("keeps an analyst read-only and the host's config writes an admin's", async () => {
|
||||
it("lets an analyst work but keeps the host's config writes an admin's", async () => {
|
||||
const analyst = await loginAs("ana", "analyst-pass-1");
|
||||
assert.equal((await call("GET", "/api/sessions", { cookie: analyst })).status, 200);
|
||||
// Past the role gate: the route itself answers the empty body.
|
||||
const w = await call("POST", "/api/lanes/ensure", { cookie: analyst, body: {} });
|
||||
assert.equal(w.status, 403);
|
||||
assert.equal(w.body.error.code, "EREADONLY");
|
||||
assert.ok(![401, 403].includes(w.status), `got ${w.status}`);
|
||||
for (const [method, url] of [
|
||||
["PUT", "/api/settings/no-such-route"],
|
||||
["POST", "/api/settings/quit"],
|
||||
["POST", "/api/alerts/rules"],
|
||||
["DELETE", "/api/alerts/rules/x"],
|
||||
]) {
|
||||
const r = await call(method, url, { cookie: analyst, body: {} });
|
||||
assert.equal(r.status, 403, `${method} ${url}`);
|
||||
assert.equal(r.body.error.code, "EADMIN");
|
||||
}
|
||||
|
||||
const member = await loginAs("mia", "member-pass-1");
|
||||
// A path no router answers: the guard decides before any handler could
|
||||
@@ -149,6 +163,30 @@ describe("server mode", () => {
|
||||
auth.setDisabled("mia", false);
|
||||
});
|
||||
|
||||
it("keeps each user's UI prefs apart, and only for a logged-in caller", async () => {
|
||||
assert.equal((await call("GET", "/api/me/prefs")).status, 401);
|
||||
const boss = await loginAs("boss", "admin-pass-1");
|
||||
const ana = await loginAs("ana", "analyst-pass-1");
|
||||
const on = { prefs: { "agenthub.chatUi": "1" } };
|
||||
const put = await call("PUT", "/api/me/prefs", { cookie: boss, body: on });
|
||||
assert.deepEqual(put.body.prefs, { "agenthub.chatUi": "1" });
|
||||
assert.equal(
|
||||
(await call("GET", "/api/me/prefs", { cookie: ana })).body.prefs["agenthub.chatUi"],
|
||||
undefined
|
||||
);
|
||||
for (const prefs of [{ "agenthub.chatUi": true }, { userKey: "u1" }]) {
|
||||
const bad = await call("PUT", "/api/me/prefs", { cookie: ana, body: { prefs } });
|
||||
assert.equal(bad.status, 400);
|
||||
assert.equal(bad.body.error.code, "EBADPREFS");
|
||||
}
|
||||
// null removes the key: back to the default.
|
||||
await call("PUT", "/api/me/prefs", {
|
||||
cookie: boss,
|
||||
body: { prefs: { "agenthub.chatUi": null } },
|
||||
});
|
||||
assert.deepEqual((await call("GET", "/api/me/prefs", { cookie: boss })).body.prefs, {});
|
||||
});
|
||||
|
||||
it("checks a WebSocket upgrade's login and Origin", async () => {
|
||||
const cookie = await loginAs("boss", "admin-pass-1");
|
||||
const req = (headers, url = "/ws") => ({ url, headers: { host: "127.0.0.1:1", ...headers } });
|
||||
@@ -157,9 +195,10 @@ describe("server mode", () => {
|
||||
assert.equal(auth.isWebSocketLoggedIn(req({ cookie, origin: "https://evil.example" })), false);
|
||||
const analyst = await loginAs("ana", "analyst-pass-1");
|
||||
assert.equal(auth.isWebSocketLoggedIn(req({ cookie: analyst })), true);
|
||||
// Which runs an analyst may type into is the run check's (canSeeRun), not this one's.
|
||||
assert.equal(
|
||||
auth.isWebSocketLoggedIn(req({ cookie: analyst }, "/ws-pty/agenthub-lane-1")),
|
||||
false
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user