@sleep2agi/commhub-server 0.9.0-preview.75 → 0.9.0-preview.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sleep2agi/commhub-server",
|
|
3
|
-
"version": "0.9.0-preview.
|
|
3
|
+
"version": "0.9.0-preview.76",
|
|
4
4
|
"description": "CommHub Server — AI Agent communication hub with MCP protocol, multi-network isolation, user auth, and MCP tools (17 collaboration-core + node/provider ops tools; authoritative list: docs-site/docs/api/mcp-tools.md).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// GET /api/requirements/people 的 display_name 字段 —— HTTP 集成测试(真实 Bun.serve,私有端口,临时库)。
|
|
2
|
+
// test2123 在真实 PostgreSQL 上原样再跑一遍(COMMHUB_TEST_PG_URL)。
|
|
3
|
+
//
|
|
4
|
+
// name 保持原样:成员 = display_name,没设回落到用户名;节点 = display_name → alias → node_name(旧 app 只读它)。
|
|
5
|
+
// display_name 另给一份,没设就是 "" —— 分享图等对外场景据此不把 admin 之类的账号名印出去。
|
|
6
|
+
// 成员的 display_name 等于用户名也返回 "":register() 没给显示名时存的就是 username。
|
|
7
|
+
|
|
8
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
9
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
const DIR = mkdtempSync(join(tmpdir(), "anet-req-people-"));
|
|
14
|
+
process.env.COMMHUB_DB ||= join(DIR, "hub.db");
|
|
15
|
+
process.env.COMMHUB_UPLOADS_DIR = join(DIR, "uploads");
|
|
16
|
+
process.env.HOST = "127.0.0.1";
|
|
17
|
+
const PW = "ReqPeoplePassw0rd!x";
|
|
18
|
+
|
|
19
|
+
let BASE = "";
|
|
20
|
+
let hub: any = null;
|
|
21
|
+
let NET = "";
|
|
22
|
+
let owner = { token: "", id: "", username: "" };
|
|
23
|
+
let named = { id: "" };
|
|
24
|
+
let plain = { id: "", username: "" };
|
|
25
|
+
let same = { id: "", username: "" };
|
|
26
|
+
|
|
27
|
+
async function people(token: string) {
|
|
28
|
+
const res = await fetch(`${BASE}/api/requirements/people?network_id=${NET}`, { headers: { Authorization: `Bearer ${token}` } });
|
|
29
|
+
return { status: res.status, body: await res.json() as any };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
beforeAll(async () => {
|
|
33
|
+
const { db } = await import("./db.js");
|
|
34
|
+
const { addNetworkMember, register } = await import("./auth.js");
|
|
35
|
+
const stamp = Date.now();
|
|
36
|
+
const o = register(`rp_owner_${stamp}`, PW, undefined, "");
|
|
37
|
+
expect(o.ok).toBe(true);
|
|
38
|
+
owner = { token: o.token!, id: o.user!.user_id, username: o.user!.username };
|
|
39
|
+
NET = o.network_id!;
|
|
40
|
+
const m = register(`rp_member_${stamp}`, PW, undefined, "");
|
|
41
|
+
expect(m.ok).toBe(true);
|
|
42
|
+
named = { id: m.user!.user_id };
|
|
43
|
+
addNetworkMember(NET, named.id, "member", owner.id, { taskAccess: "all" });
|
|
44
|
+
// owner:没有显示名(= 只有用户名);member:有显示名。
|
|
45
|
+
db.run("UPDATE users SET display_name = NULL WHERE user_id = ?1", [owner.id]);
|
|
46
|
+
db.run("UPDATE users SET display_name = ?1 WHERE user_id = ?2", ["Placeholder Person", named.id]);
|
|
47
|
+
// plain:注册时没给显示名 —— register() 存的是 display_name = username,不改。
|
|
48
|
+
const pl = register(`rp_plain_${stamp}`, PW);
|
|
49
|
+
expect(pl.ok).toBe(true);
|
|
50
|
+
plain = { id: pl.user!.user_id, username: pl.user!.username };
|
|
51
|
+
addNetworkMember(NET, plain.id, "member", owner.id, { taskAccess: "all" });
|
|
52
|
+
// same:显示名被显式设成和用户名一模一样。
|
|
53
|
+
const sm = register(`rp_same_${stamp}`, PW, undefined, "Placeholder Other");
|
|
54
|
+
expect(sm.ok).toBe(true);
|
|
55
|
+
same = { id: sm.user!.user_id, username: sm.user!.username };
|
|
56
|
+
addNetworkMember(NET, same.id, "member", owner.id, { taskAccess: "all" });
|
|
57
|
+
db.run("UPDATE users SET display_name = username WHERE user_id = ?1", [same.id]);
|
|
58
|
+
db.run("INSERT INTO nodes(node_id,node_name,alias,display_name,network_id) VALUES (?1,?2,?3,?4,?5)", ["rp-node-alias", "rp-node-name", "placeholder-alias", "", NET]);
|
|
59
|
+
db.run("INSERT INTO nodes(node_id,node_name,alias,display_name,network_id) VALUES (?1,?2,?3,?4,?5)", ["rp-node-named", "rp-node-name-2", "placeholder-alias-2", "Placeholder Agent", NET]);
|
|
60
|
+
db.run("INSERT INTO nodes(node_id,node_name,network_id) VALUES (?1,?2,?3)", ["rp-node-bare", "rp-node-bare-name", NET]);
|
|
61
|
+
const mod: any = await import("./server.js");
|
|
62
|
+
hub = mod.bootServer({ port: 0, hostname: "127.0.0.1" });
|
|
63
|
+
BASE = `http://127.0.0.1:${hub.port}`;
|
|
64
|
+
}, 30_000);
|
|
65
|
+
|
|
66
|
+
afterAll(() => {
|
|
67
|
+
try { hub?.stop?.(true); } catch {}
|
|
68
|
+
try { rmSync(DIR, { recursive: true, force: true }); } catch {}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("GET /api/requirements/people display_name", () => {
|
|
72
|
+
test("display_name is its own field, empty when unset; name keeps the username / alias fallback", async () => {
|
|
73
|
+
const r = await people(owner.token);
|
|
74
|
+
expect(r.status).toBe(200);
|
|
75
|
+
const find = (kind: string, id: string) => r.body.people.find((p: any) => p.kind === kind && p.id === id);
|
|
76
|
+
expect(find("user", owner.id)).toMatchObject({ name: owner.username, display_name: "", networkId: NET });
|
|
77
|
+
expect(find("user", named.id)).toMatchObject({ name: "Placeholder Person", display_name: "Placeholder Person" });
|
|
78
|
+
expect(find("node", "rp-node-alias")).toMatchObject({ name: "placeholder-alias", display_name: "" });
|
|
79
|
+
expect(find("node", "rp-node-named")).toMatchObject({ name: "Placeholder Agent", display_name: "Placeholder Agent" });
|
|
80
|
+
expect(find("node", "rp-node-bare")).toMatchObject({ name: "rp-node-bare-name", display_name: "" });
|
|
81
|
+
expect(r.body.people.every((p: any) => typeof p.display_name === "string")).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("a member whose display_name is just the username gets display_name \"\" (name still the username)", async () => {
|
|
85
|
+
const { db } = await import("./db.js");
|
|
86
|
+
// 前提是真的:register() 没给显示名时存的就是用户名。
|
|
87
|
+
expect(db.get<{ display_name: string }>("SELECT display_name FROM users WHERE user_id = ?1", plain.id)!.display_name).toBe(plain.username);
|
|
88
|
+
const r = await people(owner.token);
|
|
89
|
+
const find = (id: string) => r.body.people.find((p: any) => p.kind === "user" && p.id === id);
|
|
90
|
+
expect(find(plain.id)).toMatchObject({ name: plain.username, display_name: "" });
|
|
91
|
+
expect(find(same.id)).toMatchObject({ name: same.username, display_name: "" });
|
|
92
|
+
// 显示名只是「包含」用户名、不等于它:照常返回。
|
|
93
|
+
db.run("UPDATE users SET display_name = ?1 WHERE user_id = ?2", [`${same.username} (placeholder)`, same.id]);
|
|
94
|
+
const r2 = await people(owner.token);
|
|
95
|
+
expect(r2.body.people.find((p: any) => p.kind === "user" && p.id === same.id)).toMatchObject({ display_name: `${same.username} (placeholder)` });
|
|
96
|
+
db.run("UPDATE users SET display_name = username WHERE user_id = ?1", [same.id]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("sort order is still by name, then id", async () => {
|
|
100
|
+
const r = await people(owner.token);
|
|
101
|
+
const users = r.body.people.filter((p: any) => p.kind === "user");
|
|
102
|
+
const sorted = [...users].sort((a: any, b: any) => (a.name < b.name ? -1 : a.name > b.name ? 1 : a.id < b.id ? -1 : 1));
|
|
103
|
+
expect(users.map((p: any) => p.id)).toEqual(sorted.map((p: any) => p.id));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("the existing fields keep their shape for old apps", async () => {
|
|
107
|
+
const r = await people(owner.token);
|
|
108
|
+
for (const p of r.body.people) {
|
|
109
|
+
expect(Object.keys(p).sort()).toEqual(["display_name", "id", "kind", "name", "networkId"]);
|
|
110
|
+
expect(typeof p.name).toBe("string");
|
|
111
|
+
expect(p.name.length).toBeGreaterThan(0);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
package/src/requirements.ts
CHANGED
|
@@ -1155,12 +1155,16 @@ async function handleRequirementsRequestInner(ctx: RequirementsRequestContext):
|
|
|
1155
1155
|
if (url.pathname === '/api/requirements/people' && req.method === 'GET') {
|
|
1156
1156
|
const networkId = resolveRestWriteNetworkId(ctx.scope, ctx.auth, ctx.isAdmin);
|
|
1157
1157
|
if (!networkId) return jsonError('network_id_required', 400);
|
|
1158
|
-
|
|
1159
|
-
|
|
1158
|
+
// name = 显示用的名字(没设 display_name 时回落到用户名 / alias,旧 app 只读它);display_name 单独给出、没设为 ""
|
|
1159
|
+
// —— 分享图等对外场景据此判断「只有用户名」,不把 admin 之类的账号名印出去。
|
|
1160
|
+
// 成员的 display_name 等于用户名也算「没设」:register() 在没给显示名时存的就是 username(auth.ts),
|
|
1161
|
+
// 否则没设过显示名的账号(比如 admin)照样把账号名当显示名送出去。
|
|
1162
|
+
const users = db.all<{ id: string; name: string; display_name: string }>(
|
|
1163
|
+
"SELECT u.user_id AS id, COALESCE(NULLIF(u.display_name,''), u.username) AS name, CASE WHEN u.display_name IS NULL OR u.display_name = '' OR u.display_name = u.username THEN '' ELSE u.display_name END AS display_name FROM network_members m JOIN users u ON u.user_id=m.user_id WHERE m.network_id=?1 ORDER BY name, id", networkId,
|
|
1160
1164
|
);
|
|
1161
1165
|
const hidden = hiddenNodeFilter(ctx, networkId);
|
|
1162
|
-
const nodes = db.all<{ id: string; name: string }>(
|
|
1163
|
-
"SELECT node_id AS id, COALESCE(NULLIF(display_name,''), NULLIF(alias,''), node_name) AS name FROM nodes WHERE network_id=?1 ORDER BY name, id", networkId,
|
|
1166
|
+
const nodes = db.all<{ id: string; name: string; display_name: string }>(
|
|
1167
|
+
"SELECT node_id AS id, COALESCE(NULLIF(display_name,''), NULLIF(alias,''), node_name) AS name, COALESCE(display_name,'') AS display_name FROM nodes WHERE network_id=?1 ORDER BY name, id", networkId,
|
|
1164
1168
|
).filter(row => !hidden?.(row.id));
|
|
1165
1169
|
return Response.json({ ok: true, people: [...users.map(row => ({ ...row, kind: 'user', networkId })), ...nodes.map(row => ({ ...row, kind: 'node', networkId }))] });
|
|
1166
1170
|
}
|