@logicseek/hello-cc 0.1.6 → 0.1.7
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 +55 -0
- package/README.md +21 -3
- package/README.zh-CN.md +21 -3
- package/assets/screenshots/web-console.png +0 -0
- package/bin/hcc.mjs +409 -96
- package/lib/cli-runtime.mjs +2 -1
- package/lib/core/coordination/tasks.mjs +17 -1
- package/lib/core/coordination/timeline.mjs +10 -1
- package/lib/core/peers/identity.mjs +10 -2
- package/lib/web/peer-actions.mjs +42 -6
- package/lib/web/ui-template.mjs +5 -3
- package/package.json +1 -1
- package/scripts/regression.mjs +422 -51
package/scripts/regression.mjs
CHANGED
|
@@ -22,6 +22,9 @@ const fakeBin = path.join(os.tmpdir(), `hcc-reg-bin-${testId}`);
|
|
|
22
22
|
const outDir = path.join(os.tmpdir(), `hcc-reg-out-${testId}`);
|
|
23
23
|
const tmuxSession = `hcc-reg-${process.pid}`;
|
|
24
24
|
const tmuxSocketName = `hcc-reg-${testId}`.replace(/[^A-Za-z0-9_-]/g, '-');
|
|
25
|
+
const secondProjectRoot = path.join(root, 'second-project');
|
|
26
|
+
const realHome = process.env.HOME || os.homedir();
|
|
27
|
+
const realRegistryFile = path.join(realHome, '.hello-cc', 'projects.json');
|
|
25
28
|
const realTmuxBin = spawnSync('sh', ['-lc', 'command -v tmux || true'], {
|
|
26
29
|
encoding: 'utf8',
|
|
27
30
|
stdio: ['ignore', 'pipe', 'ignore']
|
|
@@ -72,6 +75,10 @@ function tmuxManagedSession(projectRoot, peer) {
|
|
|
72
75
|
return `hcc-${shortHash(projectRoot)}-${sanitizePeerPart(peer, 'peer')}`.slice(0, 80);
|
|
73
76
|
}
|
|
74
77
|
|
|
78
|
+
function tmuxManagedSessionPrefix(projectRoot) {
|
|
79
|
+
return `hcc-${shortHash(projectRoot)}-`;
|
|
80
|
+
}
|
|
81
|
+
|
|
75
82
|
function run(command, args, options = {}) {
|
|
76
83
|
const result = spawnSync(command, args, {
|
|
77
84
|
cwd: options.cwd || repoRoot,
|
|
@@ -140,6 +147,33 @@ function providerBindingRows(provider, sessionName) {
|
|
|
140
147
|
`).all(provider, sessionName));
|
|
141
148
|
}
|
|
142
149
|
|
|
150
|
+
function parsePayloadJson(value) {
|
|
151
|
+
try {
|
|
152
|
+
const parsed = JSON.parse(value || '{}');
|
|
153
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
154
|
+
} catch {
|
|
155
|
+
return {};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function eventPayloads(type, limit = 20, dbPath = path.join(root, '.hello-cc', 'mesh.db')) {
|
|
160
|
+
const db = new DatabaseSync(dbPath);
|
|
161
|
+
try {
|
|
162
|
+
return db.prepare(`
|
|
163
|
+
SELECT actor, payload
|
|
164
|
+
FROM events
|
|
165
|
+
WHERE type = ?
|
|
166
|
+
ORDER BY id DESC
|
|
167
|
+
LIMIT ?
|
|
168
|
+
`).all(type, limit).map((row) => ({
|
|
169
|
+
actor: row.actor,
|
|
170
|
+
payload: parsePayloadJson(row.payload)
|
|
171
|
+
}));
|
|
172
|
+
} finally {
|
|
173
|
+
db.close();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
143
177
|
function peerBindingIndexNames(dbPath = path.join(root, '.hello-cc', 'mesh.db')) {
|
|
144
178
|
const db = new DatabaseSync(dbPath);
|
|
145
179
|
try {
|
|
@@ -773,6 +807,48 @@ function tmuxAvailable() {
|
|
|
773
807
|
return runMaybe('tmux', ['-V']).status === 0;
|
|
774
808
|
}
|
|
775
809
|
|
|
810
|
+
function tmuxCleanupEndpoints() {
|
|
811
|
+
if (!realTmuxBin) return [{ label: 'default', command: 'tmux', args: [] }];
|
|
812
|
+
return [
|
|
813
|
+
{ label: 'test-socket', command: realTmuxBin, args: ['-L', tmuxSocketName] },
|
|
814
|
+
{ label: 'default', command: realTmuxBin, args: [] }
|
|
815
|
+
];
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function runMaybeTmuxEndpoint(endpoint, args) {
|
|
819
|
+
return runMaybe(endpoint.command, [...endpoint.args, ...args]);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function listTmuxSessionsMatching(prefixes, endpoint) {
|
|
823
|
+
const output = runMaybeTmuxEndpoint(endpoint, ['list-sessions', '-F', '#{session_name}']);
|
|
824
|
+
if (output.status !== 0) return [];
|
|
825
|
+
return (output.stdout || '')
|
|
826
|
+
.split('\n')
|
|
827
|
+
.map((line) => line.trim())
|
|
828
|
+
.filter(Boolean)
|
|
829
|
+
.filter((session) => prefixes.some((prefix) => session.startsWith(prefix)));
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function killTmuxSessionOnEndpoint(endpoint, session) {
|
|
833
|
+
runMaybeTmuxEndpoint(endpoint, ['kill-session', '-t', session]);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function assertNoRegressionTmuxSessionLeak() {
|
|
837
|
+
const prefixes = [
|
|
838
|
+
tmuxManagedSessionPrefix(root),
|
|
839
|
+
tmuxManagedSessionPrefix(secondProjectRoot)
|
|
840
|
+
];
|
|
841
|
+
const leaks = [];
|
|
842
|
+
for (const endpoint of tmuxCleanupEndpoints()) {
|
|
843
|
+
for (const session of listTmuxSessionsMatching(prefixes, endpoint)) {
|
|
844
|
+
leaks.push(`${endpoint.label}:${session}`);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (leaks.length) {
|
|
848
|
+
fail(`regression leaked hcc-managed tmux sessions:\n${leaks.join('\n')}`);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
776
852
|
function trackTmuxPane(pane) {
|
|
777
853
|
if (!pane) return;
|
|
778
854
|
const result = runMaybe('tmux', ['display-message', '-p', '-t', pane, '#S']);
|
|
@@ -1139,11 +1215,20 @@ function cleanup() {
|
|
|
1139
1215
|
if (runtimePid) {
|
|
1140
1216
|
try { process.kill(runtimePid, 'SIGTERM'); } catch {}
|
|
1141
1217
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1218
|
+
const managedPrefixes = [
|
|
1219
|
+
tmuxManagedSessionPrefix(root),
|
|
1220
|
+
tmuxManagedSessionPrefix(secondProjectRoot)
|
|
1221
|
+
];
|
|
1222
|
+
for (const endpoint of tmuxCleanupEndpoints()) {
|
|
1223
|
+
for (const session of listTmuxSessionsMatching(managedPrefixes, endpoint)) {
|
|
1224
|
+
managedTmuxSessions.add(session);
|
|
1225
|
+
}
|
|
1226
|
+
if (tmuxStarted) {
|
|
1227
|
+
killTmuxSessionOnEndpoint(endpoint, tmuxSession);
|
|
1228
|
+
}
|
|
1229
|
+
for (const session of managedTmuxSessions) {
|
|
1230
|
+
killTmuxSessionOnEndpoint(endpoint, session);
|
|
1231
|
+
}
|
|
1147
1232
|
}
|
|
1148
1233
|
if (fs.existsSync(path.join(fakeBin, 'tmux'))) {
|
|
1149
1234
|
runMaybe('tmux', ['kill-server']);
|
|
@@ -1153,8 +1238,36 @@ function cleanup() {
|
|
|
1153
1238
|
}
|
|
1154
1239
|
}
|
|
1155
1240
|
|
|
1241
|
+
function assertNoRealProjectRegistryLeak() {
|
|
1242
|
+
const after = fs.existsSync(realRegistryFile)
|
|
1243
|
+
? fs.readFileSync(realRegistryFile, 'utf8')
|
|
1244
|
+
: null;
|
|
1245
|
+
if (!after) return;
|
|
1246
|
+
|
|
1247
|
+
let parsed = null;
|
|
1248
|
+
try {
|
|
1249
|
+
parsed = JSON.parse(after);
|
|
1250
|
+
} catch {
|
|
1251
|
+
fail(`regression changed real project registry with invalid JSON: ${realRegistryFile}`);
|
|
1252
|
+
}
|
|
1253
|
+
const leakedRoots = (parsed?.projects || [])
|
|
1254
|
+
.map((project) => String(project?.root || ''))
|
|
1255
|
+
.filter((projectRoot) => {
|
|
1256
|
+
if (!projectRoot) return false;
|
|
1257
|
+
if (projectRoot === root || projectRoot === home || projectRoot === fakeBin || projectRoot === outDir) return true;
|
|
1258
|
+
return projectRoot.startsWith(`${root}${path.sep}`) ||
|
|
1259
|
+
projectRoot.startsWith(`${home}${path.sep}`) ||
|
|
1260
|
+
projectRoot.startsWith(`${fakeBin}${path.sep}`) ||
|
|
1261
|
+
projectRoot.startsWith(`${outDir}${path.sep}`) ||
|
|
1262
|
+
projectRoot.includes(`hcc-reg-${testId}`);
|
|
1263
|
+
});
|
|
1264
|
+
if (leakedRoots.length) {
|
|
1265
|
+
fail(`regression leaked temporary projects into real registry ${realRegistryFile}:\n${leakedRoots.join('\n')}`);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1156
1269
|
async function setupRegression() {
|
|
1157
|
-
log('[1/
|
|
1270
|
+
log('[1/13] web bootstrap/hooks/shims');
|
|
1158
1271
|
writeFakeTools();
|
|
1159
1272
|
fs.mkdirSync(root, { recursive: true });
|
|
1160
1273
|
fs.mkdirSync(home, { recursive: true });
|
|
@@ -1338,7 +1451,7 @@ async function setupRegression() {
|
|
|
1338
1451
|
}
|
|
1339
1452
|
|
|
1340
1453
|
async function dbWorkflow() {
|
|
1341
|
-
log('[3/
|
|
1454
|
+
log('[3/13] db workflow');
|
|
1342
1455
|
hcc(['register', '--peer', 'human', '--kind', 'human', '--role', 'operator']);
|
|
1343
1456
|
const created = hcc(['task', 'create', '--from', 'human', '--to', 'codex-a', '--title', 'full regression task', '--body', 'exercise hcc bus']);
|
|
1344
1457
|
const taskMatch = created.match(/created task #(\d+):/);
|
|
@@ -1505,7 +1618,8 @@ async function dbWorkflow() {
|
|
|
1505
1618
|
!staleLivenessState.automation.next_action.argv.includes('stale')) {
|
|
1506
1619
|
fail(`state automation did not suggest stale takeover:\n${JSON.stringify(staleLivenessState.automation, null, 2)}`);
|
|
1507
1620
|
}
|
|
1508
|
-
hcc(['task', '
|
|
1621
|
+
hcc(['task', 'takeover', '--peer', 'human', '--id', staleLivenessTaskId, '--reason', 'stale liveness cleanup', '--policy', 'stale', '--stale-after', '60']);
|
|
1622
|
+
hcc(['task', 'update', '--peer', 'human', '--id', staleLivenessTaskId, '--status', 'abandoned', '--summary', 'stale liveness cleanup']);
|
|
1509
1623
|
hcc(['task', 'update', '--peer', 'human', '--id', queuedTaskId, '--status', 'abandoned', '--summary', 'queued task cleanup']);
|
|
1510
1624
|
hcc(['handoff', 'create', '--from', 'codex-a', '--to', 'claude-a', '--task', taskId, '--summary', 'handoff summary', '--tests', 'full script', '--risks', 'none']);
|
|
1511
1625
|
if (!hcc(['handoff', 'list', '--task', taskId]).includes('handoff summary')) fail('handoff missing');
|
|
@@ -1727,8 +1841,8 @@ async function dbWorkflow() {
|
|
|
1727
1841
|
}
|
|
1728
1842
|
|
|
1729
1843
|
async function multiProjectWebWorkflow() {
|
|
1730
|
-
log('[4/
|
|
1731
|
-
const otherRoot =
|
|
1844
|
+
log('[4/13] multi-project web');
|
|
1845
|
+
const otherRoot = secondProjectRoot;
|
|
1732
1846
|
fs.mkdirSync(otherRoot, { recursive: true });
|
|
1733
1847
|
const output = hccFrom(['web', '--local', '--port', String(port), '--no-discover', '--no-guidance'], otherRoot);
|
|
1734
1848
|
if (!output.includes('web already running in background')) fail(`second project did not reuse global web:\n${output}`);
|
|
@@ -1907,6 +2021,72 @@ async function multiProjectWebWorkflow() {
|
|
|
1907
2021
|
fail(`sessions API did not expose unknown provider binding without marking it shared/known:\n${JSON.stringify(otherShellSession, null, 2)}`);
|
|
1908
2022
|
}
|
|
1909
2023
|
|
|
2024
|
+
const startProvider = async (payload) => {
|
|
2025
|
+
const response = await runtimeFetch('/api/sessions', {
|
|
2026
|
+
method: 'POST',
|
|
2027
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2028
|
+
body: JSON.stringify({
|
|
2029
|
+
env: {
|
|
2030
|
+
HOME: home,
|
|
2031
|
+
PATH: env.PATH,
|
|
2032
|
+
SHELL: process.env.SHELL || 'bash',
|
|
2033
|
+
HCC_FAKE_STAY_ALIVE: '1'
|
|
2034
|
+
},
|
|
2035
|
+
...payload
|
|
2036
|
+
})
|
|
2037
|
+
}, { root });
|
|
2038
|
+
const json = await response.json();
|
|
2039
|
+
if (!response.ok) fail(`web provider session start failed: ${JSON.stringify(json)}`);
|
|
2040
|
+
return json.session;
|
|
2041
|
+
};
|
|
2042
|
+
const stopSession = async (id, payload = null) => {
|
|
2043
|
+
const options = payload
|
|
2044
|
+
? {
|
|
2045
|
+
method: 'POST',
|
|
2046
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2047
|
+
body: JSON.stringify(payload)
|
|
2048
|
+
}
|
|
2049
|
+
: { method: 'POST' };
|
|
2050
|
+
const response = await runtimeFetch(`/api/sessions/${encodeURIComponent(id)}/stop`, options, { root });
|
|
2051
|
+
const json = await response.json();
|
|
2052
|
+
if (!response.ok) fail(`web provider session stop failed: ${JSON.stringify(json)}`);
|
|
2053
|
+
return json.session;
|
|
2054
|
+
};
|
|
2055
|
+
|
|
2056
|
+
const auditSpoofSession = await startProvider({
|
|
2057
|
+
kind: 'shell',
|
|
2058
|
+
command: 'bash --noprofile --norc',
|
|
2059
|
+
auditActorPeer: 'spoofed-web-actor',
|
|
2060
|
+
auditSource: 'spoofed-source'
|
|
2061
|
+
});
|
|
2062
|
+
const auditSpoofPeer = auditSpoofSession.peer_id || auditSpoofSession.id;
|
|
2063
|
+
const attachAudit = eventPayloads('tmux.session.attached', 80)
|
|
2064
|
+
.find((event) => event.payload?.target_peer === auditSpoofPeer);
|
|
2065
|
+
if (!attachAudit ||
|
|
2066
|
+
attachAudit.actor !== 'web' ||
|
|
2067
|
+
attachAudit.payload.actor_peer !== 'web' ||
|
|
2068
|
+
attachAudit.payload.source !== 'web' ||
|
|
2069
|
+
attachAudit.payload.actor_peer === 'spoofed-web-actor' ||
|
|
2070
|
+
attachAudit.payload.source === 'spoofed-source') {
|
|
2071
|
+
fail(`web start audit allowed spoofed actor/source:\n${JSON.stringify({ auditSpoofSession, attachAudit }, null, 2)}`);
|
|
2072
|
+
}
|
|
2073
|
+
await stopSession(auditSpoofSession.id, {
|
|
2074
|
+
auditActorPeer: 'spoofed-stop-actor',
|
|
2075
|
+
auditSource: 'spoofed-source'
|
|
2076
|
+
});
|
|
2077
|
+
const stopAudit = eventPayloads('web.session.stop_requested', 80)
|
|
2078
|
+
.find((event) => event.payload?.target_peer === auditSpoofPeer);
|
|
2079
|
+
if (!stopAudit ||
|
|
2080
|
+
stopAudit.actor !== 'web' ||
|
|
2081
|
+
stopAudit.payload.actor_peer !== 'web' ||
|
|
2082
|
+
stopAudit.payload.target_peer !== auditSpoofPeer ||
|
|
2083
|
+
stopAudit.payload.source !== 'web' ||
|
|
2084
|
+
stopAudit.payload.admin !== true ||
|
|
2085
|
+
stopAudit.payload.actor_peer === 'spoofed-stop-actor' ||
|
|
2086
|
+
stopAudit.payload.source === 'spoofed-source') {
|
|
2087
|
+
fail(`web stop audit allowed spoofed actor/source:\n${JSON.stringify({ auditSpoofSession, stopAudit }, null, 2)}`);
|
|
2088
|
+
}
|
|
2089
|
+
|
|
1910
2090
|
hcc(['register', '--peer', 'web-action-peer', '--kind', 'codex', '--role', 'peer']);
|
|
1911
2091
|
hcc(['msg', 'send', '--from', 'human', '--to', 'web-action-peer', '--body', 'web action inbox ok']);
|
|
1912
2092
|
const actionStatus = await (await runtimeFetch('/api/peers/web-action-peer/actions/status', {}, { root })).json();
|
|
@@ -1924,10 +2104,28 @@ async function multiProjectWebWorkflow() {
|
|
|
1924
2104
|
const taskOutput = hcc(['task', 'create', '--title', 'web action task', '--body', 'claim through web action']);
|
|
1925
2105
|
const taskMatch = taskOutput.match(/created task #(\d+):/);
|
|
1926
2106
|
if (!taskMatch) fail(`cannot parse web action task id:\n${taskOutput}`);
|
|
1927
|
-
const
|
|
2107
|
+
const rejectedActionNextResponse = await runtimeFetch('/api/peers/web-action-peer/actions/task-next', {
|
|
1928
2108
|
method: 'POST',
|
|
1929
2109
|
headers: { 'Content-Type': 'application/json' },
|
|
1930
2110
|
body: '{}'
|
|
2111
|
+
}, { root });
|
|
2112
|
+
const rejectedActionNext = await rejectedActionNextResponse.json();
|
|
2113
|
+
if (rejectedActionNextResponse.ok || rejectedActionNext.error?.code !== 'PEER_IDENTITY_REQUIRED') {
|
|
2114
|
+
fail(`web task-next accepted unmanaged peer identity:\n${JSON.stringify(rejectedActionNext, null, 2)}`);
|
|
2115
|
+
}
|
|
2116
|
+
const unmanagedTaskAfterReject = hccJson(['task', 'list', '--all']).find((task) => String(task.id) === taskMatch[1]);
|
|
2117
|
+
if (!unmanagedTaskAfterReject || unmanagedTaskAfterReject.owner) {
|
|
2118
|
+
fail(`rejected web task-next still changed task owner:\n${JSON.stringify(unmanagedTaskAfterReject, null, 2)}`);
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
const managedActionSession = await startProvider({ kind: 'shell', command: 'bash --noprofile --norc' });
|
|
2122
|
+
if (!managedActionSession.action_token) {
|
|
2123
|
+
fail(`managed web session did not include action token:\n${JSON.stringify(managedActionSession, null, 2)}`);
|
|
2124
|
+
}
|
|
2125
|
+
const actionNext = await (await runtimeFetch(`/api/peers/${encodeURIComponent(managedActionSession.peer_id || managedActionSession.id)}/actions/task-next`, {
|
|
2126
|
+
method: 'POST',
|
|
2127
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2128
|
+
body: JSON.stringify({ action_token: managedActionSession.action_token })
|
|
1931
2129
|
}, { root })).json();
|
|
1932
2130
|
if (!actionNext.ok || actionNext.action !== 'task-next' || String(actionNext.data?.task?.id) !== taskMatch[1]) {
|
|
1933
2131
|
fail(`web task-next action did not claim pending task #${taskMatch[1]}:\n${JSON.stringify(actionNext, null, 2)}`);
|
|
@@ -1937,31 +2135,51 @@ async function multiProjectWebWorkflow() {
|
|
|
1937
2135
|
headers: { 'Content-Type': 'application/json' },
|
|
1938
2136
|
body: JSON.stringify({ renew_locks: true })
|
|
1939
2137
|
}, { root })).json();
|
|
1940
|
-
if (
|
|
1941
|
-
fail(`web heartbeat
|
|
2138
|
+
if (actionHeartbeat.ok || actionHeartbeat.error?.code !== 'PEER_IDENTITY_REQUIRED') {
|
|
2139
|
+
fail(`web heartbeat accepted unmanaged peer identity:\n${JSON.stringify(actionHeartbeat, null, 2)}`);
|
|
1942
2140
|
}
|
|
1943
2141
|
|
|
1944
|
-
const
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
}
|
|
2142
|
+
const ownerTask = hcc(['task', 'create', '--from', 'web-lock-owner', '--title', 'web lock owner task']);
|
|
2143
|
+
const ownerTaskMatch = ownerTask.match(/created task #(\d+):/);
|
|
2144
|
+
if (!ownerTaskMatch) fail(`cannot parse web lock owner task id:\n${ownerTask}`);
|
|
2145
|
+
hcc(['task', 'claim', '--peer', 'web-lock-owner', '--id', ownerTaskMatch[1]]);
|
|
2146
|
+
const rejectedLockResponse = await runtimeFetch(`/api/peers/${encodeURIComponent(managedActionSession.peer_id || managedActionSession.id)}/actions/lock-acquire`, {
|
|
2147
|
+
method: 'POST',
|
|
2148
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2149
|
+
body: JSON.stringify({
|
|
2150
|
+
action_token: managedActionSession.action_token,
|
|
2151
|
+
resource: 'web/identity-spoof-lock',
|
|
2152
|
+
task: Number(ownerTaskMatch[1])
|
|
2153
|
+
})
|
|
2154
|
+
}, { root });
|
|
2155
|
+
const rejectedLock = await rejectedLockResponse.json();
|
|
2156
|
+
if (rejectedLockResponse.ok || rejectedLock.error?.code !== 'TASK_OWNED') {
|
|
2157
|
+
fail(`web lock-acquire did not enforce task owner:\n${JSON.stringify(rejectedLock, null, 2)}`);
|
|
2158
|
+
}
|
|
2159
|
+
const lockRowsAfterReject = hccJson(['lock', 'list', '--all']);
|
|
2160
|
+
if (lockRowsAfterReject.some((lock) => lock.resource === 'web/identity-spoof-lock')) {
|
|
2161
|
+
fail(`rejected web lock-acquire still inserted a lock:\n${JSON.stringify(lockRowsAfterReject, null, 2)}`);
|
|
2162
|
+
}
|
|
2163
|
+
hcc(['task', 'done', '--peer', managedActionSession.peer_id || managedActionSession.id, '--id', taskMatch[1], '--summary', 'web action cleanup'], {
|
|
2164
|
+
env: { ...env, HCC_PEER: managedActionSession.peer_id || managedActionSession.id }
|
|
2165
|
+
});
|
|
2166
|
+
hcc(['task', 'done', '--peer', 'web-lock-owner', '--id', ownerTaskMatch[1], '--summary', 'web lock owner cleanup'], {
|
|
2167
|
+
env: { ...env, HCC_PEER: 'web-lock-owner' }
|
|
2168
|
+
});
|
|
2169
|
+
await stopSession(managedActionSession.id);
|
|
2170
|
+
|
|
2171
|
+
hcc(['register', '--peer', 'detected-msg-peer', '--kind', 'codex', '--role', 'peer']);
|
|
2172
|
+
const spoofedDetectedMessage = await (await runtimeFetch('/api/detected/detected-msg-peer/msg', {
|
|
2173
|
+
method: 'POST',
|
|
2174
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2175
|
+
body: JSON.stringify({ from: 'web-action-peer', body: 'detected sender spoof attempt' })
|
|
2176
|
+
}, { root })).json();
|
|
2177
|
+
if (!spoofedDetectedMessage.ok) fail(`detected message send failed:\n${JSON.stringify(spoofedDetectedMessage, null, 2)}`);
|
|
2178
|
+
const detectedInbox = hccJson(['msg', 'inbox', '--peer', 'detected-msg-peer', '--all']);
|
|
2179
|
+
const detectedMsg = detectedInbox.find((message) => message.body === 'detected sender spoof attempt');
|
|
2180
|
+
if (!detectedMsg || detectedMsg.sender !== 'web') {
|
|
2181
|
+
fail(`detected message sender spoof was not forced to web:\n${JSON.stringify(detectedInbox, null, 2)}`);
|
|
2182
|
+
}
|
|
1965
2183
|
|
|
1966
2184
|
const claudeResumeName = `web-claude-resume-${testId}`;
|
|
1967
2185
|
const claudeResume = await startProvider({ kind: 'claude', mode: 'resume', resume: claudeResumeName });
|
|
@@ -2069,11 +2287,11 @@ async function multiProjectWebWorkflow() {
|
|
|
2069
2287
|
|
|
2070
2288
|
async function tmuxBackedStartWorkflow() {
|
|
2071
2289
|
if (!tmuxAvailable()) {
|
|
2072
|
-
log('[5/
|
|
2290
|
+
log('[5/13] tmux-backed start skipped (tmux not installed)');
|
|
2073
2291
|
return;
|
|
2074
2292
|
}
|
|
2075
2293
|
|
|
2076
|
-
log('[5/
|
|
2294
|
+
log('[5/13] tmux-backed start + websocket + restore');
|
|
2077
2295
|
const file = path.join(outDir, 'pty-ok');
|
|
2078
2296
|
const started = hcc(['peer', 'start', 'shell-a', '--kind', 'shell', '--', 'bash']);
|
|
2079
2297
|
const pane = parsePane(started);
|
|
@@ -2178,11 +2396,11 @@ async function tmuxBackedStartWorkflow() {
|
|
|
2178
2396
|
|
|
2179
2397
|
async function shimTmuxWorkflow() {
|
|
2180
2398
|
if (!tmuxAvailable()) {
|
|
2181
|
-
log('[6/
|
|
2399
|
+
log('[6/13] shim tmux-backed launch skipped (tmux not installed)');
|
|
2182
2400
|
return;
|
|
2183
2401
|
}
|
|
2184
2402
|
|
|
2185
|
-
log('[6/
|
|
2403
|
+
log('[6/13] shim tmux-backed launch');
|
|
2186
2404
|
const shim = path.join(home, '.hcc-shims', 'claude');
|
|
2187
2405
|
const codexShim = path.join(home, '.hcc-shims', 'codex');
|
|
2188
2406
|
const claudeVersion = run(shim, ['--version'], { cwd: root, env });
|
|
@@ -2292,11 +2510,11 @@ async function shimTmuxWorkflow() {
|
|
|
2292
2510
|
async function tmuxWorkflow() {
|
|
2293
2511
|
const tmuxVersion = runMaybe('tmux', ['-V']);
|
|
2294
2512
|
if (tmuxVersion.status !== 0) {
|
|
2295
|
-
log('[7/
|
|
2513
|
+
log('[7/13] tmux skipped (tmux not installed)');
|
|
2296
2514
|
return;
|
|
2297
2515
|
}
|
|
2298
2516
|
|
|
2299
|
-
log('[7/
|
|
2517
|
+
log('[7/13] tmux attach + websocket + force');
|
|
2300
2518
|
run('tmux', ['new-session', '-d', '-s', tmuxSession, '-c', os.tmpdir(), 'bash --noprofile --norc']);
|
|
2301
2519
|
tmuxStarted = true;
|
|
2302
2520
|
const pane = run('tmux', ['display-message', '-p', '-t', `${tmuxSession}:0.0`, '#{pane_id}']).trim();
|
|
@@ -2318,11 +2536,11 @@ async function tmuxWorkflow() {
|
|
|
2318
2536
|
|
|
2319
2537
|
async function askBroadcastWorkflow() {
|
|
2320
2538
|
if (!tmuxAvailable()) {
|
|
2321
|
-
log('[8/
|
|
2539
|
+
log('[8/13] ask/broadcast injection skipped (tmux not installed)');
|
|
2322
2540
|
return;
|
|
2323
2541
|
}
|
|
2324
2542
|
|
|
2325
|
-
log('[8/
|
|
2543
|
+
log('[8/13] ask/broadcast injection');
|
|
2326
2544
|
const askFile = path.join(outDir, 'ask-ok');
|
|
2327
2545
|
parsePane(hcc(['peer', 'start', 'shell-b', '--kind', 'shell', '--', 'bash']));
|
|
2328
2546
|
hcc(['ask', 'shell-b', `echo ASK_OK > ${askFile}`, '--from', 'human', '--inject']);
|
|
@@ -2334,7 +2552,7 @@ async function askBroadcastWorkflow() {
|
|
|
2334
2552
|
}
|
|
2335
2553
|
|
|
2336
2554
|
async function downGcPackWorkflow() {
|
|
2337
|
-
log('[9/
|
|
2555
|
+
log('[9/13] down/gc/pack');
|
|
2338
2556
|
hccMaybe(['peer', 'stop', 'shell-a']);
|
|
2339
2557
|
hccMaybe(['peer', 'stop', 'shell-b']);
|
|
2340
2558
|
await stopRuntime();
|
|
@@ -2391,7 +2609,7 @@ async function downGcPackWorkflow() {
|
|
|
2391
2609
|
}
|
|
2392
2610
|
|
|
2393
2611
|
function oldNameScan() {
|
|
2394
|
-
log('[10/
|
|
2612
|
+
log('[10/13] old-name scan');
|
|
2395
2613
|
const oldNamePattern = [
|
|
2396
2614
|
'agent' + 'mesh',
|
|
2397
2615
|
'Agent' + 'mesh',
|
|
@@ -2414,8 +2632,125 @@ function oldNameScan() {
|
|
|
2414
2632
|
if (rg.status !== 1) fail(`rg failed:\n${rg.stderr || rg.stdout}`);
|
|
2415
2633
|
}
|
|
2416
2634
|
|
|
2635
|
+
function identityEnforcementWorkflow() {
|
|
2636
|
+
log('[11/13] identity enforcement');
|
|
2637
|
+
const identityRoot = fs.mkdtempSync(path.join(os.tmpdir(), `hcc-reg-identity-root-${testId}-`));
|
|
2638
|
+
const identityHome = fs.mkdtempSync(path.join(os.tmpdir(), `hcc-reg-identity-home-${testId}-`));
|
|
2639
|
+
const identityEnv = {
|
|
2640
|
+
...env,
|
|
2641
|
+
HOME: identityHome,
|
|
2642
|
+
HCC_PEER: 'real-peer'
|
|
2643
|
+
};
|
|
2644
|
+
delete identityEnv.HCC_ROOT;
|
|
2645
|
+
delete identityEnv.HCC_DB;
|
|
2646
|
+
try {
|
|
2647
|
+
hccFrom(['register', '--peer', 'fake-peer', '--kind', 'codex', '--role', 'peer'], identityRoot, { env: identityEnv });
|
|
2648
|
+
const envOut = hccFrom(['env', '--peer', 'fake-peer'], identityRoot, { env: identityEnv });
|
|
2649
|
+
if (!envOut.includes('HCC_PEER=fake-peer')) {
|
|
2650
|
+
fail(`env did not export explicit target peer:\n${envOut}`);
|
|
2651
|
+
}
|
|
2652
|
+
const created = hccFrom(['task', 'create', '--from', 'fake-peer', '--title', 'identity guard'], identityRoot, { env: identityEnv });
|
|
2653
|
+
const match = created.match(/created task #(\d+):/);
|
|
2654
|
+
if (!match) fail(`cannot parse identity task id:\n${created}`);
|
|
2655
|
+
const taskId = match[1];
|
|
2656
|
+
hccFrom(['task', 'claim', '--peer', 'fake-peer', '--id', taskId], identityRoot, { env: identityEnv });
|
|
2657
|
+
|
|
2658
|
+
const dbPath = path.join(identityRoot, '.hello-cc', 'mesh.db');
|
|
2659
|
+
const db = new DatabaseSync(dbPath);
|
|
2660
|
+
try {
|
|
2661
|
+
const task = db.prepare('SELECT owner, created_by FROM tasks WHERE id = ?').get(Number(taskId));
|
|
2662
|
+
const fakePeer = db.prepare('SELECT id FROM peers WHERE id = ?').get('fake-peer');
|
|
2663
|
+
if (task?.owner !== 'real-peer' || task?.created_by !== 'real-peer' || fakePeer) {
|
|
2664
|
+
fail(`system peer identity was not enforced:\n${JSON.stringify({ task, fakePeer }, null, 2)}`);
|
|
2665
|
+
}
|
|
2666
|
+
db.prepare(`
|
|
2667
|
+
INSERT INTO peers(id, kind, role, worktree, branch, pid, status, capabilities, created_at, last_seen_at)
|
|
2668
|
+
VALUES ('inspect-peer', 'codex', 'peer', ?, '', NULL, 'working', '', 1, 1)
|
|
2669
|
+
`).run(identityRoot);
|
|
2670
|
+
db.prepare(`
|
|
2671
|
+
INSERT INTO peers(id, kind, role, worktree, branch, pid, status, capabilities, created_at, last_seen_at)
|
|
2672
|
+
VALUES ('owner-peer', 'codex', 'peer', ?, '', NULL, 'working', '', 1, 1)
|
|
2673
|
+
`).run(identityRoot);
|
|
2674
|
+
db.prepare(`
|
|
2675
|
+
INSERT INTO tasks(title, body, status, assignee, owner, priority, created_by, created_at, updated_at)
|
|
2676
|
+
VALUES ('owned identity guard', '', 'claimed', NULL, 'owner-peer', 100, 'owner-peer', 1, 1)
|
|
2677
|
+
`).run();
|
|
2678
|
+
} finally {
|
|
2679
|
+
db.close();
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
const statusOut = hccFrom(['status', '--peer', 'inspect-peer'], identityRoot, { env: identityEnv });
|
|
2683
|
+
if (!statusOut.includes('inbox(inspect-peer)')) {
|
|
2684
|
+
fail(`status did not inspect explicit target peer:\n${statusOut}`);
|
|
2685
|
+
}
|
|
2686
|
+
const stateOut = hccFrom(['state', '--peer', 'inspect-peer'], identityRoot, { env: identityEnv });
|
|
2687
|
+
if (!stateOut.includes('peer: inspect-peer')) {
|
|
2688
|
+
fail(`state did not inspect explicit target peer:\n${stateOut}`);
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
const owned = hccFromMaybe(['task', 'done', '--peer', 'owner-peer', '--id', '2', '--summary', 'should not pass'], identityRoot, { env: identityEnv });
|
|
2692
|
+
if (owned.status === 0 || !`${owned.stdout}\n${owned.stderr}`.includes('Task #2 is owned by owner-peer')) {
|
|
2693
|
+
fail(`owned task mutation was not rejected:\nstdout=${owned.stdout}\nstderr=${owned.stderr}`);
|
|
2694
|
+
}
|
|
2695
|
+
const forcedOwned = hccFromMaybe(['task', 'done', '--force', '--peer', 'owner-peer', '--id', '2', '--summary', 'should not pass'], identityRoot, { env: identityEnv });
|
|
2696
|
+
if (forcedOwned.status === 0 || !`${forcedOwned.stdout}\n${forcedOwned.stderr}`.includes('Task #2 is owned by owner-peer')) {
|
|
2697
|
+
fail(`forced owned task mutation was not rejected:\nstdout=${forcedOwned.stdout}\nstderr=${forcedOwned.stderr}`);
|
|
2698
|
+
}
|
|
2699
|
+
const db2 = new DatabaseSync(dbPath);
|
|
2700
|
+
try {
|
|
2701
|
+
const task = db2.prepare('SELECT owner, status FROM tasks WHERE id = 2').get();
|
|
2702
|
+
const notice = db2.prepare(`
|
|
2703
|
+
SELECT sender, recipient, kind
|
|
2704
|
+
FROM messages
|
|
2705
|
+
WHERE task_id = 2 AND kind = 'task.owner-conflict'
|
|
2706
|
+
`).get();
|
|
2707
|
+
if (task?.owner !== 'owner-peer' || task?.status !== 'claimed' ||
|
|
2708
|
+
!notice || notice.sender !== 'real-peer' || notice.recipient !== 'owner-peer') {
|
|
2709
|
+
fail(`owned task mutation did not preserve owner and notify:\n${JSON.stringify({ task, notice }, null, 2)}`);
|
|
2710
|
+
}
|
|
2711
|
+
} finally {
|
|
2712
|
+
db2.close();
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
hccFrom(['run', '--peer', 'fake-runner', '--', process.execPath, '-e', 'process.exit(0)'], identityRoot, { env: identityEnv });
|
|
2716
|
+
const db3 = new DatabaseSync(dbPath);
|
|
2717
|
+
try {
|
|
2718
|
+
const realRunner = db3.prepare('SELECT id, status FROM peers WHERE id = ?').get('real-peer');
|
|
2719
|
+
const fakeRunner = db3.prepare('SELECT id FROM peers WHERE id = ?').get('fake-runner');
|
|
2720
|
+
if (!realRunner || realRunner.status !== 'exited' || fakeRunner) {
|
|
2721
|
+
fail(`run did not enforce system peer identity:\n${JSON.stringify({ realRunner, fakeRunner }, null, 2)}`);
|
|
2722
|
+
}
|
|
2723
|
+
} finally {
|
|
2724
|
+
db3.close();
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
hccFrom(['peer', 'stop', 'fake-target'], identityRoot, { env: identityEnv });
|
|
2728
|
+
const stopRequested = eventPayloads('peer.stop.requested', 5, dbPath)
|
|
2729
|
+
.find((event) => event.payload?.target_peer === 'fake-target');
|
|
2730
|
+
if (!stopRequested ||
|
|
2731
|
+
stopRequested.actor !== 'real-peer' ||
|
|
2732
|
+
stopRequested.payload.actor_peer !== 'real-peer' ||
|
|
2733
|
+
stopRequested.payload.source !== 'cli' ||
|
|
2734
|
+
stopRequested.payload.admin !== true) {
|
|
2735
|
+
fail(`peer stop request did not record real actor and target:\n${JSON.stringify(stopRequested, null, 2)}`);
|
|
2736
|
+
}
|
|
2737
|
+
const stoppedFallback = eventPayloads('peer.stopped', 5, dbPath)
|
|
2738
|
+
.find((event) => event.payload?.target_peer === 'fake-target');
|
|
2739
|
+
if (!stoppedFallback ||
|
|
2740
|
+
stoppedFallback.actor !== 'real-peer' ||
|
|
2741
|
+
stoppedFallback.payload.actor_peer !== 'real-peer' ||
|
|
2742
|
+
stoppedFallback.payload.source !== 'cli' ||
|
|
2743
|
+
stoppedFallback.payload.admin !== true) {
|
|
2744
|
+
fail(`peer stop fallback did not record real actor and target:\n${JSON.stringify(stoppedFallback, null, 2)}`);
|
|
2745
|
+
}
|
|
2746
|
+
} finally {
|
|
2747
|
+
try { fs.rmSync(identityRoot, { recursive: true, force: true }); } catch {}
|
|
2748
|
+
try { fs.rmSync(identityHome, { recursive: true, force: true }); } catch {}
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2417
2752
|
async function syntaxAndHelp() {
|
|
2418
|
-
log('[
|
|
2753
|
+
log('[12/13] syntax/help');
|
|
2419
2754
|
run(process.execPath, ['--check', path.join(repoRoot, 'bin', 'hcc.mjs')]);
|
|
2420
2755
|
for (const file of libModuleFiles()) {
|
|
2421
2756
|
run(process.execPath, ['--check', path.join(repoRoot, file)]);
|
|
@@ -2601,7 +2936,19 @@ async function syntaxAndHelp() {
|
|
|
2601
2936
|
'} = createWebPeerActions({',
|
|
2602
2937
|
'const peerActionMatch = url.pathname.match(/^\\/api\\/peers\\/([^/]+)\\/actions\\/([^/]+)$/)',
|
|
2603
2938
|
"const readOnly = ['status', 'state', 'inbox'].includes(action)",
|
|
2604
|
-
|
|
2939
|
+
'resolveWebActionSession(reqCtx, peer, input, req)',
|
|
2940
|
+
'action_token: session.actionToken || null',
|
|
2941
|
+
"const sender = 'web';",
|
|
2942
|
+
'function auditPayload({ actor = null, target = null, source = ',
|
|
2943
|
+
'payload.actor_peer = actor',
|
|
2944
|
+
'payload.target_peer = target',
|
|
2945
|
+
"auditActorPeer: 'web'",
|
|
2946
|
+
"auditSource: 'web'",
|
|
2947
|
+
"addEvent(db, 'peer.start.requested'",
|
|
2948
|
+
"addEvent(db, 'peer.attach.requested'",
|
|
2949
|
+
"addEvent(db, 'peer.stop.requested'",
|
|
2950
|
+
"addEvent(eventDb, 'web.session.stop_requested'",
|
|
2951
|
+
"addEvent(db, 'tmux.session.gc'"
|
|
2605
2952
|
]) {
|
|
2606
2953
|
if (!hccSource.includes(expected)) fail(`web peer action API support missing: ${expected}`);
|
|
2607
2954
|
}
|
|
@@ -2618,9 +2965,17 @@ async function syntaxAndHelp() {
|
|
|
2618
2965
|
if (hccSource.includes(helper)) fail(`CLI still embeds web peer action helper: ${helper}`);
|
|
2619
2966
|
}
|
|
2620
2967
|
for (const expected of [
|
|
2968
|
+
'function webMutationPeer(peer, input = {})',
|
|
2621
2969
|
'function webPeerAction(projectCtx, peer, action, input = {})',
|
|
2970
|
+
'function webAuditPayload(peer, input = {}, extra = {})',
|
|
2971
|
+
"source: 'web'",
|
|
2972
|
+
'actor_peer: actorPeer',
|
|
2973
|
+
'target_peer: peer',
|
|
2974
|
+
"throw new CliError('PEER_IDENTITY_MISMATCH'",
|
|
2975
|
+
"throw new CliError('PEER_IDENTITY_REQUIRED'",
|
|
2622
2976
|
'claimNextTasksForPeer(db, peer, { force: Boolean(input.force), count })',
|
|
2623
2977
|
'takeOverTaskForPeer(db, peer, id, { reason, policy, staleAfter, source: ',
|
|
2978
|
+
"assertTaskOwnerForMutation(db, peer, task, 'lock-acquire')",
|
|
2624
2979
|
'const status = statusSummary(projectCtx, peer)',
|
|
2625
2980
|
"normalized === 'task-next'",
|
|
2626
2981
|
"normalized === 'lock-acquire'",
|
|
@@ -3232,11 +3587,12 @@ async function syntaxAndHelp() {
|
|
|
3232
3587
|
],
|
|
3233
3588
|
events: [
|
|
3234
3589
|
{ id: 7, type: 'message.sent', actor: 'peer-a', task_id: 2, payload: '{}', created_at: 7 },
|
|
3235
|
-
{ id: 8, type: 'task.done', actor: 'peer-a', task_id: 2, payload: JSON.stringify({ owner: 'peer-b', summary: 'done summary' }), created_at: 8 }
|
|
3590
|
+
{ 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 }
|
|
3236
3592
|
]
|
|
3237
3593
|
}, 'peer-b');
|
|
3238
3594
|
const timelineIds = timelineItems.map((item) => item.id);
|
|
3239
|
-
if (JSON.stringify(timelineIds) !== JSON.stringify(['message:2', 'message:3', 'handoff:4', 'task:5', 'event:8'])) {
|
|
3595
|
+
if (JSON.stringify(timelineIds) !== JSON.stringify(['message:2', 'message:3', 'handoff:4', 'task:5', 'event:8', 'event:9'])) {
|
|
3240
3596
|
fail(`timelineFromRows filtering/order changed: ${JSON.stringify(timelineItems)}`);
|
|
3241
3597
|
}
|
|
3242
3598
|
const inbound = timelineItems.find((item) => item.id === 'message:2');
|
|
@@ -4545,6 +4901,10 @@ async function syntaxAndHelp() {
|
|
|
4545
4901
|
if (integrationPeerIdentity.resolveCurrentPeer({ root: repoRoot }, {}, 'peer', 'shell').id !== 'env-peer') {
|
|
4546
4902
|
fail('peer identity resolveCurrentPeer ignored HCC_PEER');
|
|
4547
4903
|
}
|
|
4904
|
+
const correctedPeer = integrationPeerIdentity.resolveCurrentPeer({ root: repoRoot }, { peer: 'manual-peer' }, 'peer', 'shell');
|
|
4905
|
+
if (correctedPeer.id !== 'env-peer' || correctedPeer.corrected_from !== 'manual-peer') {
|
|
4906
|
+
fail(`peer identity did not correct explicit peer to HCC_PEER: ${JSON.stringify(correctedPeer)}`);
|
|
4907
|
+
}
|
|
4548
4908
|
delete process.env.HCC_PEER;
|
|
4549
4909
|
delete process.env.CLAUDE_CODE_SESSION_ID;
|
|
4550
4910
|
delete process.env.CLAUDECODE;
|
|
@@ -4864,7 +5224,7 @@ async function syntaxAndHelp() {
|
|
|
4864
5224
|
}
|
|
4865
5225
|
|
|
4866
5226
|
function uninstallWorkflow() {
|
|
4867
|
-
log('[
|
|
5227
|
+
log('[13/13] maintenance cleanup');
|
|
4868
5228
|
const uninstallRoot = path.join(os.tmpdir(), `hcc-reg-uninstall-root-${testId}`);
|
|
4869
5229
|
const uninstallHome = path.join(os.tmpdir(), `hcc-reg-uninstall-home-${testId}`);
|
|
4870
5230
|
fs.mkdirSync(uninstallRoot, { recursive: true });
|
|
@@ -4918,7 +5278,7 @@ async function main() {
|
|
|
4918
5278
|
process.once('SIGTERM', () => { cleanup(); process.exit(143); });
|
|
4919
5279
|
|
|
4920
5280
|
await setupRegression();
|
|
4921
|
-
log('[2/
|
|
5281
|
+
log('[2/13] runtime');
|
|
4922
5282
|
startRuntime();
|
|
4923
5283
|
await waitRuntime();
|
|
4924
5284
|
hcc(['peer', 'list']);
|
|
@@ -4930,15 +5290,26 @@ async function main() {
|
|
|
4930
5290
|
await askBroadcastWorkflow();
|
|
4931
5291
|
await downGcPackWorkflow();
|
|
4932
5292
|
oldNameScan();
|
|
5293
|
+
identityEnforcementWorkflow();
|
|
4933
5294
|
await syntaxAndHelp();
|
|
4934
5295
|
uninstallWorkflow();
|
|
5296
|
+
assertNoRealProjectRegistryLeak();
|
|
5297
|
+
cleanup();
|
|
5298
|
+
assertNoRegressionTmuxSessionLeak();
|
|
4935
5299
|
log('FULL_REGRESSION_OK');
|
|
4936
5300
|
}
|
|
4937
5301
|
|
|
4938
5302
|
main().catch((err) => {
|
|
5303
|
+
try { assertNoRealProjectRegistryLeak(); } catch (leakErr) {
|
|
5304
|
+
process.stderr.write(`${leakErr.stack || leakErr.message}\n`);
|
|
5305
|
+
}
|
|
4939
5306
|
cleanup();
|
|
4940
5307
|
process.stderr.write(`${err.stack || err.message}\n`);
|
|
4941
5308
|
process.exit(1);
|
|
4942
5309
|
}).finally(() => {
|
|
4943
|
-
|
|
5310
|
+
try {
|
|
5311
|
+
assertNoRealProjectRegistryLeak();
|
|
5312
|
+
} finally {
|
|
5313
|
+
cleanup();
|
|
5314
|
+
}
|
|
4944
5315
|
});
|