@logicseek/hello-cc 0.1.4 → 0.1.5
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 +51 -0
- package/bin/hcc.mjs +651 -2505
- package/docs/commands.md +21 -9
- package/docs/commands.zh-CN.md +19 -9
- package/lib/cli-args.mjs +89 -0
- package/lib/db-schema.mjs +324 -0
- package/lib/errors.mjs +7 -0
- package/lib/format.mjs +39 -0
- package/lib/guidance.mjs +10 -2
- package/lib/json-file.mjs +14 -0
- package/lib/runtime-paths.mjs +45 -0
- package/lib/setup.mjs +3 -474
- package/lib/shell-path.mjs +69 -0
- package/lib/shim-script.mjs +384 -0
- package/lib/web-http.mjs +55 -0
- package/lib/web-runtime.mjs +108 -0
- package/lib/web-ui-template.mjs +1909 -0
- package/package.json +1 -1
- package/scripts/regression.mjs +284 -19
package/package.json
CHANGED
package/scripts/regression.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import process from 'node:process';
|
|
|
7
7
|
import { spawnSync } from 'node:child_process';
|
|
8
8
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
9
9
|
import { DatabaseSync } from 'node:sqlite';
|
|
10
|
+
import { Readable } from 'node:stream';
|
|
10
11
|
import WebSocket from 'ws';
|
|
11
12
|
|
|
12
13
|
const repoRoot = path.resolve(import.meta.dirname, '..');
|
|
@@ -458,17 +459,21 @@ function assertLegacySchemaMigration() {
|
|
|
458
459
|
for (const column of ['parent_id', 'team_role']) {
|
|
459
460
|
if (!taskColumns.has(column)) fail(`legacy migration did not add tasks.${column}`);
|
|
460
461
|
}
|
|
462
|
+
const lockColumns = new Set(db.prepare('PRAGMA table_info(locks)').all().map((row) => row.name));
|
|
463
|
+
for (const column of ['base_resource', 'scope']) {
|
|
464
|
+
if (!lockColumns.has(column)) fail(`legacy migration did not add locks.${column}`);
|
|
465
|
+
}
|
|
461
466
|
const message = db.prepare('SELECT id, thread_id FROM messages WHERE body = ?').get('legacy-message');
|
|
462
467
|
if (!message || message.thread_id !== message.id) {
|
|
463
468
|
fail(`legacy message thread_id not backfilled:\n${JSON.stringify(message, null, 2)}`);
|
|
464
469
|
}
|
|
465
470
|
const metaVersion = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get()?.value;
|
|
466
471
|
const pragmaVersion = db.prepare('PRAGMA user_version').get().user_version;
|
|
467
|
-
if (metaVersion !== '
|
|
472
|
+
if (metaVersion !== '5' || pragmaVersion !== 5) {
|
|
468
473
|
fail(`schema version not synchronized: meta=${metaVersion} pragma=${pragmaVersion}`);
|
|
469
474
|
}
|
|
470
475
|
const migrations = db.prepare('SELECT version, name FROM schema_migrations ORDER BY version').all();
|
|
471
|
-
if (migrations.length !==
|
|
476
|
+
if (migrations.length !== 5 || migrations[4].version !== 5 || migrations[4].name !== 'scoped advisory locks') {
|
|
472
477
|
fail(`schema migrations history wrong:\n${JSON.stringify(migrations, null, 2)}`);
|
|
473
478
|
}
|
|
474
479
|
} finally {
|
|
@@ -500,7 +505,7 @@ function assertRegisteredProjectDbMigration() {
|
|
|
500
505
|
fail(`registered project DB was not migrated:\n${JSON.stringify([...taskColumns], null, 2)}`);
|
|
501
506
|
}
|
|
502
507
|
const version = db.prepare('PRAGMA user_version').get().user_version;
|
|
503
|
-
if (version !==
|
|
508
|
+
if (version !== 5) fail(`registered project DB user_version wrong: ${version}`);
|
|
504
509
|
} finally {
|
|
505
510
|
db.close();
|
|
506
511
|
}
|
|
@@ -521,7 +526,7 @@ function assertFutureSchemaMigrationHistoryRejected() {
|
|
|
521
526
|
key TEXT PRIMARY KEY,
|
|
522
527
|
value TEXT NOT NULL
|
|
523
528
|
);
|
|
524
|
-
INSERT INTO meta(key, value) VALUES ('schema_version', '
|
|
529
|
+
INSERT INTO meta(key, value) VALUES ('schema_version', '5');
|
|
525
530
|
CREATE TABLE schema_migrations (
|
|
526
531
|
version INTEGER PRIMARY KEY,
|
|
527
532
|
name TEXT NOT NULL,
|
|
@@ -534,7 +539,7 @@ function assertFutureSchemaMigrationHistoryRejected() {
|
|
|
534
539
|
db.close();
|
|
535
540
|
}
|
|
536
541
|
const result = runMaybe(process.execPath, [hccBin, '--root', futureRoot, 'status', '--peer', 'future-schema-check'], { env });
|
|
537
|
-
if (result.status === 0 || !`${result.stdout}\n${result.stderr}`.includes('Database schema version 999 is newer than this hcc (
|
|
542
|
+
if (result.status === 0 || !`${result.stdout}\n${result.stderr}`.includes('Database schema version 999 is newer than this hcc (5)')) {
|
|
538
543
|
fail(`future schema migration history was not rejected:\nstdout=${result.stdout}\nstderr=${result.stderr}`);
|
|
539
544
|
}
|
|
540
545
|
} finally {
|
|
@@ -676,6 +681,11 @@ function assertGuidanceLockPolicy(file) {
|
|
|
676
681
|
'Do not silently treat a snapshot review as final approval',
|
|
677
682
|
'Before mutating work:',
|
|
678
683
|
'Before editing or mutating shared resources:',
|
|
684
|
+
'module scope',
|
|
685
|
+
'Locks are coordination signals',
|
|
686
|
+
'--scope db-schema',
|
|
687
|
+
'--scope web-ui',
|
|
688
|
+
'narrower scoped locks',
|
|
679
689
|
'Commit-readiness checks are read-only until staging begins',
|
|
680
690
|
'lock `.git/index` only while staging and'
|
|
681
691
|
]) {
|
|
@@ -850,6 +860,13 @@ function tmuxStreamNodes() {
|
|
|
850
860
|
.map((name) => path.join(dir, name));
|
|
851
861
|
}
|
|
852
862
|
|
|
863
|
+
function libModuleFiles() {
|
|
864
|
+
return fs.readdirSync(path.join(repoRoot, 'lib'))
|
|
865
|
+
.filter((name) => name.endsWith('.mjs'))
|
|
866
|
+
.sort()
|
|
867
|
+
.map((name) => path.join('lib', name));
|
|
868
|
+
}
|
|
869
|
+
|
|
853
870
|
async function expectBoundedTmuxStream(label) {
|
|
854
871
|
let nodes = [];
|
|
855
872
|
await waitFor(() => {
|
|
@@ -1158,6 +1175,13 @@ async function dbWorkflow() {
|
|
|
1158
1175
|
if (stateAfterAck.automation.next_action.kind !== 'lock.acquire') {
|
|
1159
1176
|
fail(`state automation did not suggest lock acquire after ack:\n${JSON.stringify(stateAfterAck.automation, null, 2)}`);
|
|
1160
1177
|
}
|
|
1178
|
+
const readOnlyState = hccJson(['state', '--peer', 'codex-a', '--resource', 'src/read-only', '--intent', 'review']);
|
|
1179
|
+
if (readOnlyState.automation.next_action.kind === 'lock.acquire' || readOnlyState.automation.phase === 'acquire_lock') {
|
|
1180
|
+
fail(`read-only state suggested acquiring a lock:\n${JSON.stringify(readOnlyState.automation, null, 2)}`);
|
|
1181
|
+
}
|
|
1182
|
+
if (!(readOnlyState.automation.warnings || []).some((warning) => warning.includes('read-only'))) {
|
|
1183
|
+
fail(`read-only state did not explain no-lock behavior:\n${JSON.stringify(readOnlyState.automation, null, 2)}`);
|
|
1184
|
+
}
|
|
1161
1185
|
const queuedTask = hcc(['task', 'create', '--from', 'human', '--to', 'codex-a', '--title', 'queued while busy']);
|
|
1162
1186
|
const queuedTaskMatch = queuedTask.match(/created task #(\d+):/);
|
|
1163
1187
|
if (!queuedTaskMatch) fail(`cannot parse queued task id: ${queuedTask}`);
|
|
@@ -1181,6 +1205,93 @@ async function dbWorkflow() {
|
|
|
1181
1205
|
if (!nextAgain.includes(`current task #${taskId}`)) {
|
|
1182
1206
|
fail(`task next did not preserve current task:\n${nextAgain}`);
|
|
1183
1207
|
}
|
|
1208
|
+
const nextAgainJson = hccJson(['task', 'next', '--peer', 'codex-a']);
|
|
1209
|
+
if (String(nextAgainJson.id) !== String(taskId) || nextAgainJson.current !== true || nextAgainJson.tasks) {
|
|
1210
|
+
fail(`task next --json current task shape changed:\n${JSON.stringify(nextAgainJson, null, 2)}`);
|
|
1211
|
+
}
|
|
1212
|
+
const batchClaimIds = [];
|
|
1213
|
+
for (const title of ['batch claim one', 'batch claim two']) {
|
|
1214
|
+
const out = hcc(['task', 'create', '--from', 'human', '--title', title]);
|
|
1215
|
+
const match = out.match(/created task #(\d+):/);
|
|
1216
|
+
if (!match) fail(`cannot parse batch task id: ${out}`);
|
|
1217
|
+
batchClaimIds.push(match[1]);
|
|
1218
|
+
}
|
|
1219
|
+
const batchClaim = hccJson(['task', 'claim', '--peer', 'batch-a', '--ids', batchClaimIds.join(',')]);
|
|
1220
|
+
if (!Array.isArray(batchClaim) || batchClaim.length !== 2 || !batchClaim.every((task) => task.owner === 'batch-a')) {
|
|
1221
|
+
fail(`batch claim did not claim both tasks:\n${JSON.stringify(batchClaim, null, 2)}`);
|
|
1222
|
+
}
|
|
1223
|
+
for (const task of batchClaim) {
|
|
1224
|
+
hcc(['task', 'done', '--peer', 'batch-a', '--id', String(task.id), '--summary', 'batch claim cleanup']);
|
|
1225
|
+
}
|
|
1226
|
+
const batchNextIds = [];
|
|
1227
|
+
for (const title of ['batch next one', 'batch next two', 'batch next three']) {
|
|
1228
|
+
const out = hcc(['task', 'create', '--from', 'human', '--title', title]);
|
|
1229
|
+
const match = out.match(/created task #(\d+):/);
|
|
1230
|
+
if (!match) fail(`cannot parse batch next task id: ${out}`);
|
|
1231
|
+
batchNextIds.push(match[1]);
|
|
1232
|
+
}
|
|
1233
|
+
const batchNext = hccJson(['task', 'next', '--peer', 'batch-b', '--force', '--count', '2']);
|
|
1234
|
+
if (!batchNext?.tasks || batchNext.tasks.length !== 2 || !batchNext.tasks.every((task) => task.owner === 'batch-b')) {
|
|
1235
|
+
fail(`task next --count did not claim two tasks:\n${JSON.stringify(batchNext, null, 2)}`);
|
|
1236
|
+
}
|
|
1237
|
+
for (const task of batchNext.tasks) {
|
|
1238
|
+
hcc(['task', 'done', '--peer', 'batch-b', '--id', String(task.id), '--summary', 'batch next cleanup']);
|
|
1239
|
+
}
|
|
1240
|
+
hcc(['task', 'update', '--peer', 'human', '--id', batchNextIds[2], '--status', 'abandoned', '--summary', 'batch next leftover cleanup']);
|
|
1241
|
+
const takeoverOpen = hcc(['task', 'create', '--from', 'human', '--to', 'takeover-owner', '--title', 'takeover open']);
|
|
1242
|
+
const takeoverOpenMatch = takeoverOpen.match(/created task #(\d+):/);
|
|
1243
|
+
if (!takeoverOpenMatch) fail(`cannot parse takeover open id: ${takeoverOpen}`);
|
|
1244
|
+
const takeoverOpenId = takeoverOpenMatch[1];
|
|
1245
|
+
hcc(['task', 'claim', '--peer', 'takeover-owner', '--id', takeoverOpenId]);
|
|
1246
|
+
const blockedPolicyReject = hccMaybe(['task', 'takeover', '--peer', 'takeover-a', '--id', takeoverOpenId, '--reason', 'blocked policy smoke', '--policy', 'blocked']);
|
|
1247
|
+
if (blockedPolicyReject.status === 0 || !String(blockedPolicyReject.stderr || blockedPolicyReject.stdout).includes('does not match takeover policy blocked')) {
|
|
1248
|
+
fail(`takeover --policy blocked accepted non-blocked task:\n${blockedPolicyReject.stdout}\n${blockedPolicyReject.stderr}`);
|
|
1249
|
+
}
|
|
1250
|
+
hcc(['task', 'update', '--peer', 'takeover-owner', '--id', takeoverOpenId, '--status', 'blocked', '--summary', 'blocked for takeover smoke']);
|
|
1251
|
+
const blockedTakeover = hccJson(['task', 'takeover', '--peer', 'takeover-a', '--id', takeoverOpenId, '--reason', 'blocked takeover smoke', '--policy', 'blocked']);
|
|
1252
|
+
if (String(blockedTakeover.owner) !== 'takeover-a') {
|
|
1253
|
+
fail(`blocked takeover did not transfer owner:\n${JSON.stringify(blockedTakeover, null, 2)}`);
|
|
1254
|
+
}
|
|
1255
|
+
hcc(['task', 'done', '--peer', 'takeover-a', '--id', takeoverOpenId, '--summary', 'blocked takeover cleanup']);
|
|
1256
|
+
const staleOut = hcc(['task', 'create', '--from', 'human', '--to', 'stale-owner', '--title', 'stale takeover']);
|
|
1257
|
+
const staleMatch = staleOut.match(/created task #(\d+):/);
|
|
1258
|
+
if (!staleMatch) fail(`cannot parse stale takeover id: ${staleOut}`);
|
|
1259
|
+
const staleTaskId = staleMatch[1];
|
|
1260
|
+
hcc(['task', 'claim', '--peer', 'stale-owner', '--id', staleTaskId]);
|
|
1261
|
+
withMeshDb((db) => {
|
|
1262
|
+
const staleAt = Math.floor(Date.now() / 1000) - 7200;
|
|
1263
|
+
db.prepare('UPDATE peers SET last_seen_at = ? WHERE id = ?').run(staleAt, 'stale-owner');
|
|
1264
|
+
});
|
|
1265
|
+
const staleTakeover = hccJson(['task', 'takeover', '--peer', 'takeover-b', '--id', staleTaskId, '--reason', 'stale takeover smoke', '--policy', 'stale', '--stale-after', '60']);
|
|
1266
|
+
if (String(staleTakeover.owner) !== 'takeover-b') {
|
|
1267
|
+
fail(`stale takeover did not transfer owner:\n${JSON.stringify(staleTakeover, null, 2)}`);
|
|
1268
|
+
}
|
|
1269
|
+
const takeoverInbox = hcc(['msg', 'inbox', '--peer', 'stale-owner', '--all']);
|
|
1270
|
+
if (!takeoverInbox.includes(`Task #${staleTaskId} taken over by takeover-b`)) {
|
|
1271
|
+
fail(`stale takeover did not notify previous owner:\n${takeoverInbox}`);
|
|
1272
|
+
}
|
|
1273
|
+
hcc(['task', 'done', '--peer', 'takeover-b', '--id', staleTaskId, '--summary', 'stale takeover cleanup']);
|
|
1274
|
+
const staleLivenessOut = hcc(['task', 'create', '--from', 'human', '--to', 'stale-liveness-owner', '--title', 'stale liveness task']);
|
|
1275
|
+
const staleLivenessMatch = staleLivenessOut.match(/created task #(\d+):/);
|
|
1276
|
+
if (!staleLivenessMatch) fail(`cannot parse stale liveness task id: ${staleLivenessOut}`);
|
|
1277
|
+
const staleLivenessTaskId = staleLivenessMatch[1];
|
|
1278
|
+
hcc(['task', 'claim', '--peer', 'stale-liveness-owner', '--id', staleLivenessTaskId]);
|
|
1279
|
+
withMeshDb((db) => {
|
|
1280
|
+
const staleAt = Math.floor(Date.now() / 1000) - 7200;
|
|
1281
|
+
db.prepare('UPDATE peers SET last_seen_at = ? WHERE id = ?').run(staleAt, 'stale-liveness-owner');
|
|
1282
|
+
});
|
|
1283
|
+
const staleLivenessList = hcc(['task', 'list', '--status', 'claimed']);
|
|
1284
|
+
if (!staleLivenessList.includes(`#${staleLivenessTaskId}`) || !staleLivenessList.includes('stale/no-lock')) {
|
|
1285
|
+
fail(`task list did not surface stale/no-lock owner state:\n${staleLivenessList}`);
|
|
1286
|
+
}
|
|
1287
|
+
const staleLivenessState = hccJson(['state', '--peer', 'takeover-ready-peer']);
|
|
1288
|
+
if (staleLivenessState.automation?.phase !== 'takeover_task' ||
|
|
1289
|
+
staleLivenessState.automation?.next_action?.kind !== 'task.takeover' ||
|
|
1290
|
+
!staleLivenessState.automation.next_action.argv.includes('--policy') ||
|
|
1291
|
+
!staleLivenessState.automation.next_action.argv.includes('stale')) {
|
|
1292
|
+
fail(`state automation did not suggest stale takeover:\n${JSON.stringify(staleLivenessState.automation, null, 2)}`);
|
|
1293
|
+
}
|
|
1294
|
+
hcc(['task', 'update', '--peer', 'human', '--force', '--id', staleLivenessTaskId, '--status', 'abandoned', '--summary', 'stale liveness cleanup']);
|
|
1184
1295
|
hcc(['task', 'update', '--peer', 'human', '--id', queuedTaskId, '--status', 'abandoned', '--summary', 'queued task cleanup']);
|
|
1185
1296
|
hcc(['handoff', 'create', '--from', 'codex-a', '--to', 'claude-a', '--task', taskId, '--summary', 'handoff summary', '--tests', 'full script', '--risks', 'none']);
|
|
1186
1297
|
if (!hcc(['handoff', 'list', '--task', taskId]).includes('handoff summary')) fail('handoff missing');
|
|
@@ -1254,6 +1365,27 @@ async function dbWorkflow() {
|
|
|
1254
1365
|
}
|
|
1255
1366
|
hcc(['task', 'done', '--peer', 'codex-team-captain', '--id', teamParentId, '--summary', 'team regression done']);
|
|
1256
1367
|
|
|
1368
|
+
const scopedA = hcc(['lock', 'acquire', '--peer', 'codex-a', '--resource', 'bin/hcc.mjs', '--scope', 'db-schema', '--ttl', '60', '--reason', 'db split']);
|
|
1369
|
+
const scopedB = hcc(['lock', 'acquire', '--peer', 'codex-b', '--resource', 'bin/hcc.mjs', '--scope', 'web-ui', '--ttl', '60', '--reason', 'ui split']);
|
|
1370
|
+
if (!scopedA.includes('bin/hcc.mjs [db-schema]') || !scopedB.includes('bin/hcc.mjs [web-ui]')) {
|
|
1371
|
+
fail(`scoped lock output missing labels:\n${scopedA}\n${scopedB}`);
|
|
1372
|
+
}
|
|
1373
|
+
const scopedRows = hccJson(['lock', 'list']);
|
|
1374
|
+
if (!scopedRows.some((row) => row.base_resource === 'bin/hcc.mjs' && row.scope === 'db-schema') ||
|
|
1375
|
+
!scopedRows.some((row) => row.base_resource === 'bin/hcc.mjs' && row.scope === 'web-ui')) {
|
|
1376
|
+
fail(`scoped locks missing from list:\n${JSON.stringify(scopedRows, null, 2)}`);
|
|
1377
|
+
}
|
|
1378
|
+
const duplicateScope = hccMaybe(['lock', 'acquire', '--peer', 'claude-a', '--resource', 'bin/hcc.mjs', '--scope', 'db-schema', '--ttl', '60', '--reason', 'same scope']);
|
|
1379
|
+
if (duplicateScope.status === 0 || !`${duplicateScope.stdout}\n${duplicateScope.stderr}`.includes('conflicts with lock bin/hcc.mjs [db-schema]')) {
|
|
1380
|
+
fail(`same-scope lock was not rejected:\n${duplicateScope.stdout}\n${duplicateScope.stderr}`);
|
|
1381
|
+
}
|
|
1382
|
+
const wholeResource = hccMaybe(['lock', 'acquire', '--peer', 'claude-a', '--resource', 'bin/hcc.mjs', '--ttl', '60', '--reason', 'whole file']);
|
|
1383
|
+
if (wholeResource.status === 0 || !`${wholeResource.stdout}\n${wholeResource.stderr}`.includes('conflicts with lock bin/hcc.mjs [db-schema]')) {
|
|
1384
|
+
fail(`whole-resource lock did not conflict with scoped locks:\n${wholeResource.stdout}\n${wholeResource.stderr}`);
|
|
1385
|
+
}
|
|
1386
|
+
hcc(['lock', 'release', '--peer', 'codex-a', '--resource', 'bin/hcc.mjs', '--scope', 'db-schema']);
|
|
1387
|
+
hcc(['lock', 'release', '--peer', 'codex-b', '--resource', 'bin/hcc.mjs', '--scope', 'web-ui']);
|
|
1388
|
+
|
|
1257
1389
|
const conflictTask = hcc(['task', 'create', '--from', 'human', '--to', 'codex-b', '--title', 'conflict automation task']);
|
|
1258
1390
|
const conflictTaskMatch = conflictTask.match(/created task #(\d+):/);
|
|
1259
1391
|
if (!conflictTaskMatch) fail(`cannot parse conflict task id: ${conflictTask}`);
|
|
@@ -1270,6 +1402,23 @@ async function dbWorkflow() {
|
|
|
1270
1402
|
hcc(['lock', 'release', '--peer', 'codex-a', '--resource', 'src/conflict']);
|
|
1271
1403
|
hcc(['task', 'done', '--peer', 'codex-b', '--id', conflictTaskId, '--summary', 'conflict done']);
|
|
1272
1404
|
|
|
1405
|
+
const takeoverTask = hcc(['task', 'create', '--from', 'human', '--to', 'codex-owner', '--title', 'takeover regression task']);
|
|
1406
|
+
const takeoverTaskMatch = takeoverTask.match(/created task #(\d+):/);
|
|
1407
|
+
if (!takeoverTaskMatch) fail(`cannot parse takeover task id: ${takeoverTask}`);
|
|
1408
|
+
const takeoverTaskId = takeoverTaskMatch[1];
|
|
1409
|
+
hcc(['task', 'claim', '--peer', 'codex-owner', '--id', takeoverTaskId]);
|
|
1410
|
+
const takeoverOutput = hcc(['task', 'takeover', '--peer', 'codex-taker', '--id', takeoverTaskId, '--reason', 'owner inactive']);
|
|
1411
|
+
if (!takeoverOutput.includes(`took over task #${takeoverTaskId}`)) fail(`takeover output wrong:\n${takeoverOutput}`);
|
|
1412
|
+
const takeoverRow = hccJson(['task', 'list', '--all']).find((row) => String(row.id) === String(takeoverTaskId));
|
|
1413
|
+
if (!takeoverRow || takeoverRow.owner !== 'codex-taker' || takeoverRow.status !== 'claimed') {
|
|
1414
|
+
fail(`takeover task row wrong:\n${JSON.stringify(takeoverRow, null, 2)}`);
|
|
1415
|
+
}
|
|
1416
|
+
const takeoverOwnerInbox = hcc(['msg', 'inbox', '--peer', 'codex-owner']);
|
|
1417
|
+
if (!takeoverOwnerInbox.includes(`Task #${takeoverTaskId} taken over by codex-taker`)) {
|
|
1418
|
+
fail(`takeover did not notify previous owner:\n${takeoverOwnerInbox}`);
|
|
1419
|
+
}
|
|
1420
|
+
hcc(['task', 'done', '--peer', 'codex-taker', '--id', takeoverTaskId, '--summary', 'takeover done']);
|
|
1421
|
+
|
|
1273
1422
|
const abandoned = hcc(['task', 'create', '--from', 'human', '--title', 'abandoned regression task']);
|
|
1274
1423
|
const abandonedMatch = abandoned.match(/created task #(\d+):/);
|
|
1275
1424
|
if (!abandonedMatch) fail(`cannot parse abandoned task id: ${abandoned}`);
|
|
@@ -1961,9 +2110,7 @@ async function downGcPackWorkflow() {
|
|
|
1961
2110
|
'CHANGELOG.md',
|
|
1962
2111
|
'docs/commands.md',
|
|
1963
2112
|
'docs/commands.zh-CN.md',
|
|
1964
|
-
|
|
1965
|
-
'lib/package-meta.mjs',
|
|
1966
|
-
'lib/release-notes.mjs',
|
|
2113
|
+
...libModuleFiles(),
|
|
1967
2114
|
'scripts/github-release.mjs'
|
|
1968
2115
|
]) {
|
|
1969
2116
|
if (!files.has(file)) fail(`npm package missing ${file}`);
|
|
@@ -2030,15 +2177,14 @@ function oldNameScan() {
|
|
|
2030
2177
|
if (rg.status !== 1) fail(`rg failed:\n${rg.stderr || rg.stdout}`);
|
|
2031
2178
|
}
|
|
2032
2179
|
|
|
2033
|
-
function syntaxAndHelp() {
|
|
2180
|
+
async function syntaxAndHelp() {
|
|
2034
2181
|
log('[11/12] syntax/help');
|
|
2035
2182
|
run(process.execPath, ['--check', path.join(repoRoot, 'bin', 'hcc.mjs')]);
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
run(process.execPath, ['--check', path.join(repoRoot, 'lib', 'package-meta.mjs')]);
|
|
2040
|
-
run(process.execPath, ['--check', path.join(repoRoot, 'lib', 'release-notes.mjs')]);
|
|
2183
|
+
for (const file of libModuleFiles()) {
|
|
2184
|
+
run(process.execPath, ['--check', path.join(repoRoot, file)]);
|
|
2185
|
+
}
|
|
2041
2186
|
const hccSource = fs.readFileSync(hccBin, 'utf8');
|
|
2187
|
+
const webUiTemplateSource = fs.readFileSync(path.join(repoRoot, 'lib', 'web-ui-template.mjs'), 'utf8');
|
|
2042
2188
|
for (const expected of [
|
|
2043
2189
|
'function scheduleTmuxInputRefresh(session)',
|
|
2044
2190
|
"runTmux(['pipe-pane', '-t', session.pane]);",
|
|
@@ -2050,9 +2196,25 @@ function syntaxAndHelp() {
|
|
|
2050
2196
|
]) {
|
|
2051
2197
|
if (!hccSource.includes(expected)) fail(`web terminal input refresh support missing: ${expected}`);
|
|
2052
2198
|
}
|
|
2199
|
+
for (const expected of [
|
|
2200
|
+
'function detectedPeerCanStop(peer)',
|
|
2201
|
+
"if (['exited', 'detached'].includes(status)) return false;",
|
|
2202
|
+
'const canStop = detectedPeerCanStop(p);',
|
|
2203
|
+
'${canStop ?',
|
|
2204
|
+
"if (e.target.closest('[data-action]')) return;",
|
|
2205
|
+
'id="stopKillLabel" data-i18n="dialog.killTmux"',
|
|
2206
|
+
'id="stopCancelBtn" type="button" data-i18n="dialog.cancel"',
|
|
2207
|
+
'id="stopConfirmBtn" type="button" data-i18n="stop"'
|
|
2208
|
+
]) {
|
|
2209
|
+
if (!webUiTemplateSource.includes(expected)) fail(`web display regression guard missing: ${expected}`);
|
|
2210
|
+
}
|
|
2211
|
+
if (webUiTemplateSource.includes("p.status === 'running' ?")) {
|
|
2212
|
+
fail('detected peer action rendering still depends on status === running instead of liveness');
|
|
2213
|
+
}
|
|
2053
2214
|
for (const expected of [
|
|
2054
2215
|
'function webPeerAction(projectCtx, peer, action, input = {})',
|
|
2055
|
-
'function
|
|
2216
|
+
'function claimNextTasksForPeer(db, peer, { force = false, count = 1 } = {})',
|
|
2217
|
+
'function takeOverTaskForPeer(db, peer, id, { reason, policy = ',
|
|
2056
2218
|
'function statusSummary(ctx, peer = null, identity = null)',
|
|
2057
2219
|
'const peerActionMatch = url.pathname.match(/^\\/api\\/peers\\/([^/]+)\\/actions\\/([^/]+)$/)',
|
|
2058
2220
|
"const readOnly = ['status', 'state', 'inbox'].includes(action)",
|
|
@@ -2066,10 +2228,100 @@ function syntaxAndHelp() {
|
|
|
2066
2228
|
fail(`CLI version ${cliVersion} does not match package.json ${packageVersion}`);
|
|
2067
2229
|
}
|
|
2068
2230
|
if (!hccSource.includes("import { readPackageMeta } from '../lib/package-meta.mjs'") ||
|
|
2231
|
+
!hccSource.includes("} from '../lib/cli-args.mjs'") ||
|
|
2232
|
+
!hccSource.includes("import { CliError } from '../lib/errors.mjs'") ||
|
|
2233
|
+
!hccSource.includes("} from '../lib/db-schema.mjs'") ||
|
|
2234
|
+
!hccSource.includes("} from '../lib/format.mjs'") ||
|
|
2235
|
+
!hccSource.includes("} from '../lib/runtime-paths.mjs'") ||
|
|
2236
|
+
!hccSource.includes("} from '../lib/web-runtime.mjs'") ||
|
|
2237
|
+
!hccSource.includes("} from '../lib/web-http.mjs'") ||
|
|
2238
|
+
!hccSource.includes("import { webIndexHtml } from '../lib/web-ui-template.mjs'") ||
|
|
2069
2239
|
!hccSource.includes('const VERSION = PACKAGE_META.version') ||
|
|
2070
2240
|
!hccSource.includes('writeGuidanceForRoot(ctx.root)')) {
|
|
2071
|
-
fail('CLI still has duplicated package metadata or guidance wiring');
|
|
2072
|
-
}
|
|
2241
|
+
fail('CLI still has duplicated package metadata, cli args, DB schema helpers, format helpers, runtime paths, web runtime/HTTP/UI helpers, or guidance wiring');
|
|
2242
|
+
}
|
|
2243
|
+
if (hccSource.includes('function createBaseSchema') ||
|
|
2244
|
+
hccSource.includes('function runSchemaMigrations') ||
|
|
2245
|
+
hccSource.includes('function readSchemaVersion')) {
|
|
2246
|
+
fail('CLI still embeds DB schema or migration helpers');
|
|
2247
|
+
}
|
|
2248
|
+
for (const helper of [
|
|
2249
|
+
'function runtimeBaseUrl',
|
|
2250
|
+
'function runtimeApiUrl',
|
|
2251
|
+
'function requestUrl',
|
|
2252
|
+
'function runtimeUrlQuery',
|
|
2253
|
+
'function makeWebToken',
|
|
2254
|
+
'function validateWebTokenOpts',
|
|
2255
|
+
'function expectedWebHost',
|
|
2256
|
+
'function webRuntimeMatchesRequest',
|
|
2257
|
+
'function publicRuntimeUrl',
|
|
2258
|
+
'function localRuntimeUrl'
|
|
2259
|
+
]) {
|
|
2260
|
+
if (hccSource.includes(helper)) fail(`CLI still embeds web runtime helper: ${helper}`);
|
|
2261
|
+
}
|
|
2262
|
+
for (const helper of [
|
|
2263
|
+
'function readRequestBody',
|
|
2264
|
+
'function readJsonRequest',
|
|
2265
|
+
'function sendHttp',
|
|
2266
|
+
'function sendJson',
|
|
2267
|
+
'function sendFile',
|
|
2268
|
+
'function authOk'
|
|
2269
|
+
]) {
|
|
2270
|
+
if (hccSource.includes(helper)) fail(`CLI still embeds web HTTP helper: ${helper}`);
|
|
2271
|
+
}
|
|
2272
|
+
if (hccSource.includes('new URL(req.url')) fail('CLI still embeds raw server request URL parsing');
|
|
2273
|
+
if (hccSource.includes('function webIndexHtml()')) fail('CLI still embeds the web UI template');
|
|
2274
|
+
const webRuntime = await import(path.join(repoRoot, 'lib', 'web-runtime.mjs'));
|
|
2275
|
+
const webHttp = await import(path.join(repoRoot, 'lib', 'web-http.mjs'));
|
|
2276
|
+
const webUiTemplate = await import(path.join(repoRoot, 'lib', 'web-ui-template.mjs'));
|
|
2277
|
+
const expectEqual = (actual, expected, label) => {
|
|
2278
|
+
if (actual !== expected) fail(`${label}: expected ${expected}, got ${actual}`);
|
|
2279
|
+
};
|
|
2280
|
+
const html = webUiTemplate.webIndexHtml();
|
|
2281
|
+
if (!html.includes('<!doctype html>') ||
|
|
2282
|
+
!html.includes('<div class="app">') ||
|
|
2283
|
+
!html.includes('<script src="/assets/xterm.js"></script>')) {
|
|
2284
|
+
fail('web UI template module did not render the expected shell HTML');
|
|
2285
|
+
}
|
|
2286
|
+
const jsonReq = Readable.from(['{"ok":true}']);
|
|
2287
|
+
jsonReq.headers = {};
|
|
2288
|
+
const parsedJson = await webHttp.readJsonRequest(jsonReq);
|
|
2289
|
+
if (parsedJson.ok !== true) fail(`web HTTP helper failed to parse JSON request: ${JSON.stringify(parsedJson)}`);
|
|
2290
|
+
const emptyReq = Readable.from(['']);
|
|
2291
|
+
emptyReq.headers = {};
|
|
2292
|
+
const parsedEmpty = await webHttp.readJsonRequest(emptyReq);
|
|
2293
|
+
if (Object.keys(parsedEmpty).length !== 0) fail(`web HTTP helper failed empty request fallback: ${JSON.stringify(parsedEmpty)}`);
|
|
2294
|
+
const mockRes = {
|
|
2295
|
+
status: null,
|
|
2296
|
+
headers: null,
|
|
2297
|
+
body: '',
|
|
2298
|
+
writeHead(status, headers) { this.status = status; this.headers = headers; },
|
|
2299
|
+
end(body) { this.body = body; }
|
|
2300
|
+
};
|
|
2301
|
+
webHttp.sendJson(mockRes, 202, { ok: true });
|
|
2302
|
+
if (mockRes.status !== 202 ||
|
|
2303
|
+
mockRes.headers?.['Content-Type'] !== 'application/json; charset=utf-8' ||
|
|
2304
|
+
mockRes.headers?.['Cache-Control'] !== 'no-store' ||
|
|
2305
|
+
!mockRes.body.includes('"ok": true')) {
|
|
2306
|
+
fail(`web HTTP helper failed sendJson response: ${JSON.stringify(mockRes)}`);
|
|
2307
|
+
}
|
|
2308
|
+
if (!webHttp.authOk(new URL('http://example.test/?token=tok'), { headers: {} }, 'tok') ||
|
|
2309
|
+
!webHttp.authOk(new URL('http://example.test/'), { headers: { authorization: 'Bearer tok' } }, 'tok') ||
|
|
2310
|
+
webHttp.authOk(new URL('http://example.test/?token=bad'), { headers: {} }, 'tok')) {
|
|
2311
|
+
fail('web HTTP helper authOk token checks failed');
|
|
2312
|
+
}
|
|
2313
|
+
const wildcardRuntime = { host: '0.0.0.0', port: 8787, token: 'tok' };
|
|
2314
|
+
const ipv6WildcardRuntime = { host: '::', port: 8788, token: 'tok' };
|
|
2315
|
+
const localRuntime = { host: '127.0.0.1', port: 8789, token: 'tok' };
|
|
2316
|
+
expectEqual(webRuntime.runtimeBaseUrl('0.0.0.0', 8787), 'http://127.0.0.1:8787', 'runtimeBaseUrl 0.0.0.0');
|
|
2317
|
+
expectEqual(webRuntime.runtimeBaseUrl('::', 8788), 'http://127.0.0.1:8788', 'runtimeBaseUrl ::');
|
|
2318
|
+
expectEqual(String(webRuntime.runtimeApiUrl({ base_url: 'http://127.0.0.1:8787/base' }, '/api/state?peer=a b')), 'http://127.0.0.1:8787/api/state?peer=a%20b', 'runtimeApiUrl route');
|
|
2319
|
+
expectEqual(String(webRuntime.requestUrl({ url: '/api/state?peer=a b', headers: { host: 'example.test:8787' } })), 'http://example.test:8787/api/state?peer=a%20b', 'requestUrl host and query');
|
|
2320
|
+
expectEqual(String(webRuntime.requestUrl({ url: '', headers: {} })), 'http://localhost/', 'requestUrl fallback');
|
|
2321
|
+
expectEqual(webRuntime.publicRuntimeUrl(wildcardRuntime, '/tmp/hcc project'), 'http://<machine-ip>:8787/?token=tok&project=%2Ftmp%2Fhcc%20project', 'publicRuntimeUrl wildcard');
|
|
2322
|
+
expectEqual(webRuntime.localRuntimeUrl(wildcardRuntime, '/tmp/hcc project'), 'http://127.0.0.1:8787/?token=tok&project=%2Ftmp%2Fhcc%20project', 'localRuntimeUrl wildcard');
|
|
2323
|
+
expectEqual(webRuntime.publicRuntimeUrl(ipv6WildcardRuntime, '/tmp/hcc project'), 'http://<machine-ip>:8788/?token=tok&project=%2Ftmp%2Fhcc%20project', 'publicRuntimeUrl ipv6 wildcard');
|
|
2324
|
+
expectEqual(webRuntime.localRuntimeUrl(localRuntime, null), 'http://127.0.0.1:8789/?token=tok', 'localRuntimeUrl no project');
|
|
2073
2325
|
const mainHelp = run(process.execPath, [hccBin, '--help']);
|
|
2074
2326
|
if (mainHelp.includes('setup') || mainHelp.includes('--web-managed')) {
|
|
2075
2327
|
fail(`public help exposes maintenance or removed commands:\n${mainHelp}`);
|
|
@@ -2092,7 +2344,11 @@ function syntaxAndHelp() {
|
|
|
2092
2344
|
}
|
|
2093
2345
|
const taskHelp = run(process.execPath, [hccBin, 'task', '--help']);
|
|
2094
2346
|
if (!taskHelp.includes('task next [--peer ID] [--force]') ||
|
|
2347
|
+
!taskHelp.includes('[--count N]') ||
|
|
2095
2348
|
!taskHelp.includes('existing claimed/running/review/blocked task') ||
|
|
2349
|
+
!taskHelp.includes('task takeover [--peer ID] --id N --reason TEXT') ||
|
|
2350
|
+
!taskHelp.includes('[--policy any|blocked|stale|blocked-or-stale]') ||
|
|
2351
|
+
!taskHelp.includes('task claim [--peer ID] --id N[,N]') ||
|
|
2096
2352
|
!taskHelp.includes('task create --title TEXT --parent N')) {
|
|
2097
2353
|
fail(`task help missing current-task task next semantics:\n${taskHelp}`);
|
|
2098
2354
|
}
|
|
@@ -2101,9 +2357,18 @@ function syntaxAndHelp() {
|
|
|
2101
2357
|
fail(`team help missing expected content:\n${teamHelp}`);
|
|
2102
2358
|
}
|
|
2103
2359
|
const stateHelp = run(process.execPath, [hccBin, 'state', '--help']);
|
|
2104
|
-
if (!stateHelp.includes('hcc state') ||
|
|
2360
|
+
if (!stateHelp.includes('hcc state') ||
|
|
2361
|
+
!stateHelp.includes('--scope SCOPE') ||
|
|
2362
|
+
!stateHelp.includes('--intent read|review|work|write|stop|finish') ||
|
|
2363
|
+
!stateHelp.includes('automation.next_action.argv') ||
|
|
2364
|
+
!stateHelp.includes('automation.current_task') ||
|
|
2365
|
+
!stateHelp.includes('hcc team plan')) {
|
|
2105
2366
|
fail(`state help missing expected content:\n${stateHelp}`);
|
|
2106
2367
|
}
|
|
2368
|
+
const lockHelp = run(process.execPath, [hccBin, 'lock', '--help']);
|
|
2369
|
+
if (!lockHelp.includes('--scope SCOPE') || !lockHelp.toLowerCase().includes('different scopes on the same resource')) {
|
|
2370
|
+
fail(`lock help missing scoped lock content:\n${lockHelp}`);
|
|
2371
|
+
}
|
|
2107
2372
|
const updateHelp = run(process.execPath, [hccBin, 'update', '--help']);
|
|
2108
2373
|
if (!updateHelp.includes('hcc update') || !updateHelp.includes('npm install -g @logicseek/hello-cc@TAG')) {
|
|
2109
2374
|
fail(`update help missing expected content:\n${updateHelp}`);
|
|
@@ -2214,7 +2479,7 @@ async function main() {
|
|
|
2214
2479
|
await askBroadcastWorkflow();
|
|
2215
2480
|
await downGcPackWorkflow();
|
|
2216
2481
|
oldNameScan();
|
|
2217
|
-
syntaxAndHelp();
|
|
2482
|
+
await syntaxAndHelp();
|
|
2218
2483
|
uninstallWorkflow();
|
|
2219
2484
|
log('FULL_REGRESSION_OK');
|
|
2220
2485
|
}
|