@logicseek/hello-cc 0.1.3 → 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 +110 -0
- package/bin/hcc.mjs +865 -2430
- 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 +123 -0
- package/lib/json-file.mjs +14 -0
- package/lib/package-meta.mjs +24 -0
- package/lib/release-notes.mjs +40 -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/github-release.mjs +11 -49
- package/scripts/regression.mjs +443 -18
- package/scripts/release-notes.mjs +6 -38
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
|
]) {
|
|
@@ -809,6 +819,39 @@ async function expectResizeReplaceSnapshot(peer, marker) {
|
|
|
809
819
|
});
|
|
810
820
|
}
|
|
811
821
|
|
|
822
|
+
async function expectWebSocketInputVisible(peer, marker) {
|
|
823
|
+
await new Promise((resolve, reject) => {
|
|
824
|
+
let sawSnapshot = false;
|
|
825
|
+
let sent = false;
|
|
826
|
+
let sawMarkerAfterInput = false;
|
|
827
|
+
let sawFrameAfterInput = false;
|
|
828
|
+
const ws = new WebSocket(runtimeWsUrl(peer));
|
|
829
|
+
const timer = setTimeout(() => reject(new Error(`${peer} websocket input visibility timeout`)), 5000);
|
|
830
|
+
ws.on('message', (raw) => {
|
|
831
|
+
const msg = JSON.parse(String(raw));
|
|
832
|
+
const data = String(msg.data || '');
|
|
833
|
+
if (msg.type === 'snapshot') {
|
|
834
|
+
sawSnapshot = true;
|
|
835
|
+
if (!sent) {
|
|
836
|
+
sent = true;
|
|
837
|
+
ws.send(JSON.stringify({ type: 'input', data: `echo ${marker}\r` }));
|
|
838
|
+
}
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
if (sent && ['data', 'replace'].includes(msg.type)) {
|
|
842
|
+
sawFrameAfterInput = true;
|
|
843
|
+
if (data.includes(marker)) sawMarkerAfterInput = true;
|
|
844
|
+
}
|
|
845
|
+
if (sawSnapshot && sawFrameAfterInput && sawMarkerAfterInput) {
|
|
846
|
+
clearTimeout(timer);
|
|
847
|
+
ws.close();
|
|
848
|
+
resolve();
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
ws.on('error', reject);
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
|
|
812
855
|
function tmuxStreamNodes() {
|
|
813
856
|
const dir = path.join(root, '.hello-cc', 'bufs');
|
|
814
857
|
if (!fs.existsSync(dir)) return [];
|
|
@@ -817,6 +860,13 @@ function tmuxStreamNodes() {
|
|
|
817
860
|
.map((name) => path.join(dir, name));
|
|
818
861
|
}
|
|
819
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
|
+
|
|
820
870
|
async function expectBoundedTmuxStream(label) {
|
|
821
871
|
let nodes = [];
|
|
822
872
|
await waitFor(() => {
|
|
@@ -1125,6 +1175,13 @@ async function dbWorkflow() {
|
|
|
1125
1175
|
if (stateAfterAck.automation.next_action.kind !== 'lock.acquire') {
|
|
1126
1176
|
fail(`state automation did not suggest lock acquire after ack:\n${JSON.stringify(stateAfterAck.automation, null, 2)}`);
|
|
1127
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
|
+
}
|
|
1128
1185
|
const queuedTask = hcc(['task', 'create', '--from', 'human', '--to', 'codex-a', '--title', 'queued while busy']);
|
|
1129
1186
|
const queuedTaskMatch = queuedTask.match(/created task #(\d+):/);
|
|
1130
1187
|
if (!queuedTaskMatch) fail(`cannot parse queued task id: ${queuedTask}`);
|
|
@@ -1148,6 +1205,93 @@ async function dbWorkflow() {
|
|
|
1148
1205
|
if (!nextAgain.includes(`current task #${taskId}`)) {
|
|
1149
1206
|
fail(`task next did not preserve current task:\n${nextAgain}`);
|
|
1150
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']);
|
|
1151
1295
|
hcc(['task', 'update', '--peer', 'human', '--id', queuedTaskId, '--status', 'abandoned', '--summary', 'queued task cleanup']);
|
|
1152
1296
|
hcc(['handoff', 'create', '--from', 'codex-a', '--to', 'claude-a', '--task', taskId, '--summary', 'handoff summary', '--tests', 'full script', '--risks', 'none']);
|
|
1153
1297
|
if (!hcc(['handoff', 'list', '--task', taskId]).includes('handoff summary')) fail('handoff missing');
|
|
@@ -1221,6 +1365,27 @@ async function dbWorkflow() {
|
|
|
1221
1365
|
}
|
|
1222
1366
|
hcc(['task', 'done', '--peer', 'codex-team-captain', '--id', teamParentId, '--summary', 'team regression done']);
|
|
1223
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
|
+
|
|
1224
1389
|
const conflictTask = hcc(['task', 'create', '--from', 'human', '--to', 'codex-b', '--title', 'conflict automation task']);
|
|
1225
1390
|
const conflictTaskMatch = conflictTask.match(/created task #(\d+):/);
|
|
1226
1391
|
if (!conflictTaskMatch) fail(`cannot parse conflict task id: ${conflictTask}`);
|
|
@@ -1237,6 +1402,23 @@ async function dbWorkflow() {
|
|
|
1237
1402
|
hcc(['lock', 'release', '--peer', 'codex-a', '--resource', 'src/conflict']);
|
|
1238
1403
|
hcc(['task', 'done', '--peer', 'codex-b', '--id', conflictTaskId, '--summary', 'conflict done']);
|
|
1239
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
|
+
|
|
1240
1422
|
const abandoned = hcc(['task', 'create', '--from', 'human', '--title', 'abandoned regression task']);
|
|
1241
1423
|
const abandonedMatch = abandoned.match(/created task #(\d+):/);
|
|
1242
1424
|
if (!abandonedMatch) fail(`cannot parse abandoned task id: ${abandoned}`);
|
|
@@ -1436,13 +1618,56 @@ async function multiProjectWebWorkflow() {
|
|
|
1436
1618
|
]) {
|
|
1437
1619
|
if (!html.includes(expected)) fail(`web layout missing resizable sidebar support: ${expected}`);
|
|
1438
1620
|
}
|
|
1439
|
-
if (!html.includes('state-card') || !html.includes('peerStateView') || !html.includes('savedCardScroll') || !html.includes('lastStateRoot') || !html.includes('
|
|
1621
|
+
if (!html.includes('state-card') || !html.includes('peerStateView') || !html.includes('savedCardScroll') || !html.includes('lastStateRoot') || !html.includes("stateCardHtml('peers'")) {
|
|
1440
1622
|
fail('web state panel missing scrollable peer state UI');
|
|
1441
1623
|
}
|
|
1442
|
-
|
|
1624
|
+
for (const expected of [
|
|
1625
|
+
'function stateCardCollapsed(section)',
|
|
1626
|
+
"localStorage.getItem('hcc.stateCard.' + section + '.collapsed')",
|
|
1627
|
+
'function stateCardHtml(section, title, count, bodyHtml)',
|
|
1628
|
+
'state-card-toggle',
|
|
1629
|
+
'aria-expanded=',
|
|
1630
|
+
'state-card-collapsed',
|
|
1631
|
+
'function bindStateCardToggles()',
|
|
1632
|
+
"localStorage.setItem('hcc.stateCard.' + section + '.collapsed'",
|
|
1633
|
+
"stateCardHtml('automation'",
|
|
1634
|
+
"stateCardHtml('timeline'",
|
|
1635
|
+
"stateCardHtml('messages'",
|
|
1636
|
+
"stateCardHtml('peers'",
|
|
1637
|
+
"stateCardHtml('tasks'",
|
|
1638
|
+
"stateCardHtml('locks'",
|
|
1639
|
+
'bindStateCardToggles();'
|
|
1640
|
+
]) {
|
|
1641
|
+
if (!html.includes(expected)) fail(`web state panel missing collapsible card support: ${expected}`);
|
|
1642
|
+
}
|
|
1643
|
+
for (const expected of [
|
|
1644
|
+
'id="actionResult"',
|
|
1645
|
+
'data-action="state"',
|
|
1646
|
+
'data-action="status"',
|
|
1647
|
+
'data-action="inbox"',
|
|
1648
|
+
'data-action="task-next"',
|
|
1649
|
+
'data-action="heartbeat"',
|
|
1650
|
+
'data-action="register"',
|
|
1651
|
+
'data-terminal-action="status"',
|
|
1652
|
+
'function runPeerAction(action)',
|
|
1653
|
+
"'/api/peers/' + encodeURIComponent(info.peerId) + '/actions/' + encodeURIComponent(action)",
|
|
1654
|
+
'showActionResult(result)'
|
|
1655
|
+
]) {
|
|
1656
|
+
if (!html.includes(expected)) fail(`web actions missing API result UI: ${expected}`);
|
|
1657
|
+
}
|
|
1658
|
+
if (html.includes('data-send=') || html.includes("document.querySelectorAll('[data-send]')")) {
|
|
1659
|
+
fail('web actions still use implicit terminal command injection');
|
|
1660
|
+
}
|
|
1661
|
+
for (const expected of [
|
|
1662
|
+
'term.onData((data) => {',
|
|
1663
|
+
"ws.send(JSON.stringify({ type: 'input', data }))"
|
|
1664
|
+
]) {
|
|
1665
|
+
if (!html.includes(expected)) fail(`web terminal input forwarding missing: ${expected}`);
|
|
1666
|
+
}
|
|
1667
|
+
if (!html.includes("stateCardHtml('timeline'") || !html.includes('renderTimelineItem') || !html.includes('bodyPinned') || !html.includes('refreshCurrentState')) {
|
|
1443
1668
|
fail('web state panel missing collaboration timeline or refresh routing');
|
|
1444
1669
|
}
|
|
1445
|
-
if (!html.includes('
|
|
1670
|
+
if (!html.includes("stateCardHtml('messages'") || !html.includes('messagesData.length')) {
|
|
1446
1671
|
fail('web state panel missing dedicated messages card');
|
|
1447
1672
|
}
|
|
1448
1673
|
|
|
@@ -1463,6 +1688,40 @@ async function multiProjectWebWorkflow() {
|
|
|
1463
1688
|
fail(`second project API did not see its session:\n${JSON.stringify(otherSessions)}`);
|
|
1464
1689
|
}
|
|
1465
1690
|
|
|
1691
|
+
hcc(['register', '--peer', 'web-action-peer', '--kind', 'codex', '--role', 'peer']);
|
|
1692
|
+
hcc(['msg', 'send', '--from', 'human', '--to', 'web-action-peer', '--body', 'web action inbox ok']);
|
|
1693
|
+
const actionStatus = await (await runtimeFetch('/api/peers/web-action-peer/actions/status', {}, { root })).json();
|
|
1694
|
+
if (!actionStatus.ok || actionStatus.action !== 'status' || actionStatus.peer !== 'web-action-peer' || !actionStatus.data || typeof actionStatus.data.unread !== 'number') {
|
|
1695
|
+
fail(`web status action did not return structured status:\n${JSON.stringify(actionStatus, null, 2)}`);
|
|
1696
|
+
}
|
|
1697
|
+
const actionInbox = await (await runtimeFetch('/api/peers/web-action-peer/actions/inbox', {}, { root })).json();
|
|
1698
|
+
if (!actionInbox.ok || actionInbox.action !== 'inbox' || !(actionInbox.data?.messages || []).some((m) => m.body === 'web action inbox ok')) {
|
|
1699
|
+
fail(`web inbox action did not return unread messages:\n${JSON.stringify(actionInbox, null, 2)}`);
|
|
1700
|
+
}
|
|
1701
|
+
const actionState = await (await runtimeFetch('/api/peers/web-action-peer/actions/state', {}, { root })).json();
|
|
1702
|
+
if (!actionState.ok || actionState.action !== 'state' || actionState.data?.automation?.peer?.id !== 'web-action-peer') {
|
|
1703
|
+
fail(`web state action did not return peer automation:\n${JSON.stringify(actionState, null, 2)}`);
|
|
1704
|
+
}
|
|
1705
|
+
const taskOutput = hcc(['task', 'create', '--title', 'web action task', '--body', 'claim through web action']);
|
|
1706
|
+
const taskMatch = taskOutput.match(/created task #(\d+):/);
|
|
1707
|
+
if (!taskMatch) fail(`cannot parse web action task id:\n${taskOutput}`);
|
|
1708
|
+
const actionNext = await (await runtimeFetch('/api/peers/web-action-peer/actions/task-next', {
|
|
1709
|
+
method: 'POST',
|
|
1710
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1711
|
+
body: '{}'
|
|
1712
|
+
}, { root })).json();
|
|
1713
|
+
if (!actionNext.ok || actionNext.action !== 'task-next' || String(actionNext.data?.task?.id) !== taskMatch[1]) {
|
|
1714
|
+
fail(`web task-next action did not claim pending task #${taskMatch[1]}:\n${JSON.stringify(actionNext, null, 2)}`);
|
|
1715
|
+
}
|
|
1716
|
+
const actionHeartbeat = await (await runtimeFetch('/api/peers/web-action-peer/actions/heartbeat', {
|
|
1717
|
+
method: 'POST',
|
|
1718
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1719
|
+
body: JSON.stringify({ renew_locks: true })
|
|
1720
|
+
}, { root })).json();
|
|
1721
|
+
if (!actionHeartbeat.ok || actionHeartbeat.action !== 'heartbeat' || actionHeartbeat.data?.peer !== 'web-action-peer') {
|
|
1722
|
+
fail(`web heartbeat action did not return structured result:\n${JSON.stringify(actionHeartbeat, null, 2)}`);
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1466
1725
|
const startProvider = async (payload) => {
|
|
1467
1726
|
const response = await runtimeFetch('/api/sessions', {
|
|
1468
1727
|
method: 'POST',
|
|
@@ -1599,6 +1858,7 @@ async function tmuxBackedStartWorkflow() {
|
|
|
1599
1858
|
await waitForFile(file, 'PTY_OK', 'pty injection');
|
|
1600
1859
|
await expectWebSocketMarker('shell-a', 'WS_PTY_OK');
|
|
1601
1860
|
await expectResizeReplaceSnapshot('shell-a', 'WS_RESIZE_OK');
|
|
1861
|
+
await expectWebSocketInputVisible('shell-a', 'WS_INPUT_VISIBLE_OK');
|
|
1602
1862
|
await expectBoundedTmuxStream('tmux-backed FIFO stream');
|
|
1603
1863
|
|
|
1604
1864
|
await stopRuntime();
|
|
@@ -1845,17 +2105,29 @@ async function downGcPackWorkflow() {
|
|
|
1845
2105
|
hcc(['gc', '--older-than', '0', '--yes']);
|
|
1846
2106
|
const pack = JSON.parse(run('npm', ['pack', '--dry-run', '--json']));
|
|
1847
2107
|
const files = new Set(pack[0]?.files?.map((entry) => entry.path) || []);
|
|
1848
|
-
for (const file of [
|
|
2108
|
+
for (const file of [
|
|
2109
|
+
'assets/logo.svg',
|
|
2110
|
+
'CHANGELOG.md',
|
|
2111
|
+
'docs/commands.md',
|
|
2112
|
+
'docs/commands.zh-CN.md',
|
|
2113
|
+
...libModuleFiles(),
|
|
2114
|
+
'scripts/github-release.mjs'
|
|
2115
|
+
]) {
|
|
1849
2116
|
if (!files.has(file)) fail(`npm package missing ${file}`);
|
|
1850
2117
|
}
|
|
2118
|
+
const packageVersion = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')).version;
|
|
1851
2119
|
const releaseCheck = run('npm', ['run', 'release:check']);
|
|
1852
|
-
if (!releaseCheck.includes(
|
|
2120
|
+
if (!releaseCheck.includes(`release notes ok: ${packageVersion}`)) fail(`release check output wrong:\n${releaseCheck}`);
|
|
1853
2121
|
const releaseNotes = run('npm', ['run', 'release:notes']);
|
|
1854
|
-
if (!releaseNotes.includes(
|
|
2122
|
+
if (!releaseNotes.includes(`## ${packageVersion}`) || !releaseNotes.includes('### Summary')) {
|
|
1855
2123
|
fail(`release notes output wrong:\n${releaseNotes}`);
|
|
1856
2124
|
}
|
|
1857
|
-
const
|
|
1858
|
-
if (!
|
|
2125
|
+
const releaseNotesWithV = run(process.execPath, [path.join(repoRoot, 'scripts', 'release-notes.mjs'), '--version', `v${packageVersion}`]);
|
|
2126
|
+
if (!releaseNotesWithV.includes(`## ${packageVersion}`) || releaseNotesWithV.includes(`## v${packageVersion}`)) {
|
|
2127
|
+
fail(`release notes v-prefixed version output wrong:\n${releaseNotesWithV}`);
|
|
2128
|
+
}
|
|
2129
|
+
const githubRelease = JSON.parse(run(process.execPath, [path.join(repoRoot, 'scripts', 'github-release.mjs'), '--dry-run', '--version', packageVersion]));
|
|
2130
|
+
if (!githubRelease.ok || !githubRelease.dry_run || githubRelease.repo !== 'Dullne/hello-cc' || githubRelease.tag !== `v${packageVersion}` || githubRelease.body_length < 100) {
|
|
1859
2131
|
fail(`github release dry run output wrong:\n${JSON.stringify(githubRelease, null, 2)}`);
|
|
1860
2132
|
}
|
|
1861
2133
|
const docsIndex = fs.readFileSync(path.join(repoRoot, 'docs', 'README.md'), 'utf8');
|
|
@@ -1905,11 +2177,151 @@ function oldNameScan() {
|
|
|
1905
2177
|
if (rg.status !== 1) fail(`rg failed:\n${rg.stderr || rg.stdout}`);
|
|
1906
2178
|
}
|
|
1907
2179
|
|
|
1908
|
-
function syntaxAndHelp() {
|
|
2180
|
+
async function syntaxAndHelp() {
|
|
1909
2181
|
log('[11/12] syntax/help');
|
|
1910
2182
|
run(process.execPath, ['--check', path.join(repoRoot, 'bin', 'hcc.mjs')]);
|
|
1911
|
-
|
|
1912
|
-
|
|
2183
|
+
for (const file of libModuleFiles()) {
|
|
2184
|
+
run(process.execPath, ['--check', path.join(repoRoot, file)]);
|
|
2185
|
+
}
|
|
2186
|
+
const hccSource = fs.readFileSync(hccBin, 'utf8');
|
|
2187
|
+
const webUiTemplateSource = fs.readFileSync(path.join(repoRoot, 'lib', 'web-ui-template.mjs'), 'utf8');
|
|
2188
|
+
for (const expected of [
|
|
2189
|
+
'function scheduleTmuxInputRefresh(session)',
|
|
2190
|
+
"runTmux(['pipe-pane', '-t', session.pane]);",
|
|
2191
|
+
'if (session.inputRefreshTimer) return;',
|
|
2192
|
+
'session.inputRefreshTimer = setTimeout',
|
|
2193
|
+
'scheduleTmuxInputRefresh(session)',
|
|
2194
|
+
"if (session.inputRefreshTimer) { clearTimeout(session.inputRefreshTimer); session.inputRefreshTimer = null; }",
|
|
2195
|
+
"broadcast(session, { type: 'replace', data: refreshTmuxSnapshot(session) });"
|
|
2196
|
+
]) {
|
|
2197
|
+
if (!hccSource.includes(expected)) fail(`web terminal input refresh support missing: ${expected}`);
|
|
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
|
+
}
|
|
2214
|
+
for (const expected of [
|
|
2215
|
+
'function webPeerAction(projectCtx, peer, action, input = {})',
|
|
2216
|
+
'function claimNextTasksForPeer(db, peer, { force = false, count = 1 } = {})',
|
|
2217
|
+
'function takeOverTaskForPeer(db, peer, id, { reason, policy = ',
|
|
2218
|
+
'function statusSummary(ctx, peer = null, identity = null)',
|
|
2219
|
+
'const peerActionMatch = url.pathname.match(/^\\/api\\/peers\\/([^/]+)\\/actions\\/([^/]+)$/)',
|
|
2220
|
+
"const readOnly = ['status', 'state', 'inbox'].includes(action)",
|
|
2221
|
+
"sendJson(res, 200, webPeerAction(reqCtx, peer, action, input));"
|
|
2222
|
+
]) {
|
|
2223
|
+
if (!hccSource.includes(expected)) fail(`web peer action API support missing: ${expected}`);
|
|
2224
|
+
}
|
|
2225
|
+
const packageVersion = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')).version;
|
|
2226
|
+
const cliVersion = run(process.execPath, [hccBin, '--version']).trim();
|
|
2227
|
+
if (cliVersion !== packageVersion) {
|
|
2228
|
+
fail(`CLI version ${cliVersion} does not match package.json ${packageVersion}`);
|
|
2229
|
+
}
|
|
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'") ||
|
|
2239
|
+
!hccSource.includes('const VERSION = PACKAGE_META.version') ||
|
|
2240
|
+
!hccSource.includes('writeGuidanceForRoot(ctx.root)')) {
|
|
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');
|
|
1913
2325
|
const mainHelp = run(process.execPath, [hccBin, '--help']);
|
|
1914
2326
|
if (mainHelp.includes('setup') || mainHelp.includes('--web-managed')) {
|
|
1915
2327
|
fail(`public help exposes maintenance or removed commands:\n${mainHelp}`);
|
|
@@ -1932,7 +2344,11 @@ function syntaxAndHelp() {
|
|
|
1932
2344
|
}
|
|
1933
2345
|
const taskHelp = run(process.execPath, [hccBin, 'task', '--help']);
|
|
1934
2346
|
if (!taskHelp.includes('task next [--peer ID] [--force]') ||
|
|
2347
|
+
!taskHelp.includes('[--count N]') ||
|
|
1935
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]') ||
|
|
1936
2352
|
!taskHelp.includes('task create --title TEXT --parent N')) {
|
|
1937
2353
|
fail(`task help missing current-task task next semantics:\n${taskHelp}`);
|
|
1938
2354
|
}
|
|
@@ -1941,9 +2357,18 @@ function syntaxAndHelp() {
|
|
|
1941
2357
|
fail(`team help missing expected content:\n${teamHelp}`);
|
|
1942
2358
|
}
|
|
1943
2359
|
const stateHelp = run(process.execPath, [hccBin, 'state', '--help']);
|
|
1944
|
-
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')) {
|
|
1945
2366
|
fail(`state help missing expected content:\n${stateHelp}`);
|
|
1946
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
|
+
}
|
|
1947
2372
|
const updateHelp = run(process.execPath, [hccBin, 'update', '--help']);
|
|
1948
2373
|
if (!updateHelp.includes('hcc update') || !updateHelp.includes('npm install -g @logicseek/hello-cc@TAG')) {
|
|
1949
2374
|
fail(`update help missing expected content:\n${updateHelp}`);
|
|
@@ -2054,7 +2479,7 @@ async function main() {
|
|
|
2054
2479
|
await askBroadcastWorkflow();
|
|
2055
2480
|
await downGcPackWorkflow();
|
|
2056
2481
|
oldNameScan();
|
|
2057
|
-
syntaxAndHelp();
|
|
2482
|
+
await syntaxAndHelp();
|
|
2058
2483
|
uninstallWorkflow();
|
|
2059
2484
|
log('FULL_REGRESSION_OK');
|
|
2060
2485
|
}
|