Fix duplicate ids, validate CLI input, harden load/save
- add() uses max id + 1 so ids are never reused after rm - done/rm reject missing, non-numeric and unknown ids; add rejects empty text - load() gives a readable error on bad JSON or non-list data - save() writes a temp file and renames it - CLI checks the command before loading and no longer relies on process.exit for ls - Add regression tests for all of the above Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f0ba80a7bd
commit
ba17376132
4 files changed
+166
-26
No files matched your search
+41
-1
@@ -1,12 +1,28 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { add, complete, remove, format } from "../src/todo.js";
|
||||
import { mkdtempSync, writeFileSync, existsSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { load, save, add, has, complete, remove, format } from "../src/todo.js";
|
||||
|
||||
const tempFile = () => join(mkdtempSync(join(tmpdir(), "todo-")), "todos.json");
|
||||
|
||||
test("add appends an open todo", () => {
|
||||
const todos = add([], "buy milk");
|
||||
assert.deepEqual(todos, [{ id: 1, text: "buy milk", done: false }]);
|
||||
});
|
||||
|
||||
test("add after a remove does not reuse an existing id", () => {
|
||||
const todos = add(remove(add(add([], "a"), "b"), 1), "c");
|
||||
assert.deepEqual(todos.map((t) => t.id), [2, 3]);
|
||||
});
|
||||
|
||||
test("has reports whether an id exists", () => {
|
||||
const todos = add([], "a");
|
||||
assert.equal(has(todos, 1), true);
|
||||
assert.equal(has(todos, 2), false);
|
||||
});
|
||||
|
||||
test("complete marks only the matching todo", () => {
|
||||
const todos = complete(add(add([], "a"), "b"), 2);
|
||||
assert.equal(todos[0].done, false);
|
||||
@@ -21,3 +37,27 @@ test("remove drops the matching todo", () => {
|
||||
test("format renders an empty list", () => {
|
||||
assert.equal(format([]), "Nothing to do.");
|
||||
});
|
||||
|
||||
test("load returns an empty list for a missing file", () => {
|
||||
assert.deepEqual(load(tempFile()), []);
|
||||
});
|
||||
|
||||
test("save then load round-trips and leaves no temp file", () => {
|
||||
const file = tempFile();
|
||||
const todos = add([], "a");
|
||||
save(file, todos);
|
||||
assert.deepEqual(load(file), todos);
|
||||
assert.equal(existsSync(`${file}.tmp`), false);
|
||||
});
|
||||
|
||||
test("load rejects invalid JSON with a readable error", () => {
|
||||
const file = tempFile();
|
||||
writeFileSync(file, "{ not json");
|
||||
assert.throws(() => load(file), /Could not read .*todos\.json/);
|
||||
});
|
||||
|
||||
test("load rejects JSON that is not a list", () => {
|
||||
const file = tempFile();
|
||||
writeFileSync(file, "{}");
|
||||
assert.throws(() => load(file), /expected a list of todos/);
|
||||
});
|
||||
Reference in new issue
Block a user