project-mgr 0.1.6 → 0.1.7

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/test/core.test.ts CHANGED
@@ -13,9 +13,12 @@ import {
13
13
  databasePath,
14
14
  history,
15
15
  importStatuses,
16
+ listProjects,
16
17
  openLedger,
17
18
  parseTime,
19
+ setProjectMetadata,
18
20
  toPortableStatusEntry,
21
+ validateProjectMetadataInput,
19
22
  validateStatusEntry,
20
23
  ValidationError,
21
24
  } from "../src/index.js";
@@ -55,6 +58,14 @@ test("domain normalizes valid values and rejects invalid input", () => {
55
58
  externalRef: "ref",
56
59
  },
57
60
  );
61
+ assert.ok(
62
+ validateStatusEntry({
63
+ project: "default-time",
64
+ status: "active",
65
+ summary: "sum",
66
+ }).occurredAt,
67
+ );
68
+
58
69
  for (const invalid of [
59
70
  null,
60
71
  [],
@@ -79,12 +90,59 @@ test("domain normalizes valid values and rejects invalid input", () => {
79
90
  });
80
91
  assert.equal("id" in portable, false);
81
92
  assert.equal("recordedAt" in portable, false);
93
+
94
+ assert.deepEqual(
95
+ validateProjectMetadataInput({
96
+ project: " demo ",
97
+ displayName: " Demo Project ",
98
+ description: " Desc ",
99
+ notes: " Notes ",
100
+ }),
101
+ {
102
+ project: "demo",
103
+ displayName: "Demo Project",
104
+ description: "Desc",
105
+ notes: "Notes",
106
+ },
107
+ );
108
+
109
+ assert.deepEqual(
110
+ validateProjectMetadataInput({
111
+ project: "demo",
112
+ description: null,
113
+ notes: "",
114
+ }),
115
+ {
116
+ project: "demo",
117
+ description: null,
118
+ notes: null,
119
+ },
120
+ );
121
+
122
+ for (const invalid of [
123
+ null,
124
+ [],
125
+ { unknownKey: true },
126
+ { project: "bad space" },
127
+ { project: "demo", description: 123 },
128
+ { project: "demo", notes: "x".repeat(2_001) },
129
+ ]) {
130
+ assert.throws(() => validateProjectMetadataInput(invalid), ValidationError);
131
+ }
82
132
  });
83
133
 
