Files
claude-playground/test/cli.test.js
T
christianandClaude Opus 5.5 8dc5194438 Address review: safer save, stricter load and id parsing
- save() follows symlinks, keeps permissions, uses a unique temp file and cleans it up on failure
- load() rejects entries that aren't {id, text, done}
- add() finds the max id with reduce (no spread limit)
- CLI accepts only plain digits as ids and reports save errors readably
- Ignore *.tmp

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 00:26:22 +02:00

88 lines
2.9 KiB
JavaScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { mkdtempSync, writeFileSync, readFileSync, existsSync, chmodSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, dirname } 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/],
["hex id", ["0x1"], /Invalid id: 0x1/],
["exponent id", ["1e0"], /Invalid id: 1e0/],
["decimal id", ["1.0"], /Invalid id: 1\.0/],
["padded id", [" 1 "], /Invalid id: 1 /],
["empty id", [""], /Invalid id: \n/],
["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 failed save gives a readable error instead of a stack trace", () => {
const file = tempFile();
chmodSync(dirname(file), 0o500);
try {
const res = run(file, "add", "a");
assert.equal(res.status, 1);
assert.match(res.stderr, /^Could not save /);
assert.doesNotMatch(res.stderr, /at .*\.js:\d+/);
} finally {
chmodSync(dirname(file), 0o700);
}
});
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+/);
});