Lock during updates, repair duplicate ids, keep file identity on save

- CLI holds a lock file from load to save, so concurrent commands can't
  lose an update; stale locks from crashed runs are taken over
- load() renumbers duplicate ids from the old add() bug instead of
  refusing the file
- save() keeps owner, group and setgid on the temp file, and falls back to
  an in-place write for hard-linked files, files whose owner/group can't
  be kept, and directories that aren't writable

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
christianandClaude Opus 5.5 committed 2026-10-21 00:49:47 +02:00
1 parent f09d20cb84
commit dab492da1e
5 files changed
+128 -17

No files matched your search

+30 -4
View File
@@ -1,6 +1,6 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, writeFileSync, readdirSync, symlinkSync, lstatSync, statSync, chmodSync } from "node:fs";
import { mkdtempSync, writeFileSync, readdirSync, symlinkSync, linkSync, lstatSync, statSync, chmodSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, dirname } from "node:path";
import { load, save, add, has, complete, remove, format } from "../src/todo.js";
@@ -70,10 +70,36 @@ test("save writes through a symlink instead of replacing it", () => {
assert.deepEqual(load(real).map((t) => t.text), ["a"]);
});
test("load rejects duplicate ids left by the old add() bug", () => {
test("load renumbers duplicate ids left by the old add() bug", () => {
const file = tempFile();
writeFileSync(file, JSON.stringify([{ id: 2, text: "b", done: false }, { id: 2, text: "c", done: false }]));
assert.throws(() => load(file), /id 2 is used more than once/);
writeFileSync(file, JSON.stringify([
{ id: 2, text: "b", done: false },
{ id: 2, text: "c", done: true },
{ id: 3, text: "d", done: false },
]));
assert.deepEqual(load(file).map((t) => [t.id, t.text, t.done]), [[2, "b", false], [4, "c", true], [3, "d", false]]);
});
test("save writes in place when the file has other hard links", () => {
const file = tempFile();
const other = join(dirname(file), "other.json");
save(file, []);
linkSync(file, other);
save(file, add([], "a"));
assert.deepEqual(load(other).map((t) => t.text), ["a"]);
});
test("save writes in place when only the file, not its directory, is writable", { skip: isRoot && "root ignores file permissions" }, () => {
const file = tempFile();
save(file, []);
chmodSync(dirname(file), 0o500);
try {
save(file, add([], "a"));
assert.deepEqual(load(file).map((t) => t.text), ["a"]);
assert.deepEqual(readdirSync(dirname(file)), ["todos.json"]);
} finally {
chmodSync(dirname(file), 0o700);
}
});
test("save through a dangling symlink creates the target and keeps the link", () => {