@sleep2agi/commhub-server 0.9.0-preview.3 → 0.9.0-preview.4
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 +1 -1
- package/src/api-nodes-shape.test.ts +94 -0
- package/src/index.ts +16 -1
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.4",
|
|
4
4
|
"description": "CommHub Server \u2014 AI Agent communication hub with MCP protocol, multi-network isolation, user auth, and 17 MCP tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { afterAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { db } from "./db.js";
|
|
3
|
+
|
|
4
|
+
// Regression test for SELECT * fragility on GET /api/nodes.
|
|
5
|
+
//
|
|
6
|
+
// Prior behavior: `SELECT * FROM nodes` returned whatever columns the
|
|
7
|
+
// table happened to have. When RFC-024 added config_revision +
|
|
8
|
+
// config_snapshot columns (read by the per-node GET /api/nodes/:id/config
|
|
9
|
+
// endpoint, NOT meant for the list endpoint), they silently leaked into
|
|
10
|
+
// the list response and broke V3 Networks tests that asserted on the
|
|
11
|
+
// nodes-list row shape.
|
|
12
|
+
//
|
|
13
|
+
// Current behavior: explicit column list — the response contract is:
|
|
14
|
+
// node_id, node_name, alias, runtime, model, config_path, channels,
|
|
15
|
+
// server, hostname, network_id, created_at, updated_at
|
|
16
|
+
// This test runs the same SQL the handler uses, on a row that DOES have
|
|
17
|
+
// config_revision + config_snapshot populated, and asserts those fields
|
|
18
|
+
// do NOT appear in the returned row. If anyone reverts the handler to
|
|
19
|
+
// SELECT *, or adds an internal column to the SELECT, this test breaks.
|
|
20
|
+
|
|
21
|
+
const TEST_NETWORK = "net_test_shape";
|
|
22
|
+
const TEST_NODE = "node_test_shape_abc";
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
// Best-effort cleanup (test runs in shared test DB; rows are isolated by id)
|
|
26
|
+
try { db.run("DELETE FROM nodes WHERE node_id = ?1", [TEST_NODE]); } catch {}
|
|
27
|
+
});
|
|
28
|
+
afterAll(() => {
|
|
29
|
+
try { db.run("DELETE FROM nodes WHERE node_id = ?1", [TEST_NODE]); } catch {}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("RFC-024 regression — GET /api/nodes response shape (no SELECT *)", () => {
|
|
33
|
+
test("explicit columns: internal config_revision/config_snapshot NOT in response", () => {
|
|
34
|
+
// Insert a row with the leak-prone columns populated. Use INSERT OR
|
|
35
|
+
// REPLACE so concurrent test runs don't trip on PRIMARY KEY.
|
|
36
|
+
db.run(
|
|
37
|
+
`INSERT OR REPLACE INTO nodes (
|
|
38
|
+
node_id, node_name, alias, runtime, model, config_path,
|
|
39
|
+
channels, server, hostname, network_id,
|
|
40
|
+
config_revision, config_snapshot
|
|
41
|
+
) VALUES (
|
|
42
|
+
?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12
|
|
43
|
+
)`,
|
|
44
|
+
[TEST_NODE, "test-shape", "test-shape", "claude-agent-sdk", "claude-opus",
|
|
45
|
+
"/tmp/cfg.json", "[]", "localhost", "test-host", TEST_NETWORK,
|
|
46
|
+
7, JSON.stringify({ model: "claude-opus", flags: { x: true } })],
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Run the same SQL the handler runs (mirror; if handler changes, this
|
|
50
|
+
// assertion still locks the shape).
|
|
51
|
+
const rows = db.all<Record<string, unknown>>(
|
|
52
|
+
`SELECT node_id, node_name, alias, runtime, model,
|
|
53
|
+
config_path, channels, server, hostname,
|
|
54
|
+
network_id, created_at, updated_at
|
|
55
|
+
FROM nodes WHERE node_id = ?1`,
|
|
56
|
+
TEST_NODE,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(rows).toHaveLength(1);
|
|
60
|
+
const row = rows[0]!;
|
|
61
|
+
// Expected fields present
|
|
62
|
+
for (const k of ["node_id", "node_name", "alias", "runtime", "model",
|
|
63
|
+
"config_path", "channels", "server", "hostname",
|
|
64
|
+
"network_id", "created_at", "updated_at"]) {
|
|
65
|
+
expect(row).toHaveProperty(k);
|
|
66
|
+
}
|
|
67
|
+
// Internal fields NOT present (these are the leak the test guards against)
|
|
68
|
+
expect(row).not.toHaveProperty("config_revision");
|
|
69
|
+
expect(row).not.toHaveProperty("config_snapshot");
|
|
70
|
+
// Belt: full key set is exactly the 12 contract fields
|
|
71
|
+
expect(Object.keys(row).sort()).toEqual([
|
|
72
|
+
"alias", "channels", "config_path", "created_at", "hostname",
|
|
73
|
+
"model", "network_id", "node_id", "node_name", "runtime",
|
|
74
|
+
"server", "updated_at",
|
|
75
|
+
]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("internal columns are still readable via dedicated query (config snapshot endpoint path)", () => {
|
|
79
|
+
db.run(
|
|
80
|
+
`INSERT OR REPLACE INTO nodes (node_id, node_name, alias, network_id,
|
|
81
|
+
config_revision, config_snapshot)
|
|
82
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6)`,
|
|
83
|
+
[TEST_NODE, "test-shape", "test-shape", TEST_NETWORK,
|
|
84
|
+
42, JSON.stringify({ model: "claude-opus" })],
|
|
85
|
+
);
|
|
86
|
+
// Mirrors the SELECT in GET /api/nodes/:id/config (server/src/index.ts):
|
|
87
|
+
const cfgRow = db.get<{ config_revision: number; config_snapshot: string }>(
|
|
88
|
+
"SELECT config_revision, config_snapshot FROM nodes WHERE node_id = ?1",
|
|
89
|
+
TEST_NODE,
|
|
90
|
+
);
|
|
91
|
+
expect(cfgRow?.config_revision).toBe(42);
|
|
92
|
+
expect(cfgRow?.config_snapshot).toContain("claude-opus");
|
|
93
|
+
});
|
|
94
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1950,10 +1950,25 @@ Bun.serve({
|
|
|
1950
1950
|
}
|
|
1951
1951
|
|
|
1952
1952
|
// ── REST: nodes table (V2 Sprint 2) ──
|
|
1953
|
+
// Explicit column list — NOT `SELECT *`. Two reasons:
|
|
1954
|
+
// 1. `SELECT *` silently broadcasts any column added later to the
|
|
1955
|
+
// response (RFC-024 added config_revision/config_snapshot which
|
|
1956
|
+
// the dashboard's GET /api/nodes/:id/config endpoint owns;
|
|
1957
|
+
// RFC-028 will add more vault-flavored columns). Tests asserting
|
|
1958
|
+
// response shape break on every ALTER; integrations that key
|
|
1959
|
+
// off Object.keys() can pick up internals.
|
|
1960
|
+
// 2. The list endpoint and the per-node config snapshot endpoint
|
|
1961
|
+
// have different consumer contracts; this list should be the
|
|
1962
|
+
// stable, dashboard-facing fields only.
|
|
1963
|
+
// Add a new column to this list explicitly when a real client needs
|
|
1964
|
+
// it; do NOT switch back to SELECT *.
|
|
1953
1965
|
if (url.pathname === "/api/nodes") {
|
|
1954
1966
|
const nodeId = url.searchParams.get("node_id");
|
|
1955
1967
|
const alias = url.searchParams.get("alias");
|
|
1956
|
-
let sql =
|
|
1968
|
+
let sql = `SELECT node_id, node_name, alias, runtime, model,
|
|
1969
|
+
config_path, channels, server, hostname,
|
|
1970
|
+
network_id, created_at, updated_at
|
|
1971
|
+
FROM nodes WHERE 1=1`;
|
|
1957
1972
|
const params: any[] = [];
|
|
1958
1973
|
sql = addNetworkScope(sql, params, restScope);
|
|
1959
1974
|
if (nodeId) { sql += ` AND node_id = ?${params.length + 1}`; params.push(nodeId); }
|