84
134
  test("SQLite ledger migrates, appends, is idempotent, and queries by timeline", () => {
85
135
  const db = openLedger(":memory:");
86
- const first = addStatus(db, entry({ externalRef: "one", progress: "done" }));
136
+ const first = addStatus(
137
+ db,
138
+ entry({
139
+ externalRef: "one",
140
+ progress: "done",
141
+ }),
142
+ );
87
143
  assert.equal(first.inserted, true);
144
+ assert.equal(first.entry.progress, "done");
145
+
88
146
  assert.equal(
89
147
  addStatus(db, entry({ summary: "ignored", externalRef: "one" })).inserted,
90
148
  false,
@@ -137,8 +195,64 @@ test("SQLite ledger migrates, appends, is idempotent, and queries by timeline",
137
195
  count: number;
138
196
  }
139
197
  ).count,
140
- 1,
198
+ 2,
141
199
  );
200
+
201
+ // Test setProjectMetadata & listProjects
202
+ const meta = setProjectMetadata(db, {
203
+ project: "project-mgr",
204
+ displayName: "Project Manager",
205
+ description: "Updated Description",
206
+ notes: "Updated Notes",
207
+ });
208
+ assert.equal(meta.displayName, "Project Manager");
209
+ assert.equal(meta.description, "Updated Description");
210
+ assert.equal(meta.notes, "Updated Notes");
211
+
212
+ // Create project with notes only (no description)
213
+
214
+ const notesOnlyMeta = setProjectMetadata(db, {
215
+ project: "notes-only-meta",
216
+ notes: "Only Note",
217
+ });
218
+ assert.equal(notesOnlyMeta.description, undefined);
219
+ assert.equal(notesOnlyMeta.notes, "Only Note");
220
+
221
+ // Create project without status entries
222
+ setProjectMetadata(db, {
223
+ project: "standalone",
224
+ description: "Standalone App",
225
+ });
226
+
227
+ // Create project with tag for search
228
+ addStatus(db, entry({ project: "tag-proj", tags: ["my-special-tag"] }));
229
+ const matchedTag = listProjects(db, { query: "my-special-tag" });
230
+ assert.equal(matchedTag.length, 1);
231
+ assert.equal(matchedTag[0].project, "tag-proj");
232
+
233
+ const allProjects = listProjects(db);
234
+ assert.equal(allProjects.length, 5);
235
+
236
+ const matchedNotes = listProjects(db, { query: "Updated Notes" });
237
+ assert.equal(matchedNotes.length, 1);
238
+ assert.equal(matchedNotes[0].project, "project-mgr");
239
+
240
+ const matchedStandalone = listProjects(db, { query: "Standalone App" });
241
+ assert.equal(matchedStandalone.length, 1);
242
+ assert.equal(matchedStandalone[0].project, "standalone");
243
+
244
+ const matchedNone = listProjects(db, { query: "nonexistent" });
245
+ assert.equal(matchedNone.length, 0);
246
+
247
+ // Test clearing description and notes with null or empty string
248
+ const clearedMeta = setProjectMetadata(db, {
249
+ project: "project-mgr",
250
+ description: null,
251
+ notes: "",
252
+ });
253
+ assert.equal(clearedMeta.description, undefined);
254
+ assert.equal(clearedMeta.notes, undefined);
255
+
142
256
  db.close();
143
257
  });
144
258
 
@@ -209,7 +323,7 @@ test("CLI runs through a package-style symlink", () => {
209
323
  rmSync(directory, { recursive: true, force: true });
210
324
  });
211
325
 
