@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/commhub-server",
3
- "version": "0.9.0-preview.64",
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);
@@ -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
- if (!hasColumn && !hasIssues && !('owner' in body) && !('participants' in body)) return jsonError('empty_patch', 400);
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) });