@wipcomputer/wip-ldm-os 0.4.81 → 0.4.83

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,282 @@
1
+ #!/usr/bin/env node
2
+ // Regression test: bin ownership manifest aggregator + heal + integration.
3
+ //
4
+ // Unit: aggregateBinManifest, healBinManifest, validateDeclarations.
5
+ // Integration: real `node bin/ldm.js install` against a temp HOME with a
6
+ // fake extension declaring binFiles. Verifies install-time self-heal
7
+ // restores a missing file from the declared source. Verifies that on
8
+ // conflict the install aborts BEFORE any writes (per the design's
9
+ // strict pre-write conflict rule).
10
+
11
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync, readdirSync, copyFileSync } from 'node:fs';
12
+ import { dirname, join, resolve } from 'node:path';
13
+ import { execFileSync } from 'node:child_process';
14
+ import { tmpdir } from 'node:os';
15
+ import { fileURLToPath } from 'node:url';
16
+ import {
17
+ aggregateBinManifest,
18
+ healBinManifest,
19
+ validateDeclarations,
20
+ validateDeclaration,
21
+ } from '../lib/bin-manifest.mjs';
22
+
23
+ const repo = resolve(dirname(fileURLToPath(import.meta.url)), '..');
24
+ const cli = join(repo, 'bin', 'ldm.js');
25
+ const pkg = JSON.parse(readFileSync(join(repo, 'package.json'), 'utf8'));
26
+
27
+ let failed = 0;
28
+ function assert(cond, label, output = '') {
29
+ if (cond) {
30
+ console.log(` [PASS] ${label}`);
31
+ } else {
32
+ console.log(` [FAIL] ${label}`);
33
+ if (output) console.log(` --- ldm output (last lines) ---\n ${output.trim().split('\n').slice(-30).join('\n ')}`);
34
+ failed++;
35
+ }
36
+ }
37
+
38
+ // ── Unit: validateDeclaration ──
39
+ console.log('Unit: validateDeclaration shape checks');
40
+ {
41
+ assert(validateDeclaration({ name: 'a.sh', source: 'x/a.sh' }).length === 0, 'valid declaration passes');
42
+ assert(validateDeclaration(null).length > 0, 'null declaration fails');
43
+ assert(validateDeclaration({ source: 'x' }).length > 0, 'missing name fails');
44
+ assert(validateDeclaration({ name: 'a.sh' }).length > 0, 'missing source fails');
45
+ assert(validateDeclaration({ name: '../../etc/passwd', source: 'x' }).length > 0, 'name with .. fails');
46
+ assert(validateDeclaration({ name: 'sub/a.sh', source: 'x' }).length > 0, 'name with / fails');
47
+ assert(validateDeclaration({ name: 'a.sh', source: '../escape/a.sh' }).length > 0, 'source with .. fails');
48
+ assert(validateDeclaration({ name: 'a.sh', source: 'x', executable: 'yes' }).length > 0, 'non-boolean executable fails');
49
+ }
50
+
51
+ // ── Unit: validateDeclarations (multi-decl) ──
52
+ console.log('Unit: validateDeclarations against on-disk source');
53
+ {
54
+ const tmp = mkdtempSync(join(tmpdir(), 'mc-validate-'));
55
+ mkdirSync(join(tmp, 'src'), { recursive: true });
56
+ writeFileSync(join(tmp, 'src', 'good.sh'), '#!/bin/sh\n');
57
+ const res1 = validateDeclarations('test', tmp, [{ name: 'good.sh', source: 'src/good.sh' }]);
58
+ assert(res1.valid, 'declaration with existing source passes');
59
+
60
+ const res2 = validateDeclarations('test', tmp, [{ name: 'missing.sh', source: 'src/missing.sh' }]);
61
+ assert(!res2.valid && res2.errors.some(e => /source not found/.test(e)), 'declaration with missing source fails');
62
+
63
+ const res3 = validateDeclarations('test', tmp, [
64
+ { name: 'good.sh', source: 'src/good.sh' },
65
+ { name: 'good.sh', source: 'src/good.sh' },
66
+ ]);
67
+ assert(!res3.valid && res3.errors.some(e => /duplicate name/.test(e)), 'internal duplicate names fail');
68
+ rmSync(tmp, { recursive: true, force: true });
69
+ }
70
+
71
+ // ── Unit: aggregateBinManifest produces entries from both declarers ──
72
+ console.log('Unit: aggregateBinManifest combines LDM CLI + extensions');
73
+ {
74
+ const tmp = mkdtempSync(join(tmpdir(), 'mc-agg-'));
75
+ const ldmCliRoot = join(tmp, 'cli');
76
+ const extensionsRoot = join(tmp, 'extensions');
77
+ const binDir = join(tmp, 'bin');
78
+ mkdirSync(ldmCliRoot, { recursive: true });
79
+ mkdirSync(join(extensionsRoot, 'mc'), { recursive: true });
80
+ mkdirSync(binDir, { recursive: true });
81
+ writeFileSync(join(ldmCliRoot, 'package.json'), JSON.stringify({
82
+ wipLdmOs: { binFiles: [{ name: 'cli-tool.sh', source: 'cli-tool.sh' }] },
83
+ }));
84
+ writeFileSync(join(ldmCliRoot, 'cli-tool.sh'), '#!/bin/sh\n');
85
+ writeFileSync(join(extensionsRoot, 'mc', 'openclaw.plugin.json'), JSON.stringify({
86
+ binFiles: [{ name: 'ext-shim.sh', source: 'dist/ext-shim.sh' }],
87
+ }));
88
+ mkdirSync(join(extensionsRoot, 'mc', 'dist'), { recursive: true });
89
+ writeFileSync(join(extensionsRoot, 'mc', 'dist', 'ext-shim.sh'), '#!/bin/sh\n');
90
+
91
+ const m = aggregateBinManifest({
92
+ ldmCliRoot, extensionsRoot, binDir,
93
+ registry: { extensions: { mc: {} } },
94
+ });
95
+ assert(m.entries.length === 2, 'two entries aggregated');
96
+ assert(m.entries.some(e => e.declarer === 'wip-ldm-os' && e.name === 'cli-tool.sh'), 'LDM CLI entry present');
97
+ assert(m.entries.some(e => e.declarer === 'mc' && e.name === 'ext-shim.sh'), 'extension entry present');
98
+ assert(m.conflicts.length === 0, 'no conflicts when names are distinct');
99
+ rmSync(tmp, { recursive: true, force: true });
100
+ }
101
+
102
+ // ── Unit: aggregateBinManifest detects conflicts ──
103
+ console.log('Unit: aggregateBinManifest detects same-name conflicts');
104
+ {
105
+ const tmp = mkdtempSync(join(tmpdir(), 'mc-conflict-'));
106
+ const ldmCliRoot = join(tmp, 'cli');
107
+ const extensionsRoot = join(tmp, 'extensions');
108
+ mkdirSync(ldmCliRoot, { recursive: true });
109
+ mkdirSync(join(extensionsRoot, 'mc'), { recursive: true });
110
+ writeFileSync(join(ldmCliRoot, 'package.json'), JSON.stringify({
111
+ wipLdmOs: { binFiles: [{ name: 'shared.sh', source: 'a.sh' }] },
112
+ }));
113
+ writeFileSync(join(ldmCliRoot, 'a.sh'), '');
114
+ writeFileSync(join(extensionsRoot, 'mc', 'openclaw.plugin.json'), JSON.stringify({
115
+ binFiles: [{ name: 'shared.sh', source: 'b.sh' }],
116
+ }));
117
+ writeFileSync(join(extensionsRoot, 'mc', 'b.sh'), '');
118
+
119
+ const m = aggregateBinManifest({
120
+ ldmCliRoot, extensionsRoot, binDir: join(tmp, 'bin'),
121
+ registry: { extensions: { mc: {} } },
122
+ });
123
+ assert(m.conflicts.length === 1, 'one conflict detected');
124
+ assert(m.conflicts[0].name === 'shared.sh', 'conflict names the disputed file');
125
+ assert(m.conflicts[0].declarers.length === 2, 'both declarers listed');
126
+ rmSync(tmp, { recursive: true, force: true });
127
+ }
128
+
129
+ // ── Unit: healBinManifest read-only and heal modes ──
130
+ console.log('Unit: healBinManifest classifies and restores');
131
+ {
132
+ const tmp = mkdtempSync(join(tmpdir(), 'mc-heal-'));
133
+ const binDir = join(tmp, 'bin');
134
+ mkdirSync(binDir, { recursive: true });
135
+ const sourceDir = join(tmp, 'src');
136
+ mkdirSync(sourceDir, { recursive: true });
137
+ writeFileSync(join(sourceDir, 'a.sh'), '#!/bin/sh\necho a\n');
138
+
139
+ const entryMissing = {
140
+ name: 'a.sh', destPath: join(binDir, 'a.sh'), sourcePath: join(sourceDir, 'a.sh'),
141
+ executable: true, declarer: 'test',
142
+ };
143
+
144
+ const ro = healBinManifest([entryMissing]);
145
+ assert(ro.missing.length === 1, 'read-only reports missing');
146
+ assert(ro.healed.length === 0, 'read-only does not restore');
147
+ assert(!existsSync(entryMissing.destPath), 'no write happened in read-only mode');
148
+
149
+ const wr = healBinManifest([entryMissing], { heal: true });
150
+ assert(wr.healed.length === 1, 'heal restores missing file');
151
+ assert(existsSync(entryMissing.destPath), 'file present after heal');
152
+ assert((statSync(entryMissing.destPath).mode & 0o111) !== 0, 'restored file is executable');
153
+
154
+ // Now make it non-executable and heal again.
155
+ chmodSync(entryMissing.destPath, 0o644);
156
+ const wr2 = healBinManifest([entryMissing], { heal: true });
157
+ assert(wr2.healed.length === 1, 'heal restores executable bit');
158
+ assert((statSync(entryMissing.destPath).mode & 0o111) !== 0, 'file is executable after second heal');
159
+
160
+ // Source missing case.
161
+ rmSync(join(sourceDir, 'a.sh'));
162
+ rmSync(entryMissing.destPath);
163
+ const noSrc = healBinManifest([entryMissing], { heal: true });
164
+ assert(noSrc.sourceMissing.length === 1, 'reports source missing');
165
+ assert(noSrc.failed.length === 1, 'failed records the entry');
166
+ rmSync(tmp, { recursive: true, force: true });
167
+ }
168
+
169
+ // ── Integration: real `ldm install` heals a declared extension shim ──
170
+ console.log('Integration: ldm install restores missing extension-declared shim');
171
+ function makeIntegrationHome({ extDecls = null, extraExtension = null } = {}) {
172
+ const home = mkdtempSync(join(tmpdir(), 'ldm-bin-manifest-'));
173
+ const ldmRoot = join(home, '.ldm');
174
+ const binDir = join(ldmRoot, 'bin');
175
+ const extDir = join(ldmRoot, 'extensions');
176
+ const stateDir = join(ldmRoot, 'state');
177
+ const fakeBin = join(home, 'fakebin');
178
+ mkdirSync(binDir, { recursive: true });
179
+ mkdirSync(extDir, { recursive: true });
180
+ mkdirSync(stateDir, { recursive: true });
181
+ mkdirSync(fakeBin, { recursive: true });
182
+ writeFileSync(join(ldmRoot, 'version.json'), JSON.stringify({ version: pkg.version }, null, 2) + '\n');
183
+
184
+ // Optional fake plugin with binFiles.
185
+ const registryEntries = {};
186
+ if (extDecls) {
187
+ const pluginDir = join(extDir, 'fake-plugin');
188
+ const pluginDist = join(pluginDir, 'dist');
189
+ mkdirSync(pluginDist, { recursive: true });
190
+ writeFileSync(join(pluginDir, 'openclaw.plugin.json'), JSON.stringify({
191
+ id: 'fake-plugin', name: 'Fake Plugin', binFiles: extDecls,
192
+ }));
193
+ writeFileSync(join(pluginDist, 'fake-shim.sh'), '#!/bin/sh\necho fake\n');
194
+ chmodSync(join(pluginDist, 'fake-shim.sh'), 0o755);
195
+ registryEntries['fake-plugin'] = {};
196
+ }
197
+ if (extraExtension) {
198
+ const pluginDir = join(extDir, extraExtension.name);
199
+ mkdirSync(pluginDir, { recursive: true });
200
+ writeFileSync(join(pluginDir, 'openclaw.plugin.json'), JSON.stringify({
201
+ id: extraExtension.name, name: extraExtension.name, binFiles: extraExtension.binFiles,
202
+ }));
203
+ if (extraExtension.sourceFiles) {
204
+ for (const [rel, content] of Object.entries(extraExtension.sourceFiles)) {
205
+ const fp = join(pluginDir, rel);
206
+ mkdirSync(dirname(fp), { recursive: true });
207
+ writeFileSync(fp, content);
208
+ chmodSync(fp, 0o755);
209
+ }
210
+ }
211
+ registryEntries[extraExtension.name] = {};
212
+ }
213
+ writeFileSync(join(extDir, 'registry.json'), JSON.stringify({
214
+ _format: 'v2', extensions: registryEntries,
215
+ }, null, 2) + '\n');
216
+
217
+ // npm + crontab shims.
218
+ writeFileSync(join(fakeBin, 'npm'), '#!/bin/sh\nexit 0\n');
219
+ chmodSync(join(fakeBin, 'npm'), 0o755);
220
+ writeFileSync(join(fakeBin, 'crontab'), '#!/bin/sh\nif [ "$1" = "-l" ]; then exit 1; fi\nexit 0\n');
221
+ chmodSync(join(fakeBin, 'crontab'), 0o755);
222
+
223
+ return { home, binDir, extDir, fakeBin };
224
+ }
225
+
226
+ function runInstall({ home, fakeBin }) {
227
+ try {
228
+ return execFileSync('node', [cli, 'install'], {
229
+ env: { ...process.env, HOME: home, PATH: `${fakeBin}:${process.env.PATH}`, LDM_SELF_UPDATED: '1' },
230
+ encoding: 'utf-8',
231
+ timeout: 30000,
232
+ });
233
+ } catch (err) {
234
+ return { error: true, output: (err.stdout || '') + (err.stderr || ''), code: err.status };
235
+ }
236
+ }
237
+
238
+ // Test A: extension declares fake-shim.sh; install heal-restores it because dest is absent.
239
+ {
240
+ const w = makeIntegrationHome({
241
+ extDecls: [{ name: 'fake-shim.sh', source: 'dist/fake-shim.sh' }],
242
+ });
243
+ const out = runInstall(w);
244
+ const text = typeof out === 'string' ? out : out.output;
245
+ const restored = join(w.binDir, 'fake-shim.sh');
246
+ assert(/Restored fake-shim\.sh/.test(text), 'install logs restore', text);
247
+ assert(existsSync(restored), 'fake-shim.sh present in ~/.ldm/bin/ after install', text);
248
+ assert((statSync(restored).mode & 0o111) !== 0, 'restored shim is executable', text);
249
+ rmSync(w.home, { recursive: true, force: true });
250
+ }
251
+
252
+ // Test B: conflict between LDM CLI and a fake plugin BOTH claiming the same name.
253
+ // Install must abort BEFORE writing fake-shim.sh.
254
+ console.log('Integration: ldm install aborts on conflict before writing');
255
+ {
256
+ // The LDM CLI's wipLdmOs.binFiles already declares ldm-backup.sh.
257
+ // We add a fake plugin claiming the same name.
258
+ const w = makeIntegrationHome({
259
+ extDecls: [{ name: 'ldm-backup.sh', source: 'dist/fake-shim.sh' }],
260
+ });
261
+ const out = runInstall(w);
262
+ const text = typeof out === 'string' ? out : out.output;
263
+ assert(out.error === true, 'install exits non-zero on conflict', text);
264
+ assert(/bin manifest conflict/.test(text), 'output names the conflict', text);
265
+ assert(/aborting before seedLocalCatalog\/deployBridge\/deployScripts run/.test(text), 'output declares pre-write abort', text);
266
+ // Pre-write invariant: install bailed before any of seedLocalCatalog,
267
+ // deployBridge, or deployScripts ran. Verify each.
268
+ const backupShim = join(w.binDir, 'ldm-backup.sh');
269
+ assert(!existsSync(backupShim), 'no partial state: ldm-backup.sh was NOT written', text);
270
+ const catalogFile = join(w.home, '.ldm', 'catalog.json');
271
+ assert(!existsSync(catalogFile), 'no partial state: ~/.ldm/catalog.json was NOT seeded', text);
272
+ const bridgeDist = join(w.extDir, 'lesa-bridge', 'dist');
273
+ assert(!existsSync(bridgeDist), 'no partial state: bridge files were NOT deployed', text);
274
+ rmSync(w.home, { recursive: true, force: true });
275
+ }
276
+
277
+ console.log('');
278
+ if (failed > 0) {
279
+ console.log(`${failed} test(s) failed.`);
280
+ process.exit(1);
281
+ }
282
+ console.log('All bin manifest tests passed.');
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+ // Regression test: `ldm doctor` cron-target health check.
3
+ //
4
+ // Walks the crontab, verifies each ~/.ldm/bin/<file> referenced by a
5
+ // cron entry exists and is executable, classifies failures, and offers
6
+ // restore-from-extension-dist for known shims (--fix). This is the
7
+ // LDM-side parallel to crystal doctor's checkCaptureShim.
8
+ //
9
+ // Each case sets a fake `crontab` shim on PATH so the operator's real
10
+ // crontab is never read. Real bin/ldm.js doctor runs against a temp
11
+ // HOME with version.json + registry.json seeded so the rest of the
12
+ // doctor flow doesn't crash.
13
+
14
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
15
+ import { dirname, join, resolve } from 'node:path';
16
+ import { execFileSync } from 'node:child_process';
17
+ import { tmpdir } from 'node:os';
18
+ import { fileURLToPath } from 'node:url';
19
+
20
+ const repo = resolve(dirname(fileURLToPath(import.meta.url)), '..');
21
+ const cli = join(repo, 'bin', 'ldm.js');
22
+ const pkg = JSON.parse(readFileSync(join(repo, 'package.json'), 'utf8'));
23
+
24
+ let failed = 0;
25
+ function assert(cond, label, output = '') {
26
+ if (cond) {
27
+ console.log(` [PASS] ${label}`);
28
+ } else {
29
+ console.log(` [FAIL] ${label}`);
30
+ if (output) console.log(` --- ldm output (last lines) ---\n ${output.trim().split('\n').slice(-30).join('\n ')}`);
31
+ failed++;
32
+ }
33
+ }
34
+
35
+ function setupHome(crontabContent, { withCanonicalShim = true, shimMode = 0o755, shimContent = '#!/bin/sh\necho hi\n' } = {}) {
36
+ const home = mkdtempSync(join(tmpdir(), 'ldm-doctor-cron-'));
37
+ const ldmRoot = join(home, '.ldm');
38
+ const binDir = join(ldmRoot, 'bin');
39
+ const extDir = join(ldmRoot, 'extensions');
40
+ const stateDir = join(ldmRoot, 'state');
41
+ const fakeBin = join(home, 'fakebin');
42
+
43
+ mkdirSync(binDir, { recursive: true });
44
+ mkdirSync(extDir, { recursive: true });
45
+ mkdirSync(stateDir, { recursive: true });
46
+ mkdirSync(fakeBin, { recursive: true });
47
+
48
+ writeFileSync(join(ldmRoot, 'version.json'), JSON.stringify({ version: pkg.version }, null, 2) + '\n');
49
+
50
+ // Canonical extension dist for crystal-capture.sh (so "Run: ldm doctor --fix" hint fires).
51
+ // Also register memory-crystal in the registry and declare binFiles so the
52
+ // manifest-driven knownSources lookup can find it. Mirrors the post-merge
53
+ // state once memory-crystal-private declares its binFiles.
54
+ const registryExtensions = {};
55
+ if (withCanonicalShim) {
56
+ const mcDir = join(extDir, 'memory-crystal');
57
+ const mcDist = join(mcDir, 'dist');
58
+ mkdirSync(mcDist, { recursive: true });
59
+ writeFileSync(join(mcDist, 'crystal-capture.sh'), shimContent);
60
+ chmodSync(join(mcDist, 'crystal-capture.sh'), 0o755);
61
+ writeFileSync(join(mcDir, 'openclaw.plugin.json'), JSON.stringify({
62
+ id: 'memory-crystal',
63
+ name: 'Memory Crystal',
64
+ binFiles: [{ name: 'crystal-capture.sh', source: 'dist/crystal-capture.sh' }],
65
+ }));
66
+ registryExtensions['memory-crystal'] = {};
67
+ }
68
+ writeFileSync(join(extDir, 'registry.json'), JSON.stringify({ _format: 'v2', extensions: registryExtensions }, null, 2) + '\n');
69
+
70
+ // Fake crontab on PATH. -l prints the supplied content; anything else is a no-op.
71
+ const crontabScript = join(fakeBin, 'crontab');
72
+ const escaped = crontabContent.replace(/'/g, `'\\''`);
73
+ writeFileSync(crontabScript, `#!/bin/sh\nif [ "$1" = "-l" ]; then\n printf '%s' '${escaped}'\nfi\n`);
74
+ chmodSync(crontabScript, 0o755);
75
+
76
+ // npm shim (avoid network).
77
+ const npmShim = join(fakeBin, 'npm');
78
+ writeFileSync(npmShim, `#!/bin/sh\nexit 0\n`);
79
+ chmodSync(npmShim, 0o755);
80
+
81
+ return { home, ldmRoot, binDir, extDir, fakeBin, shimMode };
82
+ }
83
+
84
+ function runDoctor({ home, fakeBin, fix = false }) {
85
+ const args = ['doctor'];
86
+ if (fix) args.push('--fix');
87
+ try {
88
+ return execFileSync('node', [cli, ...args], {
89
+ env: { ...process.env, HOME: home, PATH: `${fakeBin}:${process.env.PATH}`, LDM_SELF_UPDATED: '1' },
90
+ encoding: 'utf-8',
91
+ timeout: 30000,
92
+ });
93
+ } catch (err) {
94
+ return (err.stdout || '') + (err.stderr || '');
95
+ }
96
+ }
97
+
98
+ const CRON_CRYSTAL = '* * * * * ~/.ldm/bin/crystal-capture.sh >> ~/.ldm/logs/crystal-capture.log 2>&1\n';
99
+ const CRON_FOREIGN = '*/5 * * * * ~/.ldm/bin/foreign-tool.sh\n';
100
+
101
+ // ── Test 1: cron present, target exists and executable ──
102
+ console.log('Test 1: cron present, target healthy');
103
+ {
104
+ const w = setupHome(CRON_CRYSTAL);
105
+ // Seed a working shim at the cron target.
106
+ const target = join(w.binDir, 'crystal-capture.sh');
107
+ writeFileSync(target, '#!/bin/sh\necho ok\n');
108
+ chmodSync(target, 0o755);
109
+ const out = runDoctor(w);
110
+ assert(/Cron targets under ~\/\.ldm\/bin\/: 1 entry, all exist and executable/.test(out), 'reports healthy summary', out);
111
+ rmSync(w.home, { recursive: true, force: true });
112
+ }
113
+
114
+ // ── Test 2: cron references missing target (known shim) ──
115
+ console.log('Test 2: cron target missing, known shim');
116
+ {
117
+ const w = setupHome(CRON_CRYSTAL);
118
+ const out = runDoctor(w);
119
+ assert(/cron target missing: .*crystal-capture\.sh/.test(out), 'reports "cron target missing"', out);
120
+ assert(/Run: ldm doctor --fix to restore from/.test(out), 'suggests --fix because canonical source exists', out);
121
+ rmSync(w.home, { recursive: true, force: true });
122
+ }
123
+
124
+ // ── Test 3: --fix restores from extension dist ──
125
+ console.log('Test 3: --fix restores from canonical source');
126
+ {
127
+ const w = setupHome(CRON_CRYSTAL);
128
+ const target = join(w.binDir, 'crystal-capture.sh');
129
+ const out = runDoctor({ ...w, fix: true });
130
+ assert(/Restored crystal-capture\.sh from/.test(out), 'announces restore', out);
131
+ assert(existsSync(target), 'shim now exists at cron target', out);
132
+ assert((statSync(target).mode & 0o111) !== 0, 'restored shim is executable', out);
133
+ rmSync(w.home, { recursive: true, force: true });
134
+ }
135
+
136
+ // ── Test 4: cron target exists but not executable ──
137
+ console.log('Test 4: cron target not executable');
138
+ {
139
+ const w = setupHome(CRON_CRYSTAL);
140
+ const target = join(w.binDir, 'crystal-capture.sh');
141
+ writeFileSync(target, '#!/bin/sh\necho ok\n');
142
+ chmodSync(target, 0o644);
143
+ const out = runDoctor(w);
144
+ assert(/cron target not executable: .*crystal-capture\.sh/.test(out), 'reports "cron target not executable"', out);
145
+ rmSync(w.home, { recursive: true, force: true });
146
+ }
147
+
148
+ // ── Test 5: cron references unknown target with no canonical source ──
149
+ console.log('Test 5: cron target missing, owner unknown');
150
+ {
151
+ const w = setupHome(CRON_FOREIGN, { withCanonicalShim: false });
152
+ const out = runDoctor(w);
153
+ assert(/cron target missing: .*foreign-tool\.sh/.test(out), 'reports "cron target missing"', out);
154
+ assert(/Owner unknown/.test(out), 'admits owner is unknown when no canonical source exists', out);
155
+ rmSync(w.home, { recursive: true, force: true });
156
+ }
157
+
158
+ // ── Test 6: empty crontab ──
159
+ console.log('Test 6: empty crontab');
160
+ {
161
+ const w = setupHome('');
162
+ const out = runDoctor(w);
163
+ assert(!/cron target/.test(out), 'no cron target diagnostic when crontab is empty', out);
164
+ rmSync(w.home, { recursive: true, force: true });
165
+ }
166
+
167
+ console.log('');
168
+ if (failed > 0) {
169
+ console.log(`${failed} test(s) failed.`);
170
+ process.exit(1);
171
+ }
172
+ console.log('All ldm doctor cron-target tests passed.');
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const root = dirname(dirname(fileURLToPath(import.meta.url)));
7
+ const cli = readFileSync(join(root, 'bin', 'ldm.js'), 'utf8');
8
+
9
+ const marker = '// Check parent packages for toolbox-style repos (#132)';
10
+ const idx = cli.indexOf(marker);
11
+ if (idx === -1) {
12
+ throw new Error('Could not find toolbox parent update block');
13
+ }
14
+
15
+ const parentBlock = cli.slice(idx, cli.indexOf('const totalUpdates = npmUpdates.length;', idx));
16
+ if (!parentBlock.includes("const npmTag = ALPHA_FLAG ? 'alpha' : BETA_FLAG ? 'beta' : 'latest';")) {
17
+ throw new Error('Toolbox parent update block does not select the requested release track');
18
+ }
19
+
20
+ if (!parentBlock.includes('dist-tags.${npmTag}')) {
21
+ throw new Error('Toolbox parent update block does not query alpha/beta dist-tags');
22
+ }
23
+
24
+ if (/const latest = execSync\(`npm view \$\{comp\.npm\} version 2>\/dev\/null`/.test(parentBlock)) {
25
+ throw new Error('Toolbox parent update block still hardcodes the stable npm version query');
26
+ }
27
+
28
+ const installMarker = '// For parent packages, installFromPath already refreshes each sub-tool';
29
+ const installIdx = cli.indexOf(installMarker);
30
+ if (installIdx === -1) {
31
+ throw new Error('Could not find parent registry refresh block');
32
+ }
33
+
34
+ const installBlock = cli.slice(installIdx, cli.indexOf('} catch (e) {', installIdx));
35
+ if (installBlock.includes('= entry.latestVersion')) {
36
+ throw new Error('Parent update block must not stamp parent versions onto sub-tool registry entries');
37
+ }
38
+
39
+ console.log('installer update-track regression checks passed');
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+ // Regression test: `ldm install` must not remove or clobber foreign shims in
3
+ // ~/.ldm/bin. Memory Crystal owns crystal-capture.sh, but the LDM installer
4
+ // deploys its own scripts into the same directory on every install.
5
+ //
6
+ // This uses the real bin/ldm.js install path against a temp HOME. External
7
+ // commands that would hit the network or operator crontab are shimmed through
8
+ // PATH so the test is deterministic in CI and local sandboxes.
9
+
10
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
11
+ import { dirname, join, resolve } from 'node:path';
12
+ import { execFileSync } from 'node:child_process';
13
+ import { tmpdir } from 'node:os';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ const repo = resolve(dirname(fileURLToPath(import.meta.url)), '..');
17
+ const cli = join(repo, 'bin', 'ldm.js');
18
+ const pkg = JSON.parse(readFileSync(join(repo, 'package.json'), 'utf8'));
19
+
20
+ function fail(message, output = '') {
21
+ console.error(`FAIL: ${message}`);
22
+ if (output) {
23
+ console.error('\n--- ldm output ---');
24
+ console.error(output.trim());
25
+ }
26
+ process.exit(1);
27
+ }
28
+
29
+ function assert(condition, message, output = '') {
30
+ if (!condition) fail(message, output);
31
+ console.log(`PASS: ${message}`);
32
+ }
33
+
34
+ const home = mkdtempSync(join(tmpdir(), 'ldm-install-bin-shim-'));
35
+
36
+ try {
37
+ const ldmRoot = join(home, '.ldm');
38
+ const binDir = join(ldmRoot, 'bin');
39
+ const extDir = join(ldmRoot, 'extensions');
40
+ const stateDir = join(ldmRoot, 'state');
41
+ const fakeBin = join(home, 'fakebin');
42
+
43
+ mkdirSync(binDir, { recursive: true });
44
+ mkdirSync(extDir, { recursive: true });
45
+ mkdirSync(stateDir, { recursive: true });
46
+ mkdirSync(fakeBin, { recursive: true });
47
+
48
+ writeFileSync(join(ldmRoot, 'version.json'), JSON.stringify({ version: pkg.version }, null, 2) + '\n');
49
+ writeFileSync(join(extDir, 'registry.json'), JSON.stringify({ extensions: {} }, null, 2) + '\n');
50
+
51
+ const shimPath = join(binDir, 'crystal-capture.sh');
52
+ const shimContent = '#!/bin/sh\nprintf "memory-crystal sentinel\\n"\n';
53
+ writeFileSync(shimPath, shimContent);
54
+ chmodSync(shimPath, 0o755);
55
+
56
+ // Avoid network-dependent version checks while still exercising real install.
57
+ const npmShim = join(fakeBin, 'npm');
58
+ writeFileSync(
59
+ npmShim,
60
+ `#!/bin/sh
61
+ if [ "$1" = "view" ]; then
62
+ case "$3" in
63
+ version|dist-tags.alpha|dist-tags.beta) printf '%s\\n' '${pkg.version}' ;;
64
+ *) printf '%s\\n' '${pkg.version}' ;;
65
+ esac
66
+ exit 0
67
+ fi
68
+ if [ "$1" = "list" ]; then
69
+ printf '{}\\n'
70
+ exit 0
71
+ fi
72
+ exit 0
73
+ `,
74
+ );
75
+ chmodSync(npmShim, 0o755);
76
+
77
+ // Avoid touching the operator's real crontab in any health path.
78
+ const crontabShim = join(fakeBin, 'crontab');
79
+ writeFileSync(crontabShim, '#!/bin/sh\nif [ "$1" = "-l" ]; then exit 1; fi\nexit 0\n');
80
+ chmodSync(crontabShim, 0o755);
81
+
82
+ let output = '';
83
+ try {
84
+ output = execFileSync('node', [cli, 'install'], {
85
+ env: {
86
+ ...process.env,
87
+ HOME: home,
88
+ PATH: `${fakeBin}:${process.env.PATH}`,
89
+ LDM_SELF_UPDATED: '1',
90
+ },
91
+ encoding: 'utf8',
92
+ timeout: 30000,
93
+ });
94
+ } catch (err) {
95
+ output = `${err.stdout || ''}${err.stderr || ''}`;
96
+ fail('real ldm install command should complete in temp HOME', output);
97
+ }
98
+
99
+ assert(existsSync(shimPath), 'foreign Memory Crystal shim still exists after ldm install', output);
100
+ assert(readFileSync(shimPath, 'utf8') === shimContent, 'foreign Memory Crystal shim content was not clobbered', output);
101
+ assert((readFileSync(shimPath, 'utf8').includes('memory-crystal sentinel')), 'foreign shim sentinel is intact', output);
102
+ assert((readFileSync(shimPath).length > 0), 'foreign shim remains non-empty', output);
103
+ assert((statSync(shimPath).mode & 0o111) !== 0, 'foreign shim remains executable', output);
104
+ assert((existsSync(join(binDir, 'ldm-backup.sh'))), 'ldm-owned scripts still deploy into ~/.ldm/bin', output);
105
+
106
+ const restored = readFileSync(shimPath);
107
+ assert(restored.length === Buffer.byteLength(shimContent), 'foreign shim size is unchanged', output);
108
+
109
+ console.log('\nldm install foreign-bin preservation regression passed.');
110
+ } finally {
111
+ rmSync(home, { recursive: true, force: true });
112
+ }
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ // scripts/validate-bin-manifest.mjs — prepublish gate for wipLdmOs.binFiles.
3
+ //
4
+ // Layer 1 of the release-blocker design (see
5
+ // ai/product/plans-prds/current/2026-04-28--cc-mini--ldm-bin-ownership-manifest-design.md).
6
+ // Runs before every publish to assert each declared bin file is real and
7
+ // internally consistent. A broken declaration cannot reach npm because
8
+ // `wip-release` calls `prepublishOnly` which calls this.
9
+ //
10
+ // Checks:
11
+ // - Each declaration has a valid shape (name + source).
12
+ // - `name` is a basename (no /, no \, no ..).
13
+ // - `source` resolves to a real file under the package root.
14
+ // - No two declarations within this package share the same `name`.
15
+ //
16
+ // This does NOT check for cross-package conflicts. That is layer 2 in
17
+ // the design and lands as a follow-up workflow once a known-extensions
18
+ // registry exists.
19
+
20
+ import { readFileSync } from 'node:fs';
21
+ import { dirname, resolve, join } from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+ import { validateDeclarations } from '../lib/bin-manifest.mjs';
24
+
25
+ const repo = resolve(dirname(fileURLToPath(import.meta.url)), '..');
26
+ const pkg = JSON.parse(readFileSync(join(repo, 'package.json'), 'utf8'));
27
+ const decls = pkg?.wipLdmOs?.binFiles;
28
+
29
+ if (!Array.isArray(decls)) {
30
+ console.log('No wipLdmOs.binFiles declared. Skipping validation.');
31
+ process.exit(0);
32
+ }
33
+
34
+ const result = validateDeclarations('wip-ldm-os', repo, decls);
35
+ if (!result.valid) {
36
+ console.error('FAIL: wipLdmOs.binFiles validation failed:');
37
+ for (const e of result.errors) console.error(` - ${e}`);
38
+ process.exit(1);
39
+ }
40
+
41
+ console.log(`OK: wipLdmOs.binFiles validated (${decls.length} entr${decls.length === 1 ? 'y' : 'ies'}).`);
@@ -28,7 +28,9 @@ These are three distinct actions. Never combine them. Never skip the dogfooding
28
28
  | **Deploy** | Ship to public | `wip-release` (version bump, npm publish, GitHub release) + `deploy-public.sh` (sync to public repo). Package is available to the world. **Still not on our machine.** |
29
29
  | **Install** | Put it on our system | `crystal init` or equivalent. Extensions updated. Hooks configured. Only when Parker says "install." |
30
30
 
31
- **After Deploy, STOP.** Do not copy files to `~/.ldm/extensions/` or `~/.openclaw/extensions/`. Do not run `npm install -g`. Do not run `npm link`. Do not touch the installed system. Tell Parker: "v0.X.Y is published. Run the install prompt when you're ready to update."
31
+ **Prerelease exception:** agents install alpha and beta tracks for validation. Use `ldm install --alpha` after an alpha release and `ldm install --beta` after a beta release. That is test work.
32
+
33
+ **After stable Deploy, STOP.** Do not copy files to `~/.ldm/extensions/` or `~/.openclaw/extensions/`. Do not run `npm install -g`. Do not run `npm link`. Do not run `ldm install` unless Parker explicitly asks. Tell Parker: "v0.X.Y is published. Run the install prompt when you're ready to update."
32
34
 
33
35
  **We always dogfood our own software.** The install prompt exists so Parker can see what's new, review the dry run, and decide to install. If agents deploy directly to extensions, the install prompt says "already up to date" and the dogfooding loop is broken.
34
36
 
@@ -31,6 +31,8 @@ ldm install --beta # beta track (npm @beta)
31
31
 
32
32
  Each track overwrites whatever is installed. Alpha, beta, and stable all use the same install path. The flag only changes which npm dist-tag is checked for updates.
33
33
 
34
+ Agents may run alpha and beta installs for prerelease validation. Stable installs are different: Parker dogfoods stable/latest releases through the install prompt unless he explicitly asks an agent to install.
35
+
34
36
  ## Source Resolution
35
37
 
36
38
  When updating, the installer finds the package in priority order: