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
@@ -0,0 +1,69 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, writeFileSync, readFileSync, existsSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const CLI = fileURLToPath(new URL("../src/cli.js", import.meta.url));
|
||||
|
||||
function run(file, ...args) {
|
||||
return spawnSync(process.execPath, [CLI, ...args], {
|
||||
env: { ...process.env, TODO_FILE: file },
|
||||
encoding: "utf8",
|
||||
});
|
||||
}
|
||||
|
||||
const tempFile = () => join(mkdtempSync(join(tmpdir(), "todo-")), "todos.json");
|
||||
|
||||
test("ls with no file prints the empty message and writes nothing", () => {
|
||||
const file = tempFile();
|
||||
const res = run(file, "ls");
|
||||
assert.equal(res.status, 0);
|
||||
assert.equal(res.stdout.trim(), "Nothing to do.");
|
||||
assert.equal(existsSync(file), false);
|
||||
});
|
||||
|
||||
test("add with no text fails without saving", () => {
|
||||
const file = tempFile();
|
||||
const res = run(file, "add", " ");
|
||||
assert.equal(res.status, 1);
|
||||
assert.match(res.stderr, /Missing todo text/);
|
||||
assert.equal(existsSync(file), false);
|
||||
});
|
||||
|
||||
for (const [label, args, message] of [
|
||||
["missing id", [], /Invalid id: \(missing\)/],
|
||||
["non-numeric id", ["abc"], /Invalid id: abc/],
|
||||
["unknown id", ["99"], /No todo with id 99/],
|
||||
]) {
|
||||
for (const cmd of ["done", "rm"]) {
|
||||
test(`${cmd} with ${label} fails and leaves the file unchanged`, () => {
|
||||
const file = tempFile();
|
||||
run(file, "add", "a");
|
||||
const before = readFileSync(file, "utf8");
|
||||
const res = run(file, cmd, ...args);
|
||||
assert.equal(res.status, 1);
|
||||
assert.match(res.stderr, message);
|
||||
assert.equal(readFileSync(file, "utf8"), before);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
test("unknown command prints usage even when the file is corrupt", () => {
|
||||
const file = tempFile();
|
||||
writeFileSync(file, "{ not json");
|
||||
const res = run(file, "--help");
|
||||
assert.equal(res.status, 1);
|
||||
assert.match(res.stderr, /Unknown command: --help\nUsage:/);
|
||||
});
|
||||
|
||||
test("a corrupt file gives a readable error instead of a stack trace", () => {
|
||||
const file = tempFile();
|
||||
writeFileSync(file, "{ not json");
|
||||
const res = run(file, "ls");
|
||||
assert.equal(res.status, 1);
|
||||
assert.match(res.stderr, /^Could not read /);
|
||||
assert.doesNotMatch(res.stderr, /at .*\.js:\d+/);
|
||||
});
|
||||
Reference in new issue
Block a user