18e574a765
A pane's tab shows the other people who have it open (pane-presence.js, in memory, fed by the /ws-pty attach; GET /api/lanes/:id/presence and the pane_presence broadcast to the owner and full collaborators) and says "Billed to <name>" for a pane a collaborator opened (startedBy on the run). The first real keystroke of each attach into a shared workspace's pane by anyone but its owner is audited as `type into <run id>` (mouse, focus and terminal replies to tmux do not count). The pty socket now pings, so a vanished browser is ended at the next re-check and leaves the presence list.
129 lines
4.5 KiB
JavaScript
129 lines
4.5 KiB
JavaScript
/**
|
|
* @file pane-presence.test.js
|
|
* @description Who has which pane open (server/lib/pane-presence.js, sharing
|
|
* phase 4): a user counted once however many attaches they hold, gone with
|
|
* their last one, listed per workspace; the first keystroke into a SHARED
|
|
* workspace's pane is audited (who and where, never what) and an unshared
|
|
* one's is not; `pane_presence` reaches the workspace's owner and the users
|
|
* it is shared with, nobody else.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
const { describe, it, before, after, beforeEach } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const path = require("node:path");
|
|
const fs = require("node:fs");
|
|
const os = require("node:os");
|
|
|
|
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "pane-presence-test-"));
|
|
process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db");
|
|
|
|
const { db } = require("../db");
|
|
const presence = require("../lib/pane-presence");
|
|
const scope = require("../lib/scope");
|
|
|
|
let alice;
|
|
let bob;
|
|
let carol;
|
|
let lane;
|
|
let lonely;
|
|
|
|
before(() => {
|
|
const user = (name) =>
|
|
Number(
|
|
db
|
|
.prepare("INSERT INTO users (username, password_hash, role) VALUES (?, 'x', 'developer')")
|
|
.run(name).lastInsertRowid
|
|
);
|
|
alice = user("alice");
|
|
bob = user("bob");
|
|
carol = user("carol");
|
|
const mk = () =>
|
|
Number(
|
|
db
|
|
.prepare("INSERT INTO lanes (cwd, tool, owner_id) VALUES (?, 'claude', ?)")
|
|
.run(`/w/${Math.random()}`, alice).lastInsertRowid
|
|
);
|
|
lane = mk();
|
|
lonely = mk();
|
|
db.prepare("INSERT INTO lane_shares (lane_id, user_id) VALUES (?, ?)").run(lane, bob);
|
|
});
|
|
|
|
after(() => fs.rmSync(TMP, { recursive: true, force: true }));
|
|
beforeEach(() => presence.__reset());
|
|
|
|
describe("pane presence", () => {
|
|
it("counts a user once per pane, until their last attach closes", () => {
|
|
const run = `agenthub-lane-${lane}-1`;
|
|
presence.join(run, { id: bob, username: "bob", displayName: "Bob B" });
|
|
presence.join(run, { id: bob, username: "bob" });
|
|
presence.join(run, { id: alice, username: "alice" });
|
|
assert.deepEqual(presence.forLane(lane)[run], [
|
|
{ id: bob, name: "Bob B" },
|
|
{ id: alice, name: "alice" },
|
|
]);
|
|
presence.leave(run, { id: bob });
|
|
assert.equal(presence.forLane(lane)[run].length, 2, "one of bob's attaches is still open");
|
|
presence.leave(run, { id: bob });
|
|
presence.leave(run, { id: alice });
|
|
assert.deepEqual(presence.forLane(lane), {});
|
|
presence.join("not-a-run", { id: bob });
|
|
assert.deepEqual(presence.users("not-a-run"), []);
|
|
});
|
|
|
|
it("audits the first keystroke only in a shared workspace", () => {
|
|
const count = () =>
|
|
db.prepare("SELECT COUNT(*) AS n FROM audit_log WHERE path LIKE 'type into %'").get().n;
|
|
presence.auditInput(`agenthub-lane-${lonely}`, { id: alice });
|
|
assert.equal(count(), 0);
|
|
presence.auditInput(`agenthub-lane-${lane}-2`, { id: bob });
|
|
const row = db
|
|
.prepare(
|
|
"SELECT user_id, owner_id, method, path FROM audit_log WHERE path LIKE 'type into %'"
|
|
)
|
|
.get();
|
|
assert.deepEqual(row, {
|
|
user_id: bob,
|
|
owner_id: alice,
|
|
method: "PTY",
|
|
path: `type into agenthub-lane-${lane}-2`,
|
|
});
|
|
});
|
|
|
|
it("is news for the workspace's owner and collaborators only", () => {
|
|
const aud = scope.broadcastAudience("pane_presence", { laneId: lane, runId: "x", users: [] });
|
|
const dev = (id) => ({ id, role: "developer" });
|
|
assert.equal(scope.audienceIncludes(aud, dev(alice)), true);
|
|
assert.equal(scope.audienceIncludes(aud, dev(bob)), true);
|
|
assert.equal(scope.audienceIncludes(aud, dev(carol)), false);
|
|
});
|
|
});
|
|
|
|
describe("what counts as typing", () => {
|
|
const { isTyping } = require("../lib/pty-attach");
|
|
it("ignores mouse, focus and the terminal answering tmux", () => {
|
|
for (const s of [
|
|
"\x1b[<0;10;5M",
|
|
"\x1b[I",
|
|
"\x1b[>0;276;0c",
|
|
"\x1b[?1;2c",
|
|
"\x1b[12;40R",
|
|
"\x1b[0n",
|
|
"\x1b[?2004;1$y",
|
|
"\x1b]11;rgb:1e1e/1e1e/1e1e\x1b\\",
|
|
"\x1b]10;rgb:ffff/ffff/ffff\x07",
|
|
"\x1bP>|xterm.js(6.0.0)\x1b\\",
|
|
]) {
|
|
assert.equal(isTyping(s), false, JSON.stringify(s));
|
|
}
|
|
assert.equal(isTyping("ls\r"), true);
|
|
assert.equal(isTyping("\x1b[A"), true, "an arrow key is typing");
|
|
});
|
|
|
|
it("never audits the owner's own typing", () => {
|
|
const before = db.prepare("SELECT COUNT(*) AS n FROM audit_log").get().n;
|
|
presence.auditInput(`agenthub-lane-${lane}`, { id: alice });
|
|
assert.equal(db.prepare("SELECT COUNT(*) AS n FROM audit_log").get().n, before);
|
|
});
|
|
});
|