@logicseek/hello-cc 0.1.7 → 0.1.9
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 +107 -0
- package/README.md +20 -7
- package/README.zh-CN.md +15 -6
- package/bin/hcc.mjs +383 -62
- package/docs/commands.md +16 -7
- package/docs/commands.zh-CN.md +14 -7
- package/docs/guide.md +24 -7
- package/docs/guide.zh-CN.md +20 -6
- package/lib/core/coordination/timeline.mjs +1 -0
- package/lib/core/sessions/launch.mjs +3 -0
- package/lib/integrations/shims/script.mjs +66 -24
- package/lib/integrations/shims.mjs +45 -6
- package/lib/runtime/client.mjs +1 -1
- package/lib/runtime/state.mjs +6 -3
- package/lib/setup.mjs +1 -0
- package/lib/shell-path.mjs +31 -16
- package/lib/ui/help.mjs +36 -1
- package/lib/web/http.mjs +8 -1
- package/package.json +1 -1
- package/scripts/regression.mjs +651 -48
package/scripts/regression.mjs
CHANGED
|
@@ -38,7 +38,8 @@ const managedTmuxSessions = new Set();
|
|
|
38
38
|
const env = {
|
|
39
39
|
...process.env,
|
|
40
40
|
HOME: home,
|
|
41
|
-
PATH: `${fakeBin}${path.delimiter}${process.env.PATH || ''}
|
|
41
|
+
PATH: `${fakeBin}${path.delimiter}${process.env.PATH || ''}`,
|
|
42
|
+
SHELL: '/bin/bash'
|
|
42
43
|
};
|
|
43
44
|
for (const key of Object.keys(env)) {
|
|
44
45
|
if (key.startsWith('HCC_')) delete env[key];
|
|
@@ -51,6 +52,13 @@ function log(message) {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
function fail(message) {
|
|
55
|
+
if (process.env.GITHUB_ACTIONS === 'true') {
|
|
56
|
+
const escaped = String(message)
|
|
57
|
+
.replace(/%/g, '%25')
|
|
58
|
+
.replace(/\r/g, '%0D')
|
|
59
|
+
.replace(/\n/g, '%0A');
|
|
60
|
+
process.stderr.write(`::error::${escaped}\n`);
|
|
61
|
+
}
|
|
54
62
|
throw new Error(message);
|
|
55
63
|
}
|
|
56
64
|
|
|
@@ -62,6 +70,20 @@ function sh(value) {
|
|
|
62
70
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
63
71
|
}
|
|
64
72
|
|
|
73
|
+
function canonicalPath(value) {
|
|
74
|
+
try { return fs.realpathSync(value); }
|
|
75
|
+
catch { return path.resolve(value); }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function samePath(a, b) {
|
|
79
|
+
return canonicalPath(a) === canonicalPath(b);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function statusValue(output, key) {
|
|
83
|
+
const match = String(output || '').match(new RegExp(`^${key}:\\s*(.*)$`, 'm'));
|
|
84
|
+
return match ? match[1].trim() : '';
|
|
85
|
+
}
|
|
86
|
+
|
|
65
87
|
function shortHash(value) {
|
|
66
88
|
return crypto.createHash('sha1').update(String(value)).digest('hex').slice(0, 8);
|
|
67
89
|
}
|
|
@@ -400,7 +422,8 @@ async function assertTmuxGcPolicy() {
|
|
|
400
422
|
}), t);
|
|
401
423
|
});
|
|
402
424
|
|
|
403
|
-
const
|
|
425
|
+
const attachTmux = realTmuxBin ? `${sh(realTmuxBin)} -L ${sh(tmuxSocketName)}` : 'tmux';
|
|
426
|
+
const client = spawnSync('tmux', ['new-session', '-d', '-s', `${tmuxSession}-gc-client`, '-e', `HCC_ROOT=${root}`, 'sh', '-lc', `unset TMUX; exec ${attachTmux} attach-session -t ${sh(attachedSession)}`], {
|
|
404
427
|
env,
|
|
405
428
|
encoding: 'utf8',
|
|
406
429
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
@@ -1023,19 +1046,34 @@ async function waitForFile(file, expected, label = file) {
|
|
|
1023
1046
|
}
|
|
1024
1047
|
|
|
1025
1048
|
async function waitForFileContent(file, expected, label = file) {
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1049
|
+
const deadline = Date.now() + 10000;
|
|
1050
|
+
let actual = '(missing)';
|
|
1051
|
+
while (Date.now() < deadline) {
|
|
1052
|
+
if (fs.existsSync(file)) {
|
|
1053
|
+
actual = fs.readFileSync(file, 'utf8').trim();
|
|
1054
|
+
if (actual === expected) break;
|
|
1055
|
+
}
|
|
1056
|
+
await sleep(100);
|
|
1057
|
+
}
|
|
1058
|
+
if (actual !== expected) {
|
|
1059
|
+
fail(`timed out waiting for ${label}\nexpected: ${expected}\nactual: ${actual}`);
|
|
1060
|
+
}
|
|
1030
1061
|
ensureFile(file, expected);
|
|
1031
1062
|
}
|
|
1032
1063
|
|
|
1033
1064
|
async function waitForFileLineCount(file, expected, label = file) {
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1065
|
+
const deadline = Date.now() + 10000;
|
|
1066
|
+
let actual = '(missing)';
|
|
1067
|
+
while (Date.now() < deadline) {
|
|
1068
|
+
if (fs.existsSync(file)) {
|
|
1069
|
+
actual = fs.readFileSync(file, 'utf8').trim();
|
|
1070
|
+
const lines = actual.split('\n').filter(Boolean);
|
|
1071
|
+
if (lines.length === expected) return;
|
|
1072
|
+
}
|
|
1073
|
+
await sleep(100);
|
|
1074
|
+
}
|
|
1075
|
+
const count = actual === '(missing)' ? 0 : actual.split('\n').filter(Boolean).length;
|
|
1076
|
+
fail(`timed out waiting for ${label}\nexpected lines: ${expected}\nactual lines: ${count}\nactual: ${actual}`);
|
|
1039
1077
|
}
|
|
1040
1078
|
|
|
1041
1079
|
async function waitRuntime() {
|
|
@@ -1051,6 +1089,19 @@ async function waitRuntime() {
|
|
|
1051
1089
|
}, 'runtime');
|
|
1052
1090
|
}
|
|
1053
1091
|
|
|
1092
|
+
function processIsAlive(pid) {
|
|
1093
|
+
try {
|
|
1094
|
+
process.kill(pid, 0);
|
|
1095
|
+
return true;
|
|
1096
|
+
} catch {
|
|
1097
|
+
return false;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
async function waitForProcessExit(pid, label, timeoutMs = 5000) {
|
|
1102
|
+
await waitFor(() => !processIsAlive(pid), label, timeoutMs);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1054
1105
|
async function expectWebSocketMarker(peer, marker) {
|
|
1055
1106
|
await new Promise((resolve, reject) => {
|
|
1056
1107
|
let sawSnapshot = false;
|
|
@@ -1077,6 +1128,21 @@ async function expectWebSocketMarker(peer, marker) {
|
|
|
1077
1128
|
});
|
|
1078
1129
|
}
|
|
1079
1130
|
|
|
1131
|
+
async function openTerminalWebSocket(peer) {
|
|
1132
|
+
return new Promise((resolve, reject) => {
|
|
1133
|
+
const ws = new WebSocket(runtimeWsUrl(peer));
|
|
1134
|
+
const timer = setTimeout(() => reject(new Error(`${peer} websocket open timeout`)), 5000);
|
|
1135
|
+
ws.on('open', () => {
|
|
1136
|
+
clearTimeout(timer);
|
|
1137
|
+
resolve(ws);
|
|
1138
|
+
});
|
|
1139
|
+
ws.on('error', (err) => {
|
|
1140
|
+
clearTimeout(timer);
|
|
1141
|
+
reject(err);
|
|
1142
|
+
});
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1080
1146
|
async function expectResizeReplaceSnapshot(peer, marker) {
|
|
1081
1147
|
await new Promise((resolve, reject) => {
|
|
1082
1148
|
let sawSnapshot = false;
|
|
@@ -1150,6 +1216,197 @@ function libModuleFiles() {
|
|
|
1150
1216
|
.map((name) => path.join('lib', name));
|
|
1151
1217
|
}
|
|
1152
1218
|
|
|
1219
|
+
function assertShimRuntimeUnavailableFallback(generateShim) {
|
|
1220
|
+
const fallbackDir = fs.mkdtempSync(path.join(os.tmpdir(), `hcc-shim-fallback-${testId}-`));
|
|
1221
|
+
try {
|
|
1222
|
+
const fakeHcc = path.join(fallbackDir, 'hcc');
|
|
1223
|
+
fs.writeFileSync(fakeHcc, `#!/usr/bin/env bash
|
|
1224
|
+
case "\${1:-}" in
|
|
1225
|
+
shim)
|
|
1226
|
+
exit 64
|
|
1227
|
+
;;
|
|
1228
|
+
find-root)
|
|
1229
|
+
if [ -n "\${HCC_FAKE_ROOT:-}" ]; then
|
|
1230
|
+
printf '%s\\n' "$HCC_FAKE_ROOT"
|
|
1231
|
+
exit 0
|
|
1232
|
+
fi
|
|
1233
|
+
exit 1
|
|
1234
|
+
;;
|
|
1235
|
+
web)
|
|
1236
|
+
exit 1
|
|
1237
|
+
;;
|
|
1238
|
+
peer)
|
|
1239
|
+
if [ "\${2:-}" = "start" ]; then
|
|
1240
|
+
printf '%s\\n' "hcc: No running web runtime found. Start hcc web first, then hcc peer start \${3:-peer}" >&2
|
|
1241
|
+
exit 1
|
|
1242
|
+
fi
|
|
1243
|
+
exit 1
|
|
1244
|
+
;;
|
|
1245
|
+
esac
|
|
1246
|
+
exit 1
|
|
1247
|
+
`, { mode: 0o755 });
|
|
1248
|
+
|
|
1249
|
+
const cases = [
|
|
1250
|
+
{
|
|
1251
|
+
tool: { name: 'claude', kind: 'claude', resumeFlag: '--resume' },
|
|
1252
|
+
args: ['--resume', 'missing-web-session'],
|
|
1253
|
+
expected: 'real-claude --resume missing-web-session'
|
|
1254
|
+
},
|
|
1255
|
+
{
|
|
1256
|
+
tool: { name: 'codex', kind: 'codex' },
|
|
1257
|
+
args: ['resume', 'missing-web-session'],
|
|
1258
|
+
expected: 'real-codex resume missing-web-session'
|
|
1259
|
+
}
|
|
1260
|
+
];
|
|
1261
|
+
|
|
1262
|
+
for (const entry of cases) {
|
|
1263
|
+
const realBin = path.join(fallbackDir, `real-${entry.tool.name}`);
|
|
1264
|
+
const shimBin = path.join(fallbackDir, entry.tool.name);
|
|
1265
|
+
const logFile = path.join(fallbackDir, `${entry.tool.name}.log`);
|
|
1266
|
+
fs.writeFileSync(realBin, `#!/usr/bin/env bash
|
|
1267
|
+
printf 'real-${entry.tool.name}'
|
|
1268
|
+
for arg in "$@"; do printf ' %s' "$arg"; done
|
|
1269
|
+
printf '\\n'
|
|
1270
|
+
if [ -n "\${HCC_FAKE_LOG:-}" ]; then
|
|
1271
|
+
printf 'real-${entry.tool.name}' >> "$HCC_FAKE_LOG"
|
|
1272
|
+
for arg in "$@"; do printf ' %s' "$arg" >> "$HCC_FAKE_LOG"; done
|
|
1273
|
+
printf '\\n' >> "$HCC_FAKE_LOG"
|
|
1274
|
+
fi
|
|
1275
|
+
`, { mode: 0o755 });
|
|
1276
|
+
fs.writeFileSync(shimBin, generateShim(fakeHcc, realBin, entry.tool), { mode: 0o755 });
|
|
1277
|
+
|
|
1278
|
+
const result = runMaybe(shimBin, entry.args, {
|
|
1279
|
+
cwd: fallbackDir,
|
|
1280
|
+
env: {
|
|
1281
|
+
...env,
|
|
1282
|
+
HCC_FAKE_ROOT: fallbackDir,
|
|
1283
|
+
HCC_FAKE_LOG: logFile
|
|
1284
|
+
}
|
|
1285
|
+
});
|
|
1286
|
+
if (result.status !== 0) {
|
|
1287
|
+
fail(`${entry.tool.name} shim did not fall back when runtime was unavailable:\nstdout=${result.stdout}\nstderr=${result.stderr}`);
|
|
1288
|
+
}
|
|
1289
|
+
if (!String(result.stdout || '').includes(entry.expected)) {
|
|
1290
|
+
fail(`${entry.tool.name} shim fallback did not launch the real provider:\nstdout=${result.stdout}\nstderr=${result.stderr}`);
|
|
1291
|
+
}
|
|
1292
|
+
const logOutput = fs.existsSync(logFile) ? fs.readFileSync(logFile, 'utf8') : '';
|
|
1293
|
+
if (!logOutput.includes(entry.expected)) {
|
|
1294
|
+
fail(`${entry.tool.name} shim fallback did not preserve provider argv:\n${logOutput}`);
|
|
1295
|
+
}
|
|
1296
|
+
if (String(result.stderr || '').includes('No running web runtime found')) {
|
|
1297
|
+
fail(`${entry.tool.name} shim leaked hcc runtime failure instead of transparent fallback:\n${result.stderr}`);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
} finally {
|
|
1301
|
+
try { fs.rmSync(fallbackDir, { recursive: true, force: true }); } catch {}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
async function assertShimIgnoresGlobalRuntime(generateShim) {
|
|
1306
|
+
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), `hcc-shim-global-runtime-${testId}-`));
|
|
1307
|
+
const fakeHome = path.join(testDir, 'home');
|
|
1308
|
+
const projectRoot = path.join(testDir, 'project-without-runtime');
|
|
1309
|
+
const requests = [];
|
|
1310
|
+
let server = null;
|
|
1311
|
+
try {
|
|
1312
|
+
fs.mkdirSync(path.join(fakeHome, '.hello-cc'), { recursive: true });
|
|
1313
|
+
fs.mkdirSync(projectRoot, { recursive: true });
|
|
1314
|
+
|
|
1315
|
+
server = http.createServer((req, res) => {
|
|
1316
|
+
requests.push({ method: req.method, url: req.url });
|
|
1317
|
+
res.setHeader('Content-Type', 'application/json');
|
|
1318
|
+
if (req.method === 'GET' && req.url.startsWith('/api/runtime')) {
|
|
1319
|
+
res.end(JSON.stringify({ ok: true }));
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
if (req.method === 'POST' && req.url.startsWith('/api/sessions')) {
|
|
1323
|
+
res.end(JSON.stringify({ session: {
|
|
1324
|
+
id: 'global-runtime-session',
|
|
1325
|
+
kind: 'claude',
|
|
1326
|
+
role: 'peer',
|
|
1327
|
+
status: 'running',
|
|
1328
|
+
pane: '%900'
|
|
1329
|
+
} }));
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
res.statusCode = 404;
|
|
1333
|
+
res.end(JSON.stringify({ error: { code: 'NOT_FOUND', message: 'not found' } }));
|
|
1334
|
+
});
|
|
1335
|
+
await new Promise((resolve, reject) => {
|
|
1336
|
+
server.once('error', reject);
|
|
1337
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
1338
|
+
});
|
|
1339
|
+
const address = server.address();
|
|
1340
|
+
const globalRuntime = {
|
|
1341
|
+
product: 'hello-cc',
|
|
1342
|
+
version: 'regression',
|
|
1343
|
+
pid: process.pid,
|
|
1344
|
+
root: testDir,
|
|
1345
|
+
db: path.join(testDir, '.hello-cc', 'mesh.db'),
|
|
1346
|
+
host: '127.0.0.1',
|
|
1347
|
+
port: address.port,
|
|
1348
|
+
base_url: `http://127.0.0.1:${address.port}`,
|
|
1349
|
+
token: '',
|
|
1350
|
+
started_at: Math.floor(Date.now() / 1000),
|
|
1351
|
+
global_runtime: true
|
|
1352
|
+
};
|
|
1353
|
+
fs.writeFileSync(path.join(fakeHome, '.hello-cc', 'runtime.json'), JSON.stringify(globalRuntime, null, 2));
|
|
1354
|
+
|
|
1355
|
+
const hccWrapper = path.join(testDir, 'hcc-wrapper');
|
|
1356
|
+
fs.writeFileSync(hccWrapper, `#!/usr/bin/env bash\nexec ${sh(process.execPath)} ${sh(hccBin)} "$@"\n`, { mode: 0o755 });
|
|
1357
|
+
|
|
1358
|
+
const cases = [
|
|
1359
|
+
{
|
|
1360
|
+
tool: { name: 'claude', kind: 'claude', resumeFlag: '--resume' },
|
|
1361
|
+
args: ['--resume', 'global-runtime-session'],
|
|
1362
|
+
expected: 'real-claude --resume global-runtime-session'
|
|
1363
|
+
},
|
|
1364
|
+
{
|
|
1365
|
+
tool: { name: 'codex', kind: 'codex' },
|
|
1366
|
+
args: ['resume', 'global-runtime-session'],
|
|
1367
|
+
expected: 'real-codex resume global-runtime-session'
|
|
1368
|
+
}
|
|
1369
|
+
];
|
|
1370
|
+
|
|
1371
|
+
for (const entry of cases) {
|
|
1372
|
+
requests.length = 0;
|
|
1373
|
+
fs.rmSync(path.join(projectRoot, '.hello-cc'), { recursive: true, force: true });
|
|
1374
|
+
const realBin = path.join(testDir, `real-${entry.tool.name}`);
|
|
1375
|
+
const shimBin = path.join(testDir, entry.tool.name);
|
|
1376
|
+
fs.writeFileSync(realBin, `#!/usr/bin/env bash
|
|
1377
|
+
printf 'real-${entry.tool.name}'
|
|
1378
|
+
for arg in "$@"; do printf ' %s' "$arg"; done
|
|
1379
|
+
printf '\\n'
|
|
1380
|
+
`, { mode: 0o755 });
|
|
1381
|
+
fs.writeFileSync(shimBin, generateShim(hccWrapper, realBin, entry.tool), { mode: 0o755 });
|
|
1382
|
+
const result = runMaybe(shimBin, entry.args, {
|
|
1383
|
+
cwd: projectRoot,
|
|
1384
|
+
env: {
|
|
1385
|
+
...env,
|
|
1386
|
+
HOME: fakeHome,
|
|
1387
|
+
HCC_SHIM_ENSURED: '1',
|
|
1388
|
+
HCC_SHIM_NO_ATTACH: '1'
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
if (result.status !== 0) {
|
|
1392
|
+
fail(`${entry.tool.name} shim failed while ignoring global runtime:\nstdout=${result.stdout}\nstderr=${result.stderr}`);
|
|
1393
|
+
}
|
|
1394
|
+
if (!String(result.stdout || '').includes(entry.expected)) {
|
|
1395
|
+
fail(`${entry.tool.name} shim used global runtime instead of falling back:\nstdout=${result.stdout}\nstderr=${result.stderr}`);
|
|
1396
|
+
}
|
|
1397
|
+
if (requests.length) {
|
|
1398
|
+
fail(`${entry.tool.name} shim contacted global runtime despite project-local restriction:\n${JSON.stringify(requests, null, 2)}`);
|
|
1399
|
+
}
|
|
1400
|
+
if (fs.existsSync(path.join(projectRoot, '.hello-cc', 'mesh.db'))) {
|
|
1401
|
+
fail(`${entry.tool.name} shim created a project database while only a global runtime existed`);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
} finally {
|
|
1405
|
+
if (server?.listening) await new Promise((resolve) => server.close(resolve));
|
|
1406
|
+
try { fs.rmSync(testDir, { recursive: true, force: true }); } catch {}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1153
1410
|
async function expectBoundedTmuxStream(label) {
|
|
1154
1411
|
let nodes = [];
|
|
1155
1412
|
await waitFor(() => {
|
|
@@ -1194,19 +1451,54 @@ function startRuntime(options = {}) {
|
|
|
1194
1451
|
if (!output.includes('web started in background')) fail(`hcc web did not report background start:\n${output}`);
|
|
1195
1452
|
}
|
|
1196
1453
|
|
|
1454
|
+
async function assertWebWrapperParentSurvives() {
|
|
1455
|
+
const wrapperRoot = fs.mkdtempSync(path.join(os.tmpdir(), `hcc-reg-wrapper-root-${testId}-`));
|
|
1456
|
+
const wrapperPort = port + 101;
|
|
1457
|
+
let wrapperPid = null;
|
|
1458
|
+
try {
|
|
1459
|
+
const script = [
|
|
1460
|
+
'set -e',
|
|
1461
|
+
`out=$(${sh(process.execPath)} ${sh(hccBin)} --root ${sh(wrapperRoot)} web --local --port ${wrapperPort} --no-discover --no-guidance)`,
|
|
1462
|
+
`printf '%s\\n' "$out"`
|
|
1463
|
+
].join('\n');
|
|
1464
|
+
const result = runMaybe('bash', ['-lc', script], { env });
|
|
1465
|
+
if (result.status !== 0) {
|
|
1466
|
+
fail(`hcc web killed or failed inside a wrapper shell:\nstdout=${result.stdout}\nstderr=${result.stderr}\nstatus=${result.status}\nsignal=${result.signal}`);
|
|
1467
|
+
}
|
|
1468
|
+
const match = String(result.stdout || '').match(/^pid:\s*(\d+)/m);
|
|
1469
|
+
if (!match) fail(`wrapper hcc web did not print background pid:\n${result.stdout}`);
|
|
1470
|
+
wrapperPid = Number.parseInt(match[1], 10);
|
|
1471
|
+
ensureFile(path.join(wrapperRoot, '.hello-cc', 'runtime.json'));
|
|
1472
|
+
} finally {
|
|
1473
|
+
runMaybe(process.execPath, [hccBin, '--root', wrapperRoot, 'down'], { env });
|
|
1474
|
+
if (wrapperPid) {
|
|
1475
|
+
try {
|
|
1476
|
+
await waitForProcessExit(wrapperPid, 'wrapper runtime process exit', 5000);
|
|
1477
|
+
} catch {
|
|
1478
|
+
try { process.kill(wrapperPid, 'SIGTERM'); } catch {}
|
|
1479
|
+
try { await waitForProcessExit(wrapperPid, 'wrapper runtime process exit after SIGTERM', 2000); } catch {}
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
try { fs.rmSync(wrapperRoot, { recursive: true, force: true }); } catch {}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1197
1486
|
async function stopRuntime() {
|
|
1198
1487
|
if (!runtimePid) return;
|
|
1488
|
+
const pid = runtimePid;
|
|
1199
1489
|
hccMaybe(['down']);
|
|
1200
|
-
|
|
1490
|
+
try {
|
|
1491
|
+
await waitForProcessExit(pid, 'runtime process exit', 5000);
|
|
1492
|
+
} catch (err) {
|
|
1493
|
+
try { process.kill(pid, 'SIGTERM'); } catch {}
|
|
1201
1494
|
try {
|
|
1202
|
-
|
|
1203
|
-
return false;
|
|
1495
|
+
await waitForProcessExit(pid, 'runtime process exit after SIGTERM', 2000);
|
|
1204
1496
|
} catch {
|
|
1205
|
-
|
|
1497
|
+
try { process.kill(pid, 'SIGKILL'); } catch {}
|
|
1498
|
+
try { await waitForProcessExit(pid, 'runtime process exit after SIGKILL', 2000); } catch {}
|
|
1499
|
+
throw err;
|
|
1206
1500
|
}
|
|
1207
|
-
}
|
|
1208
|
-
try { process.kill(runtimePid, 'SIGTERM'); } catch {}
|
|
1209
|
-
});
|
|
1501
|
+
}
|
|
1210
1502
|
runtimePid = null;
|
|
1211
1503
|
}
|
|
1212
1504
|
|
|
@@ -1315,7 +1607,12 @@ async function setupRegression() {
|
|
|
1315
1607
|
fail(`interactive bash did not keep codex shim first: ${interactiveCodex}`);
|
|
1316
1608
|
}
|
|
1317
1609
|
if (!hcc(['install-hooks', '--status']).includes('claude=yes codex=yes')) fail('hooks not installed');
|
|
1318
|
-
|
|
1610
|
+
const shimStatusOutput = hcc(['shim', 'status']);
|
|
1611
|
+
if (!shimStatusOutput.includes('claude: installed') ||
|
|
1612
|
+
!shimStatusOutput.includes('codex: installed') ||
|
|
1613
|
+
!shimStatusOutput.includes('status: complete')) {
|
|
1614
|
+
fail(`shims not fully installed:\n${shimStatusOutput}`);
|
|
1615
|
+
}
|
|
1319
1616
|
const staleShim = path.join(home, '.hcc-shims', 'claude');
|
|
1320
1617
|
fs.writeFileSync(staleShim, '#!/usr/bin/env bash\necho stale --web-managed\n', { mode: 0o755 });
|
|
1321
1618
|
const ensured = hccMaybe(['shim', 'ensure', 'claude', staleShim]);
|
|
@@ -1326,7 +1623,27 @@ async function setupRegression() {
|
|
|
1326
1623
|
if (!refreshedShim.includes('shim ensure "claude"') || refreshedShim.includes('stale --web-managed')) {
|
|
1327
1624
|
fail(`shim ensure did not refresh stale shim:\n${refreshedShim}`);
|
|
1328
1625
|
}
|
|
1626
|
+
const deadRealBin = path.join(outDir, 'removed-claude');
|
|
1627
|
+
fs.rmSync(deadRealBin, { force: true });
|
|
1628
|
+
fs.writeFileSync(staleShim, [
|
|
1629
|
+
'#!/usr/bin/env bash',
|
|
1630
|
+
'# hello-cc shim for claude (auto-generated; do not edit manually)',
|
|
1631
|
+
`# Real binary: ${deadRealBin}`,
|
|
1632
|
+
`REAL_BIN=${sh(deadRealBin)}`,
|
|
1633
|
+
'exec "$REAL_BIN" "$@"',
|
|
1634
|
+
''
|
|
1635
|
+
].join('\n'), { mode: 0o755 });
|
|
1636
|
+
const rediscovered = hccMaybe(['shim', 'ensure', 'claude', staleShim, deadRealBin]);
|
|
1637
|
+
if (rediscovered.status !== 75) {
|
|
1638
|
+
fail(`shim ensure did not request re-exec for stale real binary:\n${rediscovered.stdout}\n${rediscovered.stderr}`);
|
|
1639
|
+
}
|
|
1640
|
+
const rediscoveredShim = fs.readFileSync(staleShim, 'utf8');
|
|
1641
|
+
const expectedRealBin = path.join(fakeBin, 'claude');
|
|
1642
|
+
if (!rediscoveredShim.includes(`# Real binary: ${expectedRealBin}`) || rediscoveredShim.includes(deadRealBin)) {
|
|
1643
|
+
fail(`shim ensure preserved stale real binary instead of rediscovering PATH binary:\n${rediscoveredShim}`);
|
|
1644
|
+
}
|
|
1329
1645
|
await stopRuntime();
|
|
1646
|
+
await assertWebWrapperParentSurvives();
|
|
1330
1647
|
assertPeerBindingUniqueConstraints();
|
|
1331
1648
|
assertLegacySchemaMigration();
|
|
1332
1649
|
assertRegisteredProjectDbMigration();
|
|
@@ -1349,6 +1666,18 @@ async function setupRegression() {
|
|
|
1349
1666
|
if (unauthorizedResponse.status !== 401) fail(`default web API allowed missing token: ${unauthorizedResponse.status}`);
|
|
1350
1667
|
const tokenResponse = await runtimeFetch('/api/runtime');
|
|
1351
1668
|
if (!tokenResponse.ok) fail(`default web API rejected runtime token: ${tokenResponse.status}`);
|
|
1669
|
+
const badJsonResponse = await runtimeFetch('/api/projects', {
|
|
1670
|
+
method: 'POST',
|
|
1671
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1672
|
+
body: '{bad json'
|
|
1673
|
+
});
|
|
1674
|
+
if (badJsonResponse.status !== 400) {
|
|
1675
|
+
fail(`bad JSON request returned ${badJsonResponse.status}, expected 400:\n${await badJsonResponse.text()}`);
|
|
1676
|
+
}
|
|
1677
|
+
const badJsonBody = await badJsonResponse.json();
|
|
1678
|
+
if (badJsonBody?.error?.code !== 'BAD_REQUEST') {
|
|
1679
|
+
fail(`bad JSON request did not return BAD_REQUEST:\n${JSON.stringify(badJsonBody, null, 2)}`);
|
|
1680
|
+
}
|
|
1352
1681
|
const tokenFile = path.join(home, '.hello-cc', 'web-token');
|
|
1353
1682
|
ensureFile(tokenFile, tokenRuntime.token);
|
|
1354
1683
|
fs.rmSync(tokenFile, { force: true });
|
|
@@ -1417,16 +1746,18 @@ async function setupRegression() {
|
|
|
1417
1746
|
const childDir = path.join(root, 'packages', 'child');
|
|
1418
1747
|
fs.mkdirSync(childDir, { recursive: true });
|
|
1419
1748
|
const childFindRoot = hccFrom(['find-root'], childDir).trim();
|
|
1420
|
-
if (childFindRoot
|
|
1749
|
+
if (!samePath(childFindRoot, childDir)) fail(`child find-root mismatch: ${childFindRoot} !== ${childDir}`);
|
|
1421
1750
|
const childStatus = hccFrom(['status'], childDir);
|
|
1422
|
-
if (!
|
|
1751
|
+
if (!samePath(statusValue(childStatus, 'root'), childDir) ||
|
|
1752
|
+
!samePath(statusValue(childStatus, 'db'), path.join(childDir, '.hello-cc', 'mesh.db'))) {
|
|
1423
1753
|
fail(`child command did not stay on current path:\n${childStatus}`);
|
|
1424
1754
|
}
|
|
1425
1755
|
ensureFile(path.join(childDir, '.hello-cc', 'mesh.db'));
|
|
1426
1756
|
const explicitChildRoot = hccFrom(['--root', root, 'find-root'], childDir).trim();
|
|
1427
|
-
if (explicitChildRoot
|
|
1757
|
+
if (!samePath(explicitChildRoot, root)) fail(`explicit child find-root mismatch: ${explicitChildRoot} !== ${root}`);
|
|
1428
1758
|
const explicitChildStatus = hccFrom(['--root', root, 'status'], childDir);
|
|
1429
|
-
if (!
|
|
1759
|
+
if (!samePath(statusValue(explicitChildStatus, 'root'), root) ||
|
|
1760
|
+
!samePath(statusValue(explicitChildStatus, 'db'), path.join(root, '.hello-cc', 'mesh.db'))) {
|
|
1430
1761
|
fail(`explicit child command did not use requested root:\n${explicitChildStatus}`);
|
|
1431
1762
|
}
|
|
1432
1763
|
const childHookPayload = JSON.stringify({ session_id: 'child-hook-session', cwd: childDir, hook_event_name: 'UserPromptSubmit', prompt: 'status?' });
|
|
@@ -1565,6 +1896,38 @@ async function dbWorkflow() {
|
|
|
1565
1896
|
hcc(['task', 'done', '--peer', 'batch-b', '--id', String(task.id), '--summary', 'batch next cleanup']);
|
|
1566
1897
|
}
|
|
1567
1898
|
hcc(['task', 'update', '--peer', 'human', '--id', batchNextIds[2], '--status', 'abandoned', '--summary', 'batch next leftover cleanup']);
|
|
1899
|
+
|
|
1900
|
+
const dispatched = hccJson(['task', 'dispatch', '--from', 'human', '--to', 'dispatch-a', '--title', 'dispatch new task', '--body', 'dispatch body', '--no-inject']);
|
|
1901
|
+
if (!dispatched.task || dispatched.task.assignee !== 'dispatch-a' || dispatched.task.owner ||
|
|
1902
|
+
dispatched.injected !== false || dispatched.injection_reason !== 'no_inject' || !dispatched.message_id) {
|
|
1903
|
+
fail(`task dispatch did not create message-only assigned task:\n${JSON.stringify(dispatched, null, 2)}`);
|
|
1904
|
+
}
|
|
1905
|
+
const dispatchInbox = hcc(['msg', 'inbox', '--peer', 'dispatch-a', '--all']);
|
|
1906
|
+
if (!dispatchInbox.includes(`task #${dispatched.task.id}`) || !dispatchInbox.includes('dispatch new task')) {
|
|
1907
|
+
fail(`task dispatch durable message missing:\n${dispatchInbox}`);
|
|
1908
|
+
}
|
|
1909
|
+
const dispatchAudit = eventPayloads('task.dispatched', 20)
|
|
1910
|
+
.find((event) => Number(event.payload?.message_id) === Number(dispatched.message_id));
|
|
1911
|
+
if (!dispatchAudit ||
|
|
1912
|
+
dispatchAudit.actor !== 'human' ||
|
|
1913
|
+
dispatchAudit.payload.actor_peer !== 'human' ||
|
|
1914
|
+
dispatchAudit.payload.target_peer !== 'dispatch-a' ||
|
|
1915
|
+
dispatchAudit.payload.source !== 'cli' ||
|
|
1916
|
+
dispatchAudit.payload.injected !== false ||
|
|
1917
|
+
dispatchAudit.payload.injection_reason !== 'no_inject') {
|
|
1918
|
+
fail(`task dispatch audit payload wrong:\n${JSON.stringify(dispatchAudit, null, 2)}`);
|
|
1919
|
+
}
|
|
1920
|
+
const dispatchExistingCreated = hcc(['task', 'create', '--from', 'human', '--title', 'dispatch existing task']);
|
|
1921
|
+
const dispatchExistingMatch = dispatchExistingCreated.match(/created task #(\d+):/);
|
|
1922
|
+
if (!dispatchExistingMatch) fail(`cannot parse dispatch existing task id: ${dispatchExistingCreated}`);
|
|
1923
|
+
const dispatchExisting = hccJson(['task', 'dispatch', '--from', 'human', '--to', 'dispatch-b', '--id', dispatchExistingMatch[1], '--message', 'custom dispatch existing', '--no-inject']);
|
|
1924
|
+
if (String(dispatchExisting.task?.id) !== dispatchExistingMatch[1] ||
|
|
1925
|
+
dispatchExisting.task.assignee !== 'dispatch-b' ||
|
|
1926
|
+
dispatchExisting.message !== 'custom dispatch existing' ||
|
|
1927
|
+
dispatchExisting.injected !== false) {
|
|
1928
|
+
fail(`task dispatch --id did not assign existing task:\n${JSON.stringify(dispatchExisting, null, 2)}`);
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1568
1931
|
const takeoverOpen = hcc(['task', 'create', '--from', 'human', '--to', 'takeover-owner', '--title', 'takeover open']);
|
|
1569
1932
|
const takeoverOpenMatch = takeoverOpen.match(/created task #(\d+):/);
|
|
1570
1933
|
if (!takeoverOpenMatch) fail(`cannot parse takeover open id: ${takeoverOpen}`);
|
|
@@ -1846,8 +2209,9 @@ async function multiProjectWebWorkflow() {
|
|
|
1846
2209
|
fs.mkdirSync(otherRoot, { recursive: true });
|
|
1847
2210
|
const output = hccFrom(['web', '--local', '--port', String(port), '--no-discover', '--no-guidance'], otherRoot);
|
|
1848
2211
|
if (!output.includes('web already running in background')) fail(`second project did not reuse global web:\n${output}`);
|
|
1849
|
-
|
|
1850
|
-
if (!
|
|
2212
|
+
const outputRoot = statusValue(output, 'project');
|
|
2213
|
+
if (!samePath(outputRoot, otherRoot)) fail(`second project output did not show its root:\n${output}`);
|
|
2214
|
+
if (!output.includes(encodeURIComponent(outputRoot))) fail(`second project URL did not include project query:\n${output}`);
|
|
1851
2215
|
|
|
1852
2216
|
const otherRuntimeFile = path.join(otherRoot, '.hello-cc', 'runtime.json');
|
|
1853
2217
|
ensureFile(otherRuntimeFile);
|
|
@@ -1859,8 +2223,9 @@ async function multiProjectWebWorkflow() {
|
|
|
1859
2223
|
const projectsResponse = await runtimeFetch('/api/projects', {}, { root: otherRoot });
|
|
1860
2224
|
const projects = await projectsResponse.json();
|
|
1861
2225
|
if (!projectsResponse.ok) fail(`projects API failed: ${JSON.stringify(projects)}`);
|
|
1862
|
-
const roots =
|
|
1863
|
-
if (!roots.
|
|
2226
|
+
const roots = (projects.projects || []).map((p) => p.root);
|
|
2227
|
+
if (!roots.some((projectRoot) => samePath(projectRoot, root)) ||
|
|
2228
|
+
!roots.some((projectRoot) => samePath(projectRoot, otherRoot))) {
|
|
1864
2229
|
fail(`projects API did not include both roots:\n${JSON.stringify(projects, null, 2)}`);
|
|
1865
2230
|
}
|
|
1866
2231
|
|
|
@@ -2122,6 +2487,10 @@ async function multiProjectWebWorkflow() {
|
|
|
2122
2487
|
if (!managedActionSession.action_token) {
|
|
2123
2488
|
fail(`managed web session did not include action token:\n${JSON.stringify(managedActionSession, null, 2)}`);
|
|
2124
2489
|
}
|
|
2490
|
+
const runtimeActionToken = currentRuntime().token || '';
|
|
2491
|
+
if (managedActionSession.action_token === runtimeActionToken || managedActionSession.action_token.length < 32) {
|
|
2492
|
+
fail(`managed web session action token is not an independent session token:\n${JSON.stringify({ runtimeActionToken, managedActionSession }, null, 2)}`);
|
|
2493
|
+
}
|
|
2125
2494
|
const actionNext = await (await runtimeFetch(`/api/peers/${encodeURIComponent(managedActionSession.peer_id || managedActionSession.id)}/actions/task-next`, {
|
|
2126
2495
|
method: 'POST',
|
|
2127
2496
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -2305,11 +2674,18 @@ async function tmuxBackedStartWorkflow() {
|
|
|
2305
2674
|
await expectWebSocketInputVisible('shell-a', 'WS_INPUT_VISIBLE_OK');
|
|
2306
2675
|
await expectBoundedTmuxStream('tmux-backed FIFO stream');
|
|
2307
2676
|
|
|
2308
|
-
await
|
|
2677
|
+
const heldWs = await openTerminalWebSocket('shell-a');
|
|
2678
|
+
try {
|
|
2679
|
+
await stopRuntime();
|
|
2680
|
+
await waitFor(() => heldWs.readyState === WebSocket.CLOSED || heldWs.readyState === WebSocket.CLOSING, 'runtime websocket close', 2000);
|
|
2681
|
+
} finally {
|
|
2682
|
+
try { heldWs.close(); } catch {}
|
|
2683
|
+
}
|
|
2309
2684
|
run('tmux', ['display-message', '-p', '-t', pane, '#{pane_id}']);
|
|
2310
2685
|
startRuntime();
|
|
2311
2686
|
await waitRuntime();
|
|
2312
|
-
await waitFor(() => hcc(['peer', 'list']).includes('shell-a'), 'tmux-backed peer restore');
|
|
2687
|
+
await waitFor(() => hcc(['peer', 'list']).includes('shell-a'), 'tmux-backed peer restore after websocket shutdown');
|
|
2688
|
+
|
|
2313
2689
|
const restoredFile = path.join(outDir, 'pty-restored-ok');
|
|
2314
2690
|
hcc(['inject', 'shell-a', `echo PTY_RESTORED_OK > ${restoredFile}`]);
|
|
2315
2691
|
await waitForFile(restoredFile, 'PTY_RESTORED_OK', 'tmux restore injection');
|
|
@@ -2401,6 +2777,10 @@ async function shimTmuxWorkflow() {
|
|
|
2401
2777
|
}
|
|
2402
2778
|
|
|
2403
2779
|
log('[6/13] shim tmux-backed launch');
|
|
2780
|
+
if (!fs.existsSync(path.join(root, '.hello-cc', 'runtime.json'))) {
|
|
2781
|
+
startRuntime();
|
|
2782
|
+
}
|
|
2783
|
+
await waitRuntime();
|
|
2404
2784
|
const shim = path.join(home, '.hcc-shims', 'claude');
|
|
2405
2785
|
const codexShim = path.join(home, '.hcc-shims', 'codex');
|
|
2406
2786
|
const claudeVersion = run(shim, ['--version'], { cwd: root, env });
|
|
@@ -2471,6 +2851,9 @@ async function shimTmuxWorkflow() {
|
|
|
2471
2851
|
const firstEnvFile = path.join(outDir, 'shim-env-first');
|
|
2472
2852
|
hcc(['inject', peer, `printf '%s\\n' "$HCC_REG_VALUE" > ${firstEnvFile}`]);
|
|
2473
2853
|
await waitForFile(firstEnvFile, 'shim-first', 'shim first env');
|
|
2854
|
+
const shimInternalEnvFile = path.join(outDir, 'shim-internal-env');
|
|
2855
|
+
hcc(['inject', peer, `printf '%s|%s|%s\\n' "\${HCC_SHIM_ENSURED-unset}" "\${HCC_SKIP_SHIM_INSTALL-unset}" "\${HCC_RUNTIME_LOCAL_ONLY-unset}" > ${shimInternalEnvFile}`]);
|
|
2856
|
+
await waitForFile(shimInternalEnvFile, 'unset|unset|unset', 'shim internal env cleanup');
|
|
2474
2857
|
|
|
2475
2858
|
const restarted = run(shim, ['--resume', 'shim-regression-session'], {
|
|
2476
2859
|
cwd: root,
|
|
@@ -2492,12 +2875,34 @@ async function shimTmuxWorkflow() {
|
|
|
2492
2875
|
HCC_FAKE_STAY_ALIVE: '0',
|
|
2493
2876
|
HCC_FAKE_LOG: exitedLog
|
|
2494
2877
|
};
|
|
2495
|
-
const
|
|
2878
|
+
const exitedFirstResult = runMaybe(shim, ['--resume', exitedResume], { cwd: root, env: exitedEnv });
|
|
2879
|
+
if (exitedFirstResult.status !== 0) {
|
|
2880
|
+
const allShimLines = fs.readFileSync(shim, 'utf8').split('\n');
|
|
2881
|
+
const shimLines = allShimLines
|
|
2882
|
+
.slice(0, 35)
|
|
2883
|
+
.concat(['...'])
|
|
2884
|
+
.concat(allShimLines.slice(139))
|
|
2885
|
+
.map((line, index) => {
|
|
2886
|
+
if (index < 35) return `${index + 1}: ${line}`;
|
|
2887
|
+
if (index === 35) return line;
|
|
2888
|
+
return `${index + 104}: ${line}`;
|
|
2889
|
+
})
|
|
2890
|
+
.join('\n');
|
|
2891
|
+
fail(`${sh([shim, '--resume', exitedResume].join(' '))} failed\n${exitedFirstResult.stdout || ''}${exitedFirstResult.stderr || ''}\nshim excerpt:\n${shimLines}`);
|
|
2892
|
+
}
|
|
2893
|
+
const exitedFirst = exitedFirstResult.stdout || '';
|
|
2496
2894
|
const exitedPeerMatch = exitedFirst.match(/started\s+(\S+)\s+\(/);
|
|
2497
2895
|
if (!exitedPeerMatch) fail(`exited shim did not start a peer:\n${exitedFirst}`);
|
|
2498
2896
|
const exitedPeer = exitedPeerMatch[1];
|
|
2499
|
-
parsePane(exitedFirst);
|
|
2500
|
-
|
|
2897
|
+
const exitedPane = parsePane(exitedFirst);
|
|
2898
|
+
try {
|
|
2899
|
+
await waitForFileLineCount(exitedLog, 1, 'shim exited first provider launch');
|
|
2900
|
+
} catch (err) {
|
|
2901
|
+
const paneCapture = runMaybe('tmux', ['capture-pane', '-p', '-S', '-80', '-t', exitedPane]);
|
|
2902
|
+
const peerList = hccMaybe(['peer', 'list']);
|
|
2903
|
+
const shimHead = fs.readFileSync(shim, 'utf8').split('\n').slice(0, 18).join('\n');
|
|
2904
|
+
fail(`${err.message}\nshim output:\n${exitedFirst}\nshim stderr:\n${exitedFirstResult.stderr || ''}\npeer list:\n${peerList.stdout || ''}${peerList.stderr || ''}\npane ${exitedPane}:\n${paneCapture.stdout || ''}${paneCapture.stderr || ''}\nshim head:\n${shimHead}`);
|
|
2905
|
+
}
|
|
2501
2906
|
const fallbackFile = path.join(outDir, 'shim-exited-fallback');
|
|
2502
2907
|
hcc(['inject', exitedPeer, `printf '%s\\n' fallback > ${sh(fallbackFile)}`]);
|
|
2503
2908
|
await waitForFile(fallbackFile, 'fallback', 'shim exited fallback shell');
|
|
@@ -2546,6 +2951,16 @@ async function askBroadcastWorkflow() {
|
|
|
2546
2951
|
hcc(['ask', 'shell-b', `echo ASK_OK > ${askFile}`, '--from', 'human', '--inject']);
|
|
2547
2952
|
await waitForFile(askFile, 'ASK_OK', 'ask injection');
|
|
2548
2953
|
if (!hcc(['msg', 'inbox', '--peer', 'shell-b', '--all']).includes('ASK_OK')) fail('ask durable message missing');
|
|
2954
|
+
const shellDefaultDispatch = hccJson(['task', 'dispatch', '--from', 'human', '--to', 'shell-b', '--title', 'shell default dispatch']);
|
|
2955
|
+
if (shellDefaultDispatch.injected !== false || shellDefaultDispatch.injection_reason !== 'unsupported_session_kind') {
|
|
2956
|
+
fail(`task dispatch injected default natural-language prompt into shell session:\n${JSON.stringify(shellDefaultDispatch, null, 2)}`);
|
|
2957
|
+
}
|
|
2958
|
+
const dispatchFile = path.join(outDir, 'dispatch-ok');
|
|
2959
|
+
const shellCommandDispatch = hccJson(['task', 'dispatch', '--from', 'human', '--to', 'shell-b', '--title', 'shell command dispatch', '--message', `echo DISPATCH_OK > ${dispatchFile}`, '--force']);
|
|
2960
|
+
if (!shellCommandDispatch.injected || shellCommandDispatch.delivery !== 'message+inject') {
|
|
2961
|
+
fail(`task dispatch did not inject explicit shell-safe message:\n${JSON.stringify(shellCommandDispatch, null, 2)}`);
|
|
2962
|
+
}
|
|
2963
|
+
await waitForFile(dispatchFile, 'DISPATCH_OK', 'task dispatch injection');
|
|
2549
2964
|
const broadcastFile = path.join(outDir, 'broadcast-ok');
|
|
2550
2965
|
hcc(['broadcast', `echo BROADCAST_OK > ${broadcastFile}`, '--from', 'human', '--inject']);
|
|
2551
2966
|
await waitForFile(broadcastFile, 'BROADCAST_OK', 'broadcast injection');
|
|
@@ -2596,8 +3011,8 @@ async function downGcPackWorkflow() {
|
|
|
2596
3011
|
"tags:\n - 'v*'",
|
|
2597
3012
|
'workflow_dispatch:',
|
|
2598
3013
|
'contents: write',
|
|
2599
|
-
'actions/checkout@
|
|
2600
|
-
'actions/setup-node@
|
|
3014
|
+
'actions/checkout@v5',
|
|
3015
|
+
'actions/setup-node@v5',
|
|
2601
3016
|
'node-version: 24',
|
|
2602
3017
|
'npm run release:check',
|
|
2603
3018
|
'npm run release:github:dry-run',
|
|
@@ -2620,16 +3035,23 @@ function oldNameScan() {
|
|
|
2620
3035
|
'ACTIVE_' + 'AGENT_' + 'TTL',
|
|
2621
3036
|
'--' + 'agent\\b'
|
|
2622
3037
|
].join('|');
|
|
2623
|
-
const
|
|
2624
|
-
|
|
2625
|
-
'
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
3038
|
+
const oldNameRegex = new RegExp(oldNamePattern);
|
|
3039
|
+
const files = run('git', ['ls-files'])
|
|
3040
|
+
.split('\n')
|
|
3041
|
+
.map((file) => file.trim())
|
|
3042
|
+
.filter(Boolean)
|
|
3043
|
+
.filter((file) => !file.includes('/node_modules/') && !file.endsWith('.tgz'));
|
|
3044
|
+
const matches = [];
|
|
3045
|
+
for (const file of files) {
|
|
3046
|
+
const fullPath = path.join(repoRoot, file);
|
|
3047
|
+
let content = '';
|
|
3048
|
+
try { content = fs.readFileSync(fullPath, 'utf8'); } catch { continue; }
|
|
3049
|
+
const lines = content.split('\n');
|
|
3050
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
3051
|
+
if (oldNameRegex.test(lines[i])) matches.push(`${file}:${i + 1}: ${lines[i]}`);
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
if (matches.length) fail(`old names found:\n${matches.join('\n')}`);
|
|
2633
3055
|
}
|
|
2634
3056
|
|
|
2635
3057
|
function identityEnforcementWorkflow() {
|
|
@@ -3132,6 +3554,7 @@ async function syntaxAndHelp() {
|
|
|
3132
3554
|
'function installShims',
|
|
3133
3555
|
'function uninstallShims',
|
|
3134
3556
|
'function verifyShims',
|
|
3557
|
+
'function shimStatus',
|
|
3135
3558
|
'function findRealBinary',
|
|
3136
3559
|
'function fsExists'
|
|
3137
3560
|
]) {
|
|
@@ -3159,9 +3582,14 @@ async function syntaxAndHelp() {
|
|
|
3159
3582
|
}
|
|
3160
3583
|
if (!integrationShimScriptSource.includes('function generateShim') ||
|
|
3161
3584
|
!integrationShimScriptSource.includes('hello-cc shim for ${tool.name}') ||
|
|
3162
|
-
!integrationShimScriptSource.includes('HCC_SHIM_ENSURED')
|
|
3585
|
+
!integrationShimScriptSource.includes('HCC_SHIM_ENSURED') ||
|
|
3586
|
+
!integrationShimScriptSource.includes('HCC_RUNTIME_LOCAL_ONLY') ||
|
|
3587
|
+
!integrationShimScriptSource.includes('hcc_runtime_unavailable_start_failure')) {
|
|
3163
3588
|
fail('integrations shim script module is missing expected generateShim wiring');
|
|
3164
3589
|
}
|
|
3590
|
+
if (integrationShimScriptSource.includes('HCC_SKIP_SHIM_INSTALL=1 "$HCC_BIN" web')) {
|
|
3591
|
+
fail('integration shim script still auto-starts hcc web instead of falling back to the real provider');
|
|
3592
|
+
}
|
|
3165
3593
|
if (!shellPathSource.includes('function installPathEntry') ||
|
|
3166
3594
|
!shellPathSource.includes('function uninstallPathEntry') ||
|
|
3167
3595
|
!shellPathSource.includes('.hcc-shims')) {
|
|
@@ -3184,7 +3612,7 @@ async function syntaxAndHelp() {
|
|
|
3184
3612
|
if (typeof integrationHooks[name] !== 'function') fail(`integrations hook module missing export: ${name}`);
|
|
3185
3613
|
if (setupModule[name] !== integrationHooks[name]) fail(`setup hook export mismatch: ${name}`);
|
|
3186
3614
|
}
|
|
3187
|
-
for (const name of ['installShims', 'uninstallShims', 'verifyShims', 'findRealBinary']) {
|
|
3615
|
+
for (const name of ['installShims', 'uninstallShims', 'verifyShims', 'shimStatus', 'findRealBinary']) {
|
|
3188
3616
|
if (typeof integrationShims[name] !== 'function') fail(`integrations shim module missing export: ${name}`);
|
|
3189
3617
|
if (setupModule[name] !== integrationShims[name]) fail(`setup shim export mismatch: ${name}`);
|
|
3190
3618
|
}
|
|
@@ -3194,10 +3622,153 @@ async function syntaxAndHelp() {
|
|
|
3194
3622
|
if (setupModule.SHIM_DIR !== integrationShims.SHIM_DIR) fail('setup shim dir export mismatch');
|
|
3195
3623
|
if (typeof integrationShimScript.generateShim !== 'function') fail('integrations shim script module missing generateShim export');
|
|
3196
3624
|
if (shimScriptCompat.generateShim !== integrationShimScript.generateShim) fail('shim script compatibility export mismatch');
|
|
3625
|
+
assertShimRuntimeUnavailableFallback(integrationShimScript.generateShim);
|
|
3626
|
+
await assertShimIgnoresGlobalRuntime(integrationShimScript.generateShim);
|
|
3197
3627
|
for (const name of ['installPathEntry', 'uninstallPathEntry']) {
|
|
3198
3628
|
if (typeof shellPath[name] !== 'function') fail(`shell path module missing export: ${name}`);
|
|
3199
3629
|
if (setupModule[name] !== shellPath[name]) fail(`setup shell path export mismatch: ${name}`);
|
|
3200
3630
|
}
|
|
3631
|
+
const bashRc = '# regression rc\n[ -z "$PS1" ] && return\nexport PATH="/late:$PATH"\n';
|
|
3632
|
+
const bashPath = shellPath.insertPathEntry(bashRc, 'bash');
|
|
3633
|
+
if ((bashPath.match(/# hello-cc shims/g) || []).length !== 2 ||
|
|
3634
|
+
bashPath.indexOf('# hello-cc shims (early)') > bashPath.indexOf('[ -z "$PS1" ] && return') ||
|
|
3635
|
+
bashPath.lastIndexOf('# hello-cc shims (final)') < bashPath.indexOf('export PATH="/late:$PATH"')) {
|
|
3636
|
+
fail(`bash shell PATH insertion lost early/final placement:\n${bashPath}`);
|
|
3637
|
+
}
|
|
3638
|
+
const zshPath = shellPath.insertPathEntry('export PATH="$HOME/bin:$PATH"\n', 'zsh');
|
|
3639
|
+
if ((zshPath.match(/# hello-cc shims/g) || []).length !== 1 ||
|
|
3640
|
+
zshPath.includes('# hello-cc shims (early)') ||
|
|
3641
|
+
!zshPath.includes('# hello-cc shims (final)')) {
|
|
3642
|
+
fail(`zsh shell PATH insertion should install one final entry:\n${zshPath}`);
|
|
3643
|
+
}
|
|
3644
|
+
const fishPath = shellPath.insertPathEntry('set -gx PATH $HOME/bin $PATH\n', 'fish');
|
|
3645
|
+
if ((fishPath.match(/# hello-cc shims/g) || []).length !== 1 ||
|
|
3646
|
+
fishPath.includes('# hello-cc shims (early)') ||
|
|
3647
|
+
!fishPath.includes('# hello-cc shims (final)')) {
|
|
3648
|
+
fail(`fish shell PATH insertion should install one final entry:\n${fishPath}`);
|
|
3649
|
+
}
|
|
3650
|
+
const customRc = [
|
|
3651
|
+
'export PATH="$HOME/.hcc-shims:$PATH" # user custom shim dir',
|
|
3652
|
+
shellPath.pathEntryLine('bash', 'final')
|
|
3653
|
+
].join('\n');
|
|
3654
|
+
const cleanedRc = shellPath.removePathEntryLines(customRc);
|
|
3655
|
+
if (!cleanedRc.includes('user custom shim dir') || cleanedRc.includes('# hello-cc shims')) {
|
|
3656
|
+
fail(`shell PATH cleanup removed a non-hello-cc custom line or kept hello-cc line:\n${cleanedRc}`);
|
|
3657
|
+
}
|
|
3658
|
+
const noProviderHome = fs.mkdtempSync(path.join(os.tmpdir(), 'hcc-no-provider-home-'));
|
|
3659
|
+
try {
|
|
3660
|
+
const noProviderEnv = {
|
|
3661
|
+
...env,
|
|
3662
|
+
HOME: noProviderHome,
|
|
3663
|
+
PATH: '/usr/bin:/bin',
|
|
3664
|
+
SHELL: '/bin/bash'
|
|
3665
|
+
};
|
|
3666
|
+
const noProviderInstall = run(process.execPath, [hccBin, '--root', root, 'shim', 'install'], { env: noProviderEnv });
|
|
3667
|
+
if (!noProviderInstall.includes('no shims installed')) {
|
|
3668
|
+
fail(`shim install without providers did not report no shims:\n${noProviderInstall}`);
|
|
3669
|
+
}
|
|
3670
|
+
const noProviderBashrc = path.join(noProviderHome, '.bashrc');
|
|
3671
|
+
if (fs.existsSync(noProviderBashrc) && fs.readFileSync(noProviderBashrc, 'utf8').includes('.hcc-shims')) {
|
|
3672
|
+
fail(`shim install without providers modified shell PATH:\n${fs.readFileSync(noProviderBashrc, 'utf8')}`);
|
|
3673
|
+
}
|
|
3674
|
+
} finally {
|
|
3675
|
+
try { fs.rmSync(noProviderHome, { recursive: true, force: true }); } catch {}
|
|
3676
|
+
}
|
|
3677
|
+
const fishHome = fs.mkdtempSync(path.join(os.tmpdir(), 'hcc-fish-home-'));
|
|
3678
|
+
try {
|
|
3679
|
+
const fishEnv = {
|
|
3680
|
+
...env,
|
|
3681
|
+
HOME: fishHome,
|
|
3682
|
+
SHELL: '/usr/bin/fish'
|
|
3683
|
+
};
|
|
3684
|
+
const fishInstall = run(process.execPath, [hccBin, '--root', root, 'shim', 'install'], { env: fishEnv });
|
|
3685
|
+
const fishRc = path.join(fishHome, '.config', 'fish', 'config.fish');
|
|
3686
|
+
if (!fishInstall.includes('PATH updated') ||
|
|
3687
|
+
!fs.existsSync(fishRc) ||
|
|
3688
|
+
!fs.readFileSync(fishRc, 'utf8').includes('# hello-cc shims (final)')) {
|
|
3689
|
+
fail(`fish shim install did not create config.fish PATH entry:\n${fishInstall}\n${fs.existsSync(fishRc) ? fs.readFileSync(fishRc, 'utf8') : '(missing)'}`);
|
|
3690
|
+
}
|
|
3691
|
+
} finally {
|
|
3692
|
+
try { fs.rmSync(fishHome, { recursive: true, force: true }); } catch {}
|
|
3693
|
+
}
|
|
3694
|
+
const missingRcHome = fs.mkdtempSync(path.join(os.tmpdir(), 'hcc-missing-rc-home-'));
|
|
3695
|
+
try {
|
|
3696
|
+
const missingRcEnv = {
|
|
3697
|
+
...env,
|
|
3698
|
+
HOME: missingRcHome,
|
|
3699
|
+
SHELL: '/bin/bash'
|
|
3700
|
+
};
|
|
3701
|
+
const uninstallOutput = run(process.execPath, [hccBin, '--root', root, 'shim', 'uninstall'], { env: missingRcEnv });
|
|
3702
|
+
if (uninstallOutput.includes('PATH entry not removed') || !uninstallOutput.includes('PATH entry not present')) {
|
|
3703
|
+
fail(`shim uninstall reported missing shell rc as a failure:\n${uninstallOutput}`);
|
|
3704
|
+
}
|
|
3705
|
+
} finally {
|
|
3706
|
+
try { fs.rmSync(missingRcHome, { recursive: true, force: true }); } catch {}
|
|
3707
|
+
}
|
|
3708
|
+
const partialStatusHome = fs.mkdtempSync(path.join(os.tmpdir(), 'hcc-partial-status-home-'));
|
|
3709
|
+
try {
|
|
3710
|
+
const partialStatusDir = path.join(partialStatusHome, '.hcc-shims');
|
|
3711
|
+
fs.mkdirSync(partialStatusDir, { recursive: true });
|
|
3712
|
+
fs.writeFileSync(path.join(partialStatusDir, 'claude'), '#!/usr/bin/env bash\nexit 0\n', { mode: 0o755 });
|
|
3713
|
+
const partialStatusEnv = {
|
|
3714
|
+
...env,
|
|
3715
|
+
HOME: partialStatusHome,
|
|
3716
|
+
SHELL: '/bin/bash'
|
|
3717
|
+
};
|
|
3718
|
+
const statusOutput = run(process.execPath, [hccBin, '--root', root, 'shim', 'status'], { env: partialStatusEnv });
|
|
3719
|
+
if (!statusOutput.includes('claude: installed') ||
|
|
3720
|
+
!statusOutput.includes('codex: missing') ||
|
|
3721
|
+
!statusOutput.includes('status: partial')) {
|
|
3722
|
+
fail(`shim status did not report partial install per tool:\n${statusOutput}`);
|
|
3723
|
+
}
|
|
3724
|
+
} finally {
|
|
3725
|
+
try { fs.rmSync(partialStatusHome, { recursive: true, force: true }); } catch {}
|
|
3726
|
+
}
|
|
3727
|
+
const partialShimHome = fs.mkdtempSync(path.join(os.tmpdir(), 'hcc-partial-shim-home-'));
|
|
3728
|
+
const partialShimRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'hcc-partial-shim-root-'));
|
|
3729
|
+
try {
|
|
3730
|
+
const partialShimDir = path.join(partialShimHome, '.hcc-shims');
|
|
3731
|
+
fs.mkdirSync(partialShimDir, { recursive: true });
|
|
3732
|
+
fs.writeFileSync(path.join(partialShimDir, 'claude'), '#!/usr/bin/env bash\nexit 0\n', { mode: 0o755 });
|
|
3733
|
+
const partialShimEnv = {
|
|
3734
|
+
...env,
|
|
3735
|
+
HOME: partialShimHome,
|
|
3736
|
+
SHELL: '/bin/bash'
|
|
3737
|
+
};
|
|
3738
|
+
run(process.execPath, [hccBin, '--root', partialShimRoot, 'setup', '--quiet'], { env: partialShimEnv });
|
|
3739
|
+
for (const name of ['claude', 'codex']) {
|
|
3740
|
+
if (!fs.existsSync(path.join(partialShimDir, name))) {
|
|
3741
|
+
fail(`setup did not repair partial shim install; missing ${name}`);
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
} finally {
|
|
3745
|
+
try { fs.rmSync(partialShimHome, { recursive: true, force: true }); } catch {}
|
|
3746
|
+
try { fs.rmSync(partialShimRoot, { recursive: true, force: true }); } catch {}
|
|
3747
|
+
}
|
|
3748
|
+
const scanHome = fs.mkdtempSync(path.join(os.tmpdir(), 'hcc-scan-home-'));
|
|
3749
|
+
const scanRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'hcc-scan-root-'));
|
|
3750
|
+
try {
|
|
3751
|
+
const scanRealRoot = fs.realpathSync(scanRoot);
|
|
3752
|
+
const claudeSessionsDir = path.join(scanHome, '.claude', 'sessions');
|
|
3753
|
+
fs.mkdirSync(claudeSessionsDir, { recursive: true });
|
|
3754
|
+
fs.writeFileSync(path.join(claudeSessionsDir, 'scan-realpath.json'), JSON.stringify({
|
|
3755
|
+
pid: process.pid,
|
|
3756
|
+
sessionId: 'scan-realpath-session',
|
|
3757
|
+
cwd: scanRoot,
|
|
3758
|
+
status: 'running'
|
|
3759
|
+
}));
|
|
3760
|
+
const scanEnv = {
|
|
3761
|
+
...env,
|
|
3762
|
+
HOME: scanHome
|
|
3763
|
+
};
|
|
3764
|
+
const scanOutput = run(process.execPath, [hccBin, '--root', scanRealRoot, 'scan'], { env: scanEnv });
|
|
3765
|
+
if (!scanOutput.includes('claude') || !scanOutput.includes('scan-realpath')) {
|
|
3766
|
+
fail(`scan did not match discovered hccRoot through realpath comparison:\n${scanOutput}`);
|
|
3767
|
+
}
|
|
3768
|
+
} finally {
|
|
3769
|
+
try { fs.rmSync(scanHome, { recursive: true, force: true }); } catch {}
|
|
3770
|
+
try { fs.rmSync(scanRoot, { recursive: true, force: true }); } catch {}
|
|
3771
|
+
}
|
|
3201
3772
|
const cliRuntime = await import(path.join(repoRoot, 'lib', 'cli-runtime.mjs'));
|
|
3202
3773
|
for (const name of ['commandPath', 'createContext', 'packageRoot', 'shellCommand', 'tailFile']) {
|
|
3203
3774
|
if (typeof cliRuntime[name] !== 'function') fail(`CLI runtime module missing function: ${name}`);
|
|
@@ -3588,11 +4159,12 @@ async function syntaxAndHelp() {
|
|
|
3588
4159
|
events: [
|
|
3589
4160
|
{ id: 7, type: 'message.sent', actor: 'peer-a', task_id: 2, payload: '{}', created_at: 7 },
|
|
3590
4161
|
{ id: 8, type: 'task.done', actor: 'peer-a', task_id: 2, payload: JSON.stringify({ owner: 'peer-b', summary: 'done summary' }), created_at: 8 },
|
|
3591
|
-
{ id: 9, type: 'web.session.stop_requested', actor: 'web', task_id: null, payload: JSON.stringify({ actor_peer: 'web', target_peer: 'peer-b', peer: 'peer-b' }), created_at: 9 }
|
|
4162
|
+
{ id: 9, type: 'web.session.stop_requested', actor: 'web', task_id: null, payload: JSON.stringify({ actor_peer: 'web', target_peer: 'peer-b', peer: 'peer-b' }), created_at: 9 },
|
|
4163
|
+
{ id: 10, type: 'task.dispatched', actor: 'peer-a', task_id: 2, payload: JSON.stringify({ actor_peer: 'peer-a', target_peer: 'peer-b', peer: 'peer-b', title: 'dispatch timeline' }), created_at: 10 }
|
|
3592
4164
|
]
|
|
3593
4165
|
}, 'peer-b');
|
|
3594
4166
|
const timelineIds = timelineItems.map((item) => item.id);
|
|
3595
|
-
if (JSON.stringify(timelineIds) !== JSON.stringify(['message:2', 'message:3', 'handoff:4', 'task:5', 'event:8', 'event:9'])) {
|
|
4167
|
+
if (JSON.stringify(timelineIds) !== JSON.stringify(['message:2', 'message:3', 'handoff:4', 'task:5', 'event:8', 'event:9', 'event:10'])) {
|
|
3596
4168
|
fail(`timelineFromRows filtering/order changed: ${JSON.stringify(timelineItems)}`);
|
|
3597
4169
|
}
|
|
3598
4170
|
const inbound = timelineItems.find((item) => item.id === 'message:2');
|
|
@@ -5143,12 +5715,25 @@ async function syntaxAndHelp() {
|
|
|
5143
5715
|
if (!taskHelp.includes('task next [--peer ID] [--force]') ||
|
|
5144
5716
|
!taskHelp.includes('[--count N]') ||
|
|
5145
5717
|
!taskHelp.includes('existing claimed/running/review/blocked task') ||
|
|
5718
|
+
!taskHelp.includes('task dispatch --to ID --title TEXT') ||
|
|
5719
|
+
!taskHelp.includes('task dispatch --to ID --id N') ||
|
|
5720
|
+
!taskHelp.includes('message-only') ||
|
|
5721
|
+
!taskHelp.includes('running managed Claude/Codex terminal') ||
|
|
5146
5722
|
!taskHelp.includes('task takeover [--peer ID] --id N --reason TEXT') ||
|
|
5147
5723
|
!taskHelp.includes('[--policy any|blocked|stale|blocked-or-stale]') ||
|
|
5148
5724
|
!taskHelp.includes('task claim [--peer ID] --id N[,N]') ||
|
|
5149
5725
|
!taskHelp.includes('task create --title TEXT --parent N')) {
|
|
5150
5726
|
fail(`task help missing current-task task next semantics:\n${taskHelp}`);
|
|
5151
5727
|
}
|
|
5728
|
+
for (const expected of [
|
|
5729
|
+
"if (sub === 'dispatch') return taskDispatch",
|
|
5730
|
+
'async function taskDispatch(ctx, args)',
|
|
5731
|
+
"addEvent(eventDb, 'task.dispatched'",
|
|
5732
|
+
'function sessionLooksProviderInteractive(session)',
|
|
5733
|
+
'!customMessage && !sessionLooksProviderInteractive(session)'
|
|
5734
|
+
]) {
|
|
5735
|
+
if (!hccSource.includes(expected)) fail(`task dispatch source guard missing: ${expected}`);
|
|
5736
|
+
}
|
|
5152
5737
|
const teamHelp = run(process.execPath, [hccBin, 'team', '--help']);
|
|
5153
5738
|
if (!teamHelp.includes('hcc team') || !teamHelp.includes('team plan') || !teamHelp.includes('team start') || !teamHelp.includes('team status')) {
|
|
5154
5739
|
fail(`team help missing expected content:\n${teamHelp}`);
|
|
@@ -5177,6 +5762,19 @@ async function syntaxAndHelp() {
|
|
|
5177
5762
|
!tmuxHelp.includes('deletion requires --yes')) {
|
|
5178
5763
|
fail(`tmux help missing DB-backed gc semantics:\n${tmuxHelp}`);
|
|
5179
5764
|
}
|
|
5765
|
+
const shimHelp = run(process.execPath, [hccBin, 'shim', '--help']);
|
|
5766
|
+
if (!shimHelp.includes('hcc shim') ||
|
|
5767
|
+
!shimHelp.includes('shim install') ||
|
|
5768
|
+
!shimHelp.includes('shim status') ||
|
|
5769
|
+
!shimHelp.includes('shell PATH entry')) {
|
|
5770
|
+
fail(`shim help missing maintenance command content:\n${shimHelp}`);
|
|
5771
|
+
}
|
|
5772
|
+
const installHooksHelp = run(process.execPath, [hccBin, 'install-hooks', '--help']);
|
|
5773
|
+
if (!installHooksHelp.includes('hcc install-hooks') ||
|
|
5774
|
+
!installHooksHelp.includes('install-hooks --status') ||
|
|
5775
|
+
!installHooksHelp.includes('install-hooks --uninstall')) {
|
|
5776
|
+
fail(`install-hooks help missing maintenance command content:\n${installHooksHelp}`);
|
|
5777
|
+
}
|
|
5180
5778
|
const gcHelp = run(process.execPath, [hccBin, 'gc', '--help']);
|
|
5181
5779
|
if (!gcHelp.includes('hcc gc') ||
|
|
5182
5780
|
!gcHelp.includes('gc [--older-than DAYS] [--yes]') ||
|
|
@@ -5246,11 +5844,16 @@ function uninstallWorkflow() {
|
|
|
5246
5844
|
ensureFile(path.join(uninstallHome, '.claude', 'settings.json'));
|
|
5247
5845
|
ensureFile(path.join(uninstallHome, '.codex', 'hooks.json'));
|
|
5248
5846
|
ensureFile(path.join(uninstallHome, '.hcc-shims', 'claude'));
|
|
5847
|
+
const uninstallBashrc = path.join(uninstallHome, '.bashrc');
|
|
5848
|
+
if (!fs.readFileSync(uninstallBashrc, 'utf8').includes('.hcc-shims')) {
|
|
5849
|
+
fail('setup did not install shim PATH entry before uninstall workflow');
|
|
5850
|
+
}
|
|
5249
5851
|
|
|
5250
5852
|
const kept = run(process.execPath, [hccBin, '--root', uninstallRoot, 'uninstall'], { env: uninstallEnv });
|
|
5251
5853
|
if (!kept.includes('project data kept')) fail(`uninstall did not keep project data by default:\n${kept}`);
|
|
5252
5854
|
if (!fs.existsSync(path.join(uninstallRoot, '.hello-cc', 'mesh.db'))) fail('default uninstall removed project db');
|
|
5253
5855
|
if (fs.existsSync(path.join(uninstallHome, '.hcc-shims', 'claude'))) fail('default uninstall did not remove shim');
|
|
5856
|
+
if (fs.readFileSync(uninstallBashrc, 'utf8').includes('.hcc-shims')) fail('default uninstall did not remove shim PATH entry');
|
|
5254
5857
|
if (!run(process.execPath, [hccBin, '--root', uninstallRoot, 'install-hooks', '--status'], { env: uninstallEnv }).includes('claude=no codex=no')) {
|
|
5255
5858
|
fail('default uninstall did not remove hooks');
|
|
5256
5859
|
}
|