@wipcomputer/wip-ldm-os 0.4.85-alpha.25 → 0.4.85-alpha.27
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/bin/ldm.js +111 -35
- package/package.json +3 -2
- package/scripts/test-ldm-status-concurrency.mjs +118 -0
- package/scripts/test-ldm-status-timeout.mjs +44 -28
package/bin/ldm.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, cpSync, chmodSync, unlinkSync, readlinkSync, renameSync, statSync, lstatSync, symlinkSync } from 'node:fs';
|
|
24
24
|
import { join, basename, resolve, dirname } from 'node:path';
|
|
25
|
-
import {
|
|
25
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
26
26
|
import { fileURLToPath } from 'node:url';
|
|
27
27
|
|
|
28
28
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -3284,27 +3284,89 @@ async function cmdDoctor() {
|
|
|
3284
3284
|
|
|
3285
3285
|
const STATUS_NPM_TIMEOUT_MS = parsePositiveInt(process.env.LDM_STATUS_NPM_TIMEOUT_MS, 5000);
|
|
3286
3286
|
const STATUS_TOTAL_BUDGET_MS = parsePositiveInt(process.env.LDM_STATUS_TOTAL_BUDGET_MS, 60000);
|
|
3287
|
+
const STATUS_NPM_CONCURRENCY = parsePositiveInt(process.env.LDM_STATUS_NPM_CONCURRENCY, 8);
|
|
3288
|
+
const STATUS_NPM_REGISTRY_URL = process.env.LDM_STATUS_NPM_REGISTRY_URL || 'https://registry.npmjs.org';
|
|
3287
3289
|
|
|
3288
|
-
function npmViewVersionForStatus(pkg, timeoutMs) {
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3290
|
+
async function npmViewVersionForStatus(pkg, timeoutMs) {
|
|
3291
|
+
const controller = new AbortController();
|
|
3292
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
3293
|
+
try {
|
|
3294
|
+
const registry = STATUS_NPM_REGISTRY_URL.replace(/\/+$/, '');
|
|
3295
|
+
const response = await fetch(`${registry}/${encodeURIComponent(pkg)}`, {
|
|
3296
|
+
signal: controller.signal,
|
|
3297
|
+
headers: { accept: 'application/vnd.npm.install-v1+json, application/json' },
|
|
3298
|
+
});
|
|
3299
|
+
if (!response.ok) {
|
|
3300
|
+
const error = new Error(`npm registry returned ${response.status}`);
|
|
3301
|
+
error.statusCode = response.status;
|
|
3302
|
+
throw error;
|
|
3303
|
+
}
|
|
3304
|
+
const metadata = await response.json();
|
|
3305
|
+
return metadata?.['dist-tags']?.latest || '';
|
|
3306
|
+
} finally {
|
|
3307
|
+
clearTimeout(timeout);
|
|
3308
|
+
}
|
|
3294
3309
|
}
|
|
3295
3310
|
|
|
3296
3311
|
function remainingStatusBudgetMs(startedAt) {
|
|
3297
3312
|
return Math.max(0, STATUS_TOTAL_BUDGET_MS - (Date.now() - startedAt));
|
|
3298
3313
|
}
|
|
3299
3314
|
|
|
3315
|
+
function formatStatusElapsed(ms) {
|
|
3316
|
+
if (!Number.isFinite(ms) || ms <= 0) return '0ms';
|
|
3317
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
3318
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3300
3321
|
function classifyStatusCheckError(error) {
|
|
3301
|
-
if (error?.signal === 'SIGTERM' || error?.code === 'ETIMEDOUT' || String(error?.message || '').includes('ETIMEDOUT')) {
|
|
3322
|
+
if (error?.name === 'AbortError' || error?.signal === 'SIGTERM' || error?.code === 'ETIMEDOUT' || String(error?.message || '').includes('ETIMEDOUT')) {
|
|
3302
3323
|
return 'timeout';
|
|
3303
3324
|
}
|
|
3304
3325
|
return 'unavailable';
|
|
3305
3326
|
}
|
|
3306
3327
|
|
|
3307
|
-
function
|
|
3328
|
+
async function runStatusProbesWithConcurrency(items, concurrency, statusStartedAt) {
|
|
3329
|
+
if (items.length === 0) return [];
|
|
3330
|
+
|
|
3331
|
+
const results = new Array(items.length);
|
|
3332
|
+
const workerCount = Math.max(1, Math.min(concurrency, items.length));
|
|
3333
|
+
let nextIndex = 0;
|
|
3334
|
+
|
|
3335
|
+
async function worker() {
|
|
3336
|
+
while (nextIndex < items.length) {
|
|
3337
|
+
const index = nextIndex;
|
|
3338
|
+
nextIndex += 1;
|
|
3339
|
+
const item = items[index];
|
|
3340
|
+
const remaining = remainingStatusBudgetMs(statusStartedAt);
|
|
3341
|
+
|
|
3342
|
+
if (remaining <= 0) {
|
|
3343
|
+
results[index] = { ...item, status: 'skipped', reason: 'budget', elapsedMs: 0 };
|
|
3344
|
+
continue;
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
const timeout = Math.min(STATUS_NPM_TIMEOUT_MS, remaining);
|
|
3348
|
+
const probeStartedAt = Date.now();
|
|
3349
|
+
console.log(` ${item.name}: checking npm`);
|
|
3350
|
+
|
|
3351
|
+
try {
|
|
3352
|
+
const latest = await npmViewVersionForStatus(item.npm, timeout);
|
|
3353
|
+
results[index] = { ...item, status: 'ok', latest, elapsedMs: Date.now() - probeStartedAt };
|
|
3354
|
+
} catch (error) {
|
|
3355
|
+
results[index] = {
|
|
3356
|
+
...item,
|
|
3357
|
+
status: 'skipped',
|
|
3358
|
+
reason: classifyStatusCheckError(error),
|
|
3359
|
+
elapsedMs: Date.now() - probeStartedAt,
|
|
3360
|
+
};
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
3366
|
+
return results;
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
async function cmdStatus() {
|
|
3308
3370
|
const version = readJSON(VERSION_PATH);
|
|
3309
3371
|
const registry = readJSON(REGISTRY_PATH);
|
|
3310
3372
|
const extCount = Object.keys(registry?.extensions || {}).length;
|
|
@@ -3338,18 +3400,14 @@ function cmdStatus() {
|
|
|
3338
3400
|
console.log('');
|
|
3339
3401
|
console.log(' Checking updates:');
|
|
3340
3402
|
|
|
3341
|
-
let cliUpdate = null;
|
|
3342
|
-
try {
|
|
3343
|
-
const timeout = Math.min(STATUS_NPM_TIMEOUT_MS, remainingStatusBudgetMs(statusStartedAt));
|
|
3344
|
-
if (timeout <= 0) throw new Error('status update-check budget exhausted');
|
|
3345
|
-
console.log(' ldm cli: checking npm');
|
|
3346
|
-
const latest = npmViewVersionForStatus('@wipcomputer/wip-ldm-os', timeout);
|
|
3347
|
-
if (latest && semverNewer(latest, PKG_VERSION)) cliUpdate = latest;
|
|
3348
|
-
} catch (error) {
|
|
3349
|
-
skipped.push({ name: 'ldm cli', npm: '@wipcomputer/wip-ldm-os', reason: classifyStatusCheckError(error) });
|
|
3350
|
-
}
|
|
3351
|
-
|
|
3352
3403
|
// Check extensions against npm using registry source info (#262)
|
|
3404
|
+
const probeItems = [{
|
|
3405
|
+
kind: 'cli',
|
|
3406
|
+
name: 'ldm cli',
|
|
3407
|
+
npm: '@wipcomputer/wip-ldm-os',
|
|
3408
|
+
current: PKG_VERSION,
|
|
3409
|
+
}];
|
|
3410
|
+
|
|
3353
3411
|
const updates = [];
|
|
3354
3412
|
for (const [name, info] of Object.entries(registry?.extensions || {})) {
|
|
3355
3413
|
// Use registry source.npm (v2) or fall back to extension's package.json
|
|
@@ -3362,19 +3420,37 @@ function cmdStatus() {
|
|
|
3362
3420
|
if (!npmPkg) continue;
|
|
3363
3421
|
const currentVersion = info?.installed?.version || info.version;
|
|
3364
3422
|
if (!currentVersion) continue;
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3423
|
+
probeItems.push({
|
|
3424
|
+
kind: 'extension',
|
|
3425
|
+
name,
|
|
3426
|
+
npm: npmPkg,
|
|
3427
|
+
current: currentVersion,
|
|
3428
|
+
});
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
let cliUpdate = null;
|
|
3432
|
+
const probeResults = await runStatusProbesWithConcurrency(probeItems, STATUS_NPM_CONCURRENCY, statusStartedAt);
|
|
3433
|
+
for (const result of probeResults) {
|
|
3434
|
+
if (!result) continue;
|
|
3435
|
+
if (result.status === 'skipped') {
|
|
3436
|
+
skipped.push({
|
|
3437
|
+
name: result.name,
|
|
3438
|
+
npm: result.npm,
|
|
3439
|
+
reason: result.reason,
|
|
3440
|
+
elapsedMs: result.elapsedMs,
|
|
3441
|
+
});
|
|
3442
|
+
continue;
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
if (result.kind === 'cli') {
|
|
3446
|
+
if (result.latest && semverNewer(result.latest, PKG_VERSION)) cliUpdate = result.latest;
|
|
3447
|
+
} else if (result.latest && semverNewer(result.latest, result.current)) {
|
|
3448
|
+
updates.push({
|
|
3449
|
+
name: result.name,
|
|
3450
|
+
current: result.current,
|
|
3451
|
+
latest: result.latest,
|
|
3452
|
+
npm: result.npm,
|
|
3453
|
+
});
|
|
3378
3454
|
}
|
|
3379
3455
|
}
|
|
3380
3456
|
|
|
@@ -3407,7 +3483,7 @@ function cmdStatus() {
|
|
|
3407
3483
|
console.log('');
|
|
3408
3484
|
console.log(' Update checks skipped:');
|
|
3409
3485
|
for (const item of skipped) {
|
|
3410
|
-
console.log(` ${item.name}: [${item.reason}] ${item.npm}`);
|
|
3486
|
+
console.log(` ${item.name}: [${item.reason} ${formatStatusElapsed(item.elapsedMs)}] ${item.npm}`);
|
|
3411
3487
|
}
|
|
3412
3488
|
}
|
|
3413
3489
|
|
|
@@ -4866,7 +4942,7 @@ async function main() {
|
|
|
4866
4942
|
await cmdDoctor();
|
|
4867
4943
|
break;
|
|
4868
4944
|
case 'status':
|
|
4869
|
-
cmdStatus();
|
|
4945
|
+
await cmdStatus();
|
|
4870
4946
|
break;
|
|
4871
4947
|
case 'sessions':
|
|
4872
4948
|
await cmdSessions();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wipcomputer/wip-ldm-os",
|
|
3
|
-
"version": "0.4.85-alpha.
|
|
3
|
+
"version": "0.4.85-alpha.27",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "LDM OS: identity, memory, and sovereignty infrastructure for AI agents",
|
|
6
6
|
"engines": {
|
|
@@ -18,11 +18,12 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build:bridge": "cd src/bridge && npm install && npx tsup core.ts mcp-server.ts cli.ts openclaw.ts --format esm --dts --clean --outDir ../../dist/bridge",
|
|
20
20
|
"build": "npm run build:bridge",
|
|
21
|
-
"prepublishOnly": "npm run build:bridge && npm run validate:bin-manifest && npm run test:install-prompt-policy && npm run test:ldm-status-timeout",
|
|
21
|
+
"prepublishOnly": "npm run build:bridge && npm run validate:bin-manifest && npm run test:install-prompt-policy && npm run test:ldm-status-timeout && npm run test:ldm-status-concurrency",
|
|
22
22
|
"validate:bin-manifest": "node scripts/validate-bin-manifest.mjs",
|
|
23
23
|
"test:skill-frontmatter": "node scripts/test-skill-frontmatter.mjs",
|
|
24
24
|
"test:install-prompt-policy": "node scripts/test-install-prompt-policy.mjs",
|
|
25
25
|
"test:ldm-status-timeout": "node scripts/test-ldm-status-timeout.mjs",
|
|
26
|
+
"test:ldm-status-concurrency": "node scripts/test-ldm-status-concurrency.mjs",
|
|
26
27
|
"test:installer-update-tracks": "node scripts/test-installer-update-tracks.mjs",
|
|
27
28
|
"test:installer-hook-toolname": "node scripts/test-installer-hook-toolname.mjs",
|
|
28
29
|
"test:installer-target-self-update": "node scripts/test-installer-target-self-update.mjs",
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
10
|
+
const tempRoot = mkdtempSync(join(tmpdir(), 'ldm-status-concurrency-'));
|
|
11
|
+
const sourceVersion = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version;
|
|
12
|
+
|
|
13
|
+
function assert(condition, message) {
|
|
14
|
+
if (!condition) throw new Error(message);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function listen(server) {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
server.listen(0, '127.0.0.1', () => resolve(server.address()));
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createRegistryServer(delayMs) {
|
|
24
|
+
let active = 0;
|
|
25
|
+
let maxActive = 0;
|
|
26
|
+
const server = createServer((_req, res) => {
|
|
27
|
+
active += 1;
|
|
28
|
+
maxActive = Math.max(maxActive, active);
|
|
29
|
+
setTimeout(() => {
|
|
30
|
+
res.setHeader('content-type', 'application/json');
|
|
31
|
+
res.end(JSON.stringify({ 'dist-tags': { latest: '1.0.0' } }));
|
|
32
|
+
active -= 1;
|
|
33
|
+
}, delayMs);
|
|
34
|
+
});
|
|
35
|
+
return { server, getMaxActive: () => maxActive };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function writeFixture(home) {
|
|
39
|
+
const extensions = join(home, '.ldm', 'extensions');
|
|
40
|
+
mkdirSync(extensions, { recursive: true });
|
|
41
|
+
writeFileSync(join(home, '.ldm', 'version.json'), JSON.stringify({
|
|
42
|
+
version: '0.0.0-test',
|
|
43
|
+
installed: '2026-05-12T00:00:00.000Z',
|
|
44
|
+
updated: '2026-05-12T00:00:00.000Z',
|
|
45
|
+
}, null, 2) + '\n');
|
|
46
|
+
|
|
47
|
+
const registry = { extensions: {} };
|
|
48
|
+
for (let i = 1; i <= 8; i += 1) {
|
|
49
|
+
registry.extensions[`ext-${i}`] = {
|
|
50
|
+
source: { npm: `ext-${i}` },
|
|
51
|
+
installed: { version: '1.0.0' },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
writeFileSync(join(extensions, 'registry.json'), JSON.stringify(registry, null, 2) + '\n');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function runStatus({ concurrency, registryUrl, home }) {
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
const child = spawn(process.execPath, [join(root, 'bin', 'ldm.js'), 'status'], {
|
|
60
|
+
cwd: root,
|
|
61
|
+
env: {
|
|
62
|
+
...process.env,
|
|
63
|
+
HOME: home,
|
|
64
|
+
LDM_STATUS_NPM_REGISTRY_URL: registryUrl,
|
|
65
|
+
LDM_STATUS_NPM_CONCURRENCY: String(concurrency),
|
|
66
|
+
LDM_STATUS_NPM_TIMEOUT_MS: '2000',
|
|
67
|
+
LDM_STATUS_TOTAL_BUDGET_MS: '10000',
|
|
68
|
+
},
|
|
69
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
let stdout = '';
|
|
73
|
+
let stderr = '';
|
|
74
|
+
child.stdout.setEncoding('utf8');
|
|
75
|
+
child.stderr.setEncoding('utf8');
|
|
76
|
+
child.stdout.on('data', chunk => { stdout += chunk; });
|
|
77
|
+
child.stderr.on('data', chunk => { stderr += chunk; });
|
|
78
|
+
child.on('close', status => resolve({ status, stdout, stderr }));
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function runFixture({ concurrency, delayMs }) {
|
|
83
|
+
const home = join(tempRoot, `home-${concurrency}-${delayMs}`);
|
|
84
|
+
writeFixture(home);
|
|
85
|
+
const registry = createRegistryServer(delayMs);
|
|
86
|
+
const address = await listen(registry.server);
|
|
87
|
+
const startedAt = Date.now();
|
|
88
|
+
const result = await runStatus({
|
|
89
|
+
concurrency,
|
|
90
|
+
home,
|
|
91
|
+
registryUrl: `http://${address.address}:${address.port}`,
|
|
92
|
+
});
|
|
93
|
+
const elapsedMs = Date.now() - startedAt;
|
|
94
|
+
registry.server.closeAllConnections();
|
|
95
|
+
registry.server.close();
|
|
96
|
+
return { result, elapsedMs, maxActive: registry.getMaxActive() };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const concurrent = await runFixture({ concurrency: 4, delayMs: 500 });
|
|
101
|
+
assert(concurrent.result.status === 0, `concurrent ldm status exited ${concurrent.result.status}\nstdout:\n${concurrent.result.stdout}\nstderr:\n${concurrent.result.stderr}`);
|
|
102
|
+
assert(concurrent.elapsedMs < 3000, `concurrent ldm status should finish well before serial runtime; elapsed ${concurrent.elapsedMs}ms`);
|
|
103
|
+
assert(concurrent.maxActive >= 4, `registry server should see concurrent probes; max active ${concurrent.maxActive}`);
|
|
104
|
+
assert(concurrent.result.stdout.includes(`LDM OS v${sourceVersion}`), `status should print installed LDM OS version\n${concurrent.result.stdout}`);
|
|
105
|
+
assert(concurrent.result.stdout.includes('Extensions: 8'), `status should print extension count\n${concurrent.result.stdout}`);
|
|
106
|
+
assert(concurrent.result.stdout.includes('ext-8: checking npm'), `status should check every staged extension\n${concurrent.result.stdout}`);
|
|
107
|
+
assert(!concurrent.result.stdout.includes('Update checks skipped:'), `concurrent status should not skip checks in this fixture\n${concurrent.result.stdout}`);
|
|
108
|
+
|
|
109
|
+
const serialFallback = await runFixture({ concurrency: 1, delayMs: 10 });
|
|
110
|
+
assert(serialFallback.result.status === 0, `serial fallback ldm status exited ${serialFallback.result.status}\nstdout:\n${serialFallback.result.stdout}\nstderr:\n${serialFallback.result.stderr}`);
|
|
111
|
+
assert(serialFallback.maxActive === 1, `serial fallback should only run one probe at a time; max active ${serialFallback.maxActive}`);
|
|
112
|
+
assert(serialFallback.result.stdout.includes('ext-8: checking npm'), `serial fallback should still check every staged extension\n${serialFallback.result.stdout}`);
|
|
113
|
+
assert(!serialFallback.result.stdout.includes('Update checks skipped:'), `serial fallback should not skip checks in this fixture\n${serialFallback.result.stdout}`);
|
|
114
|
+
} finally {
|
|
115
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log('ldm status concurrency regression passed');
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
3
4
|
import { tmpdir } from 'node:os';
|
|
4
5
|
import { dirname, join } from 'node:path';
|
|
5
|
-
import {
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
7
8
|
|
|
8
9
|
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
@@ -13,9 +14,38 @@ function assert(condition, message) {
|
|
|
13
14
|
if (!condition) throw new Error(message);
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
function runStatus({ home, registryUrl }) {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
const child = spawn(process.execPath, [join(root, 'bin', 'ldm.js'), 'status'], {
|
|
20
|
+
cwd: root,
|
|
21
|
+
env: {
|
|
22
|
+
...process.env,
|
|
23
|
+
HOME: home,
|
|
24
|
+
LDM_STATUS_NPM_REGISTRY_URL: registryUrl,
|
|
25
|
+
LDM_STATUS_NPM_TIMEOUT_MS: '75',
|
|
26
|
+
LDM_STATUS_TOTAL_BUDGET_MS: '250',
|
|
27
|
+
},
|
|
28
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
let stdout = '';
|
|
32
|
+
let stderr = '';
|
|
33
|
+
child.stdout.setEncoding('utf8');
|
|
34
|
+
child.stderr.setEncoding('utf8');
|
|
35
|
+
child.stdout.on('data', chunk => { stdout += chunk; });
|
|
36
|
+
child.stderr.on('data', chunk => { stderr += chunk; });
|
|
37
|
+
child.on('close', status => resolve({ status, stdout, stderr }));
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function listen(server) {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
server.listen(0, '127.0.0.1', () => resolve(server.address()));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
16
47
|
try {
|
|
17
48
|
const home = join(tempRoot, 'home');
|
|
18
|
-
const fakeBin = join(tempRoot, 'bin');
|
|
19
49
|
const extensions = join(home, '.ldm', 'extensions');
|
|
20
50
|
|
|
21
51
|
mkdirSync(extensions, { recursive: true });
|
|
@@ -37,33 +67,19 @@ try {
|
|
|
37
67
|
},
|
|
38
68
|
}, null, 2) + '\n');
|
|
39
69
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
fi
|
|
48
|
-
echo "unexpected npm command: $*" >&2
|
|
49
|
-
exit 64
|
|
50
|
-
`);
|
|
51
|
-
chmodSync(fakeNpm, 0o755);
|
|
70
|
+
const server = createServer((_req, res) => {
|
|
71
|
+
setTimeout(() => {
|
|
72
|
+
res.setHeader('content-type', 'application/json');
|
|
73
|
+
res.end(JSON.stringify({ 'dist-tags': { latest: '9.9.9' } }));
|
|
74
|
+
}, 2000);
|
|
75
|
+
});
|
|
76
|
+
const address = await listen(server);
|
|
52
77
|
|
|
53
78
|
const startedAt = Date.now();
|
|
54
|
-
const result =
|
|
55
|
-
cwd: root,
|
|
56
|
-
encoding: 'utf8',
|
|
57
|
-
timeout: 3000,
|
|
58
|
-
env: {
|
|
59
|
-
...process.env,
|
|
60
|
-
HOME: home,
|
|
61
|
-
PATH: `${fakeBin}:${process.env.PATH || ''}`,
|
|
62
|
-
LDM_STATUS_NPM_TIMEOUT_MS: '75',
|
|
63
|
-
LDM_STATUS_TOTAL_BUDGET_MS: '250',
|
|
64
|
-
},
|
|
65
|
-
});
|
|
79
|
+
const result = await runStatus({ home, registryUrl: `http://${address.address}:${address.port}` });
|
|
66
80
|
const elapsedMs = Date.now() - startedAt;
|
|
81
|
+
server.closeAllConnections();
|
|
82
|
+
server.close();
|
|
67
83
|
|
|
68
84
|
assert(result.status === 0, `ldm status exited ${result.status}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
|
|
69
85
|
assert(elapsedMs < 2500, `ldm status should return before the process timeout; elapsed ${elapsedMs}ms`);
|
|
@@ -72,7 +88,7 @@ exit 64
|
|
|
72
88
|
assert(result.stdout.includes('Checking updates:'), `status should show progress before update checks\n${result.stdout}`);
|
|
73
89
|
assert(result.stdout.includes('hung-extension: checking npm'), `status should print the extension name before probing it\n${result.stdout}`);
|
|
74
90
|
assert(result.stdout.includes('Update checks skipped:'), `status should report skipped checks instead of hanging\n${result.stdout}`);
|
|
75
|
-
assert(
|
|
91
|
+
assert(/hung-extension: \[timeout \d+(ms|\.\d+s)\] hung-extension/.test(result.stdout), `hung extension should be reported as a timeout with elapsed time\n${result.stdout}`);
|
|
76
92
|
} finally {
|
|
77
93
|
rmSync(tempRoot, { recursive: true, force: true });
|
|
78
94
|
}
|