212
- test("CLI supports add, import, reads, errors, and export formats", () => {
326
+ test("CLI supports add, import, reads, errors, project metadata, and export formats", () => {
213
327
  const directory = mkdtempSync(join(tmpdir(), "pmgr-cli-"));
214
328
  const file = join(directory, "ledger.sqlite");
215
329
  const importFile = join(directory, "entries.jsonl");
@@ -253,6 +367,10 @@ test("CLI supports add, import, reads, errors, and export formats", () => {
253
367
  "active",
254
368
  "--summary",
255
369
  "added",
370
+ "--description",
371
+ "CLI project description",
372
+ "--notes",
373
+ "CLI project notes",
256
374
  "--progress",
257
375
  "covered",
258
376
  "--next",
@@ -263,10 +381,118 @@ test("CLI supports add, import, reads, errors, and export formats", () => {
263
381
  "cli",
264
382
  "--external-ref",
265
383
  "cli-add",
384
+ "--at",
385
+ timestamp,
266
386
  "--json",
267
387
  ]),
268
388
  );
269
389
  assert.equal(added.project, "cli");
390
+
391
+ // CLI set-project, projects, and list commands
392
+ assert.match(
393
+ execute([
394
+ "set-project",
395
+ "--project",
396
+ "custom-proj",
397
+ "--display-name",
398
+ "Custom Proj",
399
+ "--description",
400
+ "Custom description",
401
+ "--notes",
402
+ "Custom note",
403
+ "--json",
404
+ ]),
405
+ /Custom Proj/,
406
+ );
407
+
408
+ // Test set-project updates existing project with partial fields
409
+ assert.match(
410
+ execute([
411
+ "set-project",
412
+ "--project",
413
+ "custom-proj",
414
+ "--display-name",
415
+ "Custom Proj Renamed",
416
+ ]),
417
+ /Custom Proj Renamed/,
418
+ );
419
+ execute([
420
+ "set-project",
421
+ "--project",
422
+ "custom-proj",
423
+ "--description",
424
+ "Updated desc only",
425
+ ]);
426
+ execute([
427
+ "set-project",
428
+ "--project",
429
+ "custom-proj",
430
+ "--notes",
431
+ "Updated note only",
432
+ ]);
433
+ execute(["set-project", "--project", "custom-proj"]);
434
+
435
+ // Test list / projects fuzzy search across all branches and human formatting
436
+ assert.match(execute(["projects"]), /cli\tcli/);
437
+ assert.match(execute(["projects"]), /Description: CLI project description/);
438
+ assert.match(execute(["projects"]), /Notes: CLI project notes/);
439
+ assert.match(execute(["projects"]), /Latest Status: \(none\)/);
440
+
441
+ assert.match(
442
+ execute(["list", "--query", "Custom Proj Renamed"]),
443
+ /Custom Proj Renamed/,
444
+ );
445
+ assert.match(
446
+ execute(["list", "--query", "Updated desc only"]),
447
+ /Updated desc only/,
448
+ );
449
+ assert.match(
450
+ execute(["list", "--query", "Updated note only"]),
451
+ /Updated note only/,
452
+ );
453
+ assert.match(execute(["projects", "--query", "active"]), /cli/);
454
+ assert.match(execute(["projects", "--query", "added"]), /cli/);
455
+ assert.match(execute(["projects", "--query", "covered"]), /cli/);
456
+ assert.match(execute(["projects", "--query", "cli"]), /cli/);
457
+ assert.equal(
458
+ execute(["projects", "--query", "no-match"]),
459
+ "No projects found.\n",
460
+ );
461
+ assert.equal(execute(["projects", "--query", "no-match", "--json"]), "[]\n");
462
+
463
+ // Test clearing description and notes via CLI flags
464
+ assert.throws(
465
+ () =>
466
+ execute([
467
+ "set-project",
468
+ "--project",
469
+ "custom-proj",
470
+ "--description",
471
+ "desc",
472
+ "--clear-description",
473
+ ]),
474
+ /cannot specify both --description and --clear-description/,
475
+ );
476
+ assert.throws(
477
+ () =>
478
+ execute([
479
+ "set-project",
480
+ "--project",
481
+ "custom-proj",
482
+ "--notes",
483
+ "note",
484
+ "--clear-notes",
485
+ ]),
486
+ /cannot specify both --notes and --clear-notes/,
487
+ );
488
+ execute([
489
+ "set-project",
490
+ "--project",
491
+ "custom-proj",
492
+ "--clear-description",
493
+ "--clear-notes",
494
+ ]);
495
+
270
496
  assert.match(
271
497
  execute([
272
498
  "status",
@@ -277,6 +503,8 @@ test("CLI supports add, import, reads, errors, and export formats", () => {
277
503
  "active",
278
504
  "--summary",
279
505
  "human output",
506
+ "--at",
507
+ timestamp,
280
508
  ]),
281
509
  /human output/,
282
510
  );
@@ -318,8 +546,9 @@ test("CLI supports add, import, reads, errors, and export formats", () => {
318
546
  JSON.parse(execute(["history", "--limit", "2", "--json"])).map(
319
547
  (item: { project: string }) => item.project,
320
548
  ),
321
- ["human", "cli"],
549
+ ["array", "json"],
322
550
  );
551
+
323
552
  assert.equal(
324
553
  JSON.parse(
325
554
  execute([
@@ -372,7 +601,9 @@ test("CLI supports add, import, reads, errors, and export formats", () => {
372
601
  () => execute(["status", "history", "--project", "cli", "--limit", "1.5"]),
373
602
  ValidationError,
374
603
  );
604
+ assert.throws(() => execute(["set-project"]), ValidationError);
375
605
  assert.throws(() => execute(["status", "add", "--project"]), ValidationError);
606
+
376
607
  assert.throws(
377
608
  () =>
378
609
  execute(["status", "current", "--project", "cli", "--project", "again"]),