@logicseek/hello-cc 1.0.2 → 1.0.4
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 +63 -0
- package/docs/dsh.md +3 -3
- package/docs/dsh.zh-CN.md +3 -3
- package/lib/cli/app.mjs +1 -1
- package/lib/cli/commands/hook.mjs +3 -1
- package/lib/cli/commands/lifecycle.mjs +14 -3
- package/lib/cli/commands/query.mjs +15 -10
- package/lib/cli/commands/run.mjs +7 -6
- package/lib/db/connection.mjs +82 -9
- package/lib/discover.mjs +4 -4
- package/lib/integrations/hooks.mjs +132 -40
- package/lib/integrations/peers/identity.mjs +42 -7
- package/lib/process/identity.mjs +36 -15
- package/lib/runtime/client.mjs +9 -1
- package/lib/runtime/native/service.mjs +107 -24
- package/lib/runtime/native/store.mjs +63 -6
- package/lib/runtime/project-path.mjs +90 -7
- package/lib/shared/file-lock.mjs +62 -17
- package/lib/shared/socket-lock-worker.mjs +15 -6
- package/lib/web/codex-app-server.mjs +19 -1
- package/lib/web/codex-sessions.mjs +2 -0
- package/lib/web/cookie-auth.mjs +1 -1
- package/lib/web/http-routes.mjs +11 -7
- package/lib/web/http.mjs +20 -2
- package/lib/web/native-sessions.mjs +13 -6
- package/lib/web/peer-actions.mjs +27 -27
- package/lib/web/runtime-main.mjs +24 -7
- package/lib/web/runtime.mjs +18 -4
- package/lib/web/session-serialize.mjs +1 -1
- package/lib/web/startup.mjs +84 -10
- package/lib/web/task-results.mjs +8 -5
- package/lib/web/terminal-assets.mjs +18 -0
- package/lib/web/tmux-client-snapshot.mjs +85 -0
- package/lib/web/tmux-stream.mjs +3 -4
- package/lib/web/ui-codex.mjs +137 -20
- package/lib/web/ui-interactions.mjs +37 -3
- package/lib/web/ui-native.mjs +21 -9
- package/lib/web/ui-template.mjs +8 -2
- package/package.json +1 -1
- package/scripts/regression.mjs +212 -47
package/lib/web/ui-template.mjs
CHANGED
|
@@ -11,6 +11,8 @@ import { commandPaletteCss, commandPaletteHtml, commandPaletteScript } from './u
|
|
|
11
11
|
import { terminalFindCss, terminalFindHtml, terminalFindScript } from './ui-terminal-find.mjs';
|
|
12
12
|
import { workspaceCss, workspaceBarHtml, workspaceSecondaryHtml, workspaceScript } from './ui-workspace.mjs';
|
|
13
13
|
|
|
14
|
+
const SHELL_APOSTROPHE_ESCAPE = JSON.stringify(`'"'"'`);
|
|
15
|
+
|
|
14
16
|
function nonceAttribute(nonce) {
|
|
15
17
|
const value = String(nonce || '');
|
|
16
18
|
if (!/^[A-Za-z0-9_-]{16,}$/.test(value)) {
|
|
@@ -2130,11 +2132,15 @@ export function webIndexHtml({ nonce, pane = false } = {}) {
|
|
|
2130
2132
|
};
|
|
2131
2133
|
}
|
|
2132
2134
|
|
|
2135
|
+
function terminalShellQuote(value) {
|
|
2136
|
+
return "'" + String(value).split("'").join(${SHELL_APOSTROPHE_ESCAPE}) + "'";
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2133
2139
|
function terminalCommandForAction(action, info) {
|
|
2134
2140
|
const session = info.session || {};
|
|
2135
|
-
const peerId = info.peerId;
|
|
2141
|
+
const peerId = terminalShellQuote(info.peerId);
|
|
2136
2142
|
const lines = {
|
|
2137
|
-
register: \`hcc register --peer \${peerId} --kind \${session.kind || 'other'} --role \${session.role || 'peer'}\`,
|
|
2143
|
+
register: \`hcc register --peer \${peerId} --kind \${terminalShellQuote(session.kind || 'other')} --role \${terminalShellQuote(session.role || 'peer')}\`,
|
|
2138
2144
|
inbox: \`hcc msg inbox --peer \${peerId}\`,
|
|
2139
2145
|
'task-next': \`hcc task next --peer \${peerId}\`,
|
|
2140
2146
|
state: \`hcc state --peer \${peerId}\`,
|
package/package.json
CHANGED
package/scripts/regression.mjs
CHANGED
|
@@ -8,12 +8,14 @@ import path from 'node:path';
|
|
|
8
8
|
import process from 'node:process';
|
|
9
9
|
import { spawn, spawnSync } from 'node:child_process';
|
|
10
10
|
import crypto from 'node:crypto';
|
|
11
|
+
import { performance } from 'node:perf_hooks';
|
|
11
12
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
12
13
|
import { DatabaseSync } from 'node:sqlite';
|
|
13
14
|
import { Readable } from 'node:stream';
|
|
14
15
|
import { pathToFileURL } from 'node:url';
|
|
15
16
|
import WebSocket from 'ws';
|
|
16
|
-
import { inspectProcessIdentity } from '../lib/process/identity.mjs';
|
|
17
|
+
import { inspectProcessIdentity, compareProcessIdentity } from '../lib/process/identity.mjs';
|
|
18
|
+
import { redactSecrets } from '../lib/shared/redact.mjs';
|
|
17
19
|
import { applyBufferPlan, planBufferFiles } from '../lib/runtime/buffer-gc.mjs';
|
|
18
20
|
|
|
19
21
|
const repoRoot = path.resolve(import.meta.dirname, '..');
|
|
@@ -29,14 +31,16 @@ const secondProjectRoot = path.join(root, 'second-project');
|
|
|
29
31
|
const TERMINAL_WEBSOCKET_TIMEOUT_MS = 15_000;
|
|
30
32
|
const realHome = process.env.HOME || os.homedir();
|
|
31
33
|
const realRegistryFile = path.join(realHome, '.hello-cc', 'projects.json');
|
|
32
|
-
|
|
33
|
-
encoding: 'utf8',
|
|
34
|
-
stdio: ['ignore', 'pipe', 'ignore']
|
|
35
|
-
}).stdout.trim();
|
|
34
|
+
let realTmuxBin = '';
|
|
36
35
|
const port = 22000 + (process.pid % 10000);
|
|
37
36
|
|
|
38
37
|
let tmuxStarted = false;
|
|
39
38
|
let runtimePid = null;
|
|
39
|
+
let runtimeOwner = null;
|
|
40
|
+
let currentStage = 'startup';
|
|
41
|
+
let lastStopDiagnostic = null;
|
|
42
|
+
const diagnosticSecrets = new Set();
|
|
43
|
+
const fixtureFailures = new WeakSet();
|
|
40
44
|
const managedTmuxSessions = new Set();
|
|
41
45
|
|
|
42
46
|
const env = {
|
|
@@ -53,18 +57,153 @@ delete env.TMUX;
|
|
|
53
57
|
delete env.TMUX_PANE;
|
|
54
58
|
|
|
55
59
|
function log(message) {
|
|
60
|
+
currentStage = String(message);
|
|
56
61
|
process.stdout.write(`${message}\n`);
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
function fail(message) {
|
|
65
|
+
const error = fixtureFailureError(message);
|
|
60
66
|
if (process.env.GITHUB_ACTIONS === 'true') {
|
|
61
|
-
const escaped =
|
|
67
|
+
const escaped = fixtureDiagnosticText({ stage: currentStage, failure: fixtureFailureDiagnostic(error) }, diagnosticSecrets)
|
|
62
68
|
.replace(/%/g, '%25')
|
|
63
69
|
.replace(/\r/g, '%0D')
|
|
64
70
|
.replace(/\n/g, '%0A');
|
|
65
71
|
process.stderr.write(`::error::${escaped}\n`);
|
|
66
72
|
}
|
|
67
|
-
throw
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function fixtureFailureError(message) {
|
|
77
|
+
const error = new Error(String(message));
|
|
78
|
+
Object.defineProperty(error, 'fixtureStack', {
|
|
79
|
+
value: Object.freeze(new Error('fixture failure').stack.split('\n').slice(1, 7))
|
|
80
|
+
});
|
|
81
|
+
fixtureFailures.add(error);
|
|
82
|
+
return error;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function fixtureFailureDiagnostic(error) {
|
|
86
|
+
const names = ['Error', 'TypeError', 'RangeError', 'SyntaxError', 'AssertionError', 'AbortError'];
|
|
87
|
+
return {
|
|
88
|
+
name: names.includes(error?.name) ? error.name : 'Error',
|
|
89
|
+
stack: fixtureFailures.has(error)
|
|
90
|
+
? error.fixtureStack
|
|
91
|
+
: new Error('regression failure handler').stack.split('\n').slice(1, 7)
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function fixtureDiagnosticText(value, secrets = []) {
|
|
96
|
+
function scrub(text) {
|
|
97
|
+
for (const secret of secrets) {
|
|
98
|
+
if (typeof secret === 'string' && secret.length >= 8) text = text.split(secret).join('[REDACTED]');
|
|
99
|
+
}
|
|
100
|
+
return redactSecrets(text)
|
|
101
|
+
.replace(/("(?:[^"\\]*token|authorization|cookie|[^"\\]*secret|password)"\s*:\s*)"(?:\\.|[^"\\])*"/gi, '$1"[REDACTED]"');
|
|
102
|
+
}
|
|
103
|
+
function visit(entry) {
|
|
104
|
+
if (typeof entry === 'string') return scrub(entry);
|
|
105
|
+
if (Array.isArray(entry)) return entry.map(visit);
|
|
106
|
+
if (entry && typeof entry === 'object') return Object.fromEntries(Object.entries(entry).map(([key, item]) => [key, visit(item)]));
|
|
107
|
+
return entry;
|
|
108
|
+
}
|
|
109
|
+
return typeof value === 'string' ? scrub(value) : JSON.stringify(visit(redactSecrets(value)));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function fixturePsState(pid) {
|
|
113
|
+
const result = spawnSync('ps', ['-p', String(pid), '-o', 'stat='], {
|
|
114
|
+
encoding: 'utf8', timeout: 1000, stdio: ['ignore', 'pipe', 'ignore']
|
|
115
|
+
});
|
|
116
|
+
const state = String(result.stdout || '').trim();
|
|
117
|
+
return result.status === 0 && /^[A-Za-z+<>NslLWE-]{1,16}$/.test(state) ? state : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function observeFixtureOwner(owner, { inspect = inspectProcessIdentity, psState = fixturePsState } = {}) {
|
|
121
|
+
if (compareProcessIdentity(owner, owner) !== 'live') return { state: 'unknown', reason: 'missing_owner_identity' };
|
|
122
|
+
const observed = inspect(owner.pid);
|
|
123
|
+
const state = psState(owner.pid);
|
|
124
|
+
const details = { pid: owner.pid, psState: state, observedState: observed.state };
|
|
125
|
+
if (observed.state === 'dead') return { ...details, state: 'exited', reason: 'process_dead' };
|
|
126
|
+
if (/^[ZX]/i.test(state || '')) return { ...details, state: 'exited', reason: 'zombie_or_dead' };
|
|
127
|
+
if (observed.state !== 'live' || compareProcessIdentity(observed.identity, observed.identity) !== 'live') {
|
|
128
|
+
return { ...details, state: 'unknown', reason: 'identity_unavailable' };
|
|
129
|
+
}
|
|
130
|
+
details.birthHash = crypto.createHash('sha256').update(observed.identity.startToken).digest('hex');
|
|
131
|
+
details.commandHash = observed.identity.commandHash;
|
|
132
|
+
if (compareProcessIdentity(owner, observed.identity) === 'dead') return { ...details, state: 'exited', reason: 'pid_reused' };
|
|
133
|
+
if (owner.commandHash !== observed.identity.commandHash) return { ...details, state: 'unknown', reason: 'command_changed' };
|
|
134
|
+
return { ...details, state: 'same-owner', reason: 'identity_match' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function signalFixtureOwner(owner, signal, { observe = observeFixtureOwner, send = process.kill.bind(process) } = {}) {
|
|
138
|
+
const observation = observe(owner);
|
|
139
|
+
if (observation.state !== 'same-owner') return { signal, sent: false, observation };
|
|
140
|
+
try { send(owner.pid, signal); return { signal, sent: true, observation }; }
|
|
141
|
+
catch (error) { return { signal, sent: false, observation, code: error.code || 'SIGNAL_FAILED' }; }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function waitForFixtureOwnerExit(owner, timeoutMs, {
|
|
145
|
+
observe = observeFixtureOwner, monotonicNow = () => performance.now(), wait = sleep
|
|
146
|
+
} = {}) {
|
|
147
|
+
const deadline = monotonicNow() + timeoutMs;
|
|
148
|
+
for (;;) {
|
|
149
|
+
const observation = observe(owner);
|
|
150
|
+
if (observation.state === 'exited') return observation;
|
|
151
|
+
const remaining = deadline - monotonicNow();
|
|
152
|
+
if (remaining <= 0) {
|
|
153
|
+
const error = new Error('runtime fixture owner did not exit before deadline');
|
|
154
|
+
error.observation = observation;
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
await wait(Math.min(100, remaining));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function fixtureDownResult(result) {
|
|
162
|
+
let payload = null;
|
|
163
|
+
try { payload = JSON.parse(result.stdout || ''); } catch {}
|
|
164
|
+
const extra = payload?.error?.extra || payload?.error || {};
|
|
165
|
+
const code = payload?.error?.code || result.error?.code;
|
|
166
|
+
const numeric = ['pid', 'elapsedMs', 'timeoutMs'];
|
|
167
|
+
return {
|
|
168
|
+
status: Number.isSafeInteger(result.status) ? result.status : null,
|
|
169
|
+
signal: /^SIG[A-Z0-9]{1,12}$/.test(result.signal || '') ? result.signal : null,
|
|
170
|
+
code: typeof code === 'string' && /^[A-Z][A-Z0-9_]{0,79}$/.test(code) ? code : null,
|
|
171
|
+
extra: Object.fromEntries(numeric.filter(key => typeof extra[key] === 'number' &&
|
|
172
|
+
Number.isFinite(extra[key]) && extra[key] >= 0).map(key => [key, extra[key]]))
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function fixtureStopEnvironment(base) {
|
|
177
|
+
const local = { ...base, HCC_RUNTIME_LOCAL_ONLY: '1' };
|
|
178
|
+
delete local.HCC_RUNTIME_URL;
|
|
179
|
+
delete local.HCC_RUNTIME_TOKEN;
|
|
180
|
+
return local;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function rememberRuntimePid(pid) {
|
|
184
|
+
runtimePid = pid;
|
|
185
|
+
runtimeOwner = null;
|
|
186
|
+
try {
|
|
187
|
+
const pointer = currentRuntime();
|
|
188
|
+
if (pointer.token) {
|
|
189
|
+
diagnosticSecrets.add(pointer.token);
|
|
190
|
+
for (const part of pointer.token.split(/\s+/)) if (part.length >= 8) diagnosticSecrets.add(part);
|
|
191
|
+
}
|
|
192
|
+
if (pointer.process_identity?.pid === pid && compareProcessIdentity(pointer.process_identity, pointer.process_identity) === 'live') {
|
|
193
|
+
runtimeOwner = { ...pointer.process_identity };
|
|
194
|
+
}
|
|
195
|
+
} catch {}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function collectRuntimeDiagnostic(extra = {}) {
|
|
199
|
+
const diagnostic = { stage: currentStage, pid: runtimePid,
|
|
200
|
+
owner: observeFixtureOwner(runtimeOwner), pointerExists: fs.existsSync(path.join(root, '.hello-cc', 'runtime.json')), ...extra };
|
|
201
|
+
try {
|
|
202
|
+
const db = new DatabaseSync(path.join(root, '.hello-cc', 'mesh.db'), { readOnly: true, timeout: 200 });
|
|
203
|
+
try { diagnostic.events = db.prepare('SELECT id, type FROM events ORDER BY id DESC LIMIT 12').all(); }
|
|
204
|
+
finally { db.close(); }
|
|
205
|
+
} catch {}
|
|
206
|
+
return fixtureDiagnosticText(diagnostic, diagnosticSecrets);
|
|
68
207
|
}
|
|
69
208
|
|
|
70
209
|
function commandText(command, args) {
|
|
@@ -1587,15 +1726,7 @@ function assertHtmlCsp(response, html, label) {
|
|
|
1587
1726
|
}
|
|
1588
1727
|
|
|
1589
1728
|
function processIsAlive(pid) {
|
|
1590
|
-
|
|
1591
|
-
process.kill(pid, 0);
|
|
1592
|
-
// Linux keeps an exited child addressable until PID 1 reaps its zombie.
|
|
1593
|
-
// Product liveness treats that state as dead, and the regression wait must
|
|
1594
|
-
// use the same semantic boundary instead of waiting forever on kill(0).
|
|
1595
|
-
return process.platform !== 'linux' || inspectProcessIdentity(pid).state !== 'dead';
|
|
1596
|
-
} catch {
|
|
1597
|
-
return false;
|
|
1598
|
-
}
|
|
1729
|
+
return inspectProcessIdentity(pid).state !== 'dead' && !/^[ZX]/i.test(fixturePsState(pid) || '');
|
|
1599
1730
|
}
|
|
1600
1731
|
|
|
1601
1732
|
async function waitForProcessExit(pid, label, timeoutMs = 5000) {
|
|
@@ -2546,7 +2677,7 @@ function startRuntime(options = {}) {
|
|
|
2546
2677
|
const output = hcc(['web', '--local', '--port', String(port), '--no-discover', '--no-guidance'], { env: runtimeEnv });
|
|
2547
2678
|
const match = output.match(/^pid:\s*(\d+)/m);
|
|
2548
2679
|
if (!match) fail(`hcc web did not print background pid:\n${output}`);
|
|
2549
|
-
|
|
2680
|
+
rememberRuntimePid(Number.parseInt(match[1], 10));
|
|
2550
2681
|
if (!output.includes('web started in background')) fail(`hcc web did not report background start:\n${output}`);
|
|
2551
2682
|
}
|
|
2552
2683
|
|
|
@@ -2644,7 +2775,7 @@ async function webSecretRedactionWorkflow() {
|
|
|
2644
2775
|
]);
|
|
2645
2776
|
const match = output.match(/^pid:\s*(\d+)/m);
|
|
2646
2777
|
if (!match) fail(`redaction web did not print background pid`);
|
|
2647
|
-
|
|
2778
|
+
rememberRuntimePid(Number.parseInt(match[1], 10));
|
|
2648
2779
|
|
|
2649
2780
|
try {
|
|
2650
2781
|
await waitRuntime();
|
|
@@ -2738,6 +2869,7 @@ async function assertWebWrapperParentSurvives() {
|
|
|
2738
2869
|
const wrapperRoot = fs.mkdtempSync(path.join(os.tmpdir(), `hcc-reg-wrapper-root-${testId}-`));
|
|
2739
2870
|
const wrapperPort = port + 101;
|
|
2740
2871
|
let wrapperPid = null;
|
|
2872
|
+
let wrapperOwner = null;
|
|
2741
2873
|
try {
|
|
2742
2874
|
const script = [
|
|
2743
2875
|
'set -e',
|
|
@@ -2752,14 +2884,15 @@ async function assertWebWrapperParentSurvives() {
|
|
|
2752
2884
|
if (!match) fail(`wrapper hcc web did not print background pid:\n${result.stdout}`);
|
|
2753
2885
|
wrapperPid = Number.parseInt(match[1], 10);
|
|
2754
2886
|
ensureFile(path.join(wrapperRoot, '.hello-cc', 'runtime.json'));
|
|
2887
|
+
wrapperOwner = JSON.parse(fs.readFileSync(path.join(wrapperRoot, '.hello-cc', 'runtime.json'), 'utf8')).process_identity;
|
|
2755
2888
|
} finally {
|
|
2756
|
-
runMaybe(process.execPath, [hccBin, '--root', wrapperRoot, 'down'], { env });
|
|
2889
|
+
runMaybe(process.execPath, [hccBin, '--root', wrapperRoot, 'down'], { env: fixtureStopEnvironment(env) });
|
|
2757
2890
|
if (wrapperPid) {
|
|
2758
2891
|
try {
|
|
2759
|
-
await
|
|
2892
|
+
await waitForFixtureOwnerExit(wrapperOwner, 5000);
|
|
2760
2893
|
} catch {
|
|
2761
|
-
|
|
2762
|
-
try { await
|
|
2894
|
+
signalFixtureOwner(wrapperOwner, 'SIGTERM');
|
|
2895
|
+
try { await waitForFixtureOwnerExit(wrapperOwner, 2000); } catch {}
|
|
2763
2896
|
}
|
|
2764
2897
|
}
|
|
2765
2898
|
try { fs.rmSync(wrapperRoot, { recursive: true, force: true }); } catch {}
|
|
@@ -2768,28 +2901,42 @@ async function assertWebWrapperParentSurvives() {
|
|
|
2768
2901
|
|
|
2769
2902
|
async function stopRuntime() {
|
|
2770
2903
|
if (!runtimePid) return;
|
|
2771
|
-
const
|
|
2772
|
-
|
|
2904
|
+
const owner = runtimeOwner;
|
|
2905
|
+
const before = observeFixtureOwner(owner);
|
|
2906
|
+
const down = fixtureDownResult(stopFixtureCli());
|
|
2907
|
+
lastStopDiagnostic = { before, down, stack: new Error('stopRuntime').stack.split('\n').slice(1, 7), signals: [] };
|
|
2773
2908
|
try {
|
|
2774
|
-
await
|
|
2909
|
+
await waitForFixtureOwnerExit(owner, 5000);
|
|
2775
2910
|
} catch (err) {
|
|
2776
|
-
|
|
2911
|
+
lastStopDiagnostic.atDeadline = collectRuntimeDiagnostic({ before, down });
|
|
2912
|
+
lastStopDiagnostic.signals.push(signalFixtureOwner(owner, 'SIGTERM'));
|
|
2777
2913
|
try {
|
|
2778
|
-
await
|
|
2914
|
+
await waitForFixtureOwnerExit(owner, 2000);
|
|
2779
2915
|
} catch {
|
|
2780
|
-
|
|
2781
|
-
try { await
|
|
2782
|
-
throw err;
|
|
2916
|
+
lastStopDiagnostic.signals.push(signalFixtureOwner(owner, 'SIGKILL'));
|
|
2917
|
+
try { await waitForFixtureOwnerExit(owner, 2000); } catch {}
|
|
2783
2918
|
}
|
|
2919
|
+
fail(`runtime stop failed: ${collectRuntimeDiagnostic(lastStopDiagnostic)}`);
|
|
2784
2920
|
}
|
|
2921
|
+
if (down.status !== 0) fail(`runtime down command failed: ${collectRuntimeDiagnostic(lastStopDiagnostic)}`);
|
|
2785
2922
|
runtimePid = null;
|
|
2923
|
+
runtimeOwner = null;
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
function stopFixtureCli() {
|
|
2927
|
+
try {
|
|
2928
|
+
const pointer = currentRuntime();
|
|
2929
|
+
if (compareProcessIdentity(runtimeOwner, pointer.process_identity) === 'live' &&
|
|
2930
|
+
runtimeOwner.commandHash === pointer.process_identity.commandHash) {
|
|
2931
|
+
return hccMaybe(['--json', 'down'], { env: fixtureStopEnvironment(env) });
|
|
2932
|
+
}
|
|
2933
|
+
} catch {}
|
|
2934
|
+
return { status: null, error: { code: 'FIXTURE_OWNER_POINTER_UNAVAILABLE' } };
|
|
2786
2935
|
}
|
|
2787
2936
|
|
|
2788
2937
|
function cleanup() {
|
|
2789
|
-
try {
|
|
2790
|
-
if (runtimePid)
|
|
2791
|
-
try { process.kill(runtimePid, 'SIGTERM'); } catch {}
|
|
2792
|
-
}
|
|
2938
|
+
try { stopFixtureCli(); } catch {}
|
|
2939
|
+
if (runtimePid) signalFixtureOwner(runtimeOwner, 'SIGTERM');
|
|
2793
2940
|
const managedPrefixes = [
|
|
2794
2941
|
tmuxManagedSessionPrefix(root),
|
|
2795
2942
|
tmuxManagedSessionPrefix(secondProjectRoot)
|
|
@@ -2877,7 +3024,7 @@ async function setupRegression() {
|
|
|
2877
3024
|
const output = hcc(['web', '--local', '--port', String(port), '--no-discover', '--no-guidance']);
|
|
2878
3025
|
const match = output.match(/^pid:\s*(\d+)/m);
|
|
2879
3026
|
if (!match) fail(`hcc web did not print background pid during bootstrap:\n${output}`);
|
|
2880
|
-
|
|
3027
|
+
rememberRuntimePid(Number.parseInt(match[1], 10));
|
|
2881
3028
|
await waitRuntime();
|
|
2882
3029
|
for (const file of liveBufferFiles) ensureFile(file);
|
|
2883
3030
|
for (const file of liveBufferFiles) fs.rmSync(file, { force: true });
|
|
@@ -2957,7 +3104,7 @@ async function setupRegression() {
|
|
|
2957
3104
|
const directTlsOutput = hcc(['web', '--local', '--tls', '--port', String(port), '--no-discover', '--no-guidance']);
|
|
2958
3105
|
const directTlsMatch = directTlsOutput.match(/^pid:\s*(\d+)/m);
|
|
2959
3106
|
if (!directTlsMatch) fail(`direct TLS web did not print background pid:\n${directTlsOutput}`);
|
|
2960
|
-
|
|
3107
|
+
rememberRuntimePid(Number.parseInt(directTlsMatch[1], 10));
|
|
2961
3108
|
await waitFor(async () => {
|
|
2962
3109
|
const runtimeFile = path.join(root, '.hello-cc', 'runtime.json');
|
|
2963
3110
|
if (!fs.existsSync(runtimeFile)) return false;
|
|
@@ -3016,7 +3163,7 @@ async function setupRegression() {
|
|
|
3016
3163
|
const tokenOutput = hcc(['web', '--host', '0.0.0.0', '--port', String(port), '--no-discover', '--no-guidance']);
|
|
3017
3164
|
const tokenMatch = tokenOutput.match(/^pid:\s*(\d+)/m);
|
|
3018
3165
|
if (!tokenMatch) fail(`token web did not print background pid:\n${tokenOutput}`);
|
|
3019
|
-
|
|
3166
|
+
rememberRuntimePid(Number.parseInt(tokenMatch[1], 10));
|
|
3020
3167
|
if (!tokenOutput.includes('token=') || !tokenOutput.includes('open: http://<machine-ip>:')) {
|
|
3021
3168
|
fail(`default web output did not include remote token URL:\n${tokenOutput}`);
|
|
3022
3169
|
}
|
|
@@ -3200,7 +3347,7 @@ async function setupRegression() {
|
|
|
3200
3347
|
]);
|
|
3201
3348
|
const proxyPidMatch = proxyOutput.match(/^pid:\s*(\d+)/m);
|
|
3202
3349
|
if (!proxyPidMatch) fail(`trusted-proxy web did not print background pid:\n${proxyOutput}`);
|
|
3203
|
-
|
|
3350
|
+
rememberRuntimePid(Number.parseInt(proxyPidMatch[1], 10));
|
|
3204
3351
|
await waitRuntime();
|
|
3205
3352
|
const proxyRuntime = currentRuntime();
|
|
3206
3353
|
if (proxyRuntime.proxy_origin !== proxyOrigin || proxyRuntime.trust_proxy !== true) {
|
|
@@ -3286,7 +3433,7 @@ async function setupRegression() {
|
|
|
3286
3433
|
]);
|
|
3287
3434
|
const restoredPidMatch = restoredOutput.match(/^pid:\s*(\d+)/m);
|
|
3288
3435
|
if (!restoredPidMatch) fail(`post-proxy web did not print background pid:\n${restoredOutput}`);
|
|
3289
|
-
|
|
3436
|
+
rememberRuntimePid(Number.parseInt(restoredPidMatch[1], 10));
|
|
3290
3437
|
await waitRuntime();
|
|
3291
3438
|
|
|
3292
3439
|
const badJsonResponse = await runtimeFetch('/api/projects', {
|
|
@@ -3313,7 +3460,7 @@ async function setupRegression() {
|
|
|
3313
3460
|
const stableTokenOutput = hcc(['web', '--host', '0.0.0.0', '--port', String(port), '--no-discover', '--no-guidance']);
|
|
3314
3461
|
const stableTokenMatch = stableTokenOutput.match(/^pid:\s*(\d+)/m);
|
|
3315
3462
|
if (!stableTokenMatch) fail(`stable-token web did not print background pid:\n${stableTokenOutput}`);
|
|
3316
|
-
|
|
3463
|
+
rememberRuntimePid(Number.parseInt(stableTokenMatch[1], 10));
|
|
3317
3464
|
await waitRuntime();
|
|
3318
3465
|
const stableTokenRuntime = currentRuntime();
|
|
3319
3466
|
if (stableTokenRuntime.token === tokenRuntime.token) {
|
|
@@ -3327,7 +3474,7 @@ async function setupRegression() {
|
|
|
3327
3474
|
});
|
|
3328
3475
|
const fixedTokenMatch = fixedTokenOutput.match(/^pid:\s*(\d+)/m);
|
|
3329
3476
|
if (!fixedTokenMatch) fail(`fixed-token web did not print background pid:\n${fixedTokenOutput}`);
|
|
3330
|
-
|
|
3477
|
+
rememberRuntimePid(Number.parseInt(fixedTokenMatch[1], 10));
|
|
3331
3478
|
await waitRuntime();
|
|
3332
3479
|
const fixedTokenRuntime = currentRuntime();
|
|
3333
3480
|
if (fixedTokenRuntime.token !== fixedToken || !fixedTokenOutput.includes(`token=${encodeURIComponent(fixedToken)}`)) {
|
|
@@ -3338,7 +3485,7 @@ async function setupRegression() {
|
|
|
3338
3485
|
const postFixedOutput = hcc(['web', '--host', '0.0.0.0', '--port', String(port), '--no-discover', '--no-guidance']);
|
|
3339
3486
|
const postFixedMatch = postFixedOutput.match(/^pid:\s*(\d+)/m);
|
|
3340
3487
|
if (!postFixedMatch) fail(`post-fixed-token web did not print background pid:\n${postFixedOutput}`);
|
|
3341
|
-
|
|
3488
|
+
rememberRuntimePid(Number.parseInt(postFixedMatch[1], 10));
|
|
3342
3489
|
await waitRuntime();
|
|
3343
3490
|
const postFixedRuntime = currentRuntime();
|
|
3344
3491
|
if (postFixedRuntime.token === fixedToken || fs.existsSync(tokenFile)) {
|
|
@@ -3351,7 +3498,7 @@ async function setupRegression() {
|
|
|
3351
3498
|
const noTokenOutput = hcc(['web', '--local', '--port', String(port), '--no-token', '--no-discover', '--no-guidance']);
|
|
3352
3499
|
const noTokenMatch = noTokenOutput.match(/^pid:\s*(\d+)/m);
|
|
3353
3500
|
if (!noTokenMatch) fail(`explicit no-token web did not print background pid:\n${noTokenOutput}`);
|
|
3354
|
-
|
|
3501
|
+
rememberRuntimePid(Number.parseInt(noTokenMatch[1], 10));
|
|
3355
3502
|
if (noTokenOutput.includes('token=')) {
|
|
3356
3503
|
fail(`explicit no-token web output included token:\n${noTokenOutput}`);
|
|
3357
3504
|
}
|
|
@@ -4420,7 +4567,14 @@ async function multiProjectWebWorkflow() {
|
|
|
4420
4567
|
|
|
4421
4568
|
const htmlResponse = await fetch(currentRuntimeUrl('/'));
|
|
4422
4569
|
const html = await htmlResponse.text();
|
|
4423
|
-
|
|
4570
|
+
const startForm = html.match(/<form\b[^>]*\bid="startForm"[^>]*>[\s\S]*?<\/form>/)?.[0];
|
|
4571
|
+
if (!startForm) fail('web form missing startForm');
|
|
4572
|
+
// Approval details may display the working directory. The simplified launch
|
|
4573
|
+
// form must still derive it from the selected project instead of an input.
|
|
4574
|
+
if (startForm.includes('Working directory') || /<input\b[^>]*(?:id|name)="(?:cwd|workdir)"/.test(startForm)) {
|
|
4575
|
+
fail('web launch form still exposes Working directory');
|
|
4576
|
+
}
|
|
4577
|
+
for (const forbidden of ['Alias optional', 'Role tag', 'Command<input', 'commandbar', 'lineInput', 'Send text to active terminal']) {
|
|
4424
4578
|
if (html.includes(forbidden)) fail(`web form still exposes ${forbidden}`);
|
|
4425
4579
|
}
|
|
4426
4580
|
for (const expected of [
|
|
@@ -11151,11 +11305,16 @@ function gcCoverageWorkflow() {
|
|
|
11151
11305
|
}
|
|
11152
11306
|
|
|
11153
11307
|
async function main() {
|
|
11308
|
+
realTmuxBin = spawnSync('sh', ['-lc', 'command -v tmux || true'], {
|
|
11309
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore']
|
|
11310
|
+
}).stdout.trim();
|
|
11154
11311
|
process.once('SIGINT', () => { cleanup(); process.exit(130); });
|
|
11155
11312
|
process.once('SIGTERM', () => { cleanup(); process.exit(143); });
|
|
11156
11313
|
|
|
11157
11314
|
await setupRegression();
|
|
11315
|
+
currentStage = 'webSecretRedactionWorkflow';
|
|
11158
11316
|
await webSecretRedactionWorkflow();
|
|
11317
|
+
currentStage = 'cookieSessionExpiryWorkflow';
|
|
11159
11318
|
await cookieSessionExpiryWorkflow();
|
|
11160
11319
|
log('[2/13] runtime');
|
|
11161
11320
|
startRuntime();
|
|
@@ -11187,12 +11346,18 @@ async function main() {
|
|
|
11187
11346
|
log('FULL_REGRESSION_OK');
|
|
11188
11347
|
}
|
|
11189
11348
|
|
|
11190
|
-
main().catch((err) => {
|
|
11349
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === import.meta.filename) main().catch((err) => {
|
|
11350
|
+
const failure = fixtureFailureDiagnostic(err);
|
|
11351
|
+
const diagnostic = collectRuntimeDiagnostic({ failure, stop: lastStopDiagnostic });
|
|
11352
|
+
process.stderr.write(`[regression failure diagnostic] ${diagnostic}\n`);
|
|
11353
|
+
if (process.env.GITHUB_ACTIONS === 'true') {
|
|
11354
|
+
process.stderr.write(`::error::${diagnostic.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A')}\n`);
|
|
11355
|
+
}
|
|
11191
11356
|
try { assertNoRealProjectRegistryLeak(); } catch (leakErr) {
|
|
11192
|
-
process.stderr.write(`${leakErr.stack || leakErr.message}\n`);
|
|
11357
|
+
process.stderr.write(`${fixtureDiagnosticText(leakErr.stack || leakErr.message, diagnosticSecrets)}\n`);
|
|
11193
11358
|
}
|
|
11194
11359
|
cleanup();
|
|
11195
|
-
process.stderr.write(`${err.stack || err.message}\n`);
|
|
11360
|
+
process.stderr.write(`${fixtureDiagnosticText(err.stack || err.message, diagnosticSecrets)}\n`);
|
|
11196
11361
|
process.exit(1);
|
|
11197
11362
|
}).finally(() => {
|
|
11198
11363
|
try {
|