dsh-plugin-rea 2.0.6 → 2.0.8
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 +86 -0
- package/README.md +30 -4
- package/README.zh-CN.md +28 -4
- package/bin/rea-shim.mjs +666 -31
- package/check-updates.ps1 +33 -5
- package/package.json +2 -2
- package/scripts/host-compat.mjs +29 -4
- package/scripts/rea-bundle-bootstrap.ps1 +5 -0
- package/scripts/setup.mjs +30 -1
- package/scripts/setup.test.mjs +509 -8
- package/scripts/verify-branch.ps1 +8 -0
package/scripts/setup.test.mjs
CHANGED
|
@@ -213,7 +213,10 @@ const SHIM_SOURCE = path.join(path.dirname(fileURLToPath(import.meta.url)), ".."
|
|
|
213
213
|
function shimBundleRoot(shimFile, env) {
|
|
214
214
|
const run = spawnSync(process.execPath, [shimFile, "--shim-status"], {
|
|
215
215
|
encoding: "utf8",
|
|
216
|
-
|
|
216
|
+
// These tests assert bundle-root resolution, not the launch decision. Pinning
|
|
217
|
+
// the mode keeps them independent of whether THIS machine has a REA on PATH
|
|
218
|
+
// (and keeps them from paying for a probe on every spawn).
|
|
219
|
+
env: { ...process.env, REA_BUNDLE_AUTO_PROVISION: "0", REA_BUNDLE_LAUNCH: "bundle", ...env },
|
|
217
220
|
});
|
|
218
221
|
// --shim-status prints its report even when no bundle exists yet (exit 78).
|
|
219
222
|
return JSON.parse(run.stdout).bundleRoot;
|
|
@@ -821,6 +824,10 @@ function runShim(bundle, args, extraEnv = {}) {
|
|
|
821
824
|
...process.env,
|
|
822
825
|
DSH_HOME: path.join(path.dirname(bundle), "dsh-home"),
|
|
823
826
|
REA_BUNDLE_ROOT: bundle,
|
|
827
|
+
// The bundle path is what these tests assert. Without this pin the shim
|
|
828
|
+
// would take its `auto` branch and, on a machine that happens to have a REA
|
|
829
|
+
// on PATH, delegate to it instead of exercising the code under test.
|
|
830
|
+
REA_BUNDLE_LAUNCH: "bundle",
|
|
824
831
|
...extraEnv,
|
|
825
832
|
};
|
|
826
833
|
delete env.REA_BUNDLE_AUTO_PROVISION;
|
|
@@ -944,7 +951,7 @@ test("a second REA registration is reported instead of silently duplicated", ()
|
|
|
944
951
|
|
|
945
952
|
const status = spawnSync(process.execPath, [path.join(bundle, "bin", "rea-shim.mjs"), "--shim-status"], {
|
|
946
953
|
encoding: "utf8",
|
|
947
|
-
env: { ...process.env, DSH_HOME: home, REA_BUNDLE_ROOT: bundle },
|
|
954
|
+
env: { ...process.env, DSH_HOME: home, REA_BUNDLE_ROOT: bundle, REA_BUNDLE_LAUNCH: "bundle" },
|
|
948
955
|
});
|
|
949
956
|
assert.equal(JSON.parse(status.stdout).duplicateRegistration.id, "rea-mcp");
|
|
950
957
|
} finally {
|
|
@@ -1123,7 +1130,7 @@ test("the fallback vendored-REA path matches where setup installs it", () => {
|
|
|
1123
1130
|
const run = spawnSync(process.execPath, [path.join(REPO_ROOT, "bin", "rea-shim.mjs"), "--shim-status"], {
|
|
1124
1131
|
encoding: "utf8",
|
|
1125
1132
|
timeout: 60000,
|
|
1126
|
-
env: { ...process.env, REA_BUNDLE_ROOT: bundle, REA_BUNDLE_AUTO_PROVISION: "0" },
|
|
1133
|
+
env: { ...process.env, REA_BUNDLE_ROOT: bundle, REA_BUNDLE_AUTO_PROVISION: "0", REA_BUNDLE_LAUNCH: "bundle" },
|
|
1127
1134
|
});
|
|
1128
1135
|
const report = JSON.parse(run.stdout);
|
|
1129
1136
|
assert.equal(report.rea.ok, true, JSON.stringify(report.rea));
|
|
@@ -1212,16 +1219,38 @@ test("the entry loads as a cordis plugin and does nothing", async () => {
|
|
|
1212
1219
|
test("the manifest declares a DSH host range the market can read", () => {
|
|
1213
1220
|
const range = declaredHostRange(REPO_ROOT);
|
|
1214
1221
|
assert.equal(typeof range, "string", "engines.dsh must be declared; without it the market reports the plugin as unknown");
|
|
1222
|
+
// The string is pinned, not just parsed: the ceiling decides which hosts are
|
|
1223
|
+
// admitted, and `<0.3.0` vs `<0.3.0-0` is exactly the difference between
|
|
1224
|
+
// "no 0.3.0" and "0.3.0-alpha.1 is fine" once prereleases are included.
|
|
1225
|
+
assert.equal(range, ">=0.1.7-rc.2 <0.3.0-0");
|
|
1215
1226
|
assert.throws(() => satisfiesDeclaredRange("0.2.0-rc.2", "not a range"), "the gate must reject an unparseable range");
|
|
1216
1227
|
assert.doesNotThrow(() => satisfiesDeclaredRange("0.2.0-rc.2", range));
|
|
1217
1228
|
});
|
|
1218
1229
|
|
|
1230
|
+
test("the release list this gate checks is the published one", () => {
|
|
1231
|
+
// Pinned literally: deleting an entry from the list would otherwise shrink the
|
|
1232
|
+
// coverage claim silently, and every test below would still pass.
|
|
1233
|
+
assert.deepEqual(HOST_RELEASES, [
|
|
1234
|
+
"0.2.1-rc.2",
|
|
1235
|
+
"0.2.1-rc.1",
|
|
1236
|
+
"0.2.1-alpha.5",
|
|
1237
|
+
"0.2.1-alpha.4",
|
|
1238
|
+
"0.2.1-alpha.3",
|
|
1239
|
+
"0.2.1-alpha.2",
|
|
1240
|
+
"0.2.1-alpha.1",
|
|
1241
|
+
"0.2.0-rc.2",
|
|
1242
|
+
"0.2.0-rc.1",
|
|
1243
|
+
"0.1.7-rc.2",
|
|
1244
|
+
]);
|
|
1245
|
+
assert.deepEqual(OUTSIDE_HOSTS, ["0.3.0", "0.3.0-rc.1", "0.3.0-alpha.1", "0.1.7-rc.1", "0.1.7-alpha.2"]);
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1219
1248
|
test("every released DSH host is inside the declared range, and nothing untested is", () => {
|
|
1220
1249
|
const range = declaredHostRange(REPO_ROOT);
|
|
1221
1250
|
const { covered, uncovered, leaked } = checkHostCoverage(range);
|
|
1222
1251
|
assert.deepEqual(uncovered, [], `these releases fall outside ${range}`);
|
|
1223
1252
|
assert.deepEqual(leaked, [], `the declaration also admits ${leaked.join(", ")}, which nobody tested`);
|
|
1224
|
-
assert.
|
|
1253
|
+
assert.deepEqual(covered, HOST_RELEASES, "every release in the list must be covered");
|
|
1225
1254
|
});
|
|
1226
1255
|
|
|
1227
1256
|
test("the host-range gate rejects a plausible but wrong declaration", () => {
|
|
@@ -1230,10 +1259,16 @@ test("the host-range gate rejects a plausible but wrong declaration", () => {
|
|
|
1230
1259
|
// prerelease sorts BELOW its release, it drops the whole 0.2.0-rc line as well
|
|
1231
1260
|
// as 0.1.7. That is the trap this test exists for: the typo looks like it names
|
|
1232
1261
|
// the running host.
|
|
1233
|
-
const drifted = checkHostCoverage(">=0.2.0 <0.3.0");
|
|
1262
|
+
const drifted = checkHostCoverage(">=0.2.0 <0.3.0-0");
|
|
1234
1263
|
assert.deepEqual(drifted.uncovered, ["0.2.0-rc.2", "0.2.0-rc.1", "0.1.7-rc.2"], "a 0.2.0 floor must be seen dropping the 0.2.0 prereleases");
|
|
1235
|
-
//
|
|
1236
|
-
|
|
1264
|
+
// The OTHER half of the gate needs its own control: a floor low enough to admit
|
|
1265
|
+
// a host nobody audited must be reported as a leak, not silently accepted.
|
|
1266
|
+
const loose = checkHostCoverage(">=0.1.0 <0.3.0-0");
|
|
1267
|
+
assert.deepEqual(loose.leaked, ["0.1.7-rc.1", "0.1.7-alpha.2"], "a low floor must be seen admitting the earlier prereleases");
|
|
1268
|
+
// And the ceiling is load bearing: the next minor line is a host nobody audited,
|
|
1269
|
+
// including its prereleases, which `<0.3.0` would have let through.
|
|
1270
|
+
assert.equal(satisfiesDeclaredRange("0.3.0", ">=0.1.7-rc.2 <0.3.0-0"), false);
|
|
1271
|
+
assert.equal(satisfiesDeclaredRange("0.3.0-rc.1", ">=0.1.7-rc.2 <0.3.0-0"), false);
|
|
1237
1272
|
});
|
|
1238
1273
|
|
|
1239
1274
|
test("prerelease ordering matches semver, not string order", () => {
|
|
@@ -1242,5 +1277,471 @@ test("prerelease ordering matches semver, not string order", () => {
|
|
|
1242
1277
|
assert.equal(compareVersions("0.2.0-rc.2", "0.2.0-rc.1"), 1);
|
|
1243
1278
|
// String comparison would sort "alpha.10" before "alpha.5"; semver does not.
|
|
1244
1279
|
assert.equal(compareVersions("0.2.1-alpha.10", "0.2.1-alpha.5"), 1);
|
|
1245
|
-
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
test("the evaluator reads the two tolerances semver does, and refuses the rest", () => {
|
|
1283
|
+
// Build metadata carries no precedence: a host reporting it is that version.
|
|
1284
|
+
assert.equal(satisfiesDeclaredRange("0.2.0-rc.2+build.7", ">=0.1.7-rc.2 <0.3.0-0"), true);
|
|
1285
|
+
// Everything else is refused rather than guessed at — a gate that accepts `v`
|
|
1286
|
+
// prefixes and zero-padded identifiers would also accept a typo.
|
|
1287
|
+
assert.throws(() => satisfiesDeclaredRange("v0.2.0-rc.2", ">=0.1.7-rc.2 <0.3.0-0"));
|
|
1288
|
+
assert.throws(() => satisfiesDeclaredRange("0.2.1-01", ">=0.1.7-rc.2 <0.3.0-0"));
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
/* ------------------------------------------------------- the launch decision */
|
|
1292
|
+
|
|
1293
|
+
// 2.x supplies a runtime the machine may not have; when it HAS one, the row must
|
|
1294
|
+
// use it — the 1.x contract. These tests own that decision, so every one of them
|
|
1295
|
+
// isolates PATH: without it the suite would silently depend on whatever `rea`
|
|
1296
|
+
// the machine running it happens to have, which is the one thing a test of a
|
|
1297
|
+
// machine-dependent decision must not do.
|
|
1298
|
+
|
|
1299
|
+
function writeFakeRea(dir, { version = "6.9.0", exitForVersion = 0, runExit = 0, record = null } = {}) {
|
|
1300
|
+
const file = path.join(dir, process.platform === "win32" ? "rea.cmd" : "rea");
|
|
1301
|
+
if (process.platform === "win32") {
|
|
1302
|
+
const lines = ["@echo off", 'if "%1"=="--version" (', ` echo ${version}`, ` exit /b ${exitForVersion}`, ")"];
|
|
1303
|
+
if (record !== null) lines.push(`echo %* > "${record}"`);
|
|
1304
|
+
lines.push(`exit /b ${runExit}`);
|
|
1305
|
+
writeFileSync(file, lines.join("\r\n") + "\r\n");
|
|
1306
|
+
} else {
|
|
1307
|
+
const lines = ["#!/bin/sh", `if [ "$1" = "--version" ]; then echo ${version}; exit ${exitForVersion}; fi`];
|
|
1308
|
+
if (record !== null) lines.push(`echo "$*" > "${record}"`);
|
|
1309
|
+
lines.push(`exit ${runExit}`);
|
|
1310
|
+
writeFileSync(file, lines.join("\n") + "\n");
|
|
1311
|
+
chmodSync(file, 0o755);
|
|
1312
|
+
}
|
|
1313
|
+
return file;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function runShimIsolated(env, args) {
|
|
1317
|
+
const merged = { ...process.env, ...env };
|
|
1318
|
+
// Windows keeps PATH and Path as separate variables (both are "in" the
|
|
1319
|
+
// environment), and a child that reads Path while the test set PATH would see
|
|
1320
|
+
// the machine's real search path instead of the fixture's.
|
|
1321
|
+
if (process.platform === "win32" && Object.prototype.hasOwnProperty.call(env, "PATH")) delete merged.Path;
|
|
1322
|
+
return spawnSync(process.execPath, [path.join(REPO_ROOT, "bin", "rea-shim.mjs"), ...args], {
|
|
1323
|
+
encoding: "utf8",
|
|
1324
|
+
timeout: 60000,
|
|
1325
|
+
env: merged,
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
function launchFixture() {
|
|
1330
|
+
const root = mkdtempSync(path.join(tmpdir(), "rea-launch-"));
|
|
1331
|
+
const emptyPath = path.join(root, "empty-path");
|
|
1332
|
+
mkdirSync(emptyPath, { recursive: true });
|
|
1333
|
+
return {
|
|
1334
|
+
root,
|
|
1335
|
+
emptyPath,
|
|
1336
|
+
bundleRoot: path.join(root, "bundle"),
|
|
1337
|
+
bin: path.join(root, "bin"),
|
|
1338
|
+
record: path.join(root, "record.txt"),
|
|
1339
|
+
clean() {
|
|
1340
|
+
rmSync(root, { recursive: true, force: true });
|
|
1341
|
+
},
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
test("a machine with its own REA runs the 1.x way, and no bundle is required", () => {
|
|
1346
|
+
const f = launchFixture();
|
|
1347
|
+
mkdirSync(f.bin, { recursive: true });
|
|
1348
|
+
const rea = writeFakeRea(f.bin, { version: "6.9.0", record: f.record });
|
|
1349
|
+
try {
|
|
1350
|
+
const env = {
|
|
1351
|
+
PATH: f.emptyPath,
|
|
1352
|
+
REA_BUNDLE_SYSTEM_REA: rea,
|
|
1353
|
+
REA_BUNDLE_ROOT: f.bundleRoot,
|
|
1354
|
+
REA_BUNDLE_AUTO_PROVISION: "0",
|
|
1355
|
+
REA_BUNDLE_LAUNCH: "",
|
|
1356
|
+
};
|
|
1357
|
+
const status = runShimIsolated(env, ["--shim-status"]);
|
|
1358
|
+
const report = JSON.parse(status.stdout);
|
|
1359
|
+
assert.equal(status.status, 0, status.stderr);
|
|
1360
|
+
assert.equal(report.launch.mode, "system", JSON.stringify(report.launch));
|
|
1361
|
+
assert.equal(report.launch.systemRea.version, "6.9.0");
|
|
1362
|
+
// Unprovisioned and still healthy: the row does not depend on the bundle.
|
|
1363
|
+
assert.equal(report.provisioned, false);
|
|
1364
|
+
assert.equal(existsSync(path.join(f.bundleRoot, "bundle.json")), false, "no provisioning may be started");
|
|
1365
|
+
|
|
1366
|
+
const run = runShimIsolated(env, ["mcp"]);
|
|
1367
|
+
assert.equal(run.status, 0, run.stderr);
|
|
1368
|
+
assert.equal(readFileSync(f.record, "utf8").trim(), "mcp", "the system REA must receive the row's arguments");
|
|
1369
|
+
} finally {
|
|
1370
|
+
f.clean();
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
test("REA_BUNDLE_LAUNCH=bundle keeps the bundle path even with a REA on PATH", () => {
|
|
1375
|
+
const f = launchFixture();
|
|
1376
|
+
mkdirSync(f.bin, { recursive: true });
|
|
1377
|
+
const rea = writeFakeRea(f.bin);
|
|
1378
|
+
try {
|
|
1379
|
+
const run = runShimIsolated(
|
|
1380
|
+
{ PATH: f.emptyPath, REA_BUNDLE_SYSTEM_REA: rea, REA_BUNDLE_ROOT: f.bundleRoot, REA_BUNDLE_AUTO_PROVISION: "0", REA_BUNDLE_LAUNCH: "bundle" },
|
|
1381
|
+
["--shim-status"],
|
|
1382
|
+
);
|
|
1383
|
+
const report = JSON.parse(run.stdout);
|
|
1384
|
+
assert.equal(report.launch.mode, "bundle");
|
|
1385
|
+
assert.equal(report.launch.reason, "pinned by REA_BUNDLE_LAUNCH=bundle");
|
|
1386
|
+
assert.equal(run.status, 78, "an unpinned bundle that is not provisioned still reports EX_CONFIG");
|
|
1387
|
+
} finally {
|
|
1388
|
+
f.clean();
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
test("an older system REA does not displace a newer provisioned one unless pinned", () => {
|
|
1393
|
+
const f = launchFixture();
|
|
1394
|
+
mkdirSync(f.bin, { recursive: true });
|
|
1395
|
+
mkdirSync(f.bundleRoot, { recursive: true });
|
|
1396
|
+
const rea = writeFakeRea(f.bin, { version: "6.4.0" });
|
|
1397
|
+
try {
|
|
1398
|
+
writeFileSync(
|
|
1399
|
+
path.join(f.bundleRoot, "bundle.json"),
|
|
1400
|
+
JSON.stringify({ schema: 1, paths: { node: process.execPath }, versions: { reaAgents: "6.5.0" } }),
|
|
1401
|
+
);
|
|
1402
|
+
const base = { PATH: f.emptyPath, REA_BUNDLE_SYSTEM_REA: rea, REA_BUNDLE_ROOT: f.bundleRoot, REA_BUNDLE_AUTO_PROVISION: "0" };
|
|
1403
|
+
const auto = JSON.parse(runShimIsolated({ ...base, REA_BUNDLE_LAUNCH: "" }, ["--shim-status"]).stdout);
|
|
1404
|
+
assert.equal(auto.launch.mode, "bundle");
|
|
1405
|
+
assert.match(auto.launch.reason, /older than the provisioned one/);
|
|
1406
|
+
const pinned = JSON.parse(runShimIsolated({ ...base, REA_BUNDLE_LAUNCH: "system" }, ["--shim-status"]).stdout);
|
|
1407
|
+
assert.equal(pinned.launch.mode, "system");
|
|
1408
|
+
assert.match(pinned.launch.reason, /pinned by REA_BUNDLE_LAUNCH=system/);
|
|
1409
|
+
} finally {
|
|
1410
|
+
f.clean();
|
|
1411
|
+
}
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1414
|
+
test("a candidate that cannot answer --version is rejected, not launched", () => {
|
|
1415
|
+
const f = launchFixture();
|
|
1416
|
+
mkdirSync(f.bin, { recursive: true });
|
|
1417
|
+
const rea = writeFakeRea(f.bin, { exitForVersion: 7 });
|
|
1418
|
+
try {
|
|
1419
|
+
const run = runShimIsolated(
|
|
1420
|
+
{ PATH: f.emptyPath, REA_BUNDLE_SYSTEM_REA: rea, REA_BUNDLE_ROOT: f.bundleRoot, REA_BUNDLE_AUTO_PROVISION: "0", REA_BUNDLE_LAUNCH: "" },
|
|
1421
|
+
["--shim-status"],
|
|
1422
|
+
);
|
|
1423
|
+
const report = JSON.parse(run.stdout);
|
|
1424
|
+
assert.equal(report.launch.mode, "bundle");
|
|
1425
|
+
assert.equal(report.launch.systemRea, null);
|
|
1426
|
+
assert.deepEqual(report.launch.rejected.map(r => r.reason), ["exit 7"]);
|
|
1427
|
+
assert.equal(run.status, 78);
|
|
1428
|
+
} finally {
|
|
1429
|
+
f.clean();
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
test("REA_BUNDLE_LAUNCH=system refuses instead of provisioning", () => {
|
|
1434
|
+
const f = launchFixture();
|
|
1435
|
+
const missing = path.join(f.bin, "no-such-rea");
|
|
1436
|
+
try {
|
|
1437
|
+
const run = runShimIsolated(
|
|
1438
|
+
{ PATH: f.emptyPath, REA_BUNDLE_SYSTEM_REA: missing, REA_BUNDLE_ROOT: f.bundleRoot, REA_BUNDLE_AUTO_PROVISION: "1", REA_BUNDLE_LAUNCH: "system" },
|
|
1439
|
+
["mcp"],
|
|
1440
|
+
);
|
|
1441
|
+
assert.equal(run.status, 78, run.stdout);
|
|
1442
|
+
assert.match(run.stderr, /REA_BUNDLE_LAUNCH=system was requested/);
|
|
1443
|
+
assert.equal(existsSync(f.bundleRoot), false, "a pinned system launch must not create a bundle");
|
|
1444
|
+
} finally {
|
|
1445
|
+
f.clean();
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1449
|
+
test("an unknown REA_BUNDLE_LAUNCH value is reported and treated as auto", () => {
|
|
1450
|
+
const f = launchFixture();
|
|
1451
|
+
mkdirSync(f.bin, { recursive: true });
|
|
1452
|
+
const rea = writeFakeRea(f.bin, { version: "6.9.0" });
|
|
1453
|
+
try {
|
|
1454
|
+
const run = runShimIsolated(
|
|
1455
|
+
{ PATH: f.emptyPath, REA_BUNDLE_SYSTEM_REA: rea, REA_BUNDLE_ROOT: f.bundleRoot, REA_BUNDLE_AUTO_PROVISION: "0", REA_BUNDLE_LAUNCH: "nonsense" },
|
|
1456
|
+
["--shim-status"],
|
|
1457
|
+
);
|
|
1458
|
+
assert.match(run.stderr, /REA_BUNDLE_LAUNCH="nonsense" is not one of/);
|
|
1459
|
+
assert.equal(JSON.parse(run.stdout).launch.mode, "system");
|
|
1460
|
+
} finally {
|
|
1461
|
+
f.clean();
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
|
|
1465
|
+
test("the system REA is found through PATH itself, not only through the override", () => {
|
|
1466
|
+
const f = launchFixture();
|
|
1467
|
+
mkdirSync(f.bin, { recursive: true });
|
|
1468
|
+
writeFakeRea(f.bin, { version: "6.9.0" });
|
|
1469
|
+
try {
|
|
1470
|
+
const run = runShimIsolated(
|
|
1471
|
+
{ PATH: f.bin, REA_BUNDLE_ROOT: f.bundleRoot, REA_BUNDLE_AUTO_PROVISION: "0", REA_BUNDLE_LAUNCH: "" },
|
|
1472
|
+
["--shim-status"],
|
|
1473
|
+
);
|
|
1474
|
+
const report = JSON.parse(run.stdout);
|
|
1475
|
+
assert.equal(report.launch.mode, "system", JSON.stringify(report.launch));
|
|
1476
|
+
assert.equal(report.launch.systemRea.version, "6.9.0");
|
|
1477
|
+
assert.equal(run.status, 0);
|
|
1478
|
+
} finally {
|
|
1479
|
+
f.clean();
|
|
1480
|
+
}
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
test("a REA installed under a path with a space is probed and launched", () => {
|
|
1484
|
+
const f = launchFixture();
|
|
1485
|
+
const spaced = path.join(f.root, "rea space");
|
|
1486
|
+
mkdirSync(spaced, { recursive: true });
|
|
1487
|
+
const rea = writeFakeRea(spaced, { version: "6.9.0", record: f.record });
|
|
1488
|
+
try {
|
|
1489
|
+
const env = { PATH: f.emptyPath, REA_BUNDLE_SYSTEM_REA: rea, REA_BUNDLE_ROOT: f.bundleRoot, REA_BUNDLE_AUTO_PROVISION: "0", REA_BUNDLE_LAUNCH: "" };
|
|
1490
|
+
const status = JSON.parse(runShimIsolated(env, ["--shim-status"]).stdout);
|
|
1491
|
+
assert.equal(status.launch.mode, "system", JSON.stringify(status.launch));
|
|
1492
|
+
const run = runShimIsolated(env, ["mcp"]);
|
|
1493
|
+
assert.equal(run.status, 0, run.stderr);
|
|
1494
|
+
assert.equal(readFileSync(f.record, "utf8").trim(), "mcp");
|
|
1495
|
+
} finally {
|
|
1496
|
+
f.clean();
|
|
1497
|
+
}
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
test("the delegated process's exit status is the shim's exit status", () => {
|
|
1501
|
+
const f = launchFixture();
|
|
1502
|
+
mkdirSync(f.bin, { recursive: true });
|
|
1503
|
+
const rea = writeFakeRea(f.bin, { version: "6.9.0", runExit: 9 });
|
|
1504
|
+
try {
|
|
1505
|
+
const run = runShimIsolated(
|
|
1506
|
+
{ PATH: f.emptyPath, REA_BUNDLE_SYSTEM_REA: rea, REA_BUNDLE_ROOT: f.bundleRoot, REA_BUNDLE_AUTO_PROVISION: "0", REA_BUNDLE_LAUNCH: "" },
|
|
1507
|
+
["mcp"],
|
|
1508
|
+
);
|
|
1509
|
+
assert.equal(run.status, 9, `the row must see REA's own status, got ${run.status}`);
|
|
1510
|
+
} finally {
|
|
1511
|
+
f.clean();
|
|
1512
|
+
}
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
test("a REA inside the bundle is not treated as a system install", () => {
|
|
1516
|
+
const f = launchFixture();
|
|
1517
|
+
mkdirSync(f.bundleRoot, { recursive: true });
|
|
1518
|
+
const inside = writeFakeRea(f.bundleRoot, { version: "6.9.0" });
|
|
1519
|
+
try {
|
|
1520
|
+
const run = runShimIsolated(
|
|
1521
|
+
{ PATH: f.emptyPath, REA_BUNDLE_SYSTEM_REA: inside, REA_BUNDLE_ROOT: f.bundleRoot, REA_BUNDLE_AUTO_PROVISION: "0", REA_BUNDLE_LAUNCH: "" },
|
|
1522
|
+
["--shim-status"],
|
|
1523
|
+
);
|
|
1524
|
+
const report = JSON.parse(run.stdout);
|
|
1525
|
+
assert.equal(report.launch.mode, "bundle");
|
|
1526
|
+
assert.equal(report.launch.systemRea, null);
|
|
1527
|
+
assert.deepEqual(report.launch.rejected.map(r => r.reason), ["inside the bundle"]);
|
|
1528
|
+
} finally {
|
|
1529
|
+
f.clean();
|
|
1530
|
+
}
|
|
1531
|
+
});
|
|
1532
|
+
|
|
1533
|
+
test("REA_BUNDLE_LAUNCH=bundle never probes the machine", () => {
|
|
1534
|
+
const f = launchFixture();
|
|
1535
|
+
mkdirSync(f.bin, { recursive: true });
|
|
1536
|
+
// Any invocation at all leaves the record behind — including --version, which
|
|
1537
|
+
// is exactly what `auto` would run.
|
|
1538
|
+
const rea = writeFakeRea(f.bin, { version: "6.9.0", record: f.record });
|
|
1539
|
+
try {
|
|
1540
|
+
runShimIsolated(
|
|
1541
|
+
{ PATH: f.bin, REA_BUNDLE_SYSTEM_REA: rea, REA_BUNDLE_ROOT: f.bundleRoot, REA_BUNDLE_AUTO_PROVISION: "0", REA_BUNDLE_LAUNCH: "bundle" },
|
|
1542
|
+
["--shim-status"],
|
|
1543
|
+
);
|
|
1544
|
+
assert.equal(existsSync(f.record), false, "the pinned bundle path must not pay for a probe");
|
|
1545
|
+
} finally {
|
|
1546
|
+
f.clean();
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1550
|
+
test("--shim-print-env reports the system launch instead of a child environment", () => {
|
|
1551
|
+
const f = launchFixture();
|
|
1552
|
+
mkdirSync(f.bin, { recursive: true });
|
|
1553
|
+
const rea = writeFakeRea(f.bin, { version: "6.9.0", record: f.record });
|
|
1554
|
+
try {
|
|
1555
|
+
const run = runShimIsolated(
|
|
1556
|
+
{ PATH: f.emptyPath, REA_BUNDLE_SYSTEM_REA: rea, REA_BUNDLE_ROOT: f.bundleRoot, REA_BUNDLE_AUTO_PROVISION: "0", REA_BUNDLE_LAUNCH: "" },
|
|
1557
|
+
["--shim-print-env"],
|
|
1558
|
+
);
|
|
1559
|
+
const printed = JSON.parse(run.stdout);
|
|
1560
|
+
assert.equal(printed.launch, "system");
|
|
1561
|
+
assert.equal(printed.version, "6.9.0");
|
|
1562
|
+
assert.equal(existsSync(f.record), false, "printing the environment must not start REA");
|
|
1563
|
+
} finally {
|
|
1564
|
+
f.clean();
|
|
1565
|
+
}
|
|
1566
|
+
});
|
|
1567
|
+
|
|
1568
|
+
test("a candidate that wraps this shim is rejected instead of recursing", () => {
|
|
1569
|
+
// The probe asks the candidate for `--version`; a wrapper answers by running
|
|
1570
|
+
// this very shim, which would probe the wrapper again. The probe therefore runs
|
|
1571
|
+
// with REA_BUNDLE_DELEGATED=1, so the nested run takes the bundle path and
|
|
1572
|
+
// returns. Without that, one wrapper on PATH spawns a process chain that keeps
|
|
1573
|
+
// respawning after the parent is killed.
|
|
1574
|
+
const f = launchFixture();
|
|
1575
|
+
mkdirSync(f.bin, { recursive: true });
|
|
1576
|
+
const shim = path.join(REPO_ROOT, "bin", "rea-shim.mjs");
|
|
1577
|
+
const wrapper = path.join(f.bin, process.platform === "win32" ? "rea.cmd" : "rea");
|
|
1578
|
+
if (process.platform === "win32") {
|
|
1579
|
+
writeFileSync(wrapper, `@echo off\r\n"${process.execPath}" "${shim}" %*\r\n`);
|
|
1580
|
+
} else {
|
|
1581
|
+
writeFileSync(wrapper, `#!/bin/sh\nexec "${process.execPath}" "${shim}" "$@"\n`);
|
|
1582
|
+
chmodSync(wrapper, 0o755);
|
|
1583
|
+
}
|
|
1584
|
+
try {
|
|
1585
|
+
const started = Date.now();
|
|
1586
|
+
const run = runShimIsolated(
|
|
1587
|
+
{ PATH: f.emptyPath, REA_BUNDLE_SYSTEM_REA: wrapper, REA_BUNDLE_ROOT: f.bundleRoot, REA_BUNDLE_AUTO_PROVISION: "0", REA_BUNDLE_LAUNCH: "" },
|
|
1588
|
+
["--shim-status"],
|
|
1589
|
+
);
|
|
1590
|
+
const elapsed = Date.now() - started;
|
|
1591
|
+
const report = JSON.parse(run.stdout);
|
|
1592
|
+
assert.equal(report.launch.mode, "bundle");
|
|
1593
|
+
assert.deepEqual(report.launch.rejected.map(r => r.reason), ["exit 78"]);
|
|
1594
|
+
assert.ok(elapsed < 20000, `the probe must not recurse; it took ${elapsed} ms`);
|
|
1595
|
+
} finally {
|
|
1596
|
+
f.clean();
|
|
1597
|
+
}
|
|
1598
|
+
});
|
|
1599
|
+
|
|
1600
|
+
test("a ComSpec that points at nothing falls back to the real shell", () => {
|
|
1601
|
+
// Trusting ComSpec blindly would judge every .cmd REA "spawn-failed" — i.e.
|
|
1602
|
+
// report the machine as having no REA because its shell pointer is broken.
|
|
1603
|
+
const f = launchFixture();
|
|
1604
|
+
mkdirSync(f.bin, { recursive: true });
|
|
1605
|
+
const rea = writeFakeRea(f.bin, { version: "6.9.0" });
|
|
1606
|
+
try {
|
|
1607
|
+
const run = runShimIsolated(
|
|
1608
|
+
{
|
|
1609
|
+
PATH: f.emptyPath,
|
|
1610
|
+
ComSpec: path.join(f.root, "no-such-cmd.exe"),
|
|
1611
|
+
REA_BUNDLE_SYSTEM_REA: rea,
|
|
1612
|
+
REA_BUNDLE_ROOT: f.bundleRoot,
|
|
1613
|
+
REA_BUNDLE_AUTO_PROVISION: "0",
|
|
1614
|
+
REA_BUNDLE_LAUNCH: "",
|
|
1615
|
+
},
|
|
1616
|
+
["--shim-status"],
|
|
1617
|
+
);
|
|
1618
|
+
const report = JSON.parse(run.stdout);
|
|
1619
|
+
if (process.platform === "win32") {
|
|
1620
|
+
assert.equal(report.launch.mode, "system", JSON.stringify(report.launch));
|
|
1621
|
+
assert.equal(report.launch.systemRea.version, "6.9.0");
|
|
1622
|
+
} else {
|
|
1623
|
+
// POSIX never consults ComSpec; the assertion above would be incidental.
|
|
1624
|
+
assert.equal(report.launch.mode, "system");
|
|
1625
|
+
}
|
|
1626
|
+
} finally {
|
|
1627
|
+
f.clean();
|
|
1628
|
+
}
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
/* --------------------------------------------------- the mirrored launcher */
|
|
1632
|
+
|
|
1633
|
+
// The row launches `<bundle>/bin/rea-shim.mjs`, a COPY. A market upgrade replaces
|
|
1634
|
+
// the package and runs no setup, so without this the copy keeps running the code
|
|
1635
|
+
// it was made from — every fix silently stops at the old mirror. These tests pin
|
|
1636
|
+
// the repair: the copy re-execs the plugin's launcher when they differ, once.
|
|
1637
|
+
|
|
1638
|
+
function mirrorFixture() {
|
|
1639
|
+
const root = mkdtempSync(path.join(tmpdir(), "rea-mirror-"));
|
|
1640
|
+
const bundle = path.join(root, "bundle");
|
|
1641
|
+
const checkout = path.join(root, "checkout");
|
|
1642
|
+
mkdirSync(path.join(bundle, "bin"), { recursive: true });
|
|
1643
|
+
mkdirSync(path.join(checkout, "bin"), { recursive: true });
|
|
1644
|
+
copyFileSync(path.join(REPO_ROOT, "bin", "rea-shim.mjs"), path.join(bundle, "bin", "rea-shim.mjs"));
|
|
1645
|
+
writeFileSync(path.join(bundle, "mirror.json"), JSON.stringify({ schema: 1, pluginRoot: checkout, mirroredAt: "2026-10-18T00:00:00Z" }));
|
|
1646
|
+
return { root, bundle, checkout, clean() { rmSync(root, { recursive: true, force: true }); } };
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
function checkoutStub(checkout, marker) {
|
|
1650
|
+
const file = path.join(checkout, "bin", "rea-shim.mjs");
|
|
1651
|
+
writeFileSync(
|
|
1652
|
+
file,
|
|
1653
|
+
'import { writeFileSync } from "node:fs";\n' +
|
|
1654
|
+
`writeFileSync(${JSON.stringify(marker)}, "checkout");\n` +
|
|
1655
|
+
"process.exit(7);\n",
|
|
1656
|
+
);
|
|
1657
|
+
return file;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
test("a mirrored launcher re-execs the plugin's launcher when they differ", () => {
|
|
1661
|
+
const f = mirrorFixture();
|
|
1662
|
+
const marker = path.join(f.root, "checkout-ran");
|
|
1663
|
+
checkoutStub(f.checkout, marker);
|
|
1664
|
+
try {
|
|
1665
|
+
const run = runShimIsolated({ REA_BUNDLE_ROOT: f.bundle, REA_BUNDLE_AUTO_PROVISION: "0", REA_BUNDLE_LAUNCH: "bundle" }, ["mcp"]);
|
|
1666
|
+
assert.equal(run.status, 7, `expected the checkout's status, got ${run.status}: ${run.stderr}`);
|
|
1667
|
+
assert.equal(existsSync(marker), true, "the checkout launcher must have run");
|
|
1668
|
+
} finally {
|
|
1669
|
+
f.clean();
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1672
|
+
|
|
1673
|
+
test("the re-exec happens once, and a pinned guard stops it", () => {
|
|
1674
|
+
const f = mirrorFixture();
|
|
1675
|
+
const marker = path.join(f.root, "checkout-ran");
|
|
1676
|
+
checkoutStub(f.checkout, marker);
|
|
1677
|
+
try {
|
|
1678
|
+
const run = runShimIsolated(
|
|
1679
|
+
{ REA_BUNDLE_ROOT: f.bundle, REA_BUNDLE_AUTO_PROVISION: "0", REA_BUNDLE_LAUNCH: "bundle", REA_BUNDLE_SHIM_REEXEC: "1" },
|
|
1680
|
+
["mcp"],
|
|
1681
|
+
);
|
|
1682
|
+
// The guard is set, so the mirrored copy runs its own logic: bundle pinned and
|
|
1683
|
+
// nothing provisioned means EX_CONFIG, and the checkout never starts.
|
|
1684
|
+
assert.equal(run.status, 78, run.stderr);
|
|
1685
|
+
assert.equal(existsSync(marker), false, "the guard must prevent a second hop");
|
|
1686
|
+
} finally {
|
|
1687
|
+
f.clean();
|
|
1688
|
+
}
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1691
|
+
test("--shim-status reports a stale mirror instead of re-execing it", () => {
|
|
1692
|
+
const f = mirrorFixture();
|
|
1693
|
+
const marker = path.join(f.root, "checkout-ran");
|
|
1694
|
+
checkoutStub(f.checkout, marker);
|
|
1695
|
+
try {
|
|
1696
|
+
const run = runShimIsolated({ REA_BUNDLE_ROOT: f.bundle, REA_BUNDLE_AUTO_PROVISION: "0", REA_BUNDLE_LAUNCH: "bundle" }, ["--shim-status"]);
|
|
1697
|
+
const report = JSON.parse(run.stdout);
|
|
1698
|
+
assert.equal(report.mirror.stale, true, JSON.stringify(report.mirror));
|
|
1699
|
+
assert.match(report.mirror.reason, /different launcher/);
|
|
1700
|
+
assert.equal(existsSync(marker), false, "status is a query; it must not start anything");
|
|
1701
|
+
} finally {
|
|
1702
|
+
f.clean();
|
|
1703
|
+
}
|
|
1704
|
+
});
|
|
1705
|
+
|
|
1706
|
+
test("an identical mirror is not stale and is not re-exec'd", () => {
|
|
1707
|
+
const f = mirrorFixture();
|
|
1708
|
+
const same = path.join(f.checkout, "bin", "rea-shim.mjs");
|
|
1709
|
+
copyFileSync(path.join(f.bundle, "bin", "rea-shim.mjs"), same);
|
|
1710
|
+
try {
|
|
1711
|
+
const run = runShimIsolated({ REA_BUNDLE_ROOT: f.bundle, REA_BUNDLE_AUTO_PROVISION: "0", REA_BUNDLE_LAUNCH: "bundle" }, ["--shim-status"]);
|
|
1712
|
+
const report = JSON.parse(run.stdout);
|
|
1713
|
+
assert.equal(report.mirror.stale, false, JSON.stringify(report.mirror));
|
|
1714
|
+
assert.equal(report.mirror.reason, "this copy matches the plugin");
|
|
1715
|
+
} finally {
|
|
1716
|
+
f.clean();
|
|
1717
|
+
}
|
|
1718
|
+
});
|
|
1719
|
+
|
|
1720
|
+
test("a mirrored shim finds the real DSH home when the environment is scrubbed", () => {
|
|
1721
|
+
// The harness strips DSH_* from an MCP child, so with a custom DSH_HOME the
|
|
1722
|
+
// old code read ~/.dsh — the wrong file — and could never report upstream's
|
|
1723
|
+
// duplicate `serverName: rea`. setup records the home; the shim must use it.
|
|
1724
|
+
const f = mirrorFixture();
|
|
1725
|
+
const home = path.join(f.root, "custom-home");
|
|
1726
|
+
mkdirSync(home, { recursive: true });
|
|
1727
|
+
writeFileSync(
|
|
1728
|
+
path.join(home, "cordis.patch.yml"),
|
|
1729
|
+
"- insert:\n - id: rea-mcp\n name: '@deepseek-ai/dsh-mcp-client'\n config:\n serverName: rea\n",
|
|
1730
|
+
);
|
|
1731
|
+
writeFileSync(
|
|
1732
|
+
path.join(f.bundle, "mirror.json"),
|
|
1733
|
+
JSON.stringify({ schema: 1, pluginRoot: f.checkout, mirroredAt: "2026-10-18T00:00:00Z", dshHome: home }),
|
|
1734
|
+
);
|
|
1735
|
+
try {
|
|
1736
|
+
const run = runShimIsolated(
|
|
1737
|
+
// DSH_HOME="" models the scrubbed environment: the variable exists, empty.
|
|
1738
|
+
{ REA_BUNDLE_ROOT: f.bundle, REA_BUNDLE_AUTO_PROVISION: "0", REA_BUNDLE_LAUNCH: "bundle", DSH_HOME: "" },
|
|
1739
|
+
["--shim-status"],
|
|
1740
|
+
);
|
|
1741
|
+
const report = JSON.parse(run.stdout);
|
|
1742
|
+
assert.equal(report.duplicateRegistration?.id, "rea-mcp", run.stdout);
|
|
1743
|
+
assert.equal(report.duplicateRegistration.patch, path.join(home, "cordis.patch.yml"));
|
|
1744
|
+
} finally {
|
|
1745
|
+
f.clean();
|
|
1746
|
+
}
|
|
1246
1747
|
});
|
|
@@ -90,6 +90,14 @@ Check 'main resolves to a shipped file' ($pkg.main -and (Test-Path (Join-Path $r
|
|
|
90
90
|
Check 'package.json declares engines.dsh' (-not [string]::IsNullOrWhiteSpace($pkg.engines.dsh)) ''
|
|
91
91
|
$compat = & node scripts\host-compat.mjs 2>&1
|
|
92
92
|
Check 'engines.dsh covers every released DSH host' ($LASTEXITCODE -eq 0) (($compat | Select-Object -Last 2) -join ' ')
|
|
93
|
+
# Both READMEs quote the range verbatim, and a stale quote is invisible: JSON,
|
|
94
|
+
# host-compat and the audit all agree while the sentence a human reads names a
|
|
95
|
+
# different ceiling. That is exactly how `<0.3.0` survived in the docs after the
|
|
96
|
+
# declaration moved to `<0.3.0-0`, so the comparison lives here now.
|
|
97
|
+
foreach ($doc in @('README.md', 'README.zh-CN.md')) {
|
|
98
|
+
$text = Get-Content (Join-Path $root $doc) -Raw -Encoding UTF8
|
|
99
|
+
Check "$doc quotes the declared engines.dsh" $text.Contains([string]$pkg.engines.dsh) "expected $($pkg.engines.dsh)"
|
|
100
|
+
}
|
|
93
101
|
foreach ($script in @('bundle:setup', 'bundle:status', 'bundle:audit', 'bundle:test', 'bundle:host-compat')) {
|
|
94
102
|
Check "scripts.$script exists" ($pkg.scripts.PSObject.Properties.Name -contains $script) ''
|
|
95
103
|
}
|