@mmnto/totem 1.112.0 → 1.113.0
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/dist/estate-scan.d.ts +7 -0
- package/dist/estate-scan.d.ts.map +1 -1
- package/dist/estate-scan.js +5 -0
- package/dist/estate-scan.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/worktree-registry.d.ts +210 -0
- package/dist/worktree-registry.d.ts.map +1 -0
- package/dist/worktree-registry.js +286 -0
- package/dist/worktree-registry.js.map +1 -0
- package/dist/worktree-registry.test.d.ts +11 -0
- package/dist/worktree-registry.test.d.ts.map +1 -0
- package/dist/worktree-registry.test.js +297 -0
- package/dist/worktree-registry.test.js.map +1 -0
- package/dist/worktree-residue.d.ts +69 -0
- package/dist/worktree-residue.d.ts.map +1 -0
- package/dist/worktree-residue.js +228 -0
- package/dist/worktree-residue.js.map +1 -0
- package/dist/worktree-residue.test.d.ts +12 -0
- package/dist/worktree-residue.test.d.ts.map +1 -0
- package/dist/worktree-residue.test.js +178 -0
- package/dist/worktree-residue.test.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The user-level worktree registry (`~/.totem/worktrees.json`) behind
|
|
3
|
+
* `totem wt create|remove|list` (mmnto-ai/totem#2580 slice-2).
|
|
4
|
+
*
|
|
5
|
+
* A SIBLING file to `registry.json`, never a key inside it: `RegistrySchema` is
|
|
6
|
+
* `z.record(repoPath, RegistryEntrySchema)`, so any non-repo-entry key would
|
|
7
|
+
* fail schema validation and flip the WHOLE sync registry to "unreadable". The
|
|
8
|
+
* two files share one lock directory (`~/.totem`) and nothing else — the read
|
|
9
|
+
* path of `registry.json` is untouched by every verb here.
|
|
10
|
+
*
|
|
11
|
+
* Two independent lifecycles live in this file, and the independence is the
|
|
12
|
+
* point:
|
|
13
|
+
* - `roots[]` accretes every container root a worktree was ever created
|
|
14
|
+
* under and is NEVER pruned by removal. It is what keeps a recorded
|
|
15
|
+
* location reachable for `doctor --estate` after the last live entry under
|
|
16
|
+
* it is gone (PR #2586 round-3 MINOR-3).
|
|
17
|
+
* - `worktrees{}` is entry accounting: written BEFORE `git worktree add`
|
|
18
|
+
* (an intent record — a phantom entry fails VISIBLY in `wt list`, an
|
|
19
|
+
* unrecorded worktree fails invisibly), deleted ONLY after a removal has
|
|
20
|
+
* verified the directory is actually gone.
|
|
21
|
+
*
|
|
22
|
+
* Concurrency and durability copy `updateRegistryEntry` exactly: `acquireLock`
|
|
23
|
+
* on `~/.totem` serializes mutations, and every write is a PID-suffixed temp
|
|
24
|
+
* file renamed into place. Reads warn-and-degrade (`readRegistry`'s posture);
|
|
25
|
+
* mutations REFUSE to overwrite a file whose schema does not parse.
|
|
26
|
+
*/
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import os from 'node:os';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import { z } from 'zod';
|
|
31
|
+
import { TotemParseError } from './errors.js';
|
|
32
|
+
import { acquireLock } from './lock.js';
|
|
33
|
+
import { readJsonSafe } from './sys/fs.js';
|
|
34
|
+
/** The on-disk compatibility contract for `~/.totem/worktrees.json`. */
|
|
35
|
+
export const WORKTREE_REGISTRY_SCHEMA_VERSION = 1;
|
|
36
|
+
export const WorktreeEntrySchema = z
|
|
37
|
+
.object({
|
|
38
|
+
/** Home repo root (absolute) the worktree was created from. */
|
|
39
|
+
repo: z.string(),
|
|
40
|
+
/** Creating seat id (`resolveSelfSender`, or an explicit `--seat`). */
|
|
41
|
+
seat: z.string(),
|
|
42
|
+
branch: z.string(),
|
|
43
|
+
/** Issue number the worktree was cut for, when one was named. */
|
|
44
|
+
ticket: z.string().optional(),
|
|
45
|
+
/** ISO instant, stamped at the write site. */
|
|
46
|
+
createdAt: z.string(),
|
|
47
|
+
})
|
|
48
|
+
.passthrough(); // Preserve unknown fields from newer CLI versions
|
|
49
|
+
export const WorktreeFileSchema = z.object({
|
|
50
|
+
schemaVersion: z.literal(WORKTREE_REGISTRY_SCHEMA_VERSION),
|
|
51
|
+
/** Every container root ever created under — accretes, never auto-pruned. */
|
|
52
|
+
roots: z.array(z.string()),
|
|
53
|
+
/** Absolute worktree path → entry. */
|
|
54
|
+
worktrees: z.record(z.string(), WorktreeEntrySchema),
|
|
55
|
+
});
|
|
56
|
+
/** Resolve the registry directory lazily so tests can mock os.homedir(). */
|
|
57
|
+
function worktreeRegistryDir() {
|
|
58
|
+
return path.join(os.homedir(), '.totem');
|
|
59
|
+
}
|
|
60
|
+
/** Resolve the registry file path lazily so tests can mock os.homedir(). */
|
|
61
|
+
export function worktreeRegistryPath() {
|
|
62
|
+
return path.join(worktreeRegistryDir(), 'worktrees.json');
|
|
63
|
+
}
|
|
64
|
+
/** The zero-config container root (`~/.totem/worktrees`) — the ruling's Q1 default. */
|
|
65
|
+
export function defaultWorktreeRoot() {
|
|
66
|
+
return path.join(worktreeRegistryDir(), 'worktrees');
|
|
67
|
+
}
|
|
68
|
+
/** A fresh, valid, empty registry — the first-run and degraded-read value. */
|
|
69
|
+
export function emptyWorktreeFile() {
|
|
70
|
+
return { schemaVersion: WORKTREE_REGISTRY_SCHEMA_VERSION, roots: [], worktrees: {} };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Windows git and Node can disagree on drive-letter case (GCA #2293), so path
|
|
74
|
+
* identity folds on win32 ONLY — POSIX filesystems are case-sensitive and
|
|
75
|
+
* folding there conflates real paths (the author-sandbox.ts:121 precedent).
|
|
76
|
+
*/
|
|
77
|
+
function foldCase(p) {
|
|
78
|
+
return process.platform === 'win32' ? p.toLowerCase() : p;
|
|
79
|
+
}
|
|
80
|
+
/** Case-folded (win32 only), resolved key for comparing two worktree paths. */
|
|
81
|
+
export function worktreePathKey(p) {
|
|
82
|
+
return foldCase(path.resolve(p));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* No-follow existence probe: a dangling symlink/junction still EXISTS here,
|
|
86
|
+
* and the probe FAILS CLOSED — only ENOENT/ENOTDIR mean "absent". Any other
|
|
87
|
+
* errno (EACCES on a denied parent, EIO, an offline share) reports the path as
|
|
88
|
+
* PRESENT: a caller that cannot KNOW must fail loud, never delete a registry
|
|
89
|
+
* entry for a directory that may still stand (#2580 bot round, CR findings
|
|
90
|
+
* 8 + 9 — this retires the errno-tri-state deferral).
|
|
91
|
+
*/
|
|
92
|
+
export function worktreePathExists(p) {
|
|
93
|
+
// totem-context: intentional cleanup — the catch IS the answer, not a swallow: ENOENT/ENOTDIR are the two errnos that PROVE absence, and every other lstat failure deliberately reports "present" so removal fails closed instead of reading an unknowable path as gone.
|
|
94
|
+
try {
|
|
95
|
+
fs.lstatSync(p);
|
|
96
|
+
return true;
|
|
97
|
+
// totem-context: intentional cleanup — see directive above the try; dual placement so the rule fires on either the catch-keyword line or the catch-body line.
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
// A non-object throw carries no errno and cannot PROVE absence — it reads
|
|
101
|
+
// as present, the same fail-closed answer as any unrecognized failure
|
|
102
|
+
// (round 2, GCA null-safety finding).
|
|
103
|
+
const code = err !== null && typeof err === 'object' && 'code' in err
|
|
104
|
+
? err.code
|
|
105
|
+
: undefined;
|
|
106
|
+
return code !== 'ENOENT' && code !== 'ENOTDIR';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** No-follow directory probe — a symlinked/junctioned path is not a directory. */
|
|
110
|
+
function isRealDirectory(p) {
|
|
111
|
+
// totem-context: intentional cleanup — an unreadable or missing recorded root degrades to "not a directory", which the doctor coupling reads as an empty sweep rather than a scan hole (the ruling's Q3 filter).
|
|
112
|
+
try {
|
|
113
|
+
return fs.lstatSync(p).isDirectory();
|
|
114
|
+
// totem-context: intentional cleanup — see directive above the try; dual placement so the rule fires on either the catch-keyword line or the catch-body line.
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Read the worktree registry. Missing file → empty (expected on first run);
|
|
122
|
+
* any other failure warns and degrades to empty, mirroring `readRegistry` so a
|
|
123
|
+
* corrupt file can never take a read-only verb down with it.
|
|
124
|
+
*/
|
|
125
|
+
export function readWorktreeRegistry(onWarn) {
|
|
126
|
+
// totem-context: intentional cleanup — warn-not-throw is the ruled read contract (mirrors readRegistry): a corrupt registry file must never take a read-only verb down with it; the onWarn callback is the loud path, and a missing file is the expected first-run case.
|
|
127
|
+
try {
|
|
128
|
+
return readJsonSafe(worktreeRegistryPath(), WorktreeFileSchema);
|
|
129
|
+
// totem-context: intentional cleanup — see directive above the try; dual placement so the rule fires on either the catch-keyword line or the catch-body line.
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
if (err instanceof TotemParseError && err.message.includes('File not found')) {
|
|
133
|
+
// Expected until the first `wt create` — silently return empty
|
|
134
|
+
return emptyWorktreeFile();
|
|
135
|
+
}
|
|
136
|
+
onWarn?.(`Cannot read worktree registry: ${err instanceof Error ? err.message : String(err)} — using empty registry`);
|
|
137
|
+
return emptyWorktreeFile();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Find a recorded entry by path, folding case on win32 only. Returns the
|
|
142
|
+
* STORED key alongside the entry so a caller can delete exactly what it found.
|
|
143
|
+
*/
|
|
144
|
+
export function findWorktreeEntry(file, worktreePath) {
|
|
145
|
+
const wanted = worktreePathKey(worktreePath);
|
|
146
|
+
for (const [key, entry] of Object.entries(file.worktrees)) {
|
|
147
|
+
if (worktreePathKey(key) === wanted)
|
|
148
|
+
return { key, entry };
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The recorded roots that still EXIST on disk — the projection both doctor
|
|
154
|
+
* consumers sweep. A recorded root that is gone is an empty sweep, not a scan
|
|
155
|
+
* hole (the ruling's Q3), so it is filtered here rather than handed to
|
|
156
|
+
* `scanEstate` as an unscannable row.
|
|
157
|
+
*/
|
|
158
|
+
export function existingWorktreeRoots(file) {
|
|
159
|
+
const seen = new Set();
|
|
160
|
+
const roots = [];
|
|
161
|
+
for (const root of file.roots) {
|
|
162
|
+
const resolved = path.resolve(root);
|
|
163
|
+
const key = foldCase(resolved);
|
|
164
|
+
if (seen.has(key))
|
|
165
|
+
continue;
|
|
166
|
+
seen.add(key);
|
|
167
|
+
if (isRealDirectory(resolved))
|
|
168
|
+
roots.push(resolved);
|
|
169
|
+
}
|
|
170
|
+
return roots;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Partition recorded roots into the sweep classes the estate consumers hand to
|
|
174
|
+
* `scanEstate`. Only the DEFAULT root (`~/.totem/worktrees`) carries container
|
|
175
|
+
* semantics — it exists solely to hold worktrees, so location alone is husk
|
|
176
|
+
* evidence there. Every other recorded root is a location the operator also
|
|
177
|
+
* uses for other things (a shared tmp, a scratch dir); those sweep as STANDARD
|
|
178
|
+
* roots, where husk-ness needs shape evidence — so one `wt create --root`
|
|
179
|
+
* against a scratch dir can never permanently arm by-location residue rows for
|
|
180
|
+
* every unrelated directory in it (#2580 slice-2 falsification, finding 11).
|
|
181
|
+
*/
|
|
182
|
+
export function partitionWorktreeRoots(roots) {
|
|
183
|
+
const defaultKey = foldCase(path.resolve(defaultWorktreeRoot()));
|
|
184
|
+
const container = [];
|
|
185
|
+
const standard = [];
|
|
186
|
+
for (const root of roots) {
|
|
187
|
+
(foldCase(path.resolve(root)) === defaultKey ? container : standard).push(root);
|
|
188
|
+
}
|
|
189
|
+
return { container, standard };
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Load the file for MUTATION. Unlike the read path this REFUSES to proceed on
|
|
193
|
+
* a schema failure: silently replacing an unparseable registry would delete
|
|
194
|
+
* every recorded root and entry it holds (`updateRegistryEntry`'s posture).
|
|
195
|
+
*/
|
|
196
|
+
function loadForMutation(filePath) {
|
|
197
|
+
if (!fs.existsSync(filePath))
|
|
198
|
+
return emptyWorktreeFile();
|
|
199
|
+
try {
|
|
200
|
+
return readJsonSafe(filePath, WorktreeFileSchema);
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
if (err instanceof TotemParseError && err.message.includes('Schema validation failed')) {
|
|
204
|
+
throw new TotemParseError('Worktree registry file has invalid schema — refusing to overwrite.', `Delete ${filePath} to reset (recorded roots and entries are lost).`, err.cause);
|
|
205
|
+
}
|
|
206
|
+
throw err;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** PID-unique temp + rename: a concurrent reader never sees partial JSON. */
|
|
210
|
+
function writeAtomic(filePath, file) {
|
|
211
|
+
const tmpPath = `${filePath}.${process.pid}.tmp`;
|
|
212
|
+
fs.writeFileSync(tmpPath, JSON.stringify(file, null, 2) + '\n');
|
|
213
|
+
fs.renameSync(tmpPath, filePath);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Record a worktree and the root it lives under, under the `~/.totem` lock.
|
|
217
|
+
* Called BEFORE `git worktree add` — the intent record is what makes a failed
|
|
218
|
+
* creation visible instead of invisible.
|
|
219
|
+
*/
|
|
220
|
+
export async function addWorktreeEntry(args) {
|
|
221
|
+
const dir = worktreeRegistryDir();
|
|
222
|
+
const filePath = worktreeRegistryPath();
|
|
223
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
224
|
+
const release = await acquireLock(dir);
|
|
225
|
+
try {
|
|
226
|
+
const file = loadForMutation(filePath);
|
|
227
|
+
const root = path.resolve(args.root);
|
|
228
|
+
// Roots accrete and are deduped case-folded on win32 only; the recorded
|
|
229
|
+
// spelling of the FIRST writer wins so the disclosure stays stable.
|
|
230
|
+
if (!file.roots.some((existing) => foldCase(path.resolve(existing)) === foldCase(root))) {
|
|
231
|
+
file.roots.push(root);
|
|
232
|
+
}
|
|
233
|
+
// Overwrite of an occupied key is BY DESIGN, not an oversight: the only
|
|
234
|
+
// path that reaches here with an existing entry is a re-create over a
|
|
235
|
+
// STALE record (create refuses when the target directory exists, so the
|
|
236
|
+
// dir is gone and the old entry describes nothing). Replacing it is the
|
|
237
|
+
// recovery; callers must not assume exclusive-write semantics. The
|
|
238
|
+
// delete side is what carries identity (expectCreatedAt guard below).
|
|
239
|
+
file.worktrees[path.resolve(args.worktreePath)] = args.entry;
|
|
240
|
+
writeAtomic(filePath, file);
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
release();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Delete a recorded entry under the `~/.totem` lock. Roots are deliberately
|
|
248
|
+
* untouched: their durability is independent of entry lifecycle, which is what
|
|
249
|
+
* keeps an emptied container root reachable for the estate sweep.
|
|
250
|
+
*
|
|
251
|
+
* Callers must only reach this AFTER verifying the directory is absent.
|
|
252
|
+
* Returns whether an entry was actually deleted (a git-listed worktree with no
|
|
253
|
+
* registry entry — the legacy estate — is a no-op, not an error).
|
|
254
|
+
*
|
|
255
|
+
* `expectCreatedAt` is an identity guard for the verify→delete window: a
|
|
256
|
+
* concurrent `wt create` can re-record the SAME path between a removal's
|
|
257
|
+
* absence check and this lock acquisition, and an unguarded delete-by-path
|
|
258
|
+
* would erase the replacement's durable record — the invisible-unrecorded
|
|
259
|
+
* class this registry exists to prevent (#2580 bot round, Greptile P1). When
|
|
260
|
+
* the stored entry's `createdAt` differs from the expected one, the entry is
|
|
261
|
+
* left in place and `false` is returned.
|
|
262
|
+
*/
|
|
263
|
+
export async function deleteWorktreeEntry(worktreePath, opts) {
|
|
264
|
+
const dir = worktreeRegistryDir();
|
|
265
|
+
const filePath = worktreeRegistryPath();
|
|
266
|
+
if (!fs.existsSync(filePath))
|
|
267
|
+
return false;
|
|
268
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
269
|
+
const release = await acquireLock(dir);
|
|
270
|
+
try {
|
|
271
|
+
const file = loadForMutation(filePath);
|
|
272
|
+
const found = findWorktreeEntry(file, worktreePath);
|
|
273
|
+
if (found === undefined)
|
|
274
|
+
return false;
|
|
275
|
+
if (opts?.expectCreatedAt !== undefined && found.entry.createdAt !== opts.expectCreatedAt) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
delete file.worktrees[found.key];
|
|
279
|
+
writeAtomic(filePath, file);
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
release();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
//# sourceMappingURL=worktree-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worktree-registry.js","sourceRoot":"","sources":["../src/worktree-registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worktree-registry tests (mmnto-ai/totem#2580 slice-2).
|
|
3
|
+
*
|
|
4
|
+
* The file is resolved through `os.homedir()`, so every test pins
|
|
5
|
+
* HOME/USERPROFILE at a temp directory — the same fixture shape the
|
|
6
|
+
* `readRegistry` tests in doctor.test.ts use. Nothing here mocks the
|
|
7
|
+
* filesystem: the contract being pinned is what actually lands on disk
|
|
8
|
+
* (atomic write, lock, accreting roots) and a mocked fs would assert the mock.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=worktree-registry.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worktree-registry.test.d.ts","sourceRoot":"","sources":["../src/worktree-registry.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worktree-registry tests (mmnto-ai/totem#2580 slice-2).
|
|
3
|
+
*
|
|
4
|
+
* The file is resolved through `os.homedir()`, so every test pins
|
|
5
|
+
* HOME/USERPROFILE at a temp directory — the same fixture shape the
|
|
6
|
+
* `readRegistry` tests in doctor.test.ts use. Nothing here mocks the
|
|
7
|
+
* filesystem: the contract being pinned is what actually lands on disk
|
|
8
|
+
* (atomic write, lock, accreting roots) and a mocked fs would assert the mock.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as os from 'node:os';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
14
|
+
// Module-namespace exports are not spyable under ESM, so `node:fs` is mocked
|
|
15
|
+
// ONCE with a pass-through `lstatSync` whose implementation individual tests
|
|
16
|
+
// override via `mockImplementationOnce` (the ecl-gc.test.ts house pattern);
|
|
17
|
+
// every other fs call, and every unconsumed call, is the real thing.
|
|
18
|
+
vi.mock('node:fs', async (importOriginal) => {
|
|
19
|
+
const actual = await importOriginal();
|
|
20
|
+
const lstatSync = vi.fn(actual.lstatSync);
|
|
21
|
+
return { ...actual, default: { ...actual, lstatSync }, lstatSync };
|
|
22
|
+
});
|
|
23
|
+
import { addWorktreeEntry, defaultWorktreeRoot, deleteWorktreeEntry, existingWorktreeRoots, findWorktreeEntry, partitionWorktreeRoots, readWorktreeRegistry, WORKTREE_REGISTRY_SCHEMA_VERSION, WorktreeFileSchema, worktreePathExists, worktreeRegistryPath, } from './worktree-registry.js';
|
|
24
|
+
const IS_WIN32 = process.platform === 'win32';
|
|
25
|
+
let home;
|
|
26
|
+
let prevHome;
|
|
27
|
+
let prevProfile;
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
home = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'totem-wtreg-')));
|
|
30
|
+
prevHome = process.env['HOME'];
|
|
31
|
+
prevProfile = process.env['USERPROFILE'];
|
|
32
|
+
process.env['HOME'] = home;
|
|
33
|
+
process.env['USERPROFILE'] = home;
|
|
34
|
+
});
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
if (prevHome === undefined)
|
|
37
|
+
delete process.env['HOME'];
|
|
38
|
+
else
|
|
39
|
+
process.env['HOME'] = prevHome;
|
|
40
|
+
if (prevProfile === undefined)
|
|
41
|
+
delete process.env['USERPROFILE'];
|
|
42
|
+
else
|
|
43
|
+
process.env['USERPROFILE'] = prevProfile;
|
|
44
|
+
fs.rmSync(home, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
45
|
+
});
|
|
46
|
+
function entryFor(overrides = {}) {
|
|
47
|
+
return {
|
|
48
|
+
repo: path.join(home, 'repo'),
|
|
49
|
+
seat: 'totem-claude',
|
|
50
|
+
branch: 'wt/demo',
|
|
51
|
+
createdAt: '2026-08-07T10:00:00.000Z',
|
|
52
|
+
...overrides,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function writeRaw(contents) {
|
|
56
|
+
fs.mkdirSync(path.join(home, '.totem'), { recursive: true });
|
|
57
|
+
fs.writeFileSync(worktreeRegistryPath(), contents, 'utf-8');
|
|
58
|
+
}
|
|
59
|
+
// ─── Read path ──────────────────────────────────────────
|
|
60
|
+
describe('readWorktreeRegistry', () => {
|
|
61
|
+
it('returns an empty registry when the file has never been written', () => {
|
|
62
|
+
const warnings = [];
|
|
63
|
+
const file = readWorktreeRegistry((msg) => warnings.push(msg));
|
|
64
|
+
expect(file).toEqual({
|
|
65
|
+
schemaVersion: WORKTREE_REGISTRY_SCHEMA_VERSION,
|
|
66
|
+
roots: [],
|
|
67
|
+
worktrees: {},
|
|
68
|
+
});
|
|
69
|
+
// First run is not a degradation — it must not warn.
|
|
70
|
+
expect(warnings).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
// The `readRegistry` posture: a corrupt file warns LOUDLY and degrades to
|
|
73
|
+
// empty, so a read-only verb still runs and the operator still learns.
|
|
74
|
+
it('warns and degrades to empty on an unparseable file', () => {
|
|
75
|
+
writeRaw('{ not json');
|
|
76
|
+
const warnings = [];
|
|
77
|
+
const file = readWorktreeRegistry((msg) => warnings.push(msg));
|
|
78
|
+
expect(file.worktrees).toEqual({});
|
|
79
|
+
expect(warnings).toHaveLength(1);
|
|
80
|
+
expect(warnings[0]).toContain('Cannot read worktree registry');
|
|
81
|
+
});
|
|
82
|
+
it('warns on a schema-invalid file rather than trusting the shape', () => {
|
|
83
|
+
writeRaw(JSON.stringify({ schemaVersion: 2, roots: [], worktrees: {} }));
|
|
84
|
+
const warnings = [];
|
|
85
|
+
readWorktreeRegistry((msg) => warnings.push(msg));
|
|
86
|
+
expect(warnings).toHaveLength(1);
|
|
87
|
+
});
|
|
88
|
+
it('preserves unknown entry fields written by a newer CLI (passthrough)', async () => {
|
|
89
|
+
const target = path.join(home, 'container', 'repo-seat-slug');
|
|
90
|
+
await addWorktreeEntry({
|
|
91
|
+
worktreePath: target,
|
|
92
|
+
entry: { ...entryFor(), futureField: 'kept' },
|
|
93
|
+
root: path.join(home, 'container'),
|
|
94
|
+
});
|
|
95
|
+
const file = readWorktreeRegistry();
|
|
96
|
+
const found = findWorktreeEntry(file, target);
|
|
97
|
+
expect(found?.entry['futureField']).toBe('kept');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
// ─── Mutation path ──────────────────────────────────────
|
|
101
|
+
describe('addWorktreeEntry', () => {
|
|
102
|
+
it('creates the file with the entry and its root', async () => {
|
|
103
|
+
const root = path.join(home, 'container');
|
|
104
|
+
const target = path.join(root, 'repo-totem-claude-demo');
|
|
105
|
+
await addWorktreeEntry({ worktreePath: target, entry: entryFor(), root });
|
|
106
|
+
const file = readWorktreeRegistry();
|
|
107
|
+
expect(file.schemaVersion).toBe(WORKTREE_REGISTRY_SCHEMA_VERSION);
|
|
108
|
+
expect(file.roots).toEqual([path.resolve(root)]);
|
|
109
|
+
expect(findWorktreeEntry(file, target)?.entry.branch).toBe('wt/demo');
|
|
110
|
+
// The written bytes must satisfy the schema — a reader on another version
|
|
111
|
+
// parses the same file.
|
|
112
|
+
const raw = JSON.parse(fs.readFileSync(worktreeRegistryPath(), 'utf-8'));
|
|
113
|
+
expect(() => WorktreeFileSchema.parse(raw)).not.toThrow();
|
|
114
|
+
});
|
|
115
|
+
it('accretes roots, deduped case-folded on win32 ONLY', async () => {
|
|
116
|
+
const root = path.join(home, 'container');
|
|
117
|
+
await addWorktreeEntry({
|
|
118
|
+
worktreePath: path.join(root, 'a'),
|
|
119
|
+
entry: entryFor(),
|
|
120
|
+
root,
|
|
121
|
+
});
|
|
122
|
+
await addWorktreeEntry({
|
|
123
|
+
worktreePath: path.join(root, 'b'),
|
|
124
|
+
entry: entryFor(),
|
|
125
|
+
root: root.toUpperCase(),
|
|
126
|
+
});
|
|
127
|
+
const file = readWorktreeRegistry();
|
|
128
|
+
// Invariant 9: win32 folds (one root), POSIX does not (two distinct paths).
|
|
129
|
+
expect(file.roots).toHaveLength(IS_WIN32 ? 1 : 2);
|
|
130
|
+
// The FIRST spelling wins on win32, so the disclosure stays stable.
|
|
131
|
+
expect(file.roots[0]).toBe(path.resolve(root));
|
|
132
|
+
});
|
|
133
|
+
it('refuses to overwrite a file whose schema does not parse', async () => {
|
|
134
|
+
writeRaw(JSON.stringify({ schemaVersion: 99, roots: 'not-an-array' }));
|
|
135
|
+
await expect(addWorktreeEntry({
|
|
136
|
+
worktreePath: path.join(home, 'x'),
|
|
137
|
+
entry: entryFor(),
|
|
138
|
+
root: home,
|
|
139
|
+
})).rejects.toThrow(/refusing to overwrite/);
|
|
140
|
+
// The bytes are untouched — a mutation that cannot read cannot destroy.
|
|
141
|
+
expect(fs.readFileSync(worktreeRegistryPath(), 'utf-8')).toContain('not-an-array');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
describe('deleteWorktreeEntry', () => {
|
|
145
|
+
it('deletes the entry and RETAINS the root (invariant 8, registry half)', async () => {
|
|
146
|
+
const root = path.join(home, 'container');
|
|
147
|
+
const target = path.join(root, 'repo-totem-claude-demo');
|
|
148
|
+
await addWorktreeEntry({ worktreePath: target, entry: entryFor(), root });
|
|
149
|
+
expect(await deleteWorktreeEntry(target)).toBe(true);
|
|
150
|
+
const file = readWorktreeRegistry();
|
|
151
|
+
expect(file.worktrees).toEqual({});
|
|
152
|
+
// The whole point: a container root outlives every entry under it, so the
|
|
153
|
+
// location stays reachable for the estate sweep with zero live entries.
|
|
154
|
+
expect(file.roots).toEqual([path.resolve(root)]);
|
|
155
|
+
});
|
|
156
|
+
it('is a no-op (false, not an error) for a path with no entry', async () => {
|
|
157
|
+
const root = path.join(home, 'container');
|
|
158
|
+
await addWorktreeEntry({
|
|
159
|
+
worktreePath: path.join(root, 'kept'),
|
|
160
|
+
entry: entryFor(),
|
|
161
|
+
root,
|
|
162
|
+
});
|
|
163
|
+
expect(await deleteWorktreeEntry(path.join(root, 'legacy-worktree'))).toBe(false);
|
|
164
|
+
expect(Object.keys(readWorktreeRegistry().worktrees)).toHaveLength(1);
|
|
165
|
+
});
|
|
166
|
+
it('is a no-op when the file does not exist at all', async () => {
|
|
167
|
+
expect(await deleteWorktreeEntry(path.join(home, 'anything'))).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
// The verify→delete window guard (bot round, Greptile P1): a concurrent
|
|
170
|
+
// `wt create` can re-record the same path between a removal's absence check
|
|
171
|
+
// and this delete — the identity guard must leave the replacement's record.
|
|
172
|
+
it('refuses the delete and RETAINS the entry when expectCreatedAt differs', async () => {
|
|
173
|
+
const root = path.join(home, 'container');
|
|
174
|
+
const target = path.join(root, 'repo-totem-claude-demo');
|
|
175
|
+
await addWorktreeEntry({ worktreePath: target, entry: entryFor(), root });
|
|
176
|
+
expect(await deleteWorktreeEntry(target, { expectCreatedAt: '2026-08-07T23:59:59.000Z' })).toBe(false);
|
|
177
|
+
// The (replacement-shaped) entry survives untouched.
|
|
178
|
+
expect(findWorktreeEntry(readWorktreeRegistry(), target)?.entry.createdAt).toBe(entryFor().createdAt);
|
|
179
|
+
});
|
|
180
|
+
it('deletes normally when expectCreatedAt matches the stored entry', async () => {
|
|
181
|
+
const root = path.join(home, 'container');
|
|
182
|
+
const target = path.join(root, 'repo-totem-claude-demo');
|
|
183
|
+
await addWorktreeEntry({ worktreePath: target, entry: entryFor(), root });
|
|
184
|
+
expect(await deleteWorktreeEntry(target, { expectCreatedAt: entryFor().createdAt })).toBe(true);
|
|
185
|
+
expect(readWorktreeRegistry().worktrees).toEqual({});
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
// ─── Path identity (invariant 9) ────────────────────────
|
|
189
|
+
describe('findWorktreeEntry', () => {
|
|
190
|
+
it('folds case on win32 only', async () => {
|
|
191
|
+
const root = path.join(home, 'container');
|
|
192
|
+
const target = path.join(root, 'repo-totem-claude-demo');
|
|
193
|
+
await addWorktreeEntry({ worktreePath: target, entry: entryFor(), root });
|
|
194
|
+
const file = readWorktreeRegistry();
|
|
195
|
+
expect(findWorktreeEntry(file, target)).toBeDefined();
|
|
196
|
+
const shouted = findWorktreeEntry(file, target.toUpperCase());
|
|
197
|
+
if (IS_WIN32)
|
|
198
|
+
expect(shouted).toBeDefined();
|
|
199
|
+
else
|
|
200
|
+
expect(shouted).toBeUndefined();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
// ─── Root projection for the doctor coupling ────────────
|
|
204
|
+
describe('existingWorktreeRoots', () => {
|
|
205
|
+
it('keeps roots that exist and drops the ones that do not', () => {
|
|
206
|
+
const present = path.join(home, 'present');
|
|
207
|
+
fs.mkdirSync(present, { recursive: true });
|
|
208
|
+
const gone = path.join(home, 'gone');
|
|
209
|
+
const roots = existingWorktreeRoots({
|
|
210
|
+
schemaVersion: WORKTREE_REGISTRY_SCHEMA_VERSION,
|
|
211
|
+
// Duplicated deliberately — the sweep must receive each root once.
|
|
212
|
+
roots: [present, present, gone],
|
|
213
|
+
worktrees: {},
|
|
214
|
+
});
|
|
215
|
+
expect(roots).toEqual([path.resolve(present)]);
|
|
216
|
+
});
|
|
217
|
+
it('drops a recorded root that is a FILE, never sweeping it as a directory', () => {
|
|
218
|
+
const file = path.join(home, 'not-a-dir');
|
|
219
|
+
fs.writeFileSync(file, 'x', 'utf-8');
|
|
220
|
+
expect(existingWorktreeRoots({
|
|
221
|
+
schemaVersion: WORKTREE_REGISTRY_SCHEMA_VERSION,
|
|
222
|
+
roots: [file],
|
|
223
|
+
worktrees: {},
|
|
224
|
+
})).toEqual([]);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
describe('worktreePathExists', () => {
|
|
228
|
+
it('is no-follow: a dangling link still reports as PRESENT', () => {
|
|
229
|
+
const target = path.join(home, 'target');
|
|
230
|
+
fs.mkdirSync(target, { recursive: true });
|
|
231
|
+
const link = path.join(home, 'link');
|
|
232
|
+
fs.symlinkSync(target, link, IS_WIN32 ? 'junction' : 'dir');
|
|
233
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
234
|
+
// `existsSync` follows and would answer false here; the removal verb must
|
|
235
|
+
// not read a dangling junction as "already gone".
|
|
236
|
+
expect(worktreePathExists(link)).toBe(true);
|
|
237
|
+
expect(worktreePathExists(path.join(home, 'never-existed'))).toBe(false);
|
|
238
|
+
});
|
|
239
|
+
// Fail-closed (bot round, CR findings 8+9): only ENOENT/ENOTDIR PROVE
|
|
240
|
+
// absence; an unknowable path must read as present so removal fails loud
|
|
241
|
+
// rather than deleting a record for a directory that may still stand.
|
|
242
|
+
it('fails CLOSED: a non-ENOENT lstat failure reads as PRESENT', () => {
|
|
243
|
+
vi.mocked(fs.lstatSync).mockImplementationOnce(() => {
|
|
244
|
+
throw Object.assign(new Error('EACCES: permission denied'), { code: 'EACCES' });
|
|
245
|
+
});
|
|
246
|
+
expect(worktreePathExists(path.join(home, 'denied'))).toBe(true);
|
|
247
|
+
});
|
|
248
|
+
it('reads ENOENT as absent through the same mock seam', () => {
|
|
249
|
+
vi.mocked(fs.lstatSync).mockImplementationOnce(() => {
|
|
250
|
+
throw Object.assign(new Error('ENOENT: no such file or directory'), { code: 'ENOENT' });
|
|
251
|
+
});
|
|
252
|
+
expect(worktreePathExists(path.join(home, 'missing'))).toBe(false);
|
|
253
|
+
});
|
|
254
|
+
it('fails CLOSED on a non-object throw — no errno cannot prove absence', () => {
|
|
255
|
+
// Round 2, GCA null-safety finding: a throw that is not an object carries
|
|
256
|
+
// no `code`; the probe must answer present, not crash on the extraction.
|
|
257
|
+
vi.mocked(fs.lstatSync).mockImplementationOnce(() => {
|
|
258
|
+
throw null;
|
|
259
|
+
});
|
|
260
|
+
expect(worktreePathExists(path.join(home, 'null-throw'))).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
describe('defaultWorktreeRoot', () => {
|
|
264
|
+
it('is ~/.totem/worktrees under the pinned home', () => {
|
|
265
|
+
expect(path.resolve(defaultWorktreeRoot())).toBe(path.join(home, '.totem', 'worktrees'));
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
describe('partitionWorktreeRoots', () => {
|
|
269
|
+
it('routes the default root to container and everything else to standard', () => {
|
|
270
|
+
const scratch = path.join(home, 'scratch');
|
|
271
|
+
const { container, standard } = partitionWorktreeRoots([defaultWorktreeRoot(), scratch]);
|
|
272
|
+
expect(container).toEqual([defaultWorktreeRoot()]);
|
|
273
|
+
expect(standard).toEqual([scratch]);
|
|
274
|
+
});
|
|
275
|
+
it('returns empty partitions for empty input', () => {
|
|
276
|
+
expect(partitionWorktreeRoots([])).toEqual({ container: [], standard: [] });
|
|
277
|
+
});
|
|
278
|
+
it('keeps duplicate roots on their side rather than deduping — accretion is the writer’s charge', () => {
|
|
279
|
+
const scratch = path.join(home, 'scratch');
|
|
280
|
+
const { container, standard } = partitionWorktreeRoots([scratch, scratch]);
|
|
281
|
+
expect(container).toEqual([]);
|
|
282
|
+
expect(standard).toEqual([scratch, scratch]);
|
|
283
|
+
});
|
|
284
|
+
it('matches the default root case-insensitively on win32 ONLY (invariant 9)', () => {
|
|
285
|
+
const shouted = path.join(home, '.TOTEM', 'WORKTREES');
|
|
286
|
+
const { container, standard } = partitionWorktreeRoots([shouted]);
|
|
287
|
+
if (IS_WIN32) {
|
|
288
|
+
expect(container).toEqual([shouted]);
|
|
289
|
+
expect(standard).toEqual([]);
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
expect(container).toEqual([]);
|
|
293
|
+
expect(standard).toEqual([shouted]);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
//# sourceMappingURL=worktree-registry.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worktree-registry.test.js","sourceRoot":"","sources":["../src/worktree-registry.test.ts"],"names":[],"mappings":"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"}
|