Files
AgentHub/server/__tests__/omp-share.test.js
T
nntrivi2001 750d74a2af chore: v2.8.15 — Oh My Pi login sharing, omp --allow-home, Markdown Preview default, first-login password
- omp-share.js: an admin's switch (S3) runs omp's own auth broker + gateway as
  the host on loopback; a member's omp pane reaches it as a scoped client keyed
  by its pane token (clients file = live omp panes only), runs on
  --model agenthub/<model>, never holds a credential.
- omp catalog: --allow-home, so a workspace at the account's home keeps its
  sessions there (they were recorded under a temp dir and dropped).
- Settings → Appearance → Open Markdown as: Preview (default) or Raw, for
  Files/Changes, the config viewer and a chat's file panel.
- First-login password change and password rules, avatar URLs, Artifacts
  "Mine" includes installed shared skills/plugins, a member's own hook pages.
2026-10-22 10:04:30 +07:00

215 lines
8.2 KiB
JavaScript

/**
* @file omp-share.test.js
* @description Oh My Pi login sharing (server/lib/omp-share.js): a member's
* `models.yml` gets one `agenthub` provider pointing at the loopback gateway
* with the pane token's env var NAME as its key (the rest of the file kept, a
* non-mapping refused); the gateway's clients file holds only LIVE omp panes'
* token hashes; a spawn is untouched while sharing is off, for a host pane, or
* without S3; and the route refuses outside S3. No omp daemon is started.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { describe, it, before, after, afterEach } = 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(), "omp-share-"));
process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db");
process.env.DASHBOARD_LIVENESS_PROBE = "0";
process.env.LANES_ROOT = path.join(TMP, "lanes");
process.env.CLAUDE_HOME = path.join(TMP, ".claude");
process.env.DASHBOARD_DATA_DIR = path.join(TMP, "data");
process.env.AGENTHUB_OMP_GATEWAY_PORT = "4999";
const yaml = require("js-yaml");
const { createApp, startServer } = require("../index");
const share = require("../lib/omp-share");
const linux = require("../lib/linux-accounts");
const runAs = require("../lib/run-as");
const tmux = require("../lib/tmux");
const { db } = require("../db");
const realS3 = linux.s3Enabled;
const realLive = tmux.liveSessions;
const flag = path.join(TMP, "data", "omp-share", "enabled");
const clientsFile = path.join(TMP, "data", "omp-share", "clients.json");
function turnOn() {
fs.mkdirSync(path.dirname(flag), { recursive: true });
fs.writeFileSync(flag, "");
linux.s3Enabled = () => true;
}
afterEach(() => {
linux.s3Enabled = realS3;
tmux.liveSessions = realLive;
fs.rmSync(flag, { force: true });
});
after(() => fs.rmSync(TMP, { recursive: true, force: true }));
describe("mergedModels", () => {
it("adds the provider to an empty or missing file", () => {
const doc = yaml.load(share.mergedModels(""));
assert.deepEqual(doc, {
providers: {
agenthub: {
baseUrl: "http://127.0.0.1:4999",
apiKey: "AGENTHUB_PANE_TOKEN",
api: "anthropic-messages",
authHeader: true,
discovery: { type: "proxy" },
},
},
});
});
it("keeps the member's own providers and settings", () => {
const text =
"equivalence:\n x: y\nproviders:\n mine:\n baseUrl: http://h/v1\n apiKey: K\n";
const doc = yaml.load(share.mergedModels(text));
assert.equal(doc.providers.mine.baseUrl, "http://h/v1");
assert.deepEqual(doc.equivalence, { x: "y" });
assert.equal(doc.providers.agenthub.apiKey, "AGENTHUB_PANE_TOKEN");
});
it("writes nothing when the provider is already there as is", () => {
const once = share.mergedModels("");
assert.equal(share.mergedModels(once), null);
});
it("replaces a stale provider of ours", () => {
const stale = "providers:\n agenthub:\n baseUrl: http://127.0.0.1:1\n";
assert.equal(
yaml.load(share.mergedModels(stale)).providers.agenthub.baseUrl,
"http://127.0.0.1:4999"
);
});
it("refuses a file that is not a mapping", () => {
assert.throws(() => share.mergedModels("- a\n- b\n"), /mapping/);
assert.throws(() => share.mergedModels("providers: [1]\n"), /mapping/);
});
});
describe("shareModel", () => {
const host = "anthropic/claude-sonnet-5-5";
it("runs on the host's default when none was asked for", () => {
assert.equal(share.shareModel(null, host), "agenthub/anthropic/claude-sonnet-5-5");
});
it("maps a requested model onto the gateway's provider", () => {
assert.equal(share.shareModel("claude-opus-5-5", host), "agenthub/anthropic/claude-opus-5-5");
assert.equal(share.shareModel("openai-codex/gpt-5.5", host), "agenthub/openai-codex/gpt-5.5");
assert.equal(share.shareModel("agenthub/anthropic/x", host), "agenthub/anthropic/x");
});
it("leaves the choice to omp when there is nothing to go on", () => {
assert.equal(share.shareModel(null, null), null);
});
});
describe("forSpawn", () => {
const member = { login: "ah-mia", home: path.join(TMP, "home-mia") };
it("is null while sharing is off, for a host pane, or without S3", () => {
assert.equal(share.forSpawn(member, "agenthub-lane-1", "h"), null); // off
fs.mkdirSync(path.dirname(flag), { recursive: true });
fs.writeFileSync(flag, "");
assert.equal(share.forSpawn(member, "agenthub-lane-1", "h"), null); // no S3
linux.s3Enabled = () => true;
assert.equal(share.forSpawn(null, "agenthub-lane-1", "h"), null); // the host's
assert.equal(share.forSpawn(member, "agenthub-lane-1", null), null); // no token
});
it("writes the member's models.yml AS them and lists the pane's token", () => {
turnOn();
const writes = [];
const real = { read: runAs.readFileSafe, mkdir: runAs.mkdirAs, write: runAs.writeAs };
runAs.readFileSafe = () => {
const e = new Error("missing");
e.code = "ENOENT";
throw e;
};
runAs.mkdirAs = (acct, dir) => writes.push(["mkdir", acct.login, dir]);
runAs.writeAs = (acct, file, mode, content) => writes.push([mode, acct.login, file, content]);
tmux.liveSessions = () => new Set();
try {
const s = share.forSpawn(member, "agenthub-lane-7", "a".repeat(64));
assert.deepEqual(s.env, { OMP_SKIP_SETUP: "1" });
s.place();
const agentDir = path.join(member.home, ".omp", "agent");
assert.deepEqual(writes[0], ["mkdir", "ah-mia", agentDir]);
assert.equal(writes[1][0], "replace");
assert.equal(writes[1][2], path.join(agentDir, "models.yml"));
assert.equal(yaml.load(writes[1][3]).providers.agenthub.apiKey, "AGENTHUB_PANE_TOKEN");
const clients = JSON.parse(fs.readFileSync(clientsFile, "utf8"));
assert.deepEqual(clients["agenthub-lane-7"], { tokenSha256: "a".repeat(64) });
assert.equal((fs.statSync(clientsFile).mode & 0o777).toString(8), "600");
} finally {
runAs.readFileSafe = real.read;
runAs.mkdirAs = real.mkdir;
runAs.writeAs = real.write;
}
});
});
describe("liveClients", () => {
before(() => {
const ins = db.prepare(
`INSERT INTO dashboard_runs (id, cwd, status, tool, pane_token_hash) VALUES (?, '/x', ?, ?, ?)`
);
ins.run("agenthub-lane-21", "running", "omp", "b".repeat(64));
ins.run("agenthub-lane-22", "running", "omp", "c".repeat(64)); // pane gone
ins.run("agenthub-lane-23", "ended", "omp", "d".repeat(64));
ins.run("agenthub-lane-24", "running", "claude", "e".repeat(64));
});
it("lists only live omp panes", () => {
tmux.liveSessions = () => new Set(["agenthub-lane-21", "agenthub-lane-23", "agenthub-lane-24"]);
const c = share.liveClients();
assert.deepEqual(c["agenthub-lane-21"], { tokenSha256: "b".repeat(64) });
for (const id of ["agenthub-lane-22", "agenthub-lane-23", "agenthub-lane-24"]) {
assert.equal(c[id], undefined, id);
}
});
it("says nothing when tmux could not be asked", () => {
tmux.liveSessions = () => null;
assert.equal(share.liveClients(), null);
});
});
describe("/api/ai-tools/omp-sharing", () => {
let server;
let BASE;
before(async () => {
server = await startServer(createApp(), 0);
BASE = `http://127.0.0.1:${server.address().port}`;
});
after(() => server.close());
it("reports it unavailable and refuses the switch outside S3", async () => {
const got = await (await fetch(`${BASE}/api/ai-tools/omp-sharing`)).json();
assert.equal(got.available, false);
assert.equal(got.enabled, false);
const put = await fetch(`${BASE}/api/ai-tools/omp-sharing`, {
method: "PUT",
headers: { "content-type": "application/json", origin: BASE },
body: JSON.stringify({ enabled: true }),
});
assert.equal(put.status, 409);
assert.equal((await put.json()).error.code, "ENOS3");
assert.equal(fs.existsSync(flag), false);
});
it("wants a boolean", async () => {
const put = await fetch(`${BASE}/api/ai-tools/omp-sharing`, {
method: "PUT",
headers: { "content-type": "application/json", origin: BASE },
body: JSON.stringify({ enabled: "yes" }),
});
assert.equal(put.status, 400);
});
});