Files
AgentHub/server/__tests__/hub-boundary.test.js
T
nntrivi2001 c946b73d11 chore: v2.8.71 — remote clients (company hub), artifacts shared with chosen people, folders and viewable copies, fixes
Remote clients (docs/plans/remote-clients/README.md, B1–B3 for the Claude pool):
- server/hub/ holds the hub-only code (devices, remote sessions, upload ingest,
  LAN gateway), required only in server mode; hub-boundary.test.js keeps local
  code from importing it.
- A local install enrolls with `agenthub remote login` or Settings → AI tools →
  Company hub; the "Company account" AI Proxy (reserved name agenthub.company)
  sends a session through the hub's gateway, which adds a pool account's token —
  no credential reaches the machine. Only those sessions are uploaded, hourly
  and at their end; secret-shaped strings are redacted on the client.
- Gateway refuses: unknown device (401), a message without a session id, an
  unregistered session under AGENTHUB_REMOTE_GATE=block, a user over quota
  (EQUOTA), a purged session (410). Device tokens 90 days, conversations 30.
- New alert rule type remote_unmonitored: a company-account session its device
  never registered; a late registration clears the flag instead.
- The hub imports a session once per upload round (chunks carry `more`),
  not once per chunk.

Artifacts:
- Share an artifact with chosen people (artifact_shares, PATCH sharedWith,
  GET /api/artifacts/people) besides everyone; a "Shared with you" tab.
  Only the owner or an admin sees or edits who it is shared with; audited.
- Folders: each account's own arrangement (agenthub.artifacts.folders pref).
- A claude.ai page keeps a copy viewable without the publishing account: its
  HTML file (and supporting files) snapshotted at publish, or the copy Claude
  saves for a Doc / Slides / Design in artifacts-export/ (a synchronous
  PostToolUse hint hook, scripts/artifact-export-hint.js, admin switch),
  attached to the page at Stop. Viewed in HtmlFrame's sandbox, Markdown, or
  pdf.js (pdfjs-dist 6.4.299, legacy build, loaded only for a PDF).

Fixes:
- tmux: a cached /proc/<pid>/exe of an exited server falls back to PATH's
  tmux (stop the last pane, start again within 10 s failed with ENOENT).
- shelves: a shelf restores beside changes in other files.
- chat: a finished reply ends the turn even with no hook at all.
- Settings → AI tools: profile list display tweaks.
2026-11-04 15:16:49 +07:00

70 lines
2.6 KiB
JavaScript

/**
* @file hub-boundary.test.js
* @description `server/hub/` (the remote-clients hub, server mode only) is
* loaded only behind `isServerMode()`: no file outside it may require it,
* except `server/index.js` on lines that test the mode themselves. And a
* local-mode app really leaves it out — no module under `server/hub/` is in
* the require cache after `createApp()` in local mode.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { describe, it } = 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 ROOT = path.join(__dirname, "..", "..");
const HUB = path.join(ROOT, "server", "hub");
const SCAN = ["server", "bin", "scripts", "mcp", "statusline"];
const REQUIRE_HUB = /require\(\s*["'`][^"'`]*\bhub(?:\/[^"'`]*)?["'`]\s*\)/;
function jsFiles(dir) {
const out = [];
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, e.name);
if (e.isDirectory()) {
if (["node_modules", "__tests__", "dist"].includes(e.name) || full === HUB) continue;
out.push(...jsFiles(full));
} else if (/\.(c|m)?js$/.test(e.name)) {
out.push(full);
}
}
return out;
}
describe("server/hub boundary", () => {
it("nothing outside server/hub requires it, except index.js behind isServerMode()", () => {
const offenders = [];
for (const top of SCAN) {
const dir = path.join(ROOT, top);
if (!fs.existsSync(dir)) continue;
for (const file of jsFiles(dir)) {
const lines = fs.readFileSync(file, "utf8").split("\n");
lines.forEach((line, i) => {
if (!REQUIRE_HUB.test(line)) return;
const rel = path.relative(ROOT, file);
if (rel === path.join("server", "index.js") && /isServerMode\(\)/.test(line)) return;
offenders.push(`${rel}:${i + 1}: ${line.trim()}`);
});
}
}
assert.deepEqual(offenders, []);
});
it("a local-mode app loads no module of server/hub", () => {
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "hub-boundary-"));
process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db");
process.env.CLAUDE_HOME = path.join(TMP, ".claude");
process.env.DASHBOARD_DATA_DIR = path.join(TMP, "data");
delete process.env.AGENTHUB_MODE;
try {
require("../index").createApp();
const loaded = Object.keys(require.cache).filter((k) => k.startsWith(HUB + path.sep));
assert.deepEqual(loaded, []);
} finally {
fs.rmSync(TMP, { recursive: true, force: true });
}
});
});