leerness 1.36.185 → 1.36.186

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.
@@ -0,0 +1,590 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // P-0020: exercise real Git layouts and controlled discovery failures without
5
+ // enabling runtime writers, migration, providers, or any remote operation.
6
+ const assert = require('assert');
7
+ const crypto = require('crypto');
8
+ const fs = require('fs');
9
+ const os = require('os');
10
+ const path = require('path');
11
+ const cp = require('child_process');
12
+ const gateway = require('../lib/git');
13
+ const W = require('../lib/workspace-dir');
14
+ const I = require('../lib/state-inspect');
15
+ const SG = require('../lib/state-git');
16
+
17
+ // Hosted Windows runners may expose TEMP through an 8.3 name (RUNNER~1).
18
+ // Compare/inject metadata at the same canonical path the product inspects.
19
+ const tempDirectory = fs.realpathSync.native(os.tmpdir());
20
+ const arena = fs.mkdtempSync(path.join(tempDirectory, 'leerness-state-scopes-'));
21
+ const originalCwd = process.cwd();
22
+ const sourceFiles = ['state-git.js', 'state-paths.js', 'state-inventory.js', 'state-inspect.js']
23
+ .map(name => path.resolve(__dirname, '..', 'lib', name));
24
+ const fileSignature = file => ({
25
+ sha256: crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'),
26
+ mtimeNs: fs.statSync(file, { bigint: true }).mtimeNs.toString(),
27
+ });
28
+ const sourceBefore = sourceFiles.map(fileSignature);
29
+ const canonical = W.CANONICAL_WORKSPACE_DIR;
30
+ const legacy = W.LEGACY_WORKSPACE_DIR;
31
+ const sentinel = 'state-scopes-private-content-not-for-inspection';
32
+ const emptyConfig = path.join(arena, 'empty-gitconfig');
33
+ fs.writeFileSync(emptyConfig, '', 'utf8');
34
+ const env = { ...process.env, GIT_CONFIG_GLOBAL: emptyConfig, GIT_CONFIG_SYSTEM: emptyConfig,
35
+ GIT_CONFIG_NOSYSTEM: '1', GIT_TERMINAL_PROMPT: '0', GIT_DEFAULT_HASH: 'sha1',
36
+ GIT_CEILING_DIRECTORIES: arena, LC_ALL: 'C', LANGUAGE: 'C', LANG: 'C' };
37
+ for (const key of Object.keys(env)) {
38
+ if (gateway._shouldDrop(key) || key.toUpperCase() === 'LEERNESS_WORKSPACE_DIR'
39
+ || key.toUpperCase() === 'GIT_DISCOVERY_ACROSS_FILESYSTEM') delete env[key];
40
+ }
41
+ let total = 0;
42
+ let failed = 0;
43
+ let fixtureNumber = 0;
44
+
45
+ const check = (label, fn) => {
46
+ total += 1;
47
+ try { fn(); process.stdout.write(`ok - ${label}\n`); }
48
+ catch (error) {
49
+ failed += 1;
50
+ process.stderr.write(`not ok - ${label}: ${error.stack || error.message}\n`);
51
+ }
52
+ };
53
+
54
+ const directory = label => {
55
+ const root = path.join(arena, `${++fixtureNumber}-${label}`);
56
+ fs.mkdirSync(root);
57
+ return root;
58
+ };
59
+
60
+ const write = (root, relative, body) => {
61
+ const file = path.join(root, relative);
62
+ fs.mkdirSync(path.dirname(file), { recursive: true });
63
+ fs.writeFileSync(file, body, 'utf8');
64
+ };
65
+
66
+ const git = (root, args) => {
67
+ const result = cp.spawnSync('git', ['-C', root, ...args], {
68
+ env, encoding: 'utf8', timeout: 30000, windowsHide: true, shell: false,
69
+ });
70
+ assert.strictEqual(result.status, 0, `fixture git ${args.join(' ')}: ${result.stderr || result.error || result.stdout}`);
71
+ return String(result.stdout || '').trim();
72
+ };
73
+
74
+ const seed = root => {
75
+ git(root, ['init', '-b', 'main']);
76
+ git(root, ['config', 'user.name', 'State Scope Probe']);
77
+ git(root, ['config', 'user.email', 'state-scope@example.invalid']);
78
+ write(root, 'source.txt', 'fixture source\n');
79
+ write(root, `${canonical}/HARNESS_VERSION`, '1.36.185\n');
80
+ write(root, `${canonical}/manifest.json`, '{}\n');
81
+ write(root, `${canonical}/state.json`, `{broken ${sentinel}`);
82
+ write(root, `${canonical}/decisions.json`, `${sentinel}\n`);
83
+ write(root, `${canonical}/cache/sessions/private.json`, sentinel);
84
+ write(root, `${canonical}/runs/nested/run.json`, sentinel);
85
+ for (const name of ['alpha', 'beta']) write(root, `packages/${name}/${canonical}/HARNESS_VERSION`, '1\n');
86
+ git(root, ['add', '.']);
87
+ git(root, ['-c', 'commit.gpgsign=false', '-c', `core.hooksPath=${path.join(arena, 'disabled-hooks')}`,
88
+ 'commit', '-qm', 'state scopes fixture']);
89
+ };
90
+
91
+ const snapshot = root => {
92
+ const rows = [];
93
+ const visit = (file, relative) => {
94
+ const stat = fs.lstatSync(file, { bigint: true });
95
+ const meta = [relative, stat.mode.toString(), stat.size.toString(), stat.mtimeNs.toString()];
96
+ if (stat.isSymbolicLink()) rows.push([...meta, 'link', fs.readlinkSync(file)]);
97
+ else if (stat.isDirectory()) {
98
+ rows.push([...meta, 'directory']);
99
+ for (const child of fs.readdirSync(file).sort()) visit(path.join(file, child), `${relative}/${child}`);
100
+ } else rows.push([...meta, 'file', crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex')]);
101
+ };
102
+ visit(root, '.');
103
+ return JSON.stringify(rows);
104
+ };
105
+
106
+ const noWrites = fn => {
107
+ // The arena includes an unrelated cwd, every project target, and every Git
108
+ // common/private/external metadata directory. Links are recorded, not followed.
109
+ const before = snapshot(arena);
110
+ try { return fn(); }
111
+ finally { assert.strictEqual(snapshot(arena), before, 'inspection changed arena bytes or mtimes'); }
112
+ };
113
+
114
+ const inspect = (root, options = {}) => noWrites(() => I.inspectState(root, { env, ...options }));
115
+ const errorCode = (fn, code) => assert.throws(fn, error => {
116
+ assert.strictEqual(error.code, code);
117
+ assert(!String(error.message).includes(sentinel), 'diagnostic disclosed state contents');
118
+ assert(String(error.message).length < 1024, 'diagnostic is not bounded');
119
+ return true;
120
+ });
121
+
122
+ const withGitResult = (resultOrFn, fn) => {
123
+ const paths = ['../lib/state-git', '../lib/state-paths', '../lib/state-inspect'].map(require.resolve);
124
+ const cache = paths.map(file => require.cache[file]);
125
+ const original = gateway.gitSpawn;
126
+ gateway.gitSpawn = typeof resultOrFn === 'function' ? resultOrFn : () => resultOrFn;
127
+ for (const file of paths) delete require.cache[file];
128
+ try { return fn(require('../lib/state-inspect')); }
129
+ finally {
130
+ gateway.gitSpawn = original;
131
+ for (let i = 0; i < paths.length; i++) {
132
+ delete require.cache[paths[i]];
133
+ if (cache[i]) require.cache[paths[i]] = cache[i];
134
+ }
135
+ }
136
+ };
137
+
138
+ const expectedScopes = ['project', 'worktree', 'commonControl', 'immutableRecord', 'generatedView'];
139
+ const assertContract = report => {
140
+ assert.strictEqual(report.schema, 'leerness.state-inspection/v1');
141
+ assert.strictEqual(report.schemaVersion, 1);
142
+ assert.strictEqual(report.ok, true);
143
+ assert.strictEqual(report.activeLayout, 'legacy');
144
+ assert.strictEqual(report.runtimeActivated, false);
145
+ assert.strictEqual(report.migrationAvailable, false);
146
+ assert.deepStrictEqual(Object.keys(report.scopes), expectedScopes);
147
+ assert(/^project-[a-f0-9]{64}$/.test(report.projectKey));
148
+ assert(report.inventory.length > 15 && report.inventory.length < 100);
149
+ assert(report.inventory.every(row => row.migrationAvailable === false));
150
+ assert(!JSON.stringify(report).includes(sentinel));
151
+ assert(JSON.stringify(report).length < 65536, 'inspection report exceeds bounded fixture budget');
152
+ };
153
+
154
+ try {
155
+ const cwdRoot = directory('unrelated-cwd');
156
+ write(cwdRoot, `${canonical}/HARNESS_VERSION`, 'unrelated\n');
157
+ process.chdir(cwdRoot);
158
+ const mainRoot = directory('main repo 한국어');
159
+ seed(mainRoot);
160
+ const linkedA = path.join(arena, 'linked A α');
161
+ const linkedB = path.join(arena, 'linked B β');
162
+ git(mainRoot, ['worktree', 'add', '-b', 'scope-a', linkedA]);
163
+ git(mainRoot, ['worktree', 'add', '-b', 'scope-b', linkedB]);
164
+ let mainReport, aReport, bReport;
165
+
166
+ check('main and two real linked worktrees expose five honest, read-only scopes', () => {
167
+ [mainReport, aReport, bReport] = [mainRoot, linkedA, linkedB].map(root => inspect(root));
168
+ [mainReport, aReport, bReport].forEach(assertContract);
169
+ assert.strictEqual(mainReport.projectRelativePath, '.');
170
+ assert.strictEqual(mainReport.git.linkedWorktree, false);
171
+ assert.strictEqual(aReport.git.linkedWorktree, true);
172
+ assert.strictEqual(bReport.git.linkedWorktree, true);
173
+ assert.strictEqual(new Set([mainReport, aReport, bReport].map(r => r.scopes.worktree.proposedPath)).size, 3);
174
+ assert.strictEqual(new Set([mainReport, aReport, bReport].map(r => r.scopes.commonControl.proposedPath)).size, 1);
175
+ assert.strictEqual(new Set([mainReport, aReport, bReport].map(r => r.projectKey)).size, 1);
176
+ assert([mainReport, aReport, bReport].every(r => !fs.existsSync(r.scopes.worktree.proposedPath)));
177
+ assert(!fs.existsSync(mainReport.scopes.commonControl.proposedPath));
178
+ });
179
+
180
+ check('proposed scope paths match Git topology without activating or moving legacy data', () => {
181
+ const r = inspect(linkedA);
182
+ const key = r.projectKey;
183
+ assert.strictEqual(r.scopes.worktree.proposedPath, path.join(r.git.gitDir, 'leerness', 'projects', key, 'runtime'));
184
+ assert.strictEqual(r.scopes.commonControl.proposedPath, path.join(r.git.gitCommonDir, 'leerness', 'control', 'projects', key));
185
+ assert.strictEqual(r.scopes.project.currentPath, path.join(r.projectRoot, canonical));
186
+ assert.strictEqual(r.scopes.immutableRecord.proposedPaths.runs, path.join(r.projectRoot, canonical, 'records', 'runs'));
187
+ assert.strictEqual(r.scopes.generatedView.proposedPaths.worktree, path.join(r.scopes.worktree.proposedPath, 'views'));
188
+ assert.strictEqual(r.inventory.find(row => row.relativePath === 'state.json').metadata.status, 'file');
189
+ });
190
+
191
+ check('monorepo namespaces agree across worktrees and isolate sibling projects', () => {
192
+ const alpha = inspect(path.join(mainRoot, 'packages', 'alpha'));
193
+ const alphaPeer = inspect(path.join(linkedA, 'packages', 'alpha'));
194
+ const beta = inspect(path.join(mainRoot, 'packages', 'beta'));
195
+ assert.strictEqual(alpha.projectRelativePath, 'packages/alpha');
196
+ assert.strictEqual(alpha.projectKey, alphaPeer.projectKey);
197
+ assert.strictEqual(alpha.scopes.commonControl.proposedPath, alphaPeer.scopes.commonControl.proposedPath);
198
+ assert.notStrictEqual(alpha.scopes.worktree.proposedPath, alphaPeer.scopes.worktree.proposedPath);
199
+ assert.notStrictEqual(alpha.projectKey, beta.projectKey);
200
+ assert.notStrictEqual(alpha.projectKey, mainReport.projectKey);
201
+ assert.notStrictEqual(alpha.scopes.commonControl.proposedPath, beta.scopes.commonControl.proposedPath);
202
+ });
203
+
204
+ check('detaching and changing branch preserves worktree runtime identity', () => {
205
+ const before = inspect(linkedA);
206
+ git(linkedA, ['checkout', '--detach']);
207
+ const detached = inspect(linkedA);
208
+ git(linkedA, ['checkout', 'scope-a']);
209
+ const restored = inspect(linkedA);
210
+ assert.deepStrictEqual(detached.scopes, before.scopes);
211
+ assert.deepStrictEqual(restored.scopes, before.scopes);
212
+ assert.strictEqual(detached.projectKey, before.projectKey);
213
+ });
214
+
215
+ check('moving a linked worktree preserves private/control identity while updating the project path', () => {
216
+ const before = inspect(linkedA);
217
+ const moved = path.join(arena, 'moved linked A');
218
+ git(mainRoot, ['worktree', 'move', linkedA, moved]);
219
+ try {
220
+ const report = inspect(moved);
221
+ assert.strictEqual(report.projectRoot, fs.realpathSync.native(moved));
222
+ assert.strictEqual(report.projectKey, before.projectKey);
223
+ assert.strictEqual(report.git.gitDir, before.git.gitDir);
224
+ assert.strictEqual(report.scopes.worktree.proposedPath, before.scopes.worktree.proposedPath);
225
+ assert.strictEqual(report.scopes.commonControl.proposedPath, before.scopes.commonControl.proposedPath);
226
+ assert.notStrictEqual(report.scopes.project.proposedPath, before.scopes.project.proposedPath);
227
+ } finally { git(mainRoot, ['worktree', 'move', moved, linkedA]); }
228
+ });
229
+
230
+ const clone = path.join(arena, 'separate clone');
231
+ git(arena, ['clone', '--no-hardlinks', mainRoot, clone]);
232
+ check('a separate clone never shares live control despite matching relative project IDs', () => {
233
+ const report = inspect(clone);
234
+ assert.strictEqual(report.projectKey, mainReport.projectKey);
235
+ assert.notStrictEqual(report.git.gitCommonDir, mainReport.git.gitCommonDir);
236
+ assert.notStrictEqual(report.scopes.commonControl.proposedPath, mainReport.scopes.commonControl.proposedPath);
237
+ });
238
+
239
+ const separate = directory('separate-gitdir-worktree');
240
+ const externalMetadata = path.join(arena, 'external Git metadata');
241
+ git(separate, ['init', '--separate-git-dir', externalMetadata]);
242
+ write(separate, `${canonical}/HARNESS_VERSION`, '1\n');
243
+ check('a gitfile with separate Git metadata uses reported paths rather than .git-directory assumptions', () => {
244
+ const report = inspect(separate);
245
+ assert.strictEqual(report.git.gitDir, fs.realpathSync.native(externalMetadata));
246
+ assert.strictEqual(report.git.gitCommonDir, report.git.gitDir);
247
+ assert.strictEqual(report.git.linkedWorktree, false);
248
+ assert(report.scopes.worktree.proposedPath.startsWith(report.git.gitDir + path.sep));
249
+ });
250
+
251
+ const superproject = directory('superproject');
252
+ seed(superproject);
253
+ git(superproject, ['-c', 'protocol.file.allow=always', 'submodule', 'add', mainRoot, 'modules/child']);
254
+ check('an actual submodule uses its own worktree and module Git metadata', () => {
255
+ const child = path.join(superproject, 'modules', 'child');
256
+ const parentReport = inspect(superproject);
257
+ const report = inspect(child);
258
+ assert.strictEqual(report.git.worktreeRoot, fs.realpathSync.native(child));
259
+ assert.strictEqual(report.projectRelativePath, '.');
260
+ assert.strictEqual(report.git.gitDir, fs.realpathSync.native(path.join(superproject, '.git', 'modules', 'modules', 'child')));
261
+ assert.notStrictEqual(report.scopes.commonControl.proposedPath, parentReport.scopes.commonControl.proposedPath);
262
+ assert.strictEqual(report.git.gitCommonDir, report.git.gitDir);
263
+ });
264
+
265
+ const mixedWorkspace = directory('legacy-with-canonical-substrate');
266
+ write(mixedWorkspace, `${legacy}/HARNESS_VERSION`, '1\n');
267
+ write(mixedWorkspace, `${canonical}/state.json`, '{}\n');
268
+ write(mixedWorkspace, `${canonical}/cache/sessions/private.json`, sentinel);
269
+ check('legacy selection cannot hide existing canonical substrate or runtime metadata', () => {
270
+ const report = inspect(mixedWorkspace);
271
+ assert.strictEqual(report.workspace.selectedName, legacy);
272
+ const stateRow = report.inventory.find(row => row.currentPath === path.join(mixedWorkspace, canonical, 'state.json'));
273
+ assert(stateRow, 'existing canonical state was omitted from the inventory');
274
+ assert.strictEqual(stateRow.metadata.status, 'file');
275
+ assert.strictEqual(stateRow.selectedWorkspace, false);
276
+ assert(I.formatStateInspection(report, 'en').includes(`${canonical}/state.json: file`));
277
+ });
278
+
279
+ const residualLegacy = directory('canonical-with-residual-legacy');
280
+ write(residualLegacy, `${canonical}/HARNESS_VERSION`, '1\n');
281
+ write(residualLegacy, `${legacy}/runs/run-0001.json`, '{}\n');
282
+ check('canonical selection retains visibility of unselected legacy evidence', () => {
283
+ const report = inspect(residualLegacy);
284
+ assert.strictEqual(report.workspace.selectedName, canonical);
285
+ const row = report.inventory.find(item => item.currentPath === path.join(residualLegacy, legacy, 'runs'));
286
+ assert(row, 'residual legacy evidence was omitted from the inventory');
287
+ assert.strictEqual(row.metadata.status, 'directory');
288
+ assert.strictEqual(row.selectedWorkspace, false);
289
+ });
290
+
291
+ const alias = path.join(arena, 'alias junction Ω');
292
+ fs.symlinkSync(linkedB, alias, process.platform === 'win32' ? 'junction' : 'dir');
293
+ check('space, Unicode, and junction/symlink aliases resolve to one project identity', () => {
294
+ const report = inspect(alias);
295
+ assert.strictEqual(report.projectRoot, bReport.projectRoot);
296
+ assert.strictEqual(report.projectKey, bReport.projectKey);
297
+ assert.deepStrictEqual(report.scopes, bReport.scopes);
298
+ });
299
+ if (process.platform === 'win32') check('Windows case aliases do not produce duplicate project identities', () => {
300
+ assert.deepStrictEqual(inspect(linkedB.toUpperCase()).scopes, bReport.scopes);
301
+ });
302
+
303
+ check('inherited Git repository/config overrides are scrubbed without touching the foreign clone', () => {
304
+ const poisoned = { ...env, GIT_DIR: path.join(clone, '.git'), git_common_dir: path.join(clone, '.git'),
305
+ GIT_WORK_TREE: clone, GIT_INDEX_FILE: path.join(clone, '.git', 'foreign-index'),
306
+ GIT_OBJECT_DIRECTORY: path.join(clone, '.git', 'objects'), GIT_NAMESPACE: 'foreign',
307
+ GIT_CONFIG_COUNT: '1', GIT_CONFIG_KEY_0: 'core.worktree', GIT_CONFIG_VALUE_0: clone };
308
+ const calls = [];
309
+ const originalSpawn = cp.spawnSync;
310
+ cp.spawnSync = function (file, args, options) {
311
+ calls.push({ file: String(file), args, options });
312
+ return originalSpawn.call(this, file, args, options);
313
+ };
314
+ let report;
315
+ try { report = inspect(linkedB, { env: poisoned }); }
316
+ finally { cp.spawnSync = originalSpawn; }
317
+ assert.deepStrictEqual(report.scopes, bReport.scopes);
318
+ const actualGit = calls.filter(call => /^(git|git\.exe)$/i.test(path.basename(call.file)));
319
+ assert.strictEqual(actualGit.length, 1, 'topology should use one Git process');
320
+ assert(calls.every(call => /^(git|git\.exe|where\.exe)$/i.test(path.basename(call.file))), 'a non-Git/provider process was launched');
321
+ const call = actualGit[0];
322
+ assert.strictEqual(call.options.shell, false);
323
+ assert.strictEqual(call.options.timeout, 5000);
324
+ assert.strictEqual(call.options.maxBuffer, 65536);
325
+ assert(call.args.includes('--no-optional-locks'));
326
+ assert(!Object.keys(call.options.env).some(gateway._shouldDrop));
327
+ assert.strictEqual(call.options.env.GIT_CONFIG_GLOBAL, emptyConfig);
328
+ assert.strictEqual(call.options.env.LC_ALL, 'C');
329
+ });
330
+
331
+ check('inventory reads metadata only and never recursively enumerates runtime or run contents', () => {
332
+ const stateDir = path.join(mainRoot, canonical);
333
+ const originalRead = fs.readFileSync;
334
+ const originalDir = fs.readdirSync;
335
+ const originalLstat = fs.lstatSync;
336
+ const contentReads = [], directoryReads = [], cacheStats = [];
337
+ fs.readFileSync = function (file, ...args) {
338
+ const absolute = typeof file === 'string' ? path.resolve(file) : '';
339
+ if (absolute === stateDir || absolute.startsWith(stateDir + path.sep)) {
340
+ contentReads.push(absolute); throw new Error('state content must not be read');
341
+ }
342
+ return originalRead.call(this, file, ...args);
343
+ };
344
+ fs.readdirSync = function (file, ...args) {
345
+ const absolute = path.resolve(String(file));
346
+ if (absolute.startsWith(stateDir + path.sep)) {
347
+ directoryReads.push(absolute); throw new Error('nested state directories must not be traversed');
348
+ }
349
+ return originalDir.call(this, file, ...args);
350
+ };
351
+ fs.lstatSync = function (file, ...args) {
352
+ if (path.resolve(String(file)) === path.join(stateDir, 'cache')) cacheStats.push(String(file));
353
+ return originalLstat.call(this, file, ...args);
354
+ };
355
+ let report;
356
+ try { report = I.inspectState(mainRoot, { env }); }
357
+ finally { fs.readFileSync = originalRead; fs.readdirSync = originalDir; fs.lstatSync = originalLstat; }
358
+ assertContract(report);
359
+ assert.deepStrictEqual(contentReads, []);
360
+ assert.deepStrictEqual(directoryReads, []);
361
+ assert.strictEqual(cacheStats.length, 1, 'inventory should cache shared parent metadata');
362
+ });
363
+
364
+ const linkedState = directory('linked-state-parents');
365
+ const outsideState = directory('outside-state-target');
366
+ write(linkedState, `${canonical}/HARNESS_VERSION`, '1\n');
367
+ write(outsideState, 'sessions/must-not-read.json', sentinel);
368
+ fs.symlinkSync(outsideState, path.join(linkedState, canonical, 'cache'), process.platform === 'win32' ? 'junction' : 'dir');
369
+ fs.symlinkSync(outsideState, path.join(linkedState, canonical, 'state.json'), process.platform === 'win32' ? 'junction' : 'dir');
370
+ check('linked inventory parents are blocked and final links are described without following them', () => {
371
+ const report = inspect(linkedState);
372
+ assert(report.inventory.filter(row => row.relativePath.startsWith('cache/')).every(row => row.metadata.status === 'blocked' && row.metadata.parentStatus === 'link'));
373
+ assert.strictEqual(report.inventory.find(row => row.relativePath === 'state.json').metadata.status, 'link');
374
+ });
375
+
376
+ const noGit = directory('empty-non-git');
377
+ check('empty non-Git projects expose only an explicit local fallback and never initialize it', () => {
378
+ const report = inspect(noGit);
379
+ assertContract(report);
380
+ assert.strictEqual(report.git, null);
381
+ assert.strictEqual(report.workspace.status, 'absent');
382
+ assert.strictEqual(report.scopes.worktree.backend, 'project-local-fallback');
383
+ assert.strictEqual(report.scopes.commonControl.proposedPath, null);
384
+ assert.strictEqual(report.scopes.commonControl.available, false);
385
+ assert(report.warnings.includes('non_git_no_common_control'));
386
+ assert.deepStrictEqual(fs.readdirSync(noGit), []);
387
+ });
388
+
389
+ check('legacy selection is reported honestly without migration or cache creation', () => {
390
+ const root = directory('legacy-workspace');
391
+ write(root, `${legacy}/HARNESS_VERSION`, '1.36.161\n');
392
+ write(root, `${legacy}/decisions.json`, `{broken ${sentinel}`);
393
+ const report = inspect(root);
394
+ assert.strictEqual(report.workspace.selectedName, legacy);
395
+ assert.strictEqual(report.scopes.project.currentPath, path.join(root, legacy));
396
+ assert.strictEqual(report.scopes.project.proposedPath, path.join(root, canonical));
397
+ assert(report.warnings.includes('legacy_workspace_not_migrated'));
398
+ assert(!fs.existsSync(path.join(root, canonical)));
399
+ });
400
+ check('foreign canonical content is a warning, not an initialized runtime', () => {
401
+ const root = directory('foreign-workspace');
402
+ write(root, `${canonical}/user-notes.md`, sentinel);
403
+ const report = inspect(root);
404
+ assert.strictEqual(report.workspace.status, 'foreign');
405
+ assert(report.warnings.includes('workspace_not_recognized'));
406
+ });
407
+ check('conflicting live stores fail with the existing workspace error instead of choosing a winner', () => {
408
+ const root = directory('conflict');
409
+ write(root, `${canonical}/HARNESS_VERSION`, '1\n');
410
+ write(root, `${legacy}/HARNESS_VERSION`, '2\n');
411
+ errorCode(() => inspect(root), 'E_WORKSPACE_DIR_CONFLICT');
412
+ });
413
+ check('workspace directory links remain rejected without writes', () => {
414
+ const root = directory('linked-workspace');
415
+ fs.symlinkSync(outsideState, path.join(root, canonical), process.platform === 'win32' ? 'junction' : 'dir');
416
+ errorCode(() => inspect(root), 'E_WORKSPACE_DIR_SYMLINK');
417
+ });
418
+ check('explicit workspace override precedence is preserved by the new path API', () => {
419
+ const root = directory('forced-workspace');
420
+ fs.mkdirSync(path.join(root, canonical));
421
+ fs.mkdirSync(path.join(root, legacy));
422
+ const overridden = { ...env, LEERNESS_WORKSPACE_DIR: legacy };
423
+ assert.strictEqual(inspect(root, { env: overridden }).workspace.selectedName, legacy);
424
+ assert.strictEqual(inspect(root, { env: overridden, envValue: canonical }).workspace.selectedName, canonical);
425
+ assert.strictEqual(inspect(root, { env: overridden, envValue: null }).workspace.selectedName, canonical);
426
+ });
427
+ check('human format explains proposal-only status and preserves machine fields', () => {
428
+ const report = inspect(noGit);
429
+ const before = JSON.stringify(report);
430
+ const en = I.formatStateInspection(report, 'en');
431
+ const ko = I.formatStateInspection(report, 'ko');
432
+ assert(en.includes('runtime NOT activated') && en.includes('unavailable (non-Git)'));
433
+ assert(ko.includes('읽기 전용') && ko.includes('마이그레이션 미구현'));
434
+ assert.strictEqual(JSON.stringify(report), before);
435
+ });
436
+
437
+ const fileTarget = path.join(noGit, 'ordinary-file');
438
+ fs.writeFileSync(fileTarget, 'not-directory');
439
+ for (const [label, value, code] of [
440
+ ['missing directory', path.join(arena, 'does-not-exist'), 'path_not_found'],
441
+ ['file target', fileTarget, 'path_not_directory'],
442
+ ['empty path', '', 'path_invalid'],
443
+ ['whitespace path', ' ', 'path_invalid'],
444
+ ['control-character path', `${noGit}\n`, 'path_invalid'],
445
+ ]) check(`${label} fails without selecting or creating another backend`, () => errorCode(() => inspect(value), code));
446
+
447
+ const bare = directory('bare');
448
+ git(bare, ['init', '--bare']);
449
+ check('bare repository inspection fails explicitly', () => errorCode(() => inspect(bare), 'git_bare_unsupported'));
450
+ check('Git metadata directory is not mistaken for a worktree project', () => errorCode(() => inspect(mainReport.git.gitDir), 'git_metadata_path'));
451
+ for (const content of ['gitdir: missing-metadata\n', 'malformed gitfile\n']) {
452
+ check(`broken gitfile cannot fall back to project-local runtime: ${content.trim()}`, () => {
453
+ const root = directory('broken-gitfile');
454
+ write(root, '.git', content);
455
+ errorCode(() => inspect(root), 'git_repository_unavailable');
456
+ });
457
+ }
458
+ check('discovery ceiling is honored without reading the excluded parent repository', () => {
459
+ const report = inspect(path.join(mainRoot, 'packages', 'alpha'), { env: { ...env, GIT_CEILING_DIRECTORIES: mainRoot } });
460
+ assert.strictEqual(report.git, null);
461
+ assert.strictEqual(report.scopes.commonControl.available, false);
462
+ });
463
+ check('nonexistent discovery ceiling entries retain Git semantics instead of becoming path errors', () => {
464
+ const report = inspect(noGit, { env: { ...env, GIT_CEILING_DIRECTORIES: path.join(arena, 'missing-ceiling') } });
465
+ assert.strictEqual(report.git, null);
466
+ });
467
+
468
+ check('actually missing Git on PATH has an explicit missing-tool error on this platform', () => {
469
+ const missingEnv = { ...env };
470
+ for (const key of Object.keys(missingEnv)) if (key.toUpperCase() === 'PATH') delete missingEnv[key];
471
+ missingEnv.PATH = directory('empty-bin');
472
+ errorCode(() => inspect(noGit, { env: missingEnv }), 'git_missing');
473
+ });
474
+
475
+ for (const [errno, expected] of [['ENOENT', 'git_missing'], ['ETIMEDOUT', 'git_timeout'],
476
+ ['ENOBUFS', 'git_output_limit'], ['EACCES', 'git_unreadable'], ['EPERM', 'git_unreadable'], ['EIO', 'git_failed']]) {
477
+ check(`gateway ${errno} failure remains explicit and does not select a fallback`, () => {
478
+ withGitResult({ status: null, error: Object.assign(new Error('controlled failure'), { code: errno }), stdout: '', stderr: '' },
479
+ module => noWrites(() => errorCode(() => module.inspectState(noGit, { env }), expected)));
480
+ });
481
+ }
482
+ const gitDir = mainReport.git.gitDir;
483
+ const topology = `false\nfalse\n${mainReport.git.worktreeRoot}\n${gitDir}\n${gitDir}\n`;
484
+ for (const [label, result, expected] of [
485
+ ['unsupported flag echo', { status: 0, stdout: `--path-format=absolute\n${topology}`, stderr: '' }, 'git_unsupported'],
486
+ ['short topology', { status: 0, stdout: 'false\nfalse\n', stderr: '' }, 'git_output_invalid'],
487
+ ['relative topology', { status: 0, stdout: 'false\nfalse\n.\n.git\n.git\n', stderr: '' }, 'git_output_invalid'],
488
+ ['extra topology line', { status: 0, stdout: topology + 'unexpected\n', stderr: '' }, 'git_output_invalid'],
489
+ ['generic discovery failure', { status: 128, stdout: '', stderr: sentinel }, 'git_repository_unavailable'],
490
+ ['permission denial', { status: 128, stdout: '', stderr: 'fatal: Permission denied' }, 'git_unreadable'],
491
+ ['portable missing-tool denial', { status: 127, stdout: '', stderr: 'leerness: command not found on PATH: git\n' }, 'git_missing'],
492
+ ['portable unsupported-shim denial', { status: 126, stdout: '', stderr: 'leerness: unsupported Windows command shim: C:/probe/git.cmd\n' }, 'git_unsupported'],
493
+ ]) check(`${label} is bounded, fail-closed, and never exposes raw output`, () => {
494
+ withGitResult(result, module => noWrites(() => errorCode(() => module.inspectState(mainRoot, { env }), expected)));
495
+ });
496
+ check('a successful but foreign Git topology is rejected', () => {
497
+ withGitResult({ status: 0, stdout: topology, stderr: '' }, module => noWrites(() => {
498
+ errorCode(() => module.inspectState(noGit, { env }), 'git_repository_unavailable');
499
+ }));
500
+ });
501
+ check('missing directories returned by Git use a repository error, not a target-path error', () => {
502
+ const missingGit = path.join(arena, 'missing-returned-git-dir');
503
+ withGitResult({ status: 0, stdout: `false\nfalse\n${mainRoot}\n${missingGit}\n${missingGit}\n`, stderr: '' },
504
+ module => noWrites(() => errorCode(() => module.inspectState(mainRoot, { env }), 'git_repository_unavailable')));
505
+ });
506
+ check('inaccessible directories returned by Git use a bounded Git permission error', () => {
507
+ noWrites(() => {
508
+ const original = fs.realpathSync.native;
509
+ fs.realpathSync.native = function (file, ...args) {
510
+ if (path.resolve(String(file)) === gitDir) throw Object.assign(new Error(sentinel), { code: 'EACCES' });
511
+ return original.call(this, file, ...args);
512
+ };
513
+ try {
514
+ withGitResult({ status: 0, stdout: topology, stderr: '' },
515
+ module => errorCode(() => module.inspectState(mainRoot, { env }), 'git_unreadable'));
516
+ } finally { fs.realpathSync.native = original; }
517
+ });
518
+ });
519
+ for (const statTarget of [noGit, arena]) {
520
+ check(`marker discovery stat failure remains a bounded Git error: ${path.basename(statTarget)}`, () => {
521
+ noWrites(() => {
522
+ const original = fs.statSync;
523
+ fs.statSync = function (file, ...args) {
524
+ if (path.resolve(String(file)) === statTarget) throw Object.assign(new Error(sentinel), { code: 'EACCES' });
525
+ return original.call(this, file, ...args);
526
+ };
527
+ try {
528
+ withGitResult({ status: 128, stdout: '', stderr: 'fatal: not a git repository (or any of the parent directories): .git' }, () => {
529
+ const module = require('../lib/state-git');
530
+ errorCode(() => module.resolveGitTopology(noGit, { env: { ...env, GIT_CEILING_DIRECTORIES: '' } }), 'git_unreadable');
531
+ });
532
+ } finally { fs.statSync = original; }
533
+ });
534
+ });
535
+ }
536
+ check('path resolution permission failures produce bounded errors without filesystem changes', () => {
537
+ const original = fs.realpathSync.native;
538
+ fs.realpathSync.native = () => { throw Object.assign(new Error(sentinel), { code: 'EACCES' }); };
539
+ try { errorCode(() => SG.canonicalDirectory(noGit), 'path_unreadable'); }
540
+ finally { fs.realpathSync.native = original; }
541
+ });
542
+ for (const method of ['lstatSync', 'readdirSync']) {
543
+ check(`${method} permission denial cannot turn a workspace into missing or empty state`, () => {
544
+ noWrites(() => {
545
+ const original = fs[method];
546
+ fs[method] = function (file, ...args) {
547
+ if (path.resolve(String(file)) === path.join(mainRoot, canonical)) {
548
+ throw Object.assign(new Error(sentinel), { code: 'EACCES' });
549
+ }
550
+ return original.call(this, file, ...args);
551
+ };
552
+ try { errorCode(() => I.inspectState(mainRoot, { env }), 'workspace_unreadable'); }
553
+ finally { fs[method] = original; }
554
+ });
555
+ });
556
+ }
557
+ check('an unreadable inventory file is described honestly rather than silently absent', () => {
558
+ noWrites(() => {
559
+ const original = fs.lstatSync;
560
+ fs.lstatSync = function (file, ...args) {
561
+ if (path.resolve(String(file)) === path.join(mainRoot, canonical, 'state.json')) {
562
+ throw Object.assign(new Error(sentinel), { code: 'EACCES' });
563
+ }
564
+ return original.call(this, file, ...args);
565
+ };
566
+ let report;
567
+ try { report = I.inspectState(mainRoot, { env }); }
568
+ finally { fs.lstatSync = original; }
569
+ const row = report.inventory.find(item => item.relativePath === 'state.json');
570
+ assert.strictEqual(row.metadata.status, 'unreadable');
571
+ assert.strictEqual(row.metadata.errorCode, 'EACCES');
572
+ assert(!JSON.stringify(report).includes(sentinel));
573
+ });
574
+ });
575
+ } catch (error) {
576
+ check('fixture setup and probe runner complete', () => { throw error; });
577
+ } finally {
578
+ process.chdir(originalCwd);
579
+ check('module probe does not edit any product source bytes or mtimes', () => {
580
+ assert.deepStrictEqual(sourceFiles.map(fileSignature), sourceBefore);
581
+ });
582
+ const resolvedArena = path.resolve(arena);
583
+ if (path.dirname(resolvedArena) !== tempDirectory || !path.basename(resolvedArena).startsWith('leerness-state-scopes-')) {
584
+ throw new Error(`refusing cleanup outside the probe arena: ${resolvedArena}`);
585
+ }
586
+ fs.rmSync(resolvedArena, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
587
+ }
588
+
589
+ process.stdout.write(`STATE_SCOPES_PROBE ${total - failed}/${total} passed\n`);
590
+ process.exitCode = failed ? 1 : 0;