@sleep2agi/commhub-server 0.9.0-preview.64 → 0.9.0-preview.65
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/requirements-http.test.ts +45 -0
- package/src/requirements.ts +28 -3
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.65",
|
|
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",
|
|
@@ -187,6 +187,51 @@ describe("requirements stay on the hub", () => {
|
|
|
187
187
|
expect(bad.status).toBe(400);
|
|
188
188
|
});
|
|
189
189
|
|
|
190
|
+
test("an existing card can change its text without dropping omitted fields", async () => {
|
|
191
|
+
const { db } = await import("./db.js");
|
|
192
|
+
const ownerId = db.get<{ owner_id: string }>("SELECT owner_id FROM networks WHERE network_id=?1", ownerNetwork)!.owner_id;
|
|
193
|
+
const created = await api(ownerToken, "/api/requirements", {
|
|
194
|
+
method: "POST",
|
|
195
|
+
body: JSON.stringify({
|
|
196
|
+
name: "旧标题",
|
|
197
|
+
priority: "low",
|
|
198
|
+
assignee: "node-a",
|
|
199
|
+
due: "2026-10-01",
|
|
200
|
+
column: "doing",
|
|
201
|
+
owner: { kind: "user", id: ownerId },
|
|
202
|
+
}),
|
|
203
|
+
});
|
|
204
|
+
expect(created.status).toBe(201);
|
|
205
|
+
const id = created.body.requirement.id;
|
|
206
|
+
const renamed = await api(ownerToken, `/api/requirements/${id}`, {
|
|
207
|
+
method: "PATCH",
|
|
208
|
+
body: JSON.stringify({ name: "新标题" }),
|
|
209
|
+
});
|
|
210
|
+
expect(renamed.status).toBe(200);
|
|
211
|
+
expect(renamed.body.requirement.name).toBe("新标题");
|
|
212
|
+
expect(renamed.body.requirement.priority).toBe("low");
|
|
213
|
+
expect(renamed.body.requirement.assignee).toBe("node-a");
|
|
214
|
+
expect(renamed.body.requirement.due).toBe("2026-10-01");
|
|
215
|
+
expect(renamed.body.requirement.column).toBe("doing");
|
|
216
|
+
expect(renamed.body.requirement.owner).toEqual({ kind: "user", id: ownerId });
|
|
217
|
+
const cleared = await api(ownerToken, `/api/requirements/${id}`, {
|
|
218
|
+
method: "PATCH",
|
|
219
|
+
body: JSON.stringify({ due: "", assignee: "", priority: "high" }),
|
|
220
|
+
});
|
|
221
|
+
expect(cleared.status).toBe(200);
|
|
222
|
+
expect(cleared.body.requirement.name).toBe("新标题");
|
|
223
|
+
expect(cleared.body.requirement.due).toBe("");
|
|
224
|
+
expect(cleared.body.requirement.assignee).toBe("");
|
|
225
|
+
expect(cleared.body.requirement.priority).toBe("high");
|
|
226
|
+
expect(cleared.body.requirement.owner).toEqual({ kind: "user", id: ownerId });
|
|
227
|
+
expect((await api(ownerToken, `/api/requirements/${id}`, { method: "PATCH", body: JSON.stringify({ name: " " }) })).status).toBe(400);
|
|
228
|
+
expect((await api(ownerToken, `/api/requirements/${id}`, { method: "PATCH", body: JSON.stringify({ due: "2026-13-01" }) })).status).toBe(400);
|
|
229
|
+
expect((await api(viewerToken, `/api/requirements/${id}`, { method: "PATCH", body: JSON.stringify({ name: "越权" }) })).status).toBe(403);
|
|
230
|
+
expect((await api(otherToken, `/api/requirements/${id}`, { method: "PATCH", body: JSON.stringify({ name: "别人的" }) })).status).toBe(404);
|
|
231
|
+
const listed = await api(ownerToken, "/api/requirements");
|
|
232
|
+
expect(listed.body.requirements.find((row: { id: string }) => row.id === id).name).toBe("新标题");
|
|
233
|
+
});
|
|
234
|
+
|
|
190
235
|
test("empty name and a bad date are rejected", async () => {
|
|
191
236
|
const empty = await api(ownerToken, "/api/requirements", { method: "POST", body: JSON.stringify({ name: " " }) });
|
|
192
237
|
expect(empty.status).toBe(400);
|
package/src/requirements.ts
CHANGED
|
@@ -234,7 +234,11 @@ export async function handleRequirementsRequest(ctx: RequirementsRequestContext)
|
|
|
234
234
|
try { body = await bodyObject(req); } catch { return jsonError("invalid_json", 400); }
|
|
235
235
|
const hasColumn = 'column' in body;
|
|
236
236
|
const hasIssues = Object.prototype.hasOwnProperty.call(body, "issues");
|
|
237
|
-
|
|
237
|
+
const hasName = 'name' in body;
|
|
238
|
+
const hasPriority = 'priority' in body;
|
|
239
|
+
const hasDue = 'due' in body;
|
|
240
|
+
const hasAssignee = 'assignee' in body;
|
|
241
|
+
if (!hasColumn && !hasIssues && !hasName && !hasPriority && !hasDue && !hasAssignee && !('owner' in body) && !('participants' in body)) return jsonError('empty_patch', 400);
|
|
238
242
|
if (hasColumn && !COLUMNS.has(String(body.column))) return jsonError("invalid_column", 400);
|
|
239
243
|
const issues = hasIssues ? normalizeIssues(body.issues) : null;
|
|
240
244
|
if (hasIssues && issues === null) return jsonError("invalid_issues", 400);
|
|
@@ -244,12 +248,33 @@ export async function handleRequirementsRequest(ctx: RequirementsRequestContext)
|
|
|
244
248
|
const row = db.get<Row>(sql, ...params);
|
|
245
249
|
if (!row) return jsonError("requirement_not_found", 404);
|
|
246
250
|
if (!canWrite(ctx, row.network_id)) return jsonError("permission_denied", 403);
|
|
251
|
+
let name = row.title;
|
|
252
|
+
if (hasName) {
|
|
253
|
+
name = typeof body.name === "string" ? body.name.trim() : "";
|
|
254
|
+
if (!name || name.length > 80) return jsonError("invalid_name", 400);
|
|
255
|
+
}
|
|
256
|
+
let priority = row.priority;
|
|
257
|
+
if (hasPriority) {
|
|
258
|
+
if (typeof body.priority !== "string" || !PRIORITIES.has(body.priority)) return jsonError("invalid_priority", 400);
|
|
259
|
+
priority = body.priority;
|
|
260
|
+
}
|
|
261
|
+
let due = row.due_on || "";
|
|
262
|
+
if (hasDue) {
|
|
263
|
+
if (typeof body.due !== "string") return jsonError("invalid_due", 400);
|
|
264
|
+
due = body.due.trim();
|
|
265
|
+
if (due && !dueOk(due)) return jsonError("invalid_due", 400);
|
|
266
|
+
}
|
|
267
|
+
let assignee = row.assignee || "";
|
|
268
|
+
if (hasAssignee) {
|
|
269
|
+
if (typeof body.assignee !== "string") return jsonError("invalid_assignee", 400);
|
|
270
|
+
assignee = body.assignee.trim().slice(0, 80);
|
|
271
|
+
}
|
|
247
272
|
let people;
|
|
248
273
|
try { people = assignments(body, row.network_id, row); } catch (e) { return jsonError((e as Error).message, 400); }
|
|
249
274
|
const updatedAt = new Date().toISOString();
|
|
250
275
|
db.run(
|
|
251
|
-
"UPDATE requirements SET column_name = ?1, updated_at = ?2, owner_json = ?4, participants_json = ?5, issues_json = ?6 WHERE requirement_id = ?3",
|
|
252
|
-
[hasColumn ? body.column : row.column_name, updatedAt, row.requirement_id, people.ownerJson, people.participantsJson, hasIssues ? JSON.stringify(issues) : row.issues_json],
|
|
276
|
+
"UPDATE requirements SET column_name = ?1, updated_at = ?2, title = ?7, priority = ?8, due_on = ?9, assignee = ?10, owner_json = ?4, participants_json = ?5, issues_json = ?6 WHERE requirement_id = ?3",
|
|
277
|
+
[hasColumn ? body.column : row.column_name, updatedAt, row.requirement_id, people.ownerJson, people.participantsJson, hasIssues ? JSON.stringify(issues) : row.issues_json, name, priority, due || null, assignee],
|
|
253
278
|
);
|
|
254
279
|
const updated = db.get<Row>(`SELECT ${SELECT} FROM requirements WHERE requirement_id = ?1`, row.requirement_id)!;
|
|
255
280
|
return Response.json({ ok: true, requirement: toPublic(updated) });
|