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.
- package/CHANGELOG.md +4 -0
- package/README.ko.md +5 -0
- package/README.md +9 -4
- package/bin/leerness.js +50 -3
- package/docs/state-paths-api.md +92 -0
- package/docs/state-scopes.md +264 -0
- package/lib/pure-utils.js +20 -0
- package/lib/state-git.js +138 -0
- package/lib/state-inspect.js +39 -0
- package/lib/state-inventory.js +69 -0
- package/lib/state-paths.js +59 -0
- package/lib/workspace-dir.js +22 -9
- package/package.json +6 -5
- package/scripts/command-flags-probe.js +1 -1
- package/scripts/false-claim-probe.js +3 -0
- package/scripts/file-lease-probe.js +1 -1
- package/scripts/state-inspect-cli-probe.js +209 -0
- package/scripts/state-scopes-probe.js +590 -0
- package/scripts/workspace-selection-probe.js +230 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const assert = require('assert');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
const cp = require('child_process');
|
|
10
|
+
const { gitSpawn, _shouldDrop } = require('../lib/git');
|
|
11
|
+
const { LEGACY_WORKSPACE_DIR, CANONICAL_WORKSPACE_DIR } = require('../lib/workspace-dir');
|
|
12
|
+
const CLI = path.resolve(__dirname, '../bin/leerness.js');
|
|
13
|
+
// Match canonical product paths even when TEMP uses a Windows short-name alias.
|
|
14
|
+
const fixture = fs.mkdtempSync(path.join(fs.realpathSync.native(os.tmpdir()), 'leerness-state-inspect-cli-'));
|
|
15
|
+
let passed = 0;
|
|
16
|
+
|
|
17
|
+
function check(label, run) {
|
|
18
|
+
run(); passed++; console.log(`PASS ${label}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function snapshot(root) {
|
|
22
|
+
const result = {};
|
|
23
|
+
function visit(file, relative) {
|
|
24
|
+
const stat = fs.lstatSync(file);
|
|
25
|
+
const row = { mtimeMs: stat.mtimeMs, kind: stat.isDirectory() ? 'dir' : 'file' };
|
|
26
|
+
if (stat.isFile()) row.hash = crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
27
|
+
if (stat.isSymbolicLink()) row.link = fs.readlinkSync(file);
|
|
28
|
+
result[relative] = row;
|
|
29
|
+
if (stat.isDirectory()) for (const name of fs.readdirSync(file).sort()) visit(path.join(file, name), `${relative}/${name}`);
|
|
30
|
+
}
|
|
31
|
+
visit(root, '.');
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const env = { ...process.env };
|
|
36
|
+
for (const key of Object.keys(env)) {
|
|
37
|
+
if (/^(LEERNESS_|GIT_CONFIG_|GIT_CEILING_DIRECTORIES|GIT_DISCOVERY_ACROSS_FILESYSTEM|NODE_OPTIONS)/i.test(key) || _shouldDrop(key)) delete env[key];
|
|
38
|
+
}
|
|
39
|
+
const config = path.join(fixture, 'empty.gitconfig');
|
|
40
|
+
fs.writeFileSync(config, '');
|
|
41
|
+
Object.assign(env, { GIT_CONFIG_GLOBAL: config, GIT_CONFIG_SYSTEM: config,
|
|
42
|
+
LEERNESS_LANG: 'en', LEERNESS_SESSION_ID: 'inspect-fixture-session' });
|
|
43
|
+
|
|
44
|
+
function project(name, workspaceName = CANONICAL_WORKSPACE_DIR) {
|
|
45
|
+
const root = path.join(fixture, name);
|
|
46
|
+
const workspace = path.join(root, workspaceName);
|
|
47
|
+
fs.mkdirSync(workspace, { recursive: true });
|
|
48
|
+
fs.writeFileSync(path.join(workspace, 'HARNESS_VERSION'), '1.0.0\n');
|
|
49
|
+
fs.writeFileSync(path.join(workspace, 'current-state.md'), 'User context must be retained.\n');
|
|
50
|
+
fs.writeFileSync(path.join(workspace, 'manifest.json'), '{"language":"ko"}\n');
|
|
51
|
+
return root;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const cwd = project('unrelated cwd', LEGACY_WORKSPACE_DIR);
|
|
55
|
+
const target = project('target 한글');
|
|
56
|
+
const legacy = project('legacy target', LEGACY_WORKSPACE_DIR);
|
|
57
|
+
function run(args, options = {}) {
|
|
58
|
+
return cp.spawnSync(process.execPath, [...(options.nodeArgs || []), CLI, ...args], {
|
|
59
|
+
cwd: options.cwd || cwd, env: { ...env, ...options.env }, encoding: 'utf8', timeout: 15000, maxBuffer: 1024 * 1024,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function successful(args, options) {
|
|
63
|
+
const result = run(args, options);
|
|
64
|
+
assert.strictEqual(result.status, 0, result.stderr || result.stdout);
|
|
65
|
+
assert.strictEqual(result.stderr, '');
|
|
66
|
+
const report = JSON.parse(result.stdout);
|
|
67
|
+
assert.strictEqual(report.ok, true);
|
|
68
|
+
assert.strictEqual(report.runtimeActivated, false);
|
|
69
|
+
assert.strictEqual(report.migrationAvailable, false);
|
|
70
|
+
return report;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const init = gitSpawn(['init', target], { env, encoding: 'utf8', timeout: 15000 });
|
|
75
|
+
assert.strictEqual(init.status, 0, init.stderr);
|
|
76
|
+
const before = snapshot(fixture);
|
|
77
|
+
check('default execution enables ordinary bookkeeping but inspect writes no bytes or mtimes', () => {
|
|
78
|
+
assert(!Object.keys(env).some(key => /LEERNESS_(INTERNAL|NO_AUTO_WORKSPACE_MIGRATION|NO_STALE_CHECK)/i.test(key)));
|
|
79
|
+
const report = successful(['state', 'inspect', target, '--json']);
|
|
80
|
+
assert.strictEqual(report.schema, 'leerness.state-inspection/v1');
|
|
81
|
+
assert.strictEqual(report.projectRoot, fs.realpathSync.native(target));
|
|
82
|
+
assert.strictEqual(report.git.linkedWorktree, false);
|
|
83
|
+
assert.deepStrictEqual(snapshot(fixture), before);
|
|
84
|
+
});
|
|
85
|
+
check('explicit --path takes precedence over positional target', () => {
|
|
86
|
+
const report = successful(['state', 'inspect', cwd, `--path=${target}`, '--json']);
|
|
87
|
+
assert.strictEqual(report.projectRoot, fs.realpathSync.native(target));
|
|
88
|
+
});
|
|
89
|
+
check('flags may precede command; omitted target uses cwd', () => {
|
|
90
|
+
const report = successful(['--json', 'state', 'inspect'], { cwd: target });
|
|
91
|
+
assert.strictEqual(report.projectRoot, fs.realpathSync.native(target));
|
|
92
|
+
});
|
|
93
|
+
check('legacy target does not migrate and has explicit non-Git fallback', () => {
|
|
94
|
+
const report = successful(['state', 'inspect', legacy, '--json']);
|
|
95
|
+
assert.strictEqual(report.workspace.selectedName, LEGACY_WORKSPACE_DIR);
|
|
96
|
+
assert.strictEqual(report.git, null);
|
|
97
|
+
assert.strictEqual(report.scopes.commonControl.available, false);
|
|
98
|
+
assert.strictEqual(report.scopes.commonControl.proposedPath, null);
|
|
99
|
+
assert(report.warnings.includes('legacy_workspace_not_migrated'));
|
|
100
|
+
assert.deepStrictEqual(snapshot(fixture), before);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
for (const args of [
|
|
104
|
+
['state', 'inspect', target, 'extra'],
|
|
105
|
+
['state', 'inspect', target, '--path', target, '--path', legacy],
|
|
106
|
+
['state', 'inspect', target, '--path'],
|
|
107
|
+
['state', 'inspect', '--path='],
|
|
108
|
+
['state', 'inspect', '--path', ' '],
|
|
109
|
+
['state', 'inspect', '--path', path.join(fixture, 'missing')],
|
|
110
|
+
['state', 'inspect', '--path', config],
|
|
111
|
+
['state', 'inspect', target, '--apply'],
|
|
112
|
+
['state', 'inspect', target, '--force'],
|
|
113
|
+
['state', 'inspect', target, '--output=ignored'],
|
|
114
|
+
['state', 'inspect', target, '--json=false'],
|
|
115
|
+
]) {
|
|
116
|
+
check(`invalid invocation fails without writes: ${args.slice(2).join(' ')}`, () => {
|
|
117
|
+
const result = run([...args, '--json']);
|
|
118
|
+
assert.strictEqual(result.status, 1, result.stdout);
|
|
119
|
+
assert.strictEqual(JSON.parse(result.stdout).ok, false);
|
|
120
|
+
assert.strictEqual(result.stderr, '');
|
|
121
|
+
assert.deepStrictEqual(snapshot(fixture), before);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
check('English and Korean text explain proposed-only state', () => {
|
|
125
|
+
const english = run(['state', 'inspect', target, '--language', 'en']);
|
|
126
|
+
const korean = run(['state', 'inspect', target, '--language', 'ko']);
|
|
127
|
+
assert.strictEqual(english.status, 0, english.stderr);
|
|
128
|
+
assert.strictEqual(korean.status, 0, korean.stderr);
|
|
129
|
+
assert.match(english.stdout, /runtime NOT activated/);
|
|
130
|
+
assert.match(korean.stdout, /runtime 활성화 안 됨/);
|
|
131
|
+
assert.match(english.stdout, /Common-Control/);
|
|
132
|
+
assert.deepStrictEqual(snapshot(fixture), before);
|
|
133
|
+
});
|
|
134
|
+
check('catalog and both help locales advertise inspection', () => {
|
|
135
|
+
const catalog = run(['commands', '--json']);
|
|
136
|
+
const rows = Object.values(JSON.parse(catalog.stdout).categories).flat();
|
|
137
|
+
assert(rows.some(row => /^state inspect\b/.test(row.cmd)));
|
|
138
|
+
for (const lang of ['en', 'ko']) assert.match(run(['--help', '--language', lang]).stdout, /state inspect \[path\]/);
|
|
139
|
+
});
|
|
140
|
+
check('mutation and permission classifiers do not reinterpret path content', () => {
|
|
141
|
+
const { _cliMutationClass, _requiredTier } = require('../bin/leerness.js');
|
|
142
|
+
assert.strictEqual(_cliMutationClass(['state', 'inspect', 'publish'], 'state'), 'observation-only');
|
|
143
|
+
assert.strictEqual(_cliMutationClass(['state', 'show'], 'state'), 'stateful');
|
|
144
|
+
assert.strictEqual(_cliMutationClass(['state', 'start'], 'state'), 'stateful');
|
|
145
|
+
for (const prefix of ['', 'leerness ', 'npx -y leerness@1.0.0 ']) {
|
|
146
|
+
assert.strictEqual(_requiredTier(`${prefix}state inspect ./release publish/web`), 'read-only');
|
|
147
|
+
assert.strictEqual(_requiredTier(`${prefix}state inspect "C:\\work\\release (draft)\\publish"`), 'read-only');
|
|
148
|
+
assert.strictEqual(_requiredTier(`${prefix}state inspect "\\\\server\\c$\\R&D\\web"`), 'read-only');
|
|
149
|
+
assert.strictEqual(_requiredTier(`${prefix}state inspect '/work/R&D/publish'`), 'read-only');
|
|
150
|
+
// Pure classification only: these strings are never executed as commands.
|
|
151
|
+
assert.strictEqual(_requiredTier(`${prefix}state inspect . && git push`), 'git-write');
|
|
152
|
+
assert.strictEqual(_requiredTier(`${prefix}state inspect .; npm publish`), 'publish');
|
|
153
|
+
assert.strictEqual(_requiredTier(`${prefix}state inspect "C:\\work\\R&D" && git push`), 'git-write');
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Instrument the real entry point, not a test-only --internal fast path.
|
|
158
|
+
const preload = path.join(fixture, 'observe.cjs');
|
|
159
|
+
fs.writeFileSync(preload, `
|
|
160
|
+
const fs = require('fs'), cp = require('child_process'), path = require('path');
|
|
161
|
+
const metrics = { gitCalls: 0, locatorCalls: 0, otherCalls: 0, projectReads: 0, writes: 0 };
|
|
162
|
+
const spawn = cp.spawnSync;
|
|
163
|
+
cp.spawnSync = function(command, args, options) {
|
|
164
|
+
if (/(?:^|[\\\\/])git(?:\\.exe)?$/i.test(command)) metrics.gitCalls++;
|
|
165
|
+
else if (/(?:^|[\\\\/])where\\.exe$/i.test(command) && args.length === 1 && args[0] === 'git') metrics.locatorCalls++;
|
|
166
|
+
else if (!/(?:^|[\\\\/])chcp(?:\\.com)?$/i.test(command)) metrics.otherCalls++;
|
|
167
|
+
return spawn.apply(this, arguments);
|
|
168
|
+
};
|
|
169
|
+
const read = fs.readFileSync;
|
|
170
|
+
fs.readFileSync = function(file) {
|
|
171
|
+
if (String(file).startsWith(${JSON.stringify(fixture)})) metrics.projectReads++;
|
|
172
|
+
return read.apply(this, arguments);
|
|
173
|
+
};
|
|
174
|
+
for (const name of ['writeFileSync','appendFileSync','mkdirSync','renameSync','unlinkSync','rmSync','utimesSync']) {
|
|
175
|
+
fs[name] = function() { metrics.writes++; throw new Error('inspect attempted a write: ' + name); };
|
|
176
|
+
}
|
|
177
|
+
process.on('exit', () => process.stderr.write(JSON.stringify(metrics)));
|
|
178
|
+
`);
|
|
179
|
+
check('real CLI performs one Git query, zero other execution, project content reads, and writes', () => {
|
|
180
|
+
const snapshotBefore = snapshot(fixture);
|
|
181
|
+
const result = run(['state', 'inspect', target, '--json'], { nodeArgs: ['--require', preload] });
|
|
182
|
+
assert.strictEqual(result.status, 0, result.stdout + result.stderr);
|
|
183
|
+
assert.strictEqual(JSON.parse(result.stdout).ok, true);
|
|
184
|
+
assert.deepStrictEqual(JSON.parse(result.stderr), { gitCalls: 1, locatorCalls: process.platform === 'win32' ? 1 : 0,
|
|
185
|
+
otherCalls: 0, projectReads: 0, writes: 0 });
|
|
186
|
+
assert.deepStrictEqual(snapshot(fixture), snapshotBefore);
|
|
187
|
+
});
|
|
188
|
+
check('invalid inspection diagnostics use no project content for error language', () => {
|
|
189
|
+
const localeCwd = project('error locale cwd');
|
|
190
|
+
const snapshotBefore = snapshot(fixture);
|
|
191
|
+
for (const args of [
|
|
192
|
+
['state', 'inspect', target, '--apply', '--json'],
|
|
193
|
+
['state', 'inspect', target, '--unknown-inspect-flag', '--json'],
|
|
194
|
+
['state', 'inspect', target, '--json=false', '--json'],
|
|
195
|
+
['state', 'inspect', target, '--path', '--json'],
|
|
196
|
+
]) {
|
|
197
|
+
const result = run(args, { cwd: localeCwd, env: { LEERNESS_LANG: '' }, nodeArgs: ['--require', preload] });
|
|
198
|
+
assert.strictEqual(result.status, 1, result.stdout + result.stderr);
|
|
199
|
+
assert.strictEqual(JSON.parse(result.stdout).ok, false);
|
|
200
|
+
assert.deepStrictEqual(JSON.parse(result.stderr), { gitCalls: 0, locatorCalls: 0,
|
|
201
|
+
otherCalls: 0, projectReads: 0, writes: 0 });
|
|
202
|
+
assert.deepStrictEqual(snapshot(fixture), snapshotBefore);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
console.log(`state-inspect CLI probe: ${passed}/${passed} PASS`);
|
|
206
|
+
} finally {
|
|
207
|
+
// Only this probe's freshly allocated fixture is eligible for cleanup.
|
|
208
|
+
fs.rmSync(fixture, { recursive: true, force: true });
|
|
209
|
+
}
|