Reduce repeated test work and fix Windows cleanup and IME confirmation (#26608)

* ci: share test transforms and remove six redundant checks

* test: remove obsolete project copies and two build string checks

* test: use the injected clock for exact launch readiness budgets

* fix: retain IME confirmation and retire disconnected Windows PTYs

* ci: include relay installer in existing Windows boundaries

* perf: import the canonical SSH manager for filesystem operations

* test: remove 42 source-spelling checks

* test: advance deadlines after real entry and await actual PTY exit

* Preserve native Windows exit acknowledgments and skip daemon-only smoke triggers
This commit is contained in:
Neil
2026-10-08 16:08:49 -07:00
committed by GitHub
parent bb44f74d00
commit 59b9bdeb90
54 changed files with 1608 additions and 1228 deletions
+1
View File
@@ -29,6 +29,7 @@ on:
- 'electron.vite.config.ts'
- 'config/build-plugins/**'
- 'src/main/daemon/**'
- '!src/main/daemon/**/*.test.ts'
- 'src/main/providers/local-pty-provider.ts'
- 'src/main/providers/windows-shell-args.ts'
- 'src/main/providers/windows-shell-preflight-runtime.windows.test.ts'
+1
View File
@@ -3,6 +3,7 @@ on:
pull_request:
paths:
- 'src/main/pi/**'
- '!src/main/pi/**/*.test.ts'
- 'tests/tools/pi-owner-runtime-smoke.mjs'
- 'tests/tools/omp-completion-runtime-smoke.mjs'
- '.github/workflows/pi-owner-runtime.yml'
+1
View File
@@ -1295,6 +1295,7 @@ jobs:
src/relay/windows-port-scan.win32.test.ts
src/main/ssh/ssh-relay-upload-stage-windows-identity.test.ts
src/main/ssh/remote-node-runtime-store-windows.test.ts
config/scripts/node-pty-windows-pty-teardown-patch.test.mjs
# Why the :parallel variant: identical to build:release except the three
# electron-vite targets overlap instead of running back to back. The Linux package
@@ -11,6 +11,7 @@ on:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- 'src/main/daemon/**'
- '!src/main/daemon/**/*.test.ts'
- 'src/main/orcad/**'
- 'src/cli/runtime/**'
- 'src/shared/orcad-local-serve-selection.ts'
+435 -29
View File
@@ -1216,10 +1216,45 @@ index 7b286d3d64..5239ba4e40 100644
};
diff --git a/lib/windowsPtyAgent.js b/lib/windowsPtyAgent.js
index a358ffb177357e177661033c1b092f9c9d0e5f5a..26c2a4c58799ce649f5113131e4c52f7ed2d87ad 100644
--- a/lib/windowsPtyAgent.js
+++ b/lib/windowsPtyAgent.js
@@ -136,6 +136,9 @@ var WindowsPtyAgent = /** @class */ (function () {
@@ -13,6 +13,7 @@
var net_1 = require("net");
var windowsConoutConnection_1 = require("./windowsConoutConnection");
var utils_1 = require("./utils");
+var eventEmitter2_1 = require("./eventEmitter2");
var conptyNative;
var winptyNative;
/**
@@ -34,6 +35,7 @@
this._useConptyDll = _useConptyDll;
this._pid = 0;
this._innerPid = 0;
+ this._onProcessExit = new eventEmitter2_1.EventEmitter2();
if (this._useConpty === undefined || this._useConpty === true) {
this._useConpty = this._getWindowsBuildNumber() >= 18309;
}
@@ -130,12 +132,29 @@
this._ptyNative.clear(this._pty, this._useConptyDll);
}
};
+ Object.defineProperty(WindowsPtyAgent.prototype, "onProcessExit", {
+ get: function () { return this._onProcessExit.event; },
+ enumerable: false,
+ configurable: true
+ });
+ Object.defineProperty(WindowsPtyAgent.prototype, "isConpty", {
+ get: function () { return this._useConpty === true; },
+ enumerable: false,
+ configurable: true
+ });
+ WindowsPtyAgent.prototype.killAfterOutputClosed = function () {
+ var _this = this;
+ return this._conoutSocketWorker.disposeImmediately().then(function () { return _this.kill(); });
+ };
WindowsPtyAgent.prototype.kill = function () {
var _this = this;
// Tell the agent to kill the pty, this releases handles to the process
if (this._useConpty) {
if (!this._useConptyDll) {
this._inSocket.readable = false;
@@ -1229,7 +1264,7 @@ index a358ffb177357e177661033c1b092f9c9d0e5f5a..26c2a4c58799ce649f5113131e4c52f7
this._outSocket.readable = false;
this._getConsoleProcessList().then(function (consoleProcessList) {
consoleProcessList.forEach(function (pid) {
@@ -154,9 +157,10 @@ var WindowsPtyAgent = /** @class */ (function () {
@@ -154,9 +173,10 @@
// Close the input write handle to signal the end of session.
this._inSocket.destroy();
this._ptyNative.kill(this._pty, this._useConptyDll);
@@ -1243,11 +1278,28 @@ index a358ffb177357e177661033c1b092f9c9d0e5f5a..26c2a4c58799ce649f5113131e4c52f7
}
}
else {
@@ -225,6 +245,7 @@
this._flushDataAndCleanUp();
this._outSocket.on('data', function () { return _this._flushDataAndCleanUp(); });
}
+ this._onProcessExit.fire(exitCode);
};
WindowsPtyAgent.prototype._flushDataAndCleanUp = function () {
var _this = this;
diff --git a/lib/windowsTerminal.js b/lib/windowsTerminal.js
index 3c38f89..e20b3e6 100644
--- a/lib/windowsTerminal.js
+++ b/lib/windowsTerminal.js
@@ -50,6 +50,27 @@ var WindowsTerminal = /** @class */ (function (_super) {
@@ -45,22 +45,61 @@
var parsedEnv = _this._parseEnv(env);
// If the terminal is ready
_this._isReady = false;
+ _this._killRequested = false;
+ _this._killComplete = false;
+ _this._isPipeReady = false;
+ _this._exitEmitted = false;
+ _this._preconnectCleanupRequested = false;
// Functions that need to run after `ready` event is emitted.
_this._deferreds = [];
// Create new termal.
_this._agent = new windowsPtyAgent_1.WindowsPtyAgent(file, args, parsedEnv, cwd, _this._cols, _this._rows, false, opt.useConpty, opt.useConptyDll, opt.conptyInheritCursor);
_this._socket = _this._agent.outSocket;
@@ -1275,7 +1327,32 @@ index 3c38f89..e20b3e6 100644
// Not available until `ready` event emitted.
_this._pid = _this._agent.innerPid;
_this._fd = _this._agent.fd;
@@ -76,23 +99,6 @@ var WindowsTerminal = /** @class */ (function (_super) {
_this._pty = _this._agent.pty;
+ _this._agent.onProcessExit(function () {
+ if (_this._killRequested) _this._emitExit();
+ });
+ _this._socket.once('close', function () {
+ if (_this._isPipeReady && !_this._preconnectCleanupRequested && (!_this._killRequested || !_this._agent.isConpty || _this._agent.exitCode !== undefined)) _this._emitExit();
+ _this._close();
+ if (_this._killRequested && !_this._isPipeReady) _this._killAfterOutputClosed();
+ });
// The forked windows terminal is not available until `ready` event is
// emitted.
_this._socket.on('ready_datapipe', function () {
+ _this._isPipeReady = true;
+ if (_this._killRequested) {
+ _this.kill();
+ return;
+ }
// Run deferreds and set ready state once the first data event is received.
_this._socket.once('data', function () {
// Wait until the first data event is fired then we can run deferreds.
- if (!_this._isReady) {
+ if (!_this._isReady && !_this._killRequested) {
// Terminal is now ready and we can avoid having to defer method
// calls.
_this._isReady = true;
@@ -76,33 +115,25 @@
_this._deferreds = [];
}
});
@@ -1296,10 +1373,13 @@ index 3c38f89..e20b3e6 100644
- throw err;
- }
- });
// Cleanup after the socket is closed.
_this._socket.on('close', function () {
_this.emit('exit', _this._agent.exitCode);
@@ -103,6 +109,20 @@ var WindowsTerminal = /** @class */ (function (_super) {
- // Cleanup after the socket is closed.
- _this._socket.on('close', function () {
- _this.emit('exit', _this._agent.exitCode);
- _this._close();
- });
});
_this._file = file;
_this._name = name;
_this._readable = true;
_this._writable = true;
@@ -1320,7 +1400,79 @@ index 3c38f89..e20b3e6 100644
_this._forwardEvents();
return _this;
}
@@ -196,4 +216,4 @@ var WindowsTerminal = /** @class */ (function (_super) {
@@ -139,22 +170,49 @@
});
};
WindowsTerminal.prototype.destroy = function () {
- var _this = this;
- this._deferNoArgs(function () {
- _this.kill();
- });
+ this.kill();
};
WindowsTerminal.prototype.kill = function (signal) {
- var _this = this;
- this._deferNoArgs(function () {
- if (signal) {
- throw new Error('Signals not supported on windows.');
- }
- _this._close();
- _this._agent.kill();
- });
+ if (signal) {
+ throw new Error('Signals not supported on windows.');
+ }
+ // Retire input now; native close requires the forwarding pipe, not first output.
+ this._killRequested = true;
+ this._deferreds = [];
+ this._close();
+ if (this._killComplete || this._preconnectCleanup) return;
+ if (this._preconnectCleanupRequested || (!this._isPipeReady && this._socket.destroyed)) {
+ this._killAfterOutputClosed();
+ return;
+ }
+ if (!this._isPipeReady) return;
+ this._agent.kill();
+ this._killComplete = true;
+ };
+ WindowsTerminal.prototype._emitExit = function () {
+ if (this._exitEmitted) return;
+ this._exitEmitted = true;
+ this.emit('exit', this._agent.exitCode);
+ };
+ WindowsTerminal.prototype._killAfterOutputClosed = function () {
+ var _this = this;
+ if (!this._agent.isConpty || this._killComplete || this._preconnectCleanup) return;
+ this._preconnectCleanupRequested = true;
+ this._preconnectCleanup = this._agent.killAfterOutputClosed().then(function () {
+ _this._killComplete = true;
+ if (_this._agent.exitCode !== undefined) _this._emitExit();
+ }, function (error) {
+ _this._preconnectCleanup = undefined;
+ if (_this.listeners('error').length > 1) {
+ _this.emit('error', error);
+ }
+ });
+ if (this._agent.exitCode !== undefined) this._emitExit();
};
WindowsTerminal.prototype._deferNoArgs = function (deferredFn) {
+ if (this._killRequested) {
+ return;
+ }
var _this = this;
// If the terminal is ready, execute.
if (this._isReady) {
@@ -167,6 +225,9 @@
});
};
WindowsTerminal.prototype._defer = function (deferredFn, arg) {
+ if (this._killRequested) {
+ return;
+ }
var _this = this;
// If the terminal is ready, execute.
if (this._isReady) {
@@ -196,4 +257,4 @@
return WindowsTerminal;
}(terminal_1.Terminal));
exports.WindowsTerminal = WindowsTerminal;
@@ -1328,10 +1480,36 @@ index 3c38f89..e20b3e6 100644
\ No newline at end of file
+//# sourceMappingURL=windowsTerminal.js.map
diff --git a/src/windowsPtyAgent.ts b/src/windowsPtyAgent.ts
index d7054449516f0c9a62af351c2caa17331206d530..0c28a32e2e1db2b3f208ddde8443cd4e67bb1ad6 100644
--- a/src/windowsPtyAgent.ts
+++ b/src/windowsPtyAgent.ts
@@ -143,6 +143,9 @@ export class WindowsPtyAgent {
@@ -12,6 +12,7 @@
import { ArgvOrCommandLine } from './types';
import { ConoutConnection } from './windowsConoutConnection';
import { loadNativeModule } from './utils';
+import { EventEmitter2, IEvent } from './eventEmitter2';
let conptyNative: IConptyNative;
let winptyNative: IWinptyNative;
@@ -34,6 +35,9 @@
private _innerPid: number = 0;
private _closeTimeout: NodeJS.Timer | undefined;
private _exitCode: number | undefined;
+ private _onProcessExit = new EventEmitter2<number>();
+ public get onProcessExit(): IEvent<number> { return this._onProcessExit.event; }
+ public get isConpty(): boolean { return this._useConpty === true; }
private _conoutSocketWorker: ConoutConnection;
private _fd: any;
@@ -138,11 +142,18 @@
}
}
+ public killAfterOutputClosed(): Promise<void> {
+ return this._conoutSocketWorker.disposeImmediately().then(() => this.kill());
+ }
+
public kill(): void {
// Tell the agent to kill the pty, this releases handles to the process
if (this._useConpty) {
if (!this._useConptyDll) {
this._inSocket.readable = false;
@@ -1341,7 +1519,7 @@ index d7054449516f0c9a62af351c2caa17331206d530..0c28a32e2e1db2b3f208ddde8443cd4e
this._outSocket.readable = false;
this._getConsoleProcessList().then(consoleProcessList => {
consoleProcessList.forEach((pid: number) => {
@@ -159,9 +162,10 @@ export class WindowsPtyAgent {
@@ -159,9 +170,10 @@
// Close the input write handle to signal the end of session.
this._inSocket.destroy();
(this._ptyNative as IConptyNative).kill(this._pty, this._useConptyDll);
@@ -1355,14 +1533,44 @@ index d7054449516f0c9a62af351c2caa17331206d530..0c28a32e2e1db2b3f208ddde8443cd4e
}
} else {
// Because pty.kill closes the handle, it will kill most processes by itself.
@@ -227,6 +239,7 @@
this._flushDataAndCleanUp();
this._outSocket.on('data', () => this._flushDataAndCleanUp());
}
+ this._onProcessExit.fire(exitCode);
}
private _flushDataAndCleanUp(): void {
diff --git a/src/windowsTerminal.ts b/src/windowsTerminal.ts
index 13f6c6d..eda63c8 100644
--- a/src/windowsTerminal.ts
+++ b/src/windowsTerminal.ts
@@ -51,6 +51,30 @@ export class WindowsTerminal extends Terminal {
@@ -16,6 +16,12 @@
export class WindowsTerminal extends Terminal {
private _isReady: boolean;
+ private _killRequested: boolean;
+ private _killComplete: boolean;
+ private _isPipeReady: boolean;
+ private _exitEmitted = false;
+ private _preconnectCleanupRequested = false;
+ private _preconnectCleanup: Promise<void> | undefined;
private _deferreds: { run: () => void }[];
private _agent: WindowsPtyAgent;
@@ -43,6 +49,9 @@
// If the terminal is ready
this._isReady = false;
+ this._killRequested = false;
+ this._killComplete = false;
+ this._isPipeReady = false;
// Functions that need to run after `ready` event is emitted.
this._deferreds = [];
@@ -50,20 +59,58 @@
// Create new termal.
this._agent = new WindowsPtyAgent(file, args, parsedEnv, cwd, this._cols, this._rows, false, opt.useConpty, opt.useConptyDll, opt.conptyInheritCursor);
this._socket = this._agent.outSocket;
-
+
+ // Attach before readiness so a broken ConPTY output pipe cannot be unhandled.
+ this._socket.on('error', err => {
@@ -1387,15 +1595,42 @@ index 13f6c6d..eda63c8 100644
+ throw err;
+ }
+ });
+
// Not available until `ready` event emitted.
this._pid = this._agent.innerPid;
this._fd = this._agent.fd;
@@ -82,25 +108,6 @@ export class WindowsTerminal extends Terminal {
this._pty = this._agent.pty;
+ this._agent.onProcessExit(() => {
+ if (this._killRequested) this._emitExit();
+ });
+ this._socket.once('close', () => {
+ if (this._isPipeReady && !this._preconnectCleanupRequested && (!this._killRequested || !this._agent.isConpty || this._agent.exitCode !== undefined)) this._emitExit();
+ this._close();
+ if (this._killRequested && !this._isPipeReady) this._killAfterOutputClosed();
+ });
+
// The forked windows terminal is not available until `ready` event is
// emitted.
this._socket.on('ready_datapipe', () => {
+ this._isPipeReady = true;
+ if (this._killRequested) {
+ this.kill();
+ return;
+ }
// Run deferreds and set ready state once the first data event is received.
this._socket.once('data', () => {
// Wait until the first data event is fired then we can run deferreds.
- if (!this._isReady) {
+ if (!this._isReady && !this._killRequested) {
// Terminal is now ready and we can avoid having to defer method
// calls.
this._isReady = true;
@@ -82,30 +129,6 @@
}
});
-
+
- // Shutdown if `error` event is emitted.
- this._socket.on('error', err => {
- // Close terminal session.
@@ -1415,12 +1650,16 @@ index 13f6c6d..eda63c8 100644
- }
- });
-
// Cleanup after the socket is closed.
this._socket.on('close', () => {
this.emit('exit', this._agent.exitCode);
@@ -114,6 +121,19 @@ export class WindowsTerminal extends Terminal {
-
+
- // Cleanup after the socket is closed.
- this._socket.on('close', () => {
- this.emit('exit', this._agent.exitCode);
- this._close();
- });
});
@@ -114,6 +137,19 @@
this._readable = true;
this._writable = true;
+ // A ConPTY input-pipe error must retire only this terminal. Without a listener, Node promotes
@@ -1436,7 +1675,174 @@ index 13f6c6d..eda63c8 100644
+ // The failing pipe may have raced process exit; the terminal is already unwritable.
+ }
+ });
-
+
this._forwardEvents();
}
@@ -156,22 +192,52 @@
}
public destroy(): void {
- this._deferNoArgs(() => {
- this.kill();
- });
+ this.kill();
}
public kill(signal?: string): void {
- this._deferNoArgs(() => {
- if (signal) {
- throw new Error('Signals not supported on windows.');
- }
- this._close();
- this._agent.kill();
- });
+ if (signal) {
+ throw new Error('Signals not supported on windows.');
+ }
+ // Retire input now; native close requires the forwarding pipe, not first output.
+ this._killRequested = true;
+ this._deferreds = [];
+ this._close();
+ if (this._killComplete || this._preconnectCleanup) return;
+ if (this._preconnectCleanupRequested || (!this._isPipeReady && this._socket.destroyed)) {
+ this._killAfterOutputClosed();
+ return;
+ }
+ if (!this._isPipeReady) return;
+ this._agent.kill();
+ this._killComplete = true;
+ }
+
+ private _emitExit(): void {
+ if (this._exitEmitted) return;
+ this._exitEmitted = true;
+ this.emit('exit', this._agent.exitCode);
+ }
+
+ private _killAfterOutputClosed(): void {
+ if (!this._agent.isConpty || this._killComplete || this._preconnectCleanup) return;
+ this._preconnectCleanupRequested = true;
+ this._preconnectCleanup = this._agent.killAfterOutputClosed().then(() => {
+ this._killComplete = true;
+ if (this._agent.exitCode !== undefined) this._emitExit();
+ }, error => {
+ this._preconnectCleanup = undefined;
+ if (this.listeners('error').length > 1) {
+ this.emit('error', error);
+ }
+ });
+ if (this._agent.exitCode !== undefined) this._emitExit();
}
private _deferNoArgs<A>(deferredFn: () => void): void {
+ if (this._killRequested) {
+ return;
+ }
// If the terminal is ready, execute.
if (this._isReady) {
deferredFn.call(this);
@@ -185,6 +251,9 @@
}
private _defer<A>(deferredFn: (arg: A) => void, arg: A): void {
+ if (this._killRequested) {
+ return;
+ }
// If the terminal is ready, execute.
if (this._isReady) {
deferredFn.call(this, arg);
diff --git a/src/windowsConoutConnection.ts b/src/windowsConoutConnection.ts
--- a/src/windowsConoutConnection.ts
+++ b/src/windowsConoutConnection.ts
@@ -32,6 +32,7 @@
private _worker: Worker;
private _drainTimeout: NodeJS.Timeout | undefined;
private _isDisposed: boolean = false;
+ private _termination: Promise<void> | undefined;
private _onReady = new EventEmitter2<void>();
public get onReady(): IEvent<void> { return this._onReady.event; }
@@ -48,6 +49,7 @@
this._worker.on('message', (message: ConoutWorkerMessage) => {
switch (message) {
case ConoutWorkerMessage.READY:
+ if (this._termination) return;
this._onReady.fire();
return;
default:
@@ -65,7 +67,14 @@
this._drainDataAndClose();
}
+ disposeImmediately(): Promise<void> {
+ this._isDisposed = true;
+ if (this._drainTimeout) clearTimeout(this._drainTimeout);
+ return this._destroySocket();
+ }
+
connectSocket(socket: Socket): void {
+ if (this._termination) return;
socket.connect(getWorkerPipeName(this._conoutPipeName));
}
@@ -76,7 +85,10 @@
this._drainTimeout = setTimeout(() => this._destroySocket(), FLUSH_DATA_INTERVAL);
}
- private async _destroySocket(): Promise<void> {
- await this._worker.terminate();
+ private _destroySocket(): Promise<void> {
+ if (!this._termination) {
+ this._termination = this._worker.terminate().then(() => undefined);
+ }
+ return this._termination;
}
}
diff --git a/lib/windowsConoutConnection.js b/lib/windowsConoutConnection.js
--- a/lib/windowsConoutConnection.js
+++ b/lib/windowsConoutConnection.js
@@ -77,6 +77,7 @@
this._worker.on('message', function (message) {
switch (message) {
case 1 /* READY */:
+ if (_this._termination) return;
_this._onReady.fire();
return;
default:
@@ -97,7 +98,13 @@
// Drain all data from the socket before closing
this._drainDataAndClose();
};
+ ConoutConnection.prototype.disposeImmediately = function () {
+ this._isDisposed = true;
+ if (this._drainTimeout) clearTimeout(this._drainTimeout);
+ return this._destroySocket();
+ };
ConoutConnection.prototype.connectSocket = function (socket) {
+ if (this._termination) return;
socket.connect(conout_1.getWorkerPipeName(this._conoutPipeName));
};
ConoutConnection.prototype._drainDataAndClose = function () {
@@ -108,16 +115,10 @@
this._drainTimeout = setTimeout(function () { return _this._destroySocket(); }, FLUSH_DATA_INTERVAL);
};
ConoutConnection.prototype._destroySocket = function () {
- return __awaiter(this, void 0, void 0, function () {
- return __generator(this, function (_a) {
- switch (_a.label) {
- case 0: return [4 /*yield*/, this._worker.terminate()];
- case 1:
- _a.sent();
- return [2 /*return*/];
- }
- });
- });
+ if (!this._termination) {
+ this._termination = this._worker.terminate().then(function () { return undefined; });
+ }
+ return this._termination;
};
return ConoutConnection;
}());
+30
View File
@@ -0,0 +1,30 @@
diff --git a/dist/chunks/index.DpLw24bj.js b/dist/chunks/index.DpLw24bj.js
--- a/dist/chunks/index.DpLw24bj.js
+++ b/dist/chunks/index.DpLw24bj.js
@@ -12895,7 +12895,7 @@
mergedOptions.name = resolveProjectName(mergedOptions.name, index, context.ancestors.at(-1));
return {
viteConfig: parentViteConfig,
- projectConfig: resolveTestConfig(harness.logger, mergedOptions, parentViteConfig, parentConfig),
+ projectConfig: resolveTestConfig(harness.logger, mergedOptions, parentViteConfig, parentConfig, mergedOptions),
inline: true,
sharedServer: true,
ancestors: context.ancestors.length ? [...context.ancestors] : void 0
@@ -14345,7 +14345,7 @@
*
* `globalConfig` is the resolved root config, passed when resolving a project.
*/
-function resolveTestConfig(logger, options, viteConfig, globalConfig) {
+function resolveTestConfig(logger, options, viteConfig, globalConfig, rawTestConfig = viteConfig.test) {
if (options.dom) {
if (viteConfig.test?.environment != null && viteConfig.test.environment !== "happy-dom") logger.warn(withLabel("yellow", "Vitest", `Your config.test.environment ("${viteConfig.test.environment}") conflicts with --dom flag ("happy-dom"), ignoring "${viteConfig.test.environment}"`));
options.environment = "happy-dom";
@@ -14354,7 +14354,7 @@
// is merged in - the merged object cannot distinguish a default from a
// user-provided value; `viteConfig.test` is not resolved yet at this point,
// the call sites assign the resolved config to it after this function returns
- const providedOptions = captureProvidedOptions(options, viteConfig.test);
+ const providedOptions = captureProvidedOptions(options, rawTestConfig);
const resolved = deepMerge({}, configDefaults, options);
resolved.root = viteConfig.root;
resolved.providedOptions = providedOptions;
@@ -97,23 +97,136 @@ const { join, resolve } = require('node:path')
const EXPECTED_NODE_PTY_VERSION = '1.1.0'
const WINDOWS_TERMINAL_TEARDOWN_REPLACEMENTS = [
[
' var parsedEnv = _this._parseEnv(env);\n // If the terminal is ready\n _this._isReady = false;\n // Functions that need to run after `ready` event is emitted.\n _this._deferreds = [];\n // Create new termal.\n',
' var parsedEnv = _this._parseEnv(env);\n // If the terminal is ready\n _this._isReady = false;\n _this._killRequested = false;\n _this._killComplete = false;\n _this._isPipeReady = false;\n // Functions that need to run after `ready` event is emitted.\n _this._deferreds = [];\n // Create new termal.\n'
],
[
" _this._pid = _this._agent.innerPid;\n _this._fd = _this._agent.fd;\n _this._pty = _this._agent.pty;\n // The forked windows terminal is not available until `ready` event is\n // emitted.\n _this._socket.on('ready_datapipe', function () {\n // Run deferreds and set ready state once the first data event is received.\n _this._socket.once('data', function () {\n // Wait until the first data event is fired then we can run deferreds.\n if (!_this._isReady) {\n // Terminal is now ready and we can avoid having to defer method\n // calls.\n _this._isReady = true;\n",
" _this._pid = _this._agent.innerPid;\n _this._fd = _this._agent.fd;\n _this._pty = _this._agent.pty;\n // A pre-output teardown must still publish the actual pipe close.\n _this._socket.once('close', function () {\n if (_this._isPipeReady) {\n _this.emit('exit', _this._agent.exitCode);\n }\n _this._close();\n });\n // The forked windows terminal is not available until `ready` event is\n // emitted.\n _this._socket.on('ready_datapipe', function () {\n _this._isPipeReady = true;\n if (_this._killRequested) {\n _this.kill();\n return;\n }\n // Run deferreds and set ready state once the first data event is received.\n _this._socket.once('data', function () {\n // Wait until the first data event is fired then we can run deferreds.\n if (!_this._isReady && !_this._killRequested) {\n // Terminal is now ready and we can avoid having to defer method\n // calls.\n _this._isReady = true;\n"
],
[
" _this._deferreds = [];\n }\n });\n // Cleanup after the socket is closed.\n _this._socket.on('close', function () {\n _this.emit('exit', _this._agent.exitCode);\n _this._close();\n });\n });\n _this._file = file;\n _this._name = name;\n",
' _this._deferreds = [];\n }\n });\n });\n _this._file = file;\n _this._name = name;\n'
],
[
" });\n };\n WindowsTerminal.prototype.destroy = function () {\n var _this = this;\n this._deferNoArgs(function () {\n _this.kill();\n });\n };\n WindowsTerminal.prototype.kill = function (signal) {\n var _this = this;\n this._deferNoArgs(function () {\n if (signal) {\n throw new Error('Signals not supported on windows.');\n }\n _this._close();\n _this._agent.kill();\n });\n };\n WindowsTerminal.prototype._deferNoArgs = function (deferredFn) {\n var _this = this;\n // If the terminal is ready, execute.\n if (this._isReady) {\n",
" });\n };\n WindowsTerminal.prototype.destroy = function () {\n this.kill();\n };\n WindowsTerminal.prototype.kill = function (signal) {\n if (signal) {\n throw new Error('Signals not supported on windows.');\n }\n // Retire input now; native close requires the forwarding pipe, not first output.\n this._killRequested = true;\n this._deferreds = [];\n this._close();\n if (!this._isPipeReady || this._killComplete) {\n return;\n }\n this._agent.kill();\n this._killComplete = true;\n };\n WindowsTerminal.prototype._deferNoArgs = function (deferredFn) {\n if (this._killRequested) {\n return;\n }\n var _this = this;\n // If the terminal is ready, execute.\n if (this._isReady) {\n"
],
[
' });\n };\n WindowsTerminal.prototype._defer = function (deferredFn, arg) {\n var _this = this;\n // If the terminal is ready, execute.\n if (this._isReady) {\n',
' });\n };\n WindowsTerminal.prototype._defer = function (deferredFn, arg) {\n if (this._killRequested) {\n return;\n }\n var _this = this;\n // If the terminal is ready, execute.\n if (this._isReady) {\n'
]
]
const PRECONNECT_AGENT_REPLACEMENTS = [
[
'var utils_1 = require("./utils");',
'var utils_1 = require("./utils");\nvar eventEmitter2_1 = require("./eventEmitter2");'
],
[
' this._innerPid = 0;',
' this._innerPid = 0;\n this._onProcessExit = new eventEmitter2_1.EventEmitter2();'
],
[
' WindowsPtyAgent.prototype.kill = function () {',
' Object.defineProperty(WindowsPtyAgent.prototype, "onProcessExit", {\n get: function () { return this._onProcessExit.event; },\n enumerable: false,\n configurable: true\n });\n Object.defineProperty(WindowsPtyAgent.prototype, "isConpty", {\n get: function () { return this._useConpty === true; },\n enumerable: false,\n configurable: true\n });\n WindowsPtyAgent.prototype.killAfterOutputClosed = function () {\n var _this = this;\n return this._conoutSocketWorker.disposeImmediately().then(function () { return _this.kill(); });\n };\n WindowsPtyAgent.prototype.kill = function () {'
],
[
" this._outSocket.on('data', function () { return _this._flushDataAndCleanUp(); });\n }\n };",
" this._outSocket.on('data', function () { return _this._flushDataAndCleanUp(); });\n }\n this._onProcessExit.fire(exitCode);\n };"
]
]
const PRECONNECT_TERMINAL_REPLACEMENTS = [
[
' _this._isPipeReady = false;',
' _this._isPipeReady = false;\n _this._exitEmitted = false;\n _this._preconnectCleanupRequested = false;'
],
[
" // A pre-output teardown must still publish the actual pipe close.\n _this._socket.once('close', function () {\n if (_this._isPipeReady) {\n _this.emit('exit', _this._agent.exitCode);\n }\n _this._close();\n });",
" _this._agent.onProcessExit(function () {\n if (_this._killRequested) _this._emitExit();\n });\n _this._socket.once('close', function () {\n if (_this._isPipeReady && !_this._preconnectCleanupRequested && (!_this._killRequested || !_this._agent.isConpty || _this._agent.exitCode !== undefined)) _this._emitExit();\n _this._close();\n if (_this._killRequested && !_this._isPipeReady) _this._killAfterOutputClosed();\n });"
],
[
' if (!this._isPipeReady || this._killComplete) {\n return;\n }',
' if (this._killComplete || this._preconnectCleanup) return;\n if (this._preconnectCleanupRequested || (!this._isPipeReady && this._socket.destroyed)) {\n this._killAfterOutputClosed();\n return;\n }\n if (!this._isPipeReady) return;'
],
[
' WindowsTerminal.prototype._deferNoArgs =',
" WindowsTerminal.prototype._emitExit = function () {\n if (this._exitEmitted) return;\n this._exitEmitted = true;\n this.emit('exit', this._agent.exitCode);\n };\n WindowsTerminal.prototype._killAfterOutputClosed = function () {\n var _this = this;\n if (!this._agent.isConpty || this._killComplete || this._preconnectCleanup) return;\n this._preconnectCleanupRequested = true;\n this._preconnectCleanup = this._agent.killAfterOutputClosed().then(function () {\n _this._killComplete = true;\n if (_this._agent.exitCode !== undefined) _this._emitExit();\n }, function (error) {\n _this._preconnectCleanup = undefined;\n if (_this.listeners('error').length > 1) {\n _this.emit('error', error);\n }\n });\n if (this._agent.exitCode !== undefined) this._emitExit();\n };\n WindowsTerminal.prototype._deferNoArgs ="
]
]
const PRECONNECT_CONOUT_REPLACEMENTS = [
[
' case 1 /* READY */:\n _this._onReady.fire();',
' case 1 /* READY */:\n if (_this._termination) return;\n _this._onReady.fire();'
],
[
' ConoutConnection.prototype.connectSocket = function (socket) {\n socket.connect',
' ConoutConnection.prototype.disposeImmediately = function () {\n this._isDisposed = true;\n if (this._drainTimeout) clearTimeout(this._drainTimeout);\n return this._destroySocket();\n };\n ConoutConnection.prototype.connectSocket = function (socket) {\n if (this._termination) return;\n socket.connect'
],
[
' ConoutConnection.prototype._destroySocket = function () {\n return __awaiter(this, void 0, void 0, function () {\n return __generator(this, function (_a) {\n switch (_a.label) {\n case 0: return [4 /*yield*/, this._worker.terminate()];\n case 1:\n _a.sent();\n return [2 /*return*/];\n }\n });\n });\n };\n',
' ConoutConnection.prototype._destroySocket = function () {\n if (!this._termination) {\n this._termination = this._worker.terminate().then(function () { return undefined; });\n }\n return this._termination;\n };\n'
]
]
/** Each entry is one published file, its patched form, and the edits between them. */
const PATCH_TARGETS = [
{
relativePath: ['lib', 'windowsConoutConnection.js'],
originalSha256: '1440f70908fb1f55911ac8e936a1230f68a9c00c03096fcef6788eac6aad9d62',
patchedSha256: '37fab0688764326444509f7ffb24369f99fda4fbe29db72dccfad52c457ba244',
replacements: PRECONNECT_CONOUT_REPLACEMENTS
},
{
relativePath: ['lib', 'windowsPtyAgent.js'],
originalSha256: '8636d16b38266112204061a22b135734177c242837982fd3a4055be726efa64a',
patchedSha256: '1e23ef480569e73706e3ab4f5482c7e553c76f51414ae8e7b0bdcc2fd75f7280',
patchedSha256: '3c14daf8d0ec2d1e2d66435caa5fb2b629b230e237873594e623e79e6a7d1223',
previousPatchedSha256: '1e23ef480569e73706e3ab4f5482c7e553c76f51414ae8e7b0bdcc2fd75f7280',
previousReplacements: PRECONNECT_AGENT_REPLACEMENTS,
replacements: [
[
' this._ptyNative.kill(this._pty, this._useConptyDll);\n this._conoutSocketWorker.dispose();\n',
' this._ptyNative.kill(this._pty, this._useConptyDll);\n this._conoutSocketWorker.dispose();\n // Orca: released AFTER the console-list fork and the native kill, not before them.\n // Destroying conin first aborts teardown partway -- measured on a Windows SSH relay\n // as +2 File and +1 Process handles per terminal, against +1 File unpatched.\n this._inSocket.destroy();\n'
]
],
...PRECONNECT_AGENT_REPLACEMENTS
]
},
{
relativePath: ['lib', 'windowsTerminal.js'],
originalSha256: 'c3a65716f53fed0135a8a633373d5f9c2ab092544d651f27ef0a67096dd3bcd9',
patchedSha256: '8247ecd69be8b18257050fb026b290024612c5ffc6d492ff1d46f81e613be2cf',
patchedSha256: '5dfeb1dda46645e1d77964ad4d07072b34f0a897dfc7e785137cc39e755b4641',
additionalPreviousVariants: [
{
sha256: '3060c6514a8e9e3285f91b9b549930e7d25d59d4cf7e1ed3a25b9a680dd1ded5',
replacements: PRECONNECT_TERMINAL_REPLACEMENTS
},
{
sha256: '598755ee75307d041a72cd7c7c4e12ae4a4bbf35eee19da67b42b61fdae0d4d6',
replacements: [
[
" _this._preconnectCleanup = undefined;\n _this.emit('error', error);",
" _this._preconnectCleanup = undefined;\n if (_this.listeners('error').length > 1) {\n _this.emit('error', error);\n }"
],
[
' if (_this._preconnectCleanupRequested) _this._emitExit();',
' if (_this._killRequested) _this._emitExit();'
],
[
' if (_this._isPipeReady && !_this._preconnectCleanupRequested) _this._emitExit();',
' if (_this._isPipeReady && !_this._preconnectCleanupRequested && (!_this._killRequested || !_this._agent.isConpty || _this._agent.exitCode !== undefined)) _this._emitExit();'
]
]
}
],
previousPatchedSha256: '8247ecd69be8b18257050fb026b290024612c5ffc6d492ff1d46f81e613be2cf',
previousReplacements: [
...WINDOWS_TERMINAL_TEARDOWN_REPLACEMENTS,
...PRECONNECT_TERMINAL_REPLACEMENTS
],
replacements: [
[
' _this._agent = new windowsPtyAgent_1.WindowsPtyAgent(file, args, parsedEnv, cwd, _this._cols, _this._rows, false, opt.useConpty, opt.useConptyDll, opt.conptyInheritCursor);\n _this._socket = _this._agent.outSocket;\n // Not available until `ready` event emitted.\n _this._pid = _this._agent.innerPid;',
@@ -130,7 +243,9 @@ const PATCH_TARGETS = [
[
'exports.WindowsTerminal = WindowsTerminal;\n//# sourceMappingURL=windowsTerminal.js.map',
'exports.WindowsTerminal = WindowsTerminal;\n//# sourceMappingURL=windowsTerminal.js.map\n'
]
],
...WINDOWS_TERMINAL_TEARDOWN_REPLACEMENTS,
...PRECONNECT_TERMINAL_REPLACEMENTS
]
}
]
@@ -159,19 +274,27 @@ function assertPatchedNodePtyWindowsTeardown(relayDir = process.cwd()) {
}
function patchNodePtyWindowsTeardown(relayDir = process.cwd()) {
const pending = []
for (const target of PATCH_TARGETS) {
const inspected = inspectTarget(relayDir, target)
const sourceHash = sourceSha256(inspected.source)
if (sourceHash === target.patchedSha256) {
continue
}
if (sourceHash !== target.originalSha256) {
const replacements =
sourceHash === target.originalSha256
? target.replacements
: sourceHash === target.previousPatchedSha256
? target.previousReplacements
: target.additionalPreviousVariants?.find((entry) => entry.sha256 === sourceHash)
?.replacements
if (!replacements) {
throw new Error(
`Refusing to patch unexpected node-pty source in ${target.relativePath.join('/')}`
)
}
let patchedSource = inspected.source
for (const [from, to] of target.replacements) {
for (const [from, to] of replacements) {
// Why the count check: an anchor that matched twice would patch the wrong site silently, and
// the hash below would then reject a tree this script had already rewritten.
if (patchedSource.split(from).length - 1 !== 1) {
@@ -179,11 +302,19 @@ function patchNodePtyWindowsTeardown(relayDir = process.cwd()) {
}
patchedSource = patchedSource.replace(from, to)
}
const temporaryPath = `${inspected.filePath}.orca-patch-${process.pid}`
if (sourceSha256(patchedSource) !== target.patchedSha256) {
throw new Error(
`Refusing to install unexpected patched node-pty source in ${target.relativePath.join('/')}`
)
}
pending.push({ filePath: inspected.filePath, patchedSource })
}
for (const { filePath, patchedSource } of pending) {
const temporaryPath = `${filePath}.orca-patch-${process.pid}`
// Why: a terminated remote install must leave either known source version recoverable on reconnect.
try {
writeFileSync(temporaryPath, patchedSource)
renameSync(temporaryPath, inspected.filePath)
renameSync(temporaryPath, filePath)
} finally {
rmSync(temporaryPath, { force: true })
}
+2 -21
View File
@@ -6528,7 +6528,7 @@
"pnpm exec vitest run --config config/vitest.config.ts src/shared/browser-client-host-reconciliation-protocol.test.ts src/main/browser/paired-runtime-browser-host-reconciliation-negotiation.test.ts src/main/runtime/rpc/methods/browser-client-host-reconciliation.test.ts src/main/runtime/browser-host-lease-registry.test.ts src/main/browser/paired-runtime-browser-client-host.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/shared/browser-client-host-protocol.test.ts src/main/browser/browser-client-host-attach-request.test.ts src/main/browser/browser-client-page-command-executor.test.ts src/main/browser/browser-client-page-command-integration.test.ts src/main/browser/browser-client-page-inventory.test.ts src/main/browser/paired-runtime-browser-client-host-composition.test.ts src/main/browser/paired-runtime-browser-client-host.test.ts src/main/browser/paired-runtime-browser-host-lease.test.ts src/main/runtime/browser-host-lease-registry.test.ts src/main/runtime/rpc/methods/browser-client-host.test.ts src/main/runtime/browser-host-page-reconciliation-plan.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/main/browser/browser-client-host-reconciliation-command-order.test.ts src/main/browser/browser-client-page-reconciliation-adapters.test.ts src/main/browser/browser-route-prepared-page-rekey.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/shared/browser-network-capabilities.test.ts src/shared/browser-network-tunnel-protocol.test.ts src/main/browser/browser-network-tunnel-session.test.ts src/main/browser/remote-browser-socks-server.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/shared/browser-network-tunnel-protocol.test.ts src/main/browser/browser-network-tunnel-session.test.ts src/main/browser/remote-browser-socks-server.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/main/browser/browser-network-tunnel-client.test.ts src/main/browser/paired-runtime-browser-network-route.test.ts src/main/runtime/runtime-binary-message-router.test.ts src/main/runtime/rpc/methods/browser-network-tunnel.test.ts src/main/runtime/browser-network-tunnel-paired-runtime.integration.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/main/browser/browser-network-tunnel-outbound-memory-budget.test.ts src/main/browser/browser-network-tunnel-client-memory-budget.test.ts src/main/browser/browser-network-tunnel-session-aggregate-memory.test.ts src/shared/ws-outbound-backpressure-queue.test.ts src/shared/remote-runtime-client.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/shared/browser-client-host-protocol.test.ts src/shared/ws-outbound-backpressure-queue.test.ts src/shared/remote-runtime-client.test.ts src/main/runtime/browser-host-lease-registry.test.ts src/main/runtime/runtime-rpc-browser-host-admission.test.ts src/main/runtime/rpc/methods/browser-client-host.test.ts src/main/runtime/rpc/methods/browser-network-tunnel.test.ts src/main/browser/paired-runtime-browser-host-lease.test.ts src/main/browser/paired-runtime-browser-network-route.test.ts src/main/runtime/browser-network-tunnel-paired-runtime.integration.test.ts src/main/browser/browser-network-tunnel-client.test.ts src/main/browser/browser-network-tunnel-client-memory-budget.test.ts src/main/browser/browser-network-tunnel-session.test.ts src/main/browser/browser-network-tunnel-session-aggregate-memory.test.ts src/main/browser/browser-network-tunnel-outbound-memory-budget.test.ts src/main/browser/remote-browser-socks-server.test.ts",
@@ -6544,7 +6544,7 @@
"pnpm exec vitest run --config config/vitest.config.ts src/shared/browser-client-host-protocol.test.ts src/main/browser/browser-client-host-attach-request.test.ts src/main/browser/browser-client-network-route-registry.test.ts src/main/browser/paired-runtime-browser-client-host-composition.test.ts src/main/browser/paired-runtime-browser-client-host.test.ts src/main/browser/paired-runtime-browser-client-host-reconnect.test.ts src/main/browser/paired-runtime-browser-host-lease.test.ts src/main/browser/paired-runtime-browser-host-lease-reconnect.test.ts src/main/browser/browser-host-lease-reconnect-delay.test.ts src/main/browser/paired-runtime-browser-network-route.test.ts src/main/runtime/browser-host-command-ledger.test.ts src/main/runtime/browser-host-command-ledger-capacity.test.ts src/main/runtime/browser-host-lease-placement-retirement.test.ts src/main/runtime/browser-host-lease-registry.test.ts src/main/runtime/runtime-rpc-browser-host-admission.test.ts src/main/runtime/rpc/methods/browser-client-host.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/main/browser/browser-client-network-route-registry.test.ts src/main/browser/paired-runtime-browser-client-host-composition.test.ts src/main/browser/paired-runtime-browser-client-host-registry.test.ts src/main/browser/paired-runtime-browser-client-host-runtime.test.ts src/main/browser/browser-client-page-command-executor.test.ts src/main/browser/browser-session-startup.test.ts src/main/ipc/runtime-environments-subscription-teardown.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/browser-host-command-ledger.test.ts src/main/runtime/browser-host-command-ledger-capacity.test.ts src/main/runtime/browser-host-lease-registry.test.ts src/main/runtime/rpc/methods/browser-client-host.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/browser-host-lease-registry.test.ts src/main/browser/browser-network-deferred-socket.test.ts src/main/browser/browser-network-execution-route.test.ts src/main/browser/paired-runtime-browser-network-route.test.ts src/main/runtime/rpc/methods/browser-network-tunnel.test.ts src/main/browser/ssh-browser-network-execution-route.test.ts src/main/browser/system-ssh-socks-client-socket.test.ts src/main/ssh/system-ssh-dynamic-forward-process.test.ts src/shared/browser-client-host-protocol.test.ts src/shared/browser-network-capabilities.test.ts src/main/ssh/system-ssh-forward-process.test.ts src/main/ssh/ssh-system-fallback.test.ts src/main/ssh/ssh-port-forward.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/browser-host-lease-registry.test.ts src/main/browser/browser-network-deferred-socket.test.ts src/main/browser/browser-network-execution-route.test.ts src/main/browser/paired-runtime-browser-network-route.test.ts src/main/runtime/rpc/methods/browser-network-tunnel.test.ts src/main/browser/ssh-browser-network-execution-route.test.ts src/main/browser/system-ssh-socks-client-socket.test.ts src/main/ssh/system-ssh-dynamic-forward-process.test.ts src/shared/browser-client-host-protocol.test.ts src/main/ssh/system-ssh-forward-process.test.ts src/main/ssh/ssh-system-fallback.test.ts src/main/ssh/ssh-port-forward.test.ts",
"ORCA_RUN_DOCKER_SSH_BROWSER_E2E=1 pnpm exec vitest run --config config/vitest.config.ts tests/e2e/ssh-browser-network-execution-route.docker.unit.test.ts",
"ORCA_RUN_DOCKER_SSH_BROWSER_E2E=1 node_modules/.bin/vitest run --config config/vitest.config.ts tests/e2e/ssh-browser-network-execution-route.docker.unit.test.ts"
],
@@ -6554,7 +6554,6 @@
"src/main/runtime/runtime-browser-client-page-recovery.test.ts",
"src/main/runtime/rpc/methods/session-tab-browser-placement-mutations.test.ts",
"src/main/runtime/rpc/methods/session-tab-browser-placement-projection.test.ts",
"src/shared/browser-network-capabilities.test.ts",
"src/shared/browser-client-host-protocol.test.ts",
"src/shared/browser-client-host-reconciliation-protocol.test.ts",
"src/shared/browser-network-tunnel-protocol.test.ts",
@@ -7251,15 +7250,6 @@
"durationSeconds": 0.2,
"summary": "Four files passed 26 two-phase retirement, exact replacement, pre-selection admission, runtime/epoch/host/page authority, live-lease fencing, monotonic generation, invalid-identity, default 256-placement capacity, and stale-settlement tests."
},
{
"date": "2026-08-14",
"runner": "local",
"platform": "macos",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/browser-host-lease-registry.test.ts src/main/browser/browser-network-deferred-socket.test.ts src/main/browser/browser-network-execution-route.test.ts src/main/browser/paired-runtime-browser-network-route.test.ts src/main/runtime/rpc/methods/browser-network-tunnel.test.ts src/main/browser/ssh-browser-network-execution-route.test.ts src/main/browser/system-ssh-socks-client-socket.test.ts src/main/ssh/system-ssh-dynamic-forward-process.test.ts src/shared/browser-client-host-protocol.test.ts src/shared/browser-network-capabilities.test.ts src/main/ssh/system-ssh-forward-process.test.ts src/main/ssh/ssh-system-fallback.test.ts src/main/ssh/ssh-port-forward.test.ts",
"result": "passed",
"durationSeconds": 2.6,
"summary": "Thirteen files passed 142 structural route-key, native-shape, live lease-bound SSH grant, startup cancellation, diagnostic redaction, deferred-socket release, capability negotiation, exact SSH authority and client, ssh2 forwarding, standalone system-SSH dynamic-forward, active-probe cleanup, static-forward option reuse, internal SOCKS domain, and exact release tests."
},
{
"date": "2026-08-14",
"runner": "local",
@@ -7296,15 +7286,6 @@
"durationSeconds": 3.4,
"summary": "Eleven files passed 67 lease, reserved long-poll and host admission, token-safe placement retirement, authority, capability, route-generation, adversarial-revocation, lifecycle, flow-control, SOCKS, and real paired E2EE tests."
},
{
"date": "2026-08-13",
"runner": "local",
"platform": "macos",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/shared/browser-network-capabilities.test.ts src/shared/browser-network-tunnel-protocol.test.ts src/main/browser/browser-network-tunnel-session.test.ts src/main/browser/remote-browser-socks-server.test.ts",
"result": "passed",
"durationSeconds": 0.4,
"summary": "Four files passed 25 deterministic capability, codec, credit, generation, half-close, SOCKS routing, remote-DNS, and fail-closed tests."
},
{
"date": "2026-08-14",
"runner": "local",
@@ -98,26 +98,6 @@ describe('timing-weighted shard selection', () => {
expect.arrayContaining([...NODE_RUNTIME_INCLUDE, measurementFile])
)
}
for (const project of projects) {
expect(project.extends).toBe(false)
expect(project.define).toEqual(config.define)
expect(project.resolve).toEqual(config.resolve)
expect(project.test.execArgv).toEqual(['--no-experimental-webstorage', '--expose-gc'])
expect(project.test.setupFiles).toEqual([
resolve('config/scripts/vitest-real-agent-home-write-guard.ts'),
resolve('config/scripts/vitest-bun-node-builtins.ts'),
resolve('config/scripts/happy-dom-offscreen-canvas.ts'),
resolve('config/scripts/happy-dom-mutation-observer-retention.ts'),
resolve('config/scripts/vitest-host-ports-setup.ts'),
resolve('config/scripts/vitest-caller-identity-env-setup.ts')
])
expect(project.test).toMatchObject({
environment: 'node',
server: { deps: { inline: ['zod'] } },
hookTimeout: 60_000,
testTimeout: 30_000
})
}
})
it('keeps nested/serial E2E files atomic and fails closed on discovery errors', () => {
@@ -1,5 +1,5 @@
import { build } from 'esbuild'
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
@@ -54,12 +54,4 @@ describe('foreign SQLite reader build smoke', () => {
)
expect(() => smokeForeignSqliteReaderWorker(directory)).toThrow('smoke failed')
})
it('runs in the orcad build under both runtimes', () => {
const source = readFileSync(resolve('config/scripts/build-orcad.mjs'), 'utf8')
expect(source).toContain(
"{ label: 'foreign SQLite reader worker', smoke: smokeForeignSqliteReaderWorker }"
)
expect(source).toContain('await smoke(OUT_DIR, { runtimePath: nodeRuntimePath })')
})
})
@@ -1,104 +0,0 @@
/**
* One owner for the mirror the hybrid shell reads (ruling 35).
*
* `mirrored-storage-keys.ts` holds the map the shell builds every `init` from, synchronously, and
* before this the fourteen writers of a mirrored key noted it themselves — first, then persisted.
* On the page a persist can be refused, so twelve of them left the map holding a value no store
* had taken and the next `init` handed the page exactly that; the other two undid it by hand.
*
* Source-scanning rather than behavioural, and about existence rather than shape: what a
* behavioural case cannot say is that no thirteenth writer appears next week. Each row below is a
* module that owns a mirrored key, so deleting its write path reds that row by name, and every
* failure quotes the line it found.
*/
import { globSync, readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
const mobileDir = fileURLToPath(new URL('../../mobile/', import.meta.url))
const MIRROR_MODULE = 'src/storage/mirrored-storage-keys.ts'
/** Every module that persists a key the shell mirrors, with the constant each one writes. */
const MIRRORED_WRITERS = [
{
file: 'src/storage/preferences.ts',
keys: ['TEXT_SCALE_KEY', 'SIDEBAR_WIDTH_KEY', 'DOCK_WIDTH_KEY']
},
{ file: 'src/storage/session-view-preferences.ts', keys: ['DEFAULT_SESSION_VIEW_KEY'] },
{
file: 'src/terminal/terminal-accessory-layout.ts',
keys: ['TERMINAL_ACCESSORY_LAYOUT_STORAGE_KEY']
},
{ file: 'src/components/CustomKeyModal.tsx', keys: ['CUSTOM_ACCESSORY_KEYS_STORAGE_KEY'] },
{
file: 'src/worktree/last-visited-worktree-repo.ts',
keys: ['LAST_VISITED_WORKTREE_STORAGE_KEY']
}
]
/**
* The one caller of the note-then-persist path, which is the shell taking a value the page has
* already applied into a store that refuses nothing.
*
* Counted rather than described: the module says the census holds it to one caller, and until
* this row nothing did. A second caller is either a writer that wants the ordering without the
* store that earns it, or a page-reachable module that would note a refusal as an accepted write.
*/
const NOTE_FIRST_CALLER = 'src/mobile-web-shell/use-page-host-snapshot.ts'
/** Every module under `mobile/src`, so a new caller cannot arrive in a file no row names. */
function mobileSources() {
return globSync('src/**/*.{ts,tsx}', { cwd: mobileDir }).sort()
}
/** The line a match sits on, so a failure names what it found rather than only that it found one. */
function linesMatching(source, pattern) {
return source
.split('\n')
.map((line, index) => ({ line: line.trim(), at: index + 1 }))
.filter((entry) => pattern.test(entry.line))
}
function read(file) {
return readFileSync(new URL(file, new URL(mobileDir, 'file:///')), 'utf8')
}
describe('the mirrored storage write path', () => {
it('is the only thing that writes the map, which no other module can reach', () => {
const owner = read(MIRROR_MODULE)
// The map itself: a second module holding a reference to it would be a second owner, and the
// map is not exported, so this is what says so.
expect(linesMatching(owner, /^export (const|let) mirror\b/)).toEqual([])
expect(linesMatching(owner, /^export function note\b/)).toEqual([])
})
it(`calls the note-first path from ${NOTE_FIRST_CALLER} and nowhere else`, () => {
const callers = mobileSources().filter((file) => {
if (file === MIRROR_MODULE) {
return false
}
return linesMatching(read(file), /\bwriteMirroredStorage\(/).length > 0
})
expect(callers).toEqual([NOTE_FIRST_CALLER])
})
for (const row of MIRRORED_WRITERS) {
it(`writes ${row.file} through the one path and never around it`, () => {
const source = read(row.file)
expect(linesMatching(source, /\bpersistMirrored\(/).length).toBeGreaterThan(0)
// Around it would be a store call naming a key the map holds, which is the shape every one
// of these had before: note the map, then persist, and nothing between the two agreeing.
for (const key of row.keys) {
expect(
linesMatching(source, new RegExp(`AsyncStorage\\.(setItem|removeItem)\\(\\s*${key}\\b`)),
`${row.file} writes ${key} past the mirror`
).toEqual([])
}
expect(
linesMatching(source, /\bnoteMirroredWrite\b/),
`${row.file} notes the map itself`
).toEqual([])
})
}
})
@@ -1,200 +0,0 @@
import { existsSync } from 'node:fs'
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, relative } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
/**
* Every `setNativeProps` write the mobile app makes, and the web sibling it owes.
*
* `setNativeProps` is a React Native host-component method. On React Native Web a ref is the DOM
* node itself and has no such method, so the call is not a write that does nothing — it is a
* `TypeError`. Where it is made from a render or an effect, that throw reaches the page's fault
* boundary and takes the whole screen down; the shell then tears the view out from under the
* route, which is how one imperative write becomes a 0x0 terminal and a keyboard that never
* opens.
*
* Fenced by absence rather than by a list of approved callers: a module that starts writing this
* way is held to the rule without anyone remembering to add it here. The rule names the file
* rather than the call, because a `.web.ts(x)` sibling is what the page bundler resolves in its
* place, and that substitution is per module.
*
* Both trees the bundler resolves out of are scanned, and `app` is judged more strictly: a route
* file may not make the call at all, because expo-router registers a `.web.tsx` beside a route as
* a second route rather than as a platform sibling, so the split has to live in `src`.
*
* Two offences this caught, both invisible natively and both now fixed by one seam:
* `use-terminal-live-pending-input-flush.ts` cleared the live-input field from the session route's
* mount effect, and `use-terminal-live-accessory-input-commit.ts` wrote the shortened text into it
* when the accessory bar erased a character. Both now call
* `src/terminal/terminal-live-input-text-write.ts`, which ships the sibling.
*/
const mobileDir = join(fileURLToPath(new URL('../../', import.meta.url)), 'mobile')
/**
* Both trees the page bundler resolves modules out of. `app` is the route tree the shell serves,
* and it is judged by a stricter rule: expo-router registers a `.web.tsx` beside a route as a
* second route rather than as a platform sibling, so a route file cannot own a platform split at
* all and the write has to move into a `src` module that can.
*/
const SCANNED = ['src', 'app']
const SOURCE = /\.tsx?$/
const WEB_SIBLING = /\.web\.tsx?$/
const TEST = /\.(test|spec)\.tsx?$/
/** The call, not the word: a docstring naming it is not a write. */
const NATIVE_WRITE = /\.setNativeProps\(/
async function listFiles(directory) {
const out = []
for (const entry of await readdir(directory, { withFileTypes: true })) {
if (entry.name === 'node_modules') {
continue
}
const entryPath = join(directory, entry.name)
if (entry.isDirectory()) {
out.push(...(await listFiles(entryPath)))
} else if (entry.isFile() && SOURCE.test(entry.name) && !TEST.test(entry.name)) {
out.push(entryPath)
}
}
return out
}
const posix = (path) => path.split('\\').join('/')
const exists = (path) =>
readFile(path).then(
() => true,
() => false
)
/** Takes the root so the census can be run against a scratch tree and shown to fail. */
export async function nativePropsWriteSites(rootDir) {
const found = []
for (const tree of SCANNED) {
const directory = join(rootDir, tree)
if (!existsSync(directory)) {
continue
}
for (const file of await listFiles(directory)) {
if (NATIVE_WRITE.test(await readFile(file, 'utf8'))) {
found.push(posix(relative(rootDir, file)))
}
}
}
return found.sort()
}
/** Every `src` module that writes this way and has no `.web.ts(x)` to be replaced by. */
export async function nativePropsWritesWithoutWebSibling(rootDir) {
const offenders = []
for (const site of await nativePropsWriteSites(rootDir)) {
if (WEB_SIBLING.test(site) || !site.startsWith('src/')) {
continue
}
const base = join(rootDir, site).replace(SOURCE, '')
const hasSibling = (await exists(`${base}.web.ts`)) || (await exists(`${base}.web.tsx`))
if (!hasSibling) {
offenders.push(site)
}
}
return offenders
}
/** Every route file that writes this way, which no sibling can rescue. */
export async function routeTreeNativePropsWrites(rootDir) {
return (await nativePropsWriteSites(rootDir)).filter((site) => site.startsWith('app/'))
}
describe('the setNativeProps writes the mobile app makes', () => {
it('gives every one of them a web sibling for the page to resolve instead', async () => {
expect(await nativePropsWritesWithoutWebSibling(mobileDir)).toEqual([])
})
it('makes none of them from a route file, which can carry no platform sibling', async () => {
expect(await routeTreeNativePropsWrites(mobileDir)).toEqual([])
})
it('makes none of them from a web sibling, where the method does not exist at all', async () => {
// The other half of the same rule. A sibling that kept the native call would satisfy the case
// above — the file it replaces has one — and still throw on the page.
expect(
(await nativePropsWriteSites(mobileDir)).filter((site) => WEB_SIBLING.test(site))
).toEqual([])
})
it('reads writes at all, so the two rules above are not vacuous', async () => {
// An empty offender list is also what a tree with no `setNativeProps` in it produces. These
// are the three seams that own every such write today, each beside its own `.web.ts`, and
// the list spans both scanned trees so `app` having none is a reading rather than a gap.
expect(await nativePropsWriteSites(mobileDir)).toEqual([
'src/browser/browser-frame-layer-paint.ts',
'src/terminal/terminal-live-input-text-write.ts',
'src/terminal/terminal-settings-scroll-lock.ts'
])
})
})
async function withScratch(run) {
const scratch = await mkdtemp(join(tmpdir(), 'orca-mobile-set-native-props-'))
try {
return await run(scratch)
} finally {
await rm(scratch, { recursive: true, force: true })
}
}
async function plant(scratch, file, source) {
await mkdir(join(scratch, file, '..'), { recursive: true })
await writeFile(join(scratch, file), source, 'utf8')
}
const WRITES = 'export const paint = (ref) => ref.current?.setNativeProps({ text: "" })\n'
// A census that happens to be run against a tree with no offence in it passes for the wrong
// reason. These plant the shapes it claims to judge and show what it would report.
describe('the census scan', () => {
it('names a native module that writes with no sibling beside it', async () => {
await withScratch(async (scratch) => {
await plant(scratch, 'src/terminal/use-terminal-live-pending-input-flush.ts', WRITES)
await plant(scratch, 'src/terminal/use-terminal-live-accessory-input-commit.ts', WRITES)
expect(await nativePropsWritesWithoutWebSibling(scratch)).toEqual([
'src/terminal/use-terminal-live-accessory-input-commit.ts',
'src/terminal/use-terminal-live-pending-input-flush.ts'
])
})
})
it('clears a native module once its sibling is on disk, for .ts and for .tsx', async () => {
await withScratch(async (scratch) => {
await plant(scratch, 'src/terminal/write.ts', WRITES)
await plant(scratch, 'src/terminal/write.web.ts', 'export const paint = () => {}\n')
await plant(scratch, 'src/browser/Pane.tsx', WRITES)
await plant(scratch, 'src/browser/Pane.web.tsx', 'export const paint = () => {}\n')
expect(await nativePropsWritesWithoutWebSibling(scratch)).toEqual([])
})
})
it('names a route file that writes, and is not satisfied by a sibling beside it', async () => {
await withScratch(async (scratch) => {
await plant(scratch, 'app/terminal-settings.tsx', WRITES)
// Planted to show the stricter rule is not the sibling rule wearing a different name: a
// route with a `.web.tsx` next to it is still an offence, because expo-router reads that
// file as a second route.
await plant(scratch, 'app/terminal-settings.web.tsx', 'export default () => null\n')
expect(await routeTreeNativePropsWrites(scratch)).toEqual(['app/terminal-settings.tsx'])
expect(await nativePropsWritesWithoutWebSibling(scratch)).toEqual([])
})
})
it('skips tests, which write this way to prove the native call throws', async () => {
await withScratch(async (scratch) => {
await plant(scratch, 'src/terminal/write.test.ts', WRITES)
await plant(scratch, 'src/terminal/write.spec.tsx', WRITES)
expect(await nativePropsWriteSites(scratch)).toEqual([])
})
})
})
@@ -13,6 +13,7 @@
// (`src/main/rate-limits/claude-pty.ts`) does omit the option and so does run this hunk, but no
// user-visible pane does. The divergence pinned below is about which
// branch each host runs for terminals -- not about a regression in the panes users open.
import { createHash } from 'node:crypto'
import { createRequire } from 'node:module'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
@@ -26,7 +27,7 @@ const {
const projectDir = resolve(import.meta.dirname, '..', '..')
const cleanupDirs = []
const PATCHED_FILES = ['windowsPtyAgent.js', 'windowsTerminal.js']
const PATCHED_FILES = ['windowsPtyAgent.js', 'windowsTerminal.js', 'windowsConoutConnection.js']
/** The hunks config/patches/node-pty@1.1.0.patch adds to the installed desktop tree. */
const DESKTOP_HUNKS = {
@@ -138,6 +139,126 @@ describe('Windows SSH relay node-pty ConPTY teardown patch', () => {
)
})
it.each([
'8247ecd69be8b18257050fb026b290024612c5ffc6d492ff1d46f81e613be2cf',
'598755ee75307d041a72cd7c7c4e12ae4a4bbf35eee19da67b42b61fdae0d4d6'
])(
'upgrades the exact %s Windows terminal patch without changing relay agent ordering',
(legacySha256) => {
const fixture = writeNodePtyFixture('1.1.0')
const asset = readFileSync(
join(projectDir, 'config', 'relay-assets', 'node-pty-1.1.0-windows-pty-teardown-patch.cjs'),
'utf8'
)
const { PATCH_TARGETS } = loadPatchTargets(asset)
const target = PATCH_TARGETS.find(
(entry) => entry.relativePath.at(-1) === 'windowsTerminal.js'
)
const publishedPath = join(fixture.libDir, 'windowsTerminal.js')
const publishedTerminal = readFileSync(publishedPath, 'utf8')
patchNodePtyWindowsTeardown(fixture.root)
const relayAgentPath = join(fixture.libDir, 'windowsPtyAgent.js')
const relayAgent = readFileSync(relayAgentPath, 'utf8')
expect(createHash('sha256').update(relayAgent).digest('hex')).toBe(
'3c14daf8d0ec2d1e2d66435caa5fb2b629b230e237873594e623e79e6a7d1223'
)
let legacy = publishedTerminal
const variant = target.additionalPreviousVariants.find(
(entry) => entry.sha256 === legacySha256
)
const priorReplacements =
legacySha256 === target.previousPatchedSha256
? target.replacements.slice(
0,
target.replacements.length - target.previousReplacements.length
)
: target.replacements
for (const [from, to] of priorReplacements) {
expect(legacy.split(from).length - 1).toBe(1)
legacy = legacy.replace(from, to)
}
if (variant) {
for (const [from, to] of variant.replacements.toReversed()) {
expect(legacy.split(to).length - 1).toBe(1)
legacy = legacy.replace(to, from)
}
}
expect(createHash('sha256').update(legacy).digest('hex')).toBe(legacySha256)
writeFileSync(publishedPath, legacy)
patchNodePtyWindowsTeardown(fixture.root)
expect(readFileSync(relayAgentPath, 'utf8')).toBe(relayAgent)
expect(() => assertPatchedNodePtyWindowsTeardown(fixture.root)).not.toThrow()
expect(readFileSync(publishedPath, 'utf8')).toBe(
readFileSync(desktopPath('windowsTerminal.js'), 'utf8')
)
const installed = PATCHED_FILES.map((file) =>
readFileSync(join(fixture.libDir, file), 'utf8')
)
patchNodePtyWindowsTeardown(fixture.root)
expect(PATCHED_FILES.map((file) => readFileSync(join(fixture.libDir, file), 'utf8'))).toEqual(
installed
)
}
)
it('finishes interrupted previous relay upgrades in dependency order', () => {
const fixture = writeNodePtyFixture('1.1.0')
const originalConout = readFileSync(join(fixture.libDir, 'windowsConoutConnection.js'), 'utf8')
const asset = readFileSync(
join(projectDir, 'config', 'relay-assets', 'node-pty-1.1.0-windows-pty-teardown-patch.cjs'),
'utf8'
)
const { PATCH_TARGETS } = loadPatchTargets(asset)
patchNodePtyWindowsTeardown(fixture.root)
for (const file of ['windowsPtyAgent.js', 'windowsTerminal.js']) {
const target = PATCH_TARGETS.find((entry) => entry.relativePath.at(-1) === file)
const variant =
file === 'windowsPtyAgent.js'
? { sha256: target.previousPatchedSha256, replacements: target.previousReplacements }
: target.additionalPreviousVariants[0]
let previous = readFileSync(join(fixture.libDir, file), 'utf8')
for (const [from, to] of variant.replacements.toReversed()) {
expect(previous.split(to).length - 1).toBe(1)
previous = previous.replace(to, from)
}
expect(createHash('sha256').update(previous).digest('hex')).toBe(
file === 'windowsPtyAgent.js'
? '1e23ef480569e73706e3ab4f5482c7e553c76f51414ae8e7b0bdcc2fd75f7280'
: '3060c6514a8e9e3285f91b9b549930e7d25d59d4cf7e1ed3a25b9a680dd1ded5'
)
writeFileSync(join(fixture.libDir, file), previous)
}
writeFileSync(join(fixture.libDir, 'windowsConoutConnection.js'), originalConout)
const previous = PATCHED_FILES.map((file) => readFileSync(join(fixture.libDir, file), 'utf8'))
const completed = []
const installOrder = ['windowsConoutConnection.js', 'windowsPtyAgent.js', 'windowsTerminal.js']
patchNodePtyWindowsTeardown(fixture.root)
for (const file of installOrder) {
completed.push(readFileSync(join(fixture.libDir, file), 'utf8'))
}
for (let prefix = 0; prefix < installOrder.length; prefix += 1) {
for (const [index, file] of PATCHED_FILES.entries()) {
writeFileSync(join(fixture.libDir, file), previous[index])
}
for (let index = 0; index < prefix; index += 1) {
writeFileSync(join(fixture.libDir, installOrder[index]), completed[index])
}
patchNodePtyWindowsTeardown(fixture.root)
expect(() => assertPatchedNodePtyWindowsTeardown(fixture.root)).not.toThrow()
}
expect(() => assertPatchedNodePtyWindowsTeardown(fixture.root)).not.toThrow()
for (const file of ['windowsTerminal.js', 'windowsConoutConnection.js']) {
expect(readFileSync(join(fixture.libDir, file), 'utf8')).toBe(
readFileSync(desktopPath(file), 'utf8')
)
}
const once = PATCHED_FILES.map((file) => readFileSync(join(fixture.libDir, file), 'utf8'))
patchNodePtyWindowsTeardown(fixture.root)
expect(PATCHED_FILES.map((file) => readFileSync(join(fixture.libDir, file), 'utf8'))).toEqual(
once
)
})
it('refuses a different package version or unexpected source', () => {
const wrongVersion = writeNodePtyFixture('1.2.0-beta.11')
expect(() => patchNodePtyWindowsTeardown(wrongVersion.root)).toThrow('expected 1.1.0')
@@ -146,7 +267,11 @@ describe('Windows SSH relay node-pty ConPTY teardown patch', () => {
const drifted = writeNodePtyFixture('1.1.0')
const path = join(drifted.libDir, file)
writeFileSync(path, `${readFileSync(path, 'utf8')}\n// drift`)
const before = PATCHED_FILES.map((entry) => readFileSync(join(drifted.libDir, entry), 'utf8'))
expect(() => patchNodePtyWindowsTeardown(drifted.root)).toThrow('unexpected node-pty')
expect(
PATCHED_FILES.map((entry) => readFileSync(join(drifted.libDir, entry), 'utf8'))
).toEqual(before)
}
})
@@ -170,7 +295,7 @@ function writeNodePtyFixture(version) {
writeFileSync(join(root, 'node_modules', 'node-pty', 'package.json'), JSON.stringify({ version }))
for (const file of PATCHED_FILES) {
const desktop = readFileSync(desktopPath(file), 'utf8')
for (const [marker] of DESKTOP_HUNKS[file]) {
for (const [marker] of DESKTOP_HUNKS[file] || []) {
expect(desktop).toContain(marker)
}
writeFileSync(join(libDir, file), unapplyDesktopHunks(file, desktop))
@@ -188,7 +313,17 @@ function writeNodePtyFixture(version) {
*/
function unapplyDesktopHunks(file, desktop) {
if (file === 'windowsPtyAgent.js') {
const asset = readFileSync(
join(projectDir, 'config', 'relay-assets', 'node-pty-1.1.0-windows-pty-teardown-patch.cjs'),
'utf8'
)
const { PATCH_TARGETS } = loadPatchTargets(asset)
const target = PATCH_TARGETS.find((entry) => entry.relativePath.at(-1) === file)
let published = desktop
for (const [from, to] of target.replacements.slice(1).toReversed()) {
expect(published.split(to).length - 1).toBe(1)
published = published.replace(to, from)
}
for (const [patched, original] of DESKTOP_HUNKS[file]) {
expect(published.split(patched).length - 1).toBe(1)
published = published.replace(patched, original)
@@ -1,4 +1,3 @@
import { readFileSync } from 'node:fs'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { resolvePnpmCliInvocation } from './pnpm-cli-invocation.mjs'
@@ -216,16 +215,3 @@ describe('resolvePnpmCliInvocation', () => {
expect(fileExists).not.toHaveBeenCalled()
})
})
describe('pnpm 12 native-cli callers', () => {
it('reinvokes pnpm through the helper rather than `node $npm_execpath`', () => {
for (const file of [
'./build-native-for-platform.mjs',
'./run-ssh-docker-bulk-open-freeze-e2e.mjs'
]) {
const source = readFileSync(new URL(file, import.meta.url), 'utf8')
expect(source).toContain("from './pnpm-cli-invocation.mjs'")
expect(source).not.toMatch(/process\.execPath,\s*\[\s*(?:pnpmEntry|npmExecPath)/)
}
})
})
@@ -92,12 +92,6 @@ describe('renderer web client projection', () => {
expect(output.nM()).toBe(3)
})
it('keeps the build-only manifest out of packaged apps', () => {
const builderConfig = readFileSync(resolve('config/electron-builder.config.cjs'), 'utf8')
expect(builderConfig).toContain("'!out/renderer/.vite{,/**/*}'")
})
it('copies and minifies only the web dependency closure', async () => {
const root = createRendererFixture()
const result = await projectFixture(root)
@@ -1,91 +0,0 @@
import { readFileSync } from 'node:fs'
import { parse } from 'yaml'
import { describe, expect, it } from 'vitest'
const release = parse(readFileSync('.github/workflows/release-cut.yml', 'utf8'))
const mac = parse(readFileSync('.github/workflows/release-mac-build.yml', 'utf8'))
const javascript = parse(readFileSync('.github/workflows/release-javascript.yml', 'utf8'))
const comparison = parse(readFileSync('.github/workflows/release-javascript-benchmark.yml', 'utf8'))
describe('release JavaScript job boundaries', () => {
it('compiles the release tag while blocking gates run, with no publishing permission', () => {
const job = release.jobs['release-javascript']
expect(job.needs).toBe('cut')
expect(job.permissions).toEqual({ contents: 'read' })
expect(job.with.ref).toBe('refs/tags/${{ needs.cut.outputs.tag }}')
expect(javascript.permissions).toEqual({ contents: 'read' })
expect(javascript.jobs.bundle['runs-on']).toBe('ubuntu-latest')
expect(javascript.jobs.bundle.steps[0].with.ref).toBe('${{ inputs.ref }}')
expect(javascript.env.ORCA_BACKGROUND_LAUNCH).toBe('1')
})
it.each(['build', 'build-mac'])(
'keeps %s behind signing gates and the bundle verdict',
(name) => {
const job = release.jobs[name]
expect(job.needs).toEqual(
expect.arrayContaining([
'cut',
'create-release',
'release-preflight',
'relay-windows-process-tree',
'release-javascript'
])
)
expect(job.if).toContain("needs.release-preflight.result == 'success'")
expect(job.if).toContain("needs.release-javascript.result == 'success'")
}
)
it('retains full compilation for old refs but fails rather than rebuilding a broken shared artifact', () => {
const job = javascript.jobs.bundle
const support = job.steps.find((step) => step.id === 'source')
expect(support.run).toContain('build:release:javascript')
expect(support.run).toContain('build:release:host')
const compile = job.steps.find((step) => step.name === 'Build and archive release JavaScript')
expect(compile.if).toBe("steps.source.outputs.supported == 'true'")
const download = release.jobs.build.steps.find(
(step) => step.name === 'Download release JavaScript'
)
expect(download.if).toBe("needs.release-javascript.outputs.supported == 'true'")
expect(download['continue-on-error']).toBeUndefined()
const build = release.jobs.build.steps.find((step) => step.name === 'Build app')
expect(build.run).toMatch(/artifact\.mjs restore\s+pnpm run build:release:host/)
expect(build.run).toMatch(/else\s+pnpm run build:release/)
expect(build['continue-on-error']).toBeUndefined()
})
it('restores the same parent-run bundle on macOS and retains native and runtime checks', () => {
const download = mac.jobs['build-mac'].steps.find(
(step) => step.name === 'Download release JavaScript from the release run'
)
expect(download.with['run-id']).toBe('${{ inputs.release_run_id }}')
expect(download.with.name).toBe('release-javascript')
const build = mac.jobs['build-mac'].steps.find((step) => step.name === 'Build app')
expect(build.env.ORCA_RELEASE_JAVASCRIPT_SOURCE_SHA).toBe('${{ inputs.javascript_source_sha }}')
expect(build.run).toContain('pnpm run build:release:host')
for (const job of [release.jobs.build, mac.jobs['build-mac']]) {
expect(job.steps.map((step) => step.name)).toEqual(
expect.arrayContaining([
'Gate runtime file-watcher process isolation',
'Gate SSH relay watcher process isolation'
])
)
}
})
it('compares both build paths on all packaging hosts without publishing credentials', () => {
expect(comparison.jobs.measure.strategy.matrix.mode).toEqual(['baseline', 'shared'])
expect(comparison.jobs.measure.strategy.matrix.host).toHaveLength(4)
expect(comparison.permissions).toEqual({ contents: 'read' })
expect(comparison.env.ORCA_POSTHOG_WRITE_KEY).toBe('ci-build-comparison')
expect(comparison.jobs.bundle.secrets.ORCA_POSTHOG_WRITE_KEY).toBe('ci-build-comparison')
const steps = comparison.jobs.measure.steps
expect(steps.find((step) => step.name === 'Download shared JavaScript').if).toBe(
"matrix.mode == 'shared'"
)
expect(steps.find((step) => step.name === 'Measure release build').run).toContain(
'release-javascript-benchmark.mjs'
)
})
})
@@ -1,5 +1,5 @@
import { build } from 'esbuild'
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
@@ -63,12 +63,4 @@ describe('session scanner service build smoke', () => {
)
expect(() => smokeSessionScannerService(directory)).toThrow('smoke failed')
})
it('runs in the orcad build under both runtimes', () => {
const source = readFileSync(resolve('config/scripts/build-orcad.mjs'), 'utf8')
expect(source).toContain(
"{ label: 'session scanner service', smoke: smokeSessionScannerService }"
)
expect(source).toContain('await smoke(OUT_DIR, { runtimePath: nodeRuntimePath })')
})
})
@@ -0,0 +1,231 @@
'use strict'
const assert = require('node:assert/strict')
const { createHash } = require('node:crypto')
const { readFileSync, writeSync } = require('node:fs')
const { dirname, resolve } = require('node:path')
const pty = require('node-pty')
const utilsPath = require.resolve('node-pty/lib/utils')
const loaded = require(utilsPath).loadNativeModule('conpty')
const native = loaded.module
const [backend, faultMode] = process.argv.slice(2)
assert.equal(process.platform, 'win32')
assert.ok(backend === 'dll' || backend === 'inbox')
assert.ok(faultMode === undefined || faultMode === 'retry')
const retryFault = faultMode === 'retry'
assert.equal(native.assignCurrentProcessToJob(), true, 'Host crash cleanup must own descendants')
function alive(pid) {
try {
process.kill(pid, 0)
return true
} catch (error) {
return error.code === 'EPERM'
}
}
function report(phase, details) {
writeSync(1, `${JSON.stringify({ phase, ...details })}\n`)
}
async function exercise() {
const { createStressObserver, loadedStressInputHashes } =
await import('./windows-pty-table-stress-observer.mjs')
const processTree = require('@vscode/windows-process-tree')
const {
assertWindowsProcessTreeCreationTime
} = require('./windows-process-tree-creation-time.cjs')
assertWindowsProcessTreeCreationTime({ module: processTree })
const addonPath = resolve(dirname(utilsPath), loaded.dir, 'conpty.node')
const extraInputs = [
require.resolve('node-pty/lib/windowsConoutConnection'),
require.resolve('node-pty/lib/worker/conoutSocketWorker'),
require.resolve('@vscode/windows-process-tree')
].map((path) => {
const bytes = readFileSync(path)
return { path, bytes: bytes.length, sha256: createHash('sha256').update(bytes).digest('hex') }
})
report('inputs', {
backend,
node: process.version,
inputs: loadedStressInputHashes(addonPath, require.resolve),
extraInputs
})
const term = pty.spawn(
process.execPath,
['-e', 'process.stdin.resume();setInterval(() => {}, 1000)'],
{
cwd: process.cwd(),
cols: 80,
rows: 24,
useConpty: true,
useConptyDll: backend === 'dll',
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' }
}
)
const pid = term.pid
let dataCallbacks = 0
let exitCallbacks = 0
const unexpectedExits = []
let nativeExitCallbacks = 0
let forwardingConnections = 0
let faultApplied = false
const errors = []
const unhandledRejections = []
let firstKillFault = false
let retried = false
const record = {
proc: term,
exited: false,
get closed() {
return term._killRequested === true
}
}
term.onData(() => {
dataCallbacks += 1
})
term.onExit((event) => {
if (
nativeExitCallbacks !== 1 ||
!Number.isInteger(event.exitCode) ||
event.exitCode !== term._agent.exitCode
) {
unexpectedExits.push({
nativeExitCallbacks,
exitCode: event.exitCode ?? null,
nativeExitCode: term._agent.exitCode ?? null
})
}
exitCallbacks += 1
record.exited = true
})
if (retryFault) {
assert.equal(term.listeners('error').length, 1, 'Only the constructor error listener may exist')
process.on('unhandledRejection', (error) => {
unhandledRejections.push(String(error))
report('unhandled-cleanup-rejection', { error: String(error) })
})
const originalKill = term._agent.kill.bind(term._agent)
term._agent.kill = () => {
if (!firstKillFault) {
firstKillFault = true
report('injected-first-kill-failure', { pid })
throw new Error('ORCA_FIRST_PRECONNECT_KILL_FAILURE')
}
originalKill()
}
} else {
term.on('error', (error) => {
errors.push(String(error))
report('terminal-error', { error: String(error) })
})
}
const observer = createStressObserver((phase, details) => {
if (phase === 'native-exit-callback') {
nativeExitCallbacks += 1
}
report(phase, details)
})
observer.watch(record, { round: 0, slot: 0 })
term._socket.on('ready_datapipe', () => {
forwardingConnections += 1
})
const originalCreationTimeMs = processTree.getProcessCreationTime(pid) ?? null
assert.ok(Number.isSafeInteger(originalCreationTimeMs) && originalCreationTimeMs > 0)
assert.equal(alive(pid), true)
assert.ok(native.listJobProcessIds(term._pty, pid)?.includes(pid))
report('child-incarnation', { pid, originalCreationTimeMs })
// The Agent's earlier listener starts the real connection before this resource fault.
term._agent._conoutSocketWorker.onReady(() => {
assert.equal(term._isPipeReady, false, 'The resource fault must precede forwarding readiness')
assert.equal(dataCallbacks, 0, 'The resource fault must precede delivered output')
report('before-closed-forwarding-teardown', { pid, dataCallbacks, forwardingConnections })
term.destroy()
term._socket.destroy()
faultApplied = true
report('closed-forwarding-teardown', {
pid,
outputDestroyed: term._socket.destroyed,
killRequested: term._killRequested === true
})
})
const deadline = Date.now() + 15_000
while ((!faultApplied || alive(pid) || exitCallbacks === 0) && Date.now() < deadline) {
if (retryFault && firstKillFault && !retried) {
await new Promise((resolve) => setImmediate(resolve))
assert.equal(processTree.getProcessCreationTime(pid), originalCreationTimeMs)
assert.equal(alive(pid), true, 'The failed first kill must leave the actual child live')
assert.ok(native.listJobProcessIds(term._pty, pid)?.includes(pid))
assert.equal(nativeExitCallbacks, 0)
assert.equal(exitCallbacks, 0)
retried = true
report('retry-after-first-kill-failure', { pid, nativeExitCallbacks, exitCallbacks })
term.destroy()
}
await new Promise((resolve) => setTimeout(resolve, 50))
}
const currentCreationTimeMs = processTree.getProcessCreationTime(pid) ?? null
report('retirement-state', {
backend,
pid,
originalCreationTimeMs,
currentCreationTimeMs,
incarnation:
Number.isSafeInteger(currentCreationTimeMs) && currentCreationTimeMs > 0
? currentCreationTimeMs === originalCreationTimeMs
? 'same'
: 'different'
: 'unverifiable',
faultApplied,
pidIsAlive: alive(pid),
ownedJobProcessIds: native.listJobProcessIds(term._pty, pid) ?? null,
nativeExitCallbacks,
nativeExitCode: Number.isInteger(term._agent.exitCode) ? term._agent.exitCode : null,
publicExitCallbacks: exitCallbacks,
unexpectedExits,
forwardingConnections,
dataCallbacks,
inputDestroyed: term._agent.inSocket.destroyed,
outputDestroyed: term._socket.destroyed,
pipeReady: term._isPipeReady === true,
firstDataReady: term._isReady === true,
killRequested: term._killRequested === true,
killReturned: term._killComplete === true,
errors,
retryFault,
firstKillFault,
retried,
unhandledRejections
})
assert.equal(faultApplied, true, 'The actual worker-ready resource fault must be reached')
assert.equal(forwardingConnections, 0, 'A retired forwarding connection must not reopen')
assert.equal(alive(pid), false, 'Closed-forwarding public teardown must kill the real child')
assert.equal(
nativeExitCallbacks,
1,
'The actual native exit acknowledgment must arrive exactly once'
)
assert.equal(exitCallbacks, 1, 'Actual public exit must arrive exactly once')
assert.deepEqual(
unexpectedExits,
[],
'Requested ConPTY teardown must publish the actual native acknowledgment and exit code'
)
assert.equal(term._agent.inSocket.destroyed, true)
assert.deepEqual(errors, [])
if (retryFault) {
assert.equal(firstKillFault, true, 'The actual first kill failure must be reached')
assert.equal(retried, true, 'Public teardown must retry the failed native cleanup')
assert.deepEqual(
unhandledRejections,
[],
'Cleanup failure must not create an unhandled rejection'
)
}
report('complete', { backend, pid, nativeExitCallbacks, exitCallbacks, retryFault })
}
exercise().catch((error) => {
writeSync(2, `${error.stack}\n`)
process.exitCode = 1
})
@@ -0,0 +1,226 @@
'use strict'
const assert = require('node:assert/strict')
const { createHash } = require('node:crypto')
const { readFileSync, writeSync } = require('node:fs')
const { dirname, resolve } = require('node:path')
const pty = require('node-pty')
const utilsPath = require.resolve('node-pty/lib/utils')
const loaded = require(utilsPath).loadNativeModule('conpty')
const native = loaded.module
const [operation, fence] = process.argv.slice(2)
assert.equal(process.platform, 'win32')
assert.ok(operation === 'kill' || operation === 'destroy')
assert.ok(fence === 'immediate' || fence === 'connected')
assert.equal(native.assignCurrentProcessToJob(), true, 'Host crash cleanup must own descendants')
function alive(pid) {
try {
process.kill(pid, 0)
return true
} catch (error) {
return error.code === 'EPERM'
}
}
async function exercise() {
const { createStressObserver, loadedStressInputHashes } =
await import('./windows-pty-table-stress-observer.mjs')
const processTree = require('@vscode/windows-process-tree')
const {
assertWindowsProcessTreeCreationTime
} = require('./windows-process-tree-creation-time.cjs')
assertWindowsProcessTreeCreationTime({ module: processTree })
const processTreeEntry = require.resolve('@vscode/windows-process-tree')
const identityInputs = [
processTreeEntry,
resolve(dirname(processTreeEntry), '..', 'build', 'Release', 'windows_process_tree.node'),
require.resolve('./windows-process-tree-creation-time.cjs')
].map((path) => {
const bytes = readFileSync(path)
return { path, bytes: bytes.length, sha256: createHash('sha256').update(bytes).digest('hex') }
})
const addonPath = resolve(dirname(utilsPath), loaded.dir, 'conpty.node')
writeSync(
1,
`${JSON.stringify({
phase: 'inputs',
node: process.version,
inputs: loadedStressInputHashes(addonPath, require.resolve),
identityInputs
})}\n`
)
const term = pty.spawn(
process.execPath,
['-e', 'process.stdin.resume();setInterval(() => {}, 1000)'],
{
cwd: process.cwd(),
cols: 80,
rows: 24,
useConpty: true,
useConptyDll: true,
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' }
}
)
let dataCallbacks = 0
let exitCallbacks = 0
const unexpectedExits = []
let nativeExitCallbacks = 0
const record = {
proc: term,
exited: false,
get closed() {
return term._killRequested === true
}
}
const observer = createStressObserver((phase, details) => {
if (phase === 'native-exit-callback') {
nativeExitCallbacks += 1
}
writeSync(1, `${JSON.stringify({ phase, ...details })}\n`)
})
term.onData(() => {
dataCallbacks += 1
})
term.onExit((event) => {
if (
nativeExitCallbacks !== 1 ||
!Number.isInteger(event.exitCode) ||
event.exitCode !== term._agent.exitCode
) {
unexpectedExits.push({
nativeExitCallbacks,
exitCode: event.exitCode ?? null,
nativeExitCode: term._agent.exitCode ?? null
})
}
exitCallbacks += 1
record.exited = true
})
observer.watch(record, { round: 0, slot: 0 })
const pid = term.pid
const originalCreationTimeMs = processTree.getProcessCreationTime(pid) ?? null
writeSync(1, `${JSON.stringify({ phase: 'child-incarnation', pid, originalCreationTimeMs })}\n`)
assert.ok(
Number.isSafeInteger(originalCreationTimeMs) && originalCreationTimeMs > 0,
'The actual child creation time must be readable before teardown'
)
function reportFence(stage) {
writeSync(
1,
`${JSON.stringify({
phase: 'fence',
stage,
operation,
fence,
pid,
dataCallbacks,
inputDestroyed: term._agent.inSocket.destroyed
})}\n`
)
}
assert.ok(Number.isInteger(pid) && pid > 0)
assert.equal(alive(pid), true)
assert.ok(native.listJobProcessIds(term._pty, pid)?.includes(pid))
assert.equal(dataCallbacks, 0)
term.write('queued input')
const connected = new Promise((resolve, reject) => {
// Observe the real forwarding connection; do not suppress or fabricate output.
term._socket.once('ready_datapipe', () => {
try {
assert.equal(dataCallbacks, 0, 'The fence must precede delivered first data')
reportFence('forwarding-connected')
if (fence === 'connected') {
assert.equal(alive(pid), true)
assert.ok(native.listJobProcessIds(term._pty, pid)?.includes(pid))
reportFence('before-public-teardown')
term[operation]()
reportFence('after-public-teardown')
}
// This is actual pipe retirement; old queued teardown leaves it open here.
reportFence('pre-first-data-retirement')
assert.equal(
term._agent.inSocket.destroyed,
true,
'Public teardown must retire the actual input pipe at forwarding connection before first data'
)
term[operation]()
term.write('retired input')
term.resize(81, 24)
term.clear()
resolve()
} catch (error) {
reject(error)
}
})
})
if (fence === 'immediate') {
reportFence('before-public-teardown')
term[operation]()
reportFence('after-public-teardown')
term[operation]()
}
const deadline = Date.now() + 15_000
let timeout
try {
await Promise.race([
connected,
new Promise((_, reject) => {
timeout = setTimeout(
() => reject(new Error('Forwarding connection did not arrive')),
15_000
)
})
])
} finally {
clearTimeout(timeout)
}
while ((alive(pid) || exitCallbacks === 0) && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 50))
}
const currentCreationTimeMs = processTree.getProcessCreationTime(pid) ?? null
writeSync(
1,
`${JSON.stringify({
phase: 'retirement-state',
operation,
fence,
pid,
originalCreationTimeMs,
currentCreationTimeMs,
incarnation:
Number.isSafeInteger(currentCreationTimeMs) && currentCreationTimeMs > 0
? currentCreationTimeMs === originalCreationTimeMs
? 'same'
: 'different'
: 'unverifiable',
pidIsAlive: alive(pid),
ownedJobProcessIds: native.listJobProcessIds(term._pty, pid) ?? null,
nativeExitCallbacks,
nativeExitCode: Number.isInteger(term._agent.exitCode) ? term._agent.exitCode : null,
publicExitCallbacks: exitCallbacks,
unexpectedExits,
dataCallbacks,
inputDestroyed: term._agent.inSocket.destroyed,
outputDestroyed: term._socket.destroyed,
pipeReady: term._isPipeReady === true,
firstDataReady: term._isReady === true,
killRequested: term._killRequested === true,
killReturned: term._killComplete === true
})}\n`
)
assert.equal(alive(pid), false, 'Public teardown must kill the real child')
assert.equal(exitCallbacks, 1, 'Actual public exit must arrive exactly once')
assert.deepEqual(
unexpectedExits,
[],
'Requested ConPTY teardown must publish the actual native acknowledgment and exit code'
)
writeSync(1, `${JSON.stringify({ phase: 'complete', operation, fence, pid, exitCallbacks })}\n`)
}
exercise().catch((error) => {
writeSync(2, `${error.stack}\n`)
process.exitCode = 1
})
+27 -6
View File
@@ -42,10 +42,8 @@ const testOptions = {
testTimeout: 30_000
}
const nodeProject = {
extends: false,
...transforms,
extends: true,
test: {
...testOptions,
name: process.versions.bun ? 'node-runtime' : 'node',
env: { ORCA_VITEST_RUNTIME: process.versions.bun ? 'node-runtime' : 'node' },
include: process.versions.bun ? NODE_RUNTIME_INCLUDE : UNIT_INCLUDE,
@@ -60,13 +58,12 @@ const projects = [
...(process.versions.bun
? [
{
extends: false,
...transforms,
extends: true,
test: {
...testOptions,
name: 'bun',
env: { ORCA_VITEST_RUNTIME: 'bun' },
pool: 'forks',
include: UNIT_INCLUDE,
exclude: [...testOptions.exclude, ...NODE_RUNTIME_INCLUDE, measurementFile],
sequence: { groupOrder: 1 }
}
@@ -90,8 +87,32 @@ const projects = [
export default defineConfig({
...transforms,
plugins: [
{
name: 'orca-test-container-selectors',
enforce: 'post',
config: {
order: 'post',
handler(config) {
if (!config.test?.projects) {
return
}
// Keep root coverage exclusions without inheriting duplicate project selectors.
if (config.test.include?.length === 0) {
config.test.include = testOptions.include
}
if (config.test.exclude?.length === 0) {
config.test.exclude = testOptions.exclude
}
}
}
}
],
test: {
...testOptions,
// Inline projects own their selectors; empty root arrays avoid concatenating them.
include: [],
exclude: [],
sequence: { sequencer: balancedShards ? TimingSequencer : RuntimeSequencer },
...(balancedShards
? {
+6 -5
View File
@@ -175,7 +175,8 @@ patchedDependencies:
'@xterm/xterm@6.1.0-beta.303': dd0ccc59cd1ccf99f4d76e5aa2456da165fa0804dce19a833d7638bd07ffa393
i18next-cli@1.74.2: 7955b89d3aa229f477408608d331f78c85148a85a85913729f89fac65ad8b207
lint-staged@16.4.0: 7333b3837f80a7fbd045964db6d76ba4fc118e49134bdbabb00585b6b7b60673
node-pty@1.1.0: 92c95cffab383d86b3b13a460c75a08074468ebf1f3911db8e874e083201f192
node-pty@1.1.0: 64f39642b03aeb629d562c834bf0e2fdaf1ccba5cf90b1a3e2f4c63654bb7d74
vitest@5.0.3: 3d512e2cb30542921690d9fa925edf503d92dd7d58a4c4dd2aa2fa69e9a243f9
ws@8.22.0: 9393e04dd9a90b01eb7aa35fcd5b1431b5038ce999fbbd82651136193ba51f14
importers:
@@ -217,7 +218,7 @@ importers:
version: 3.3.1
node-pty:
specifier: ^1.1.0
version: 1.1.0(patch_hash=92c95cffab383d86b3b13a460c75a08074468ebf1f3911db8e874e083201f192)
version: 1.1.0(patch_hash=64f39642b03aeb629d562c834bf0e2fdaf1ccba5cf90b1a3e2f4c63654bb7d74)
posthog-node:
specifier: ^5.52.5
version: 5.52.5
@@ -611,7 +612,7 @@ importers:
version: rolldown-vite@7.3.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)
vitest:
specifier: 5.0.3
version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@25.9.5)(happy-dom@20.14.5)(msw@2.14.3(@types/node@25.9.5)(typescript@7.0.2))(rolldown-vite@7.3.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))
version: 5.0.3(patch_hash=3d512e2cb30542921690d9fa925edf503d92dd7d58a4c4dd2aa2fa69e9a243f9)(@opentelemetry/api@1.9.1)(@types/node@25.9.5)(happy-dom@20.14.5)(msw@2.14.3(@types/node@25.9.5)(typescript@7.0.2))(rolldown-vite@7.3.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))
vscode-oniguruma:
specifier: ^2.0.1
version: 2.0.1
@@ -13799,7 +13800,7 @@ snapshots:
node-int64@0.4.0: {}
node-pty@1.1.0(patch_hash=92c95cffab383d86b3b13a460c75a08074468ebf1f3911db8e874e083201f192):
node-pty@1.1.0(patch_hash=64f39642b03aeb629d562c834bf0e2fdaf1ccba5cf90b1a3e2f4c63654bb7d74):
dependencies:
node-addon-api: 7.1.1
@@ -15327,7 +15328,7 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@25.9.5)(happy-dom@20.14.5)(msw@2.14.3(@types/node@25.9.5)(typescript@7.0.2))(rolldown-vite@7.3.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)):
vitest@5.0.3(patch_hash=3d512e2cb30542921690d9fa925edf503d92dd7d58a4c4dd2aa2fa69e9a243f9)(@opentelemetry/api@1.9.1)(@types/node@25.9.5)(happy-dom@20.14.5)(msw@2.14.3(@types/node@25.9.5)(typescript@7.0.2))(rolldown-vite@7.3.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)):
dependencies:
'@types/chai': 5.2.3
'@vitest/mocker': 5.0.3(msw@2.14.3(@types/node@25.9.5)(typescript@7.0.2))(rolldown-vite@7.3.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))
+1
View File
@@ -64,3 +64,4 @@ patchedDependencies:
'@vscode/windows-process-tree@0.8.0': config/patches/@vscode__windows-process-tree@0.8.0.patch
i18next-cli@1.74.2: config/patches/i18next-cli@1.74.2.patch
ws@8.22.0: config/patches/ws@8.22.0.patch
vitest@5.0.3: config/patches/vitest@5.0.3.patch
+14
View File
@@ -16,3 +16,17 @@ export function relaunchApp(reason: AppRelaunchReason, data?: CrashReportBreadcr
recordDurableCrashBreadcrumb('app_relaunch_requested', { ...data, reason })
runWithLaunchPath(() => app.relaunch())
}
export async function runBeforeRelaunchCleanup(
onBeforeRelaunch?: () => void | Promise<void>
): Promise<void> {
try {
await onBeforeRelaunch?.()
} catch (error) {
// Why: best-effort cleanup must never block relaunch; log only error.name to avoid leaking secrets.
console.warn(
'[app] Pre-relaunch cleanup failed; continuing relaunch:',
error instanceof Error ? error.name : typeof error
)
}
}
@@ -38,12 +38,6 @@ import { recordCoalescedDurableCrashBreadcrumb } from './durable-crash-breadcrum
* taskkill (neither runs in main); and the browser-route Electron probes,
* which are reached only from `*.electron.test.ts`.
*
* `main-process-tree-kill-gate.test.ts` is the ratchet that keeps that list
* closed: it counts `/pid` call sites against gate admissions per file, so a new
* pid-addressed kill fails it whether it lands in a new file or inside a family
* that already asks the gate. It does not see a `/pid` argument built from a
* variable.
*
* A daemon or relay kill missing from the count is a diagnostics gap, not a
* missed suspect: those hosts cannot reach a Chromium pid in the first place
* (see `orca-chromium-process-pids.ts`), and a group or Job-Object kill can
@@ -8,6 +8,8 @@ import { DaemonServer } from './daemon-server'
import { getDaemonSocketPath } from './daemon-spawner'
import type { DaemonFileLog } from './daemon-file-log'
import { HistoryReader } from './history-reader'
import { getHistorySessionDirName } from './history-paths'
import { readTerminalHistoryCheckpoint } from './terminal-history-checkpoint-reader'
import type { HistoryCheckpointResult } from './terminal-history-manager-options'
import type { SubprocessHandle } from './session-subprocess-handle'
import type { TerminalSnapshot } from './types'
@@ -16,6 +18,8 @@ const REATTACH_BUDGET_MS = 2_000
// Why above DURABLE_HISTORY_OVERLAY_DEADLINE_MS: this asserts the reattach gives up on its own
// stalled checkpoint, so the window must outlast the deadline it is proving.
const DEADLINE_BUDGET_MS = 20_000
const scheduleBudgetTimeout = setTimeout
const clearBudgetTimeout = clearTimeout
function createMockSubprocess(): SubprocessHandle & { emitData: (data: string) => void } {
let onData: ((data: string) => void) | undefined
@@ -46,12 +50,12 @@ function createMockSubprocess(): SubprocessHandle & { emitData: (data: string) =
async function withinBudget<T>(work: Promise<T>, budgetMs: number): Promise<T | 'timed-out'> {
let timer: ReturnType<typeof setTimeout> | undefined
const deadline = new Promise<'timed-out'>((resolve) => {
timer = setTimeout(() => resolve('timed-out'), budgetMs)
timer = scheduleBudgetTimeout(() => resolve('timed-out'), budgetMs)
})
try {
return await Promise.race([work, deadline])
} finally {
clearTimeout(timer)
clearBudgetTimeout(timer)
}
}
@@ -95,12 +99,22 @@ describe('STA-4173 reattach isolation from a stalled checkpoint', () => {
*/
async function stallCheckpointFor(stalledSessionId: string): Promise<{
entered: () => number
enteredPromise: Promise<void>
completedPromise: Promise<HistoryCheckpointResult>
release: () => void
}> {
const manager = adapter.getHistoryManager()
expect(manager).not.toBeNull()
const original = manager!.checkpoint.bind(manager!)
let entered = 0
let resolveEntered = (): void => {}
const enteredPromise = new Promise<void>((resolve) => {
resolveEntered = resolve
})
let resolveCompleted: (result: HistoryCheckpointResult) => void = () => {}
const completedPromise = new Promise<HistoryCheckpointResult>((resolve) => {
resolveCompleted = resolve
})
let release = (): void => {}
const stalled = new Promise<void>((resolve) => {
release = resolve
@@ -115,11 +129,14 @@ describe('STA-4173 reattach isolation from a stalled checkpoint', () => {
return await original(sessionId, snapshot, opts)
}
entered += 1
resolveEntered()
await stalled
return await original(sessionId, snapshot, opts)
const result = await original(sessionId, snapshot, opts)
resolveCompleted(result)
return result
}
)
return { entered: () => entered, release }
return { entered: () => entered, enteredPromise, completedPromise, release }
}
async function spawnWithOutput(sessionId: string, output: string): Promise<string> {
@@ -159,10 +176,36 @@ describe('STA-4173 reattach isolation from a stalled checkpoint', () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
try {
const reattach = await withinBudget(
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'], shouldClearNativeTimers: true })
const pendingReattach = withinBudget(
adapter.spawn({ cols: 80, rows: 24, sessionId: stalledId, cwd: '/tmp' }),
DEADLINE_BUDGET_MS
)
let settled = false
void pendingReattach.then(
() => {
settled = true
},
() => {
settled = true
}
)
const checkpointEntry = await withinBudget(
Promise.race([
stall.enteredPromise.then(() => 'entered' as const),
pendingReattach.then(() => 'reattach-settled' as const)
]),
DEADLINE_BUDGET_MS
)
expect(checkpointEntry, 'Reattach must enter its checkpoint before settling').toBe('entered')
await vi.advanceTimersByTimeAsync(4_999)
expect(settled).toBe(false)
expect(warn).not.toHaveBeenCalledWith(
'[history] durable snapshot overlay deadline exceeded:',
stalledId
)
await vi.advanceTimersByTimeAsync(1)
const reattach = await pendingReattach
expect(stall.entered()).toBeGreaterThan(0)
expect(reattach).not.toBe('timed-out')
@@ -174,6 +217,7 @@ describe('STA-4173 reattach isolation from a stalled checkpoint', () => {
stalledId
)
} finally {
vi.useRealTimers()
stall.release()
warn.mockRestore()
}
@@ -226,13 +270,48 @@ describe('STA-4173 reattach isolation from a stalled checkpoint', () => {
const stalledId = await spawnWithOutput('resumed-session', 'RESUMED_OUTPUT\r\n')
const stall = await stallCheckpointFor(stalledId)
const reattach = await withinBudget(
adapter.spawn({ cols: 80, rows: 24, sessionId: stalledId, cwd: '/tmp' }),
DEADLINE_BUDGET_MS
try {
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'], shouldClearNativeTimers: true })
const pendingReattach = withinBudget(
adapter.spawn({ cols: 80, rows: 24, sessionId: stalledId, cwd: '/tmp' }),
DEADLINE_BUDGET_MS
)
let settled = false
void pendingReattach.then(
() => {
settled = true
},
() => {
settled = true
}
)
const checkpointEntry = await withinBudget(
Promise.race([
stall.enteredPromise.then(() => 'entered' as const),
pendingReattach.then(() => 'reattach-settled' as const)
]),
DEADLINE_BUDGET_MS
)
expect(checkpointEntry, 'Reattach must enter its checkpoint before settling').toBe('entered')
await vi.advanceTimersByTimeAsync(4_999)
expect(settled).toBe(false)
await vi.advanceTimersByTimeAsync(1)
const reattach = await pendingReattach
expect(reattach).not.toBe('timed-out')
} finally {
vi.useRealTimers()
stall.release()
}
expect(await withinBudget(stall.completedPromise, DEADLINE_BUDGET_MS)).toBe('committed')
const checkpointRead = await readTerminalHistoryCheckpoint(
join(dir, 'history', getHistorySessionDirName(stalledId), 'checkpoint.json')
)
expect(reattach).not.toBe('timed-out')
stall.release()
if (checkpointRead.status !== 'readable') {
throw new Error(`Abandoned checkpoint is ${checkpointRead.status}`)
}
expect(
`${checkpointRead.checkpoint.scrollbackAnsi}${checkpointRead.checkpoint.snapshotAnsi}`
).toContain('RESUMED_OUTPUT')
// Why this is the whole point of not cancelling: the reattach degraded, and the durable write
// the deadline walked away from still lands on disk for the next restore.
await vi.waitFor(async () => {
@@ -17,8 +17,44 @@ import { tmpdir } from 'node:os'
import { basename, join } from 'node:path'
import { DaemonServer } from './daemon-server'
import { checkDaemonHealth } from './daemon-health'
import { createNdjsonParser } from './ndjson'
import type { SubprocessHandle } from './session-subprocess-handle'
const scheduleHealthEntryTimeout = setTimeout
const cancelHealthEntryTimeout = clearTimeout
async function waitForHealthEntry<T>(entry: Promise<T>, health: Promise<unknown>): Promise<T> {
let timeout: ReturnType<typeof setTimeout> | undefined
let healthSettled = false
const earlySettlement = health.then(
() => {
healthSettled = true
throw new Error('Daemon health settled before its required socket/RPC entry')
},
(error) => {
healthSettled = true
throw error
}
)
const entryBudget = new Promise<never>((_resolve, reject) => {
timeout = scheduleHealthEntryTimeout(
() => reject(new Error('Daemon health socket/RPC entry exceeded its real 3000ms budget')),
3_000
)
})
try {
return await Promise.race([entry, earlySettlement, entryBudget])
} catch (error) {
// Let the real health owner close its socket before server teardown.
if (!healthSettled) {
await vi.advanceTimersByTimeAsync(3_000)
}
throw error
} finally {
cancelHealthEntryTimeout(timeout)
}
}
function createMockSubprocess(): SubprocessHandle {
return {
pid: 55555,
@@ -60,22 +96,45 @@ describe('issue #6814 repro: daemon failure-mode classification', () => {
// never returns) cannot be distinguished by a richer status. It times out
// and classifies as 'unreachable', the SAME bucket as a dead daemon.
it('WEDGED: a daemon whose health RPC never resolves classifies as unreachable (NOT degraded)', async () => {
let resolveHealthEntered = (): void => {}
const healthEntered = new Promise<void>((resolve) => {
resolveHealthEntered = resolve
})
const server = new DaemonServer({
socketPath,
tokenPath,
// Why: simulate a hung event loop — the probe never settles.
ptySpawnHealthCheck: vi.fn(() => new Promise<void>(() => {})),
ptySpawnHealthCheck: vi.fn(() => {
resolveHealthEntered()
return new Promise<void>(() => {})
}),
spawnSubprocess: () => createMockSubprocess()
})
await server.start()
try {
const health = await checkDaemonHealth(socketPath, tokenPath)
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'], shouldClearNativeTimers: true })
const pendingHealth = checkDaemonHealth(socketPath, tokenPath)
let settled = false
void pendingHealth.then(
() => {
settled = true
},
() => {
settled = true
}
)
await waitForHealthEntry(healthEntered, pendingHealth)
await vi.advanceTimersByTimeAsync(2_999)
expect(settled).toBe(false)
await vi.advanceTimersByTimeAsync(1)
const health = await pendingHealth
// This is the key finding: wedged != degraded. #6830's degraded fallback
// does NOT engage here; recovery still depends on the unreachable-path
// (grace-bounded preserve-if-live-else-replace) logic, not the degraded
// provider.
expect(health).toBe('unreachable')
} finally {
vi.useRealTimers()
await server.shutdown()
}
}, 15000)
@@ -85,17 +144,43 @@ describe('issue #6814 repro: daemon failure-mode classification', () => {
// classifies as 'unreachable' — the launcher's grace-bounded path must
// eventually replace it rather than preserve it forever.
it('WEDGED-HELLO: a daemon that accepts connections but never answers hello classifies as unreachable (#8689)', async () => {
let resolveHello: (message: unknown) => void = () => {}
const helloReceived = new Promise<unknown>((resolve) => {
resolveHello = resolve
})
const wedged = createServer((sock) => {
// Swallow the hello bytes but never write a response — the exact wedge.
sock.on('data', () => {})
const parser = createNdjsonParser(resolveHello)
sock.on('data', (chunk) => parser.feed(chunk.toString()))
})
await new Promise<void>((resolve) => wedged.listen(socketPath, () => resolve()))
// The health check reads the token before connecting, so it must exist.
writeFileSync(tokenPath, 'wedged-token')
try {
const health = await checkDaemonHealth(socketPath, tokenPath)
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'], shouldClearNativeTimers: true })
const pendingHealth = checkDaemonHealth(socketPath, tokenPath)
let settled = false
void pendingHealth.then(
() => {
settled = true
},
() => {
settled = true
}
)
expect(await waitForHealthEntry(helloReceived, pendingHealth)).toMatchObject({
type: 'hello',
token: 'wedged-token',
clientId: 'health-check',
role: 'control'
})
await vi.advanceTimersByTimeAsync(2_999)
expect(settled).toBe(false)
await vi.advanceTimersByTimeAsync(1)
const health = await pendingHealth
expect(health).toBe('unreachable')
} finally {
vi.useRealTimers()
await new Promise<void>((resolve) => wedged.close(() => resolve()))
}
}, 15000)
@@ -1,61 +0,0 @@
import { dirname, join, relative, resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { scanSourceTree, type ScannedFile } from '../../shared/source-scan/source-tree-scan'
/**
* Readers open other apps' databases synchronously, so only the worker thread
* may call them. This keeps a reader from being imported onto the main thread.
*/
const ALLOWED_IMPORTERS = new Set([
'src/main/foreign-sqlite-readers/foreign-sqlite-reader-entry.ts',
'src/main/foreign-sqlite-readers/foreign-sqlite-reader-dispatch.ts'
])
const READERS_DIRECTORY = 'src/main/foreign-sqlite-readers/readers/'
const SCANNED_EXTENSIONS = /\.(?:[cm]?ts|tsx|[cm]?js)$/
const SPECIFIER_PATTERN =
/(?:\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*|\bvi\.mock\s*\(\s*|^\s*import\s+)['"]([^'"]+)['"]/gm
function toRepoPath(repoRoot: string, path: string): string {
return relative(repoRoot, path).split('\\').join('/')
}
function importsReaders(repoRoot: string, file: ScannedFile): boolean {
for (const match of file.source.matchAll(SPECIFIER_PATTERN)) {
const specifier = match[1] ?? ''
const target = specifier.startsWith('.')
? toRepoPath(repoRoot, resolve(dirname(file.path), specifier))
: specifier
if (`${target}/`.includes(READERS_DIRECTORY)) {
return true
}
}
return false
}
describe('foreign SQLite readers import boundary', () => {
const repoRoot = resolve(__dirname, '..', '..', '..')
const files = ['src', 'config', 'tests'].flatMap((root) =>
scanSourceTree(join(repoRoot, root), { includeTests: true, extensions: SCANNED_EXTENSIONS })
)
const importers = files
.filter((file) => !toRepoPath(repoRoot, file.path).startsWith(READERS_DIRECTORY))
.filter((file) => importsReaders(repoRoot, file))
.map((file) => toRepoPath(repoRoot, file.path))
it('scans a plausible number of files', () => {
// A broken root or extension list would make the guard silently vacuous.
expect(files.length).toBeGreaterThan(500)
})
it('finds the importers it allows', () => {
// Presence check: a pattern that matched nothing would pass the next test trivially.
expect(importers).toContain('src/main/foreign-sqlite-readers/foreign-sqlite-reader-dispatch.ts')
})
it('has no importer outside the worker entry and its dispatch', () => {
expect(
importers.filter((path) => !ALLOWED_IMPORTERS.has(path)),
'Readers run only on the foreign SQLite reader worker; call them through foreign-sqlite-reader-spawn.ts.'
).toEqual([])
})
})
+3 -1
View File
@@ -1,5 +1,6 @@
import { EventEmitter } from 'node:events'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as AppRelaunch from '../app-relaunch'
const {
handlers,
@@ -97,7 +98,8 @@ vi.mock('../tray/system-tray', () => ({
destroySystemTray: destroySystemTrayMock
}))
vi.mock('../app-relaunch', () => ({
vi.mock('../app-relaunch', async (importOriginal) => ({
...(await importOriginal<typeof AppRelaunch>()),
relaunchApp: relaunchAppMock
}))
+1 -15
View File
@@ -6,7 +6,7 @@ import { is } from '@electron-toolkit/utils'
import type { AppIdentity } from '../../shared/app-identity'
import type { MarkdownDocument } from '../../shared/filesystem-entry-types'
import type { FloatingTerminalCwdRequest } from '../../shared/ui-chrome-types'
import { relaunchApp } from '../app-relaunch'
import { relaunchApp, runBeforeRelaunchCleanup } from '../app-relaunch'
import { quitProcess } from '../startup/process-quit-request'
import type { Store } from '../persistence'
import { getDevInstanceIdentity } from '../startup/dev-instance-identity'
@@ -186,17 +186,3 @@ export function registerAppHandlers(store: Store, options: RegisterAppHandlersOp
pickFloatingWorkspaceDirectory(event, store)
)
}
async function runBeforeRelaunchCleanup(
onBeforeRelaunch?: () => void | Promise<void>
): Promise<void> {
try {
await onBeforeRelaunch?.()
} catch (error) {
// Why: best-effort cleanup must never block relaunch; log only error.name to avoid leaking secrets.
console.warn(
'[app] Pre-relaunch cleanup failed; continuing relaunch:',
error instanceof Error ? error.name : typeof error
)
}
}
@@ -1,6 +1,7 @@
import path from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { FileUploadSession, IFilesystemProvider } from '../providers/types'
import type * as SshTargetRegistry from '../ssh/ssh-target-registry'
const handlers = new Map<string, (_event: unknown, args: unknown) => Promise<unknown>>()
const {
@@ -31,7 +32,10 @@ vi.mock('fs/promises', () => ({
copyFile: copyFileMock,
readdir: readdirMock
}))
vi.mock('./ssh', () => ({ getSshConnectionManager: getConnMgrMock }))
vi.mock('../ssh/ssh-target-registry', async () => {
const actual = await vi.importActual<typeof SshTargetRegistry>('../ssh/ssh-target-registry')
return { ...actual, getSshConnectionManager: getConnMgrMock }
})
import { registerFilesystemMutationHandlers } from './filesystem-mutations'
import {
@@ -1,6 +1,7 @@
import path from 'node:path'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { FileUploadSession, IFilesystemProvider } from '../providers/types'
import type * as SshTargetRegistry from '../ssh/ssh-target-registry'
const { getConnMgrMock, lstatMock, providerMock, readdirMock, realpathMock } = vi.hoisted(() => ({
getConnMgrMock: vi.fn(),
@@ -18,7 +19,10 @@ vi.mock('node:fs/promises', () => ({
vi.mock('./filesystem-path-containment', () => ({
isENOENT: (error: NodeJS.ErrnoException) => error.code === 'ENOENT'
}))
vi.mock('./ssh', () => ({ getSshConnectionManager: getConnMgrMock }))
vi.mock('../ssh/ssh-target-registry', async () => {
const actual = await vi.importActual<typeof SshTargetRegistry>('../ssh/ssh-target-registry')
return { ...actual, getSshConnectionManager: getConnMgrMock }
})
vi.mock('../providers/ssh-filesystem-dispatch', () => ({
requireSshFilesystemProvider: providerMock
}))
+5 -1
View File
@@ -3,6 +3,7 @@ import { constants } from 'node:fs'
import { Readable, Writable } from 'node:stream'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { FileUploadSession, IFilesystemProvider } from '../providers/types'
import type * as SshTargetRegistry from '../ssh/ssh-target-registry'
const handlers = new Map<string, (_event: unknown, args: unknown) => Promise<unknown>>()
const {
@@ -43,7 +44,10 @@ vi.mock('fs/promises', () => ({
unlink: unlinkMock,
rm: vi.fn()
}))
vi.mock('./ssh', () => ({ getSshConnectionManager: getConnMgrMock }))
vi.mock('../ssh/ssh-target-registry', async () => {
const actual = await vi.importActual<typeof SshTargetRegistry>('../ssh/ssh-target-registry')
return { ...actual, getSshConnectionManager: getConnMgrMock }
})
import { registerFilesystemMutationHandlers } from './filesystem-mutations'
import {
+1 -1
View File
@@ -1,7 +1,7 @@
import { lstat } from 'node:fs/promises'
import { basename, posix, resolve } from 'node:path'
import { isENOENT } from './filesystem-path-containment'
import { getSshConnectionManager } from './ssh'
import { getSshConnectionManager } from '../ssh/ssh-target-registry'
import { requireSshFilesystemProvider } from '../providers/ssh-filesystem-dispatch'
import type { FileUploadSession, IFilesystemProvider } from '../providers/types'
import type { ImportItemResult } from '../../shared/filesystem-import-result-types'
@@ -1,184 +0,0 @@
import { readFileSync, readdirSync, statSync } from 'node:fs'
import { join, relative, resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
/**
* The ratchet behind the guard's claim to be a choke point.
*
* `admitSelfInitiatedTreeKill` is only "one decision" for as long as every
* pid-addressed `taskkill /pid <pid> /t /f` in Electron main asks it. Each such
* kill can land on a recycled pid that is now one of Orca's own Chromium
* processes (#10680), and an ungated one is also invisible to
* `selfInitiatedTreeKillCount`, which makes a zero read as exculpatory when it
* is not. A new family fails here rather than in the field.
*
* Exactly what is enforced, so no comment elsewhere claims more: per file, the
* number of gate admissions must be at least the number of `/pid` call sites.
* Counting sites rather than files is the point — a file-granular scan would let
* a second, ungated taskkill land inside a family that already mentions the gate,
* which is the shape the six highest-risk files now have. What it still cannot
* see: a site that pairs an ungated kill with a second admission of an already
* gated one in the same file, and a kill whose `/pid` argument is itself built
* from a variable.
*/
const REPOSITORY_ROOT = resolve(__dirname, '..', '..')
const MAIN_DIRECTORY = 'src/main/'
const SCANNED_EXTENSIONS = ['.ts', '.tsx']
const IGNORED_DIRECTORIES = new Set([
'node_modules',
'dist',
'out',
'build',
'.git',
'__fixtures__'
])
/**
* One match per call site. Keyed on the `/pid` argument rather than the program
* name because `/pid <n>` is what makes the kill pid-addressed — it walks
* whatever tree owns that pid *now* — and because the literal survives a
* `taskkill` spawned through a constant or a variable, which a quoted-program
* pattern misses entirely.
*/
const PID_ADDRESSED_KILL_SITE = /['"]\/pid['"]/gi
/**
* A call, not an import or a comment: `admitSelfInitiatedTreeKill` in main, and
* `admitProcessTreeKill` for the `src/shared` seam main installs the same gate
* into, which shared code cannot import directly.
*/
const GATE_ADMISSION = /\badmit(?:SelfInitiatedTreeKill|ProcessTreeKill)\s*\(/g
function countMatches(source: string, pattern: RegExp): number {
return source.match(pattern)?.length ?? 0
}
/** Sites left over once each admission in the file has claimed one. */
function ungatedKillSiteCount(source: string): number {
return Math.max(
countMatches(source, PID_ADDRESSED_KILL_SITE) - countMatches(source, GATE_ADMISSION),
0
)
}
/**
* Only ever shrinks. Each entry states why the gate cannot reach it — never
* "not got to yet", which is what a new ungated family would also look like.
*/
const UNGATED_TASKKILL_ALLOWLIST = new Map<string, string>([
[
'src/main/browser/browser-route-egress-electron-launch.ts',
'Electron probe reached only from *.electron.test.ts; kills the probe Electron it spawned'
],
[
'src/main/browser/browser-route-persisted-worker-electron-process.ts',
'Electron probe reached only from *.electron.test.ts; kills the probe Electron it spawned'
],
[
'src/cli/handlers/interactive-login-interruption.ts',
'CLI host: no Chromium pid on the machine to reach, and no reader for the ring'
],
[
'src/relay/subprocess-tree-termination.ts',
'Relay host: same, and the relay cannot import the main-process gate'
]
])
function isTestFile(path: string): boolean {
return /\.(?:test|spec)\.tsx?$/.test(path) || /(?:test-harness|test-fixture|fixture)/.test(path)
}
function scanSourceFiles(directory: string, found: string[] = []): string[] {
for (const entry of readdirSync(directory)) {
if (IGNORED_DIRECTORIES.has(entry)) {
continue
}
const path = join(directory, entry)
if (statSync(path).isDirectory()) {
scanSourceFiles(path, found)
continue
}
if (SCANNED_EXTENSIONS.some((extension) => entry.endsWith(extension)) && !isTestFile(path)) {
found.push(path)
}
}
return found
}
// Only the Node-side hosts: a renderer or preload cannot spawn a process at all.
const SCANNED_HOSTS = ['src/main', 'src/shared', 'src/cli', 'src/relay']
/** Scanned once at import: 10k files is seconds, and every case below reuses it. */
const PID_ADDRESSED_KILL_FILES = SCANNED_HOSTS.flatMap((host) =>
scanSourceFiles(join(REPOSITORY_ROOT, host))
.map((path) => ({
path: relative(REPOSITORY_ROOT, path).split('\\').join('/'),
source: readFileSync(path, 'utf8')
}))
.filter((file) => countMatches(file.source, PID_ADDRESSED_KILL_SITE) > 0)
)
function pidAddressedKillFiles(): { path: string; source: string }[] {
return PID_ADDRESSED_KILL_FILES
}
describe('main-process tree-kill gate', () => {
it('finds the taskkill families it is meant to police', () => {
// Falsifiable: a scanner that matched nothing would pass every case below.
expect(pidAddressedKillFiles().map((file) => file.path)).toContain(
'src/main/windows-process-tree-kill.ts'
)
})
it('routes every pid-addressed taskkill in Electron main through the gate', () => {
const ungated = pidAddressedKillFiles()
.filter((file) => file.path.startsWith(MAIN_DIRECTORY))
.filter((file) => ungatedKillSiteCount(file.source) > 0)
.map((file) => file.path)
.filter((path) => !UNGATED_TASKKILL_ALLOWLIST.has(path))
expect(ungated).toEqual([])
})
it('leaves no pid-addressed taskkill outside main unaccounted for', () => {
const unaccounted = pidAddressedKillFiles()
.filter((file) => !file.path.startsWith(MAIN_DIRECTORY))
.filter((file) => ungatedKillSiteCount(file.source) > 0)
.map((file) => file.path)
.filter((path) => !UNGATED_TASKKILL_ALLOWLIST.has(path))
expect(unaccounted).toEqual([])
})
it('counts call sites, not files: a second ungated kill in a gated file is caught', () => {
// The failure a file-granular scan let through: one gate mention exempting
// every taskkill in the file.
const gated = `
import { admitSelfInitiatedTreeKill } from './own-chromium-tree-kill-guard'
if (admitSelfInitiatedTreeKill({ pid, site: 's', scope: 'win-taskkill-tree' })) {
spawn('taskkill', ['/pid', String(pid), '/t', '/f'])
}
`
expect(ungatedKillSiteCount(gated)).toBe(0)
expect(
ungatedKillSiteCount(`${gated}\nspawn('taskkill', ['/pid', String(other), '/t', '/f'])`)
).toBe(1)
})
it('sees a kill whose program name comes from a constant', () => {
// A quoted-program pattern misses this shape; the `/pid` argument does not.
expect(
ungatedKillSiteCount(`
const KILLER = 'taskkill'
spawn(KILLER, ['/pid', String(pid), '/t', '/f'])
`)
).toBe(1)
})
it('keeps the allowlist honest: every entry still spawns a taskkill', () => {
const spawning = new Set(pidAddressedKillFiles().map((file) => file.path))
expect([...UNGATED_TASKKILL_ALLOWLIST.keys()].filter((path) => !spawning.has(path))).toEqual([])
})
})
@@ -1,96 +0,0 @@
// Architectural-invariant test (telemetry-error-tracking.md §Architecture):
//
// "Nothing in `src/main/telemetry/` imports from `src/main/observability/`
// or vice versa. The two lanes never share a code path."
//
// Cross-contamination is the failure mode the entire two-lane split is
// counter-designed against. oxlint's plugin set does not include
// `import-x`, and adding eslint just for this rule is a heavier lift than
// the rule warrants. A vitest test that grep-scans the two directories is
// adequate, runs in <50 ms, and fails CI loudly if a future PR adds the
// wrong import.
//
// What it catches:
// - any `from '../observability'` / `from '../observability/...'` inside
// `src/main/telemetry/`
// - any `from '../telemetry'` / `from '../telemetry/...'` inside
// `src/main/observability/`
// - the same with deeper relative paths (`../../observability/...`)
// - the same with absolute-from-src forms if anyone introduces them
//
// What it does NOT catch:
// - dynamic `await import()` / `require()` — neither lane uses these,
// and adding a regex catch would create false positives. If a future
// change introduces dynamic loading, extend the regex set.
// - re-exports through a third module. The simplest workaround
// (re-export `observability` from a "neutral" module) is the same
// anti-pattern this test is preventing; reviewers should reject it.
//
// Both directions of the rule are checked symmetrically — the asymmetric
// alternative (only one-way) leaves the door open to a `setOptIn` →
// `bundle` callback chain that smuggles consent state across the boundary.
import { readdirSync, readFileSync, statSync } from 'node:fs'
import { join, relative } from 'node:path'
import { describe, expect, it } from 'vitest'
const REPO_ROOT = join(__dirname, '..', '..', '..')
const TELEMETRY_DIR = join(REPO_ROOT, 'src', 'main', 'telemetry')
const OBSERVABILITY_DIR = join(REPO_ROOT, 'src', 'main', 'observability')
function listTsFiles(dir: string): string[] {
const out: string[] = []
for (const entry of readdirSync(dir)) {
const full = join(dir, entry)
if (statSync(full).isDirectory()) {
out.push(...listTsFiles(full))
} else if (entry.endsWith('.ts') || entry.endsWith('.tsx')) {
out.push(full)
}
}
return out
}
function findOffendingImports(file: string, forbiddenSegment: string): string[] {
const text = readFileSync(file, 'utf8')
// Match `from '<path>'`, `from "<path>"`, dynamic `import('<path>')` and
// `require('<path>')`. The `<path>` capture is what we inspect.
const importRe = /(?:from\s+|import\(|require\()\s*(['"])([^'"]+)\1/g
const offenders: string[] = []
let m: RegExpExecArray | null
while ((m = importRe.exec(text)) !== null) {
const spec = m[2]
if (spec.includes(forbiddenSegment)) {
offenders.push(spec)
}
}
return offenders
}
describe('architectural invariant — telemetry / observability lane isolation', () => {
it('no file in src/main/telemetry/ imports from observability', () => {
const files = listTsFiles(TELEMETRY_DIR)
expect(files.length).toBeGreaterThan(0) // sanity: directory exists
const violations = files.flatMap((f) => {
const bad = findOffendingImports(f, 'observability')
return bad.map((spec) => `${relative(REPO_ROOT, f)}: imports '${spec}'`)
})
expect(violations, violations.join('\n')).toEqual([])
})
it('no file in src/main/observability/ imports from telemetry', () => {
const files = listTsFiles(OBSERVABILITY_DIR)
expect(files.length).toBeGreaterThan(0)
const violations = files.flatMap((f) => {
// Allow this very file — the test references the path string for its
// own message, and the whitelist is one specific filename rather than
// a directory exemption.
if (f.endsWith('architecture.test.ts')) {
return []
}
const bad = findOffendingImports(f, 'telemetry')
return bad.map((spec) => `${relative(REPO_ROOT, f)}: imports '${spec}'`)
})
expect(violations, violations.join('\n')).toEqual([])
})
})
@@ -244,18 +244,3 @@ describe.each(['opencode', 'opencode2'] as const)('%s plugin OpenCode 2 lineage'
await cleanup?.()
})
})
describe('OpenCode 2 session client shim', () => {
it('keeps the enveloped SDK result untouched for the OpenCode 1 server path', () => {
const source = _internals.getOpenCodePluginSource()
// The shim only wraps a bare session record; server() still hands the raw SDK client through.
expect(source).toContain(
'return result && typeof result.id === "string" ? { data: result } : result;'
)
expect(source).toContain(
'handleLifecycleEvent(client, normalizeNextLifecycleEvent(event), factoryID)'
)
expect(source).toContain('const client = _ctx?.client;')
})
})
+1 -2
View File
@@ -16,8 +16,7 @@ import { setProcessTreeKillGate } from '../shared/child-process/process-tree-kil
* lifetimes (sync, fire-and-forget, timeout ladder), and three more live in
* `src/shared` and reach this through `process-tree-kill-gate`, so a guard that
* only lived in the tree-kill helper would cover one of nine.
* `main-process-tree-kill-gate.test.ts` holds that set closed by counting `/pid`
* call sites against gate admissions per file, not by file. Returns false
* Returns false
* when the caller must not walk that pid's tree; the caller still kills its own
* root through the child handle (`refused-tree-kill-root-termination.test.ts`),
* so a refusal is never a process leak.
@@ -61,7 +61,6 @@ async function retiredPty(
if (destroy) {
;(term as unknown as { destroy?: () => void }).destroy?.()
}
await new Promise<void>((resolve) => setTimeout(resolve, 400))
return { term, spawnFd }
}
@@ -94,8 +94,4 @@ describe('client-hosted reconciliation hold on published session tabs', () => {
(await runtime.listMobileSessionTabs(`id:${WT}`, DEVICE_B)).clientHostedPagesUnreconciled
).toBe(undefined)
})
// The fan-out sites (`emitMobileSessionTabsSnapshot`, the notify loops) share this one seam, and
// the projection census in client-hosted-page-reconciliation-window.test.ts is what keeps them
// from growing a second, unheld path.
})
@@ -1,5 +1,3 @@
import { readFileSync, readdirSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import {
ClientHostedPageReconciliationWindow,
@@ -122,41 +120,3 @@ describe('holdFor', () => {
expect(window.holdFor(stamped, DEVICE_A, OPENED_AT + WINDOW_MS)).toEqual(frame)
})
})
// The hold is only correct if every snapshot leaving the runtime for a client goes through the
// per-client seam. A `project` call that bypasses it publishes an unheld -- or another client's --
// answer, which is invisible to any behavioral test that does not happen to cover that call site.
describe('session-tabs projection census', () => {
it('routes every client projection in orca-runtime through the per-client seam', () => {
const source = readOrcaRuntimeSourceFamily()
const direct = source.match(/this\.clientSessionTabSelections\.project\(/g) ?? []
// Exactly two: inside `projectMobileSessionTabsForClient` itself, and the removed-worktree
// frame, which announces a deletion the client asked for rather than answering about contents.
expect(direct).toHaveLength(2)
expect(source).toContain('this.clientSessionTabSelections.project(removed,')
})
it('keeps the hold flags out of every other runtime publication site', () => {
const source = readOrcaRuntimeSourceFamily()
expect(source).not.toContain('clientHostedPagesUnreconciled')
expect(source).not.toContain('agentSessionsUnverifiable')
})
})
function readOrcaRuntimeSourceFamily(): string {
return readdirSync(import.meta.dirname)
.filter(
(name) =>
(name === 'orca-runtime.ts' || name.startsWith('orca-runtime-')) &&
name.endsWith('.ts') &&
!name.includes('.test.') &&
!name.endsWith('-fixtures.ts') &&
!name.endsWith('-test-harness.ts') &&
!name.endsWith('-mock-registry.ts')
)
.sort()
.map((name) => readFileSync(join(import.meta.dirname, name), 'utf8'))
.join('\n')
}
@@ -308,7 +308,8 @@ describe('writing a launch prompt into a terminal agent', () => {
handle: 'term_1',
agent,
freshLaunch: true,
text: 'do the thing'
text: 'do the thing',
clock: fakeClock()
})
expect(delivered).toBe(true)
@@ -368,7 +369,8 @@ describe('writing a launch prompt into a terminal agent', () => {
handle: 'term_existing',
agent: 'grok',
freshLaunch: false,
text: 'do the thing'
text: 'do the thing',
clock: fakeClock()
})
expect(delivered).toBe(true)
@@ -1,38 +0,0 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
// Why #11935: a pre-`ready` graceful quit is deferred, so a lock-losing headless `orca serve`
// kept booting into Linux Ozone/X11 init, died with SIGSEGV, and systemd restarted it forever
// until the leaked AppImage FUSE mounts hit the kernel's 1000-mount ceiling.
describe('headless lock-loss exit contract', () => {
const preflightSource = readFileSync(
join(process.cwd(), 'src/main/startup/main-process-preflight.ts'),
'utf8'
)
const entrySource = readFileSync(join(process.cwd(), 'src/main/index.ts'), 'utf8')
it('exits the lock-losing launch immediately instead of scheduling a graceful quit', () => {
const gateStart = preflightSource.indexOf('if (!hasLock) {')
// Why: bound the anchor — an unresolved indexOf slices to EOF and passes vacuously.
expect(gateStart).toBeGreaterThanOrEqual(0)
const gateEnd = preflightSource.indexOf('\n }', gateStart)
expect(gateEnd).toBeGreaterThan(gateStart)
const gate = preflightSource.slice(gateStart, gateEnd)
expect(gate).toContain('app.exit(SINGLE_INSTANCE_ALREADY_RUNNING_EXIT_CODE)')
expect(gate).not.toContain('app.quit()')
})
it('keeps a duplicate serve launch from promoting the live server to a desktop window', () => {
const activationStart = entrySource.indexOf('function requestDesktopActivation(')
expect(activationStart).toBeGreaterThanOrEqual(0)
const activationEnd = entrySource.indexOf('\n}', activationStart)
expect(activationEnd).toBeGreaterThan(activationStart)
expect(entrySource.slice(activationStart, activationEnd)).toContain(
'shouldActivateDesktopForSecondInstance(argv)'
)
})
})
@@ -103,6 +103,98 @@ describeOnWindows('ConPTY job ownership', () => {
expect(isPtyJobOwnershipAvailable()).toBe(true)
})
it.each([
['kill', 'connected'],
['destroy', 'immediate']
] as const)(
'retires a quiet child with %s at the %s fence',
async (operation, fence) => {
const result = await runProcess({
program: process.execPath,
args: [
join(process.cwd(), 'config', 'scripts', 'windows-pty-pre-ready-teardown.cjs'),
operation,
fence
],
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' },
timeoutMs: 20_000
})
if (result.code === 0 && !result.timedOut) {
console.log(result.stdout)
if (result.stderr) {
console.error(result.stderr)
}
}
expect(result, `${result.stdout}\n${result.stderr}`).toMatchObject({
code: 0,
timedOut: false
})
expect(result.stdout).toContain('"phase":"complete"')
expect(result.stdout).toContain('"exitCallbacks":1')
},
30_000
)
it.each(['dll', 'inbox'] as const)(
'retires a closed forwarding connection with the %s backend',
async (backend) => {
const result = await runProcess({
program: process.execPath,
args: [
join(process.cwd(), 'config', 'scripts', 'windows-pty-closed-forwarding-teardown.cjs'),
backend
],
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' },
timeoutMs: 20_000
})
if (result.code === 0 && !result.timedOut) {
console.log(result.stdout)
if (result.stderr) {
console.error(result.stderr)
}
}
expect(result, `${result.stdout}\n${result.stderr}`).toMatchObject({
code: 0,
timedOut: false
})
expect(result.stdout).toContain('"phase":"complete"')
expect(result.stdout).toContain('"nativeExitCallbacks":1')
expect(result.stdout).toContain('"exitCallbacks":1')
},
30_000
)
it.each(['dll', 'inbox'] as const)(
'retries closed-forwarding cleanup after a failed first kill with the %s backend',
async (backend) => {
const result = await runProcess({
program: process.execPath,
args: [
join(process.cwd(), 'config', 'scripts', 'windows-pty-closed-forwarding-teardown.cjs'),
backend,
'retry'
],
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' },
timeoutMs: 20_000
})
if (result.code === 0 && !result.timedOut) {
console.log(result.stdout)
if (result.stderr) {
console.error(result.stderr)
}
}
expect(result, `${result.stdout}\n${result.stderr}`).toMatchObject({
code: 0,
timedOut: false
})
expect(result.stdout).toContain('"phase":"complete"')
expect(result.stdout).toContain('"nativeExitCallbacks":1')
expect(result.stdout).toContain('"exitCallbacks":1')
expect(result.stdout).toContain('"retryFault":true')
},
30_000
)
it('keeps the native table intact while shell cleanup overlaps new terminals', async () => {
const result = await runProcess({
program: process.execPath,
@@ -1,6 +1,4 @@
// @vitest-environment happy-dom
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import {
applyBrowserPageViewportLayout,
@@ -19,15 +17,6 @@ import {
syncBrowserPageChromeInset
} from './browser-page-viewport'
function readPresetViewportCssRule(): string {
const css = readFileSync(resolve(import.meta.dirname, '../../../assets/main.css'), 'utf8')
const body =
css.match(new RegExp(`\\.${BROWSER_PAGE_PRESET_VIEWPORT_CLASS_NAME}\\s*\\{(?<body>[^}]*)\\}`))
?.groups?.body ?? ''
return body.replace(/\s+/g, ' ').trim()
}
function mountSlotViewport(workspaceTabId: string): HTMLDivElement {
const root = document.createElement('div')
root.className = 'relative flex min-h-0 flex-1 flex-col'
@@ -113,13 +102,6 @@ describe('ensureBrowserPageViewport', () => {
expect(viewport.content.style.height).toBe('')
})
it('divides the preset DIP size by the live UI zoom factor', () => {
expect(readPresetViewportCssRule()).toBe(
'width: calc(var(--browser-page-viewport-width) / var(--ui-zoom-factor, 1)); ' +
'height: calc(var(--browser-page-viewport-height) / var(--ui-zoom-factor, 1));'
)
})
it('clears preset dimensions when no preset is active', () => {
mountSlotViewport('workspace-1')
const viewport = ensureBrowserPageViewport('page-1', 'workspace-1')!
@@ -1,5 +1,3 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import type { FeatureWallSetupProgressInput } from './feature-wall-setup-progress'
import { getFeatureWallSetupProgress } from './feature-wall-setup-progress'
@@ -80,19 +78,6 @@ describe('getFeatureWallSetupProgress', () => {
])
})
it('renders Setup before Milestones and numbers Milestones after Setup', () => {
const source = readFileSync(
join(process.cwd(), 'src/renderer/src/components/feature-wall/FeatureWallSetupChecklist.tsx'),
'utf8'
)
const setupSectionIndex = source.indexOf('steps={setupSteps}')
const milestonesSectionIndex = source.indexOf('steps={parallelWorkSteps}')
expect(setupSectionIndex).toBeGreaterThanOrEqual(0)
expect(milestonesSectionIndex).toBeGreaterThan(setupSectionIndex)
expect(source).toContain('startOrdinal={setupSteps.length + 1}')
})
it('auto-selects incomplete parallel work after setup steps are complete', () => {
const progress = getFeatureWallSetupProgress(
makeInput({
@@ -444,18 +444,4 @@ describe('native chat composer drop scoping', () => {
])
expect(readNativeChatAttachmentCache('chat-b')).toEqual([])
})
it('control: an unowned control drop does not attach images to either chat', async () => {
const view = render(
<>
<ComposerProbe pane="chat-a" />
<ComposerProbe pane="chat-b" hidden />
<div data-testid="unowned-editor" />
</>
)
await dropTwoImages(view.container.querySelector('[data-testid="unowned-editor"]')!)
expect(electron.send).not.toHaveBeenCalled()
expect(readNativeChatAttachmentCache('chat-a')).toEqual([])
expect(readNativeChatAttachmentCache('chat-b')).toEqual([])
})
})
@@ -356,15 +356,24 @@ describe('workspace linked work editor', () => {
it.each([
{ key: 'Enter', keyCode: 13, isComposing: true },
{ key: 'Enter', keyCode: 229 },
{ key: 'Enter', keyCode: 13 }
{ key: 'Enter', keyCode: 13 },
null
])('keeps notes open through IME confirmation and redispatch: %j', async (event) => {
openDialog()
const notes = screen.getByRole('textbox', { name: 'Notes' })
fireEvent.compositionStart(notes)
fireEvent.change(notes, { target: { value: '確定' } })
fireEvent.keyDown(notes, event)
if (event) {
fireEvent.keyDown(notes, event)
} else {
fireEvent.keyUp(notes, { key: 'f', keyCode: 70, isComposing: true })
}
fireEvent.compositionEnd(notes)
fireEvent.keyUp(notes, { key: 'Enter', keyCode: 13 })
if (event) {
fireEvent.keyUp(notes, { key: 'Enter', keyCode: 13 })
} else {
fireEvent.input(notes, { data: '定', inputType: 'insertText', isComposing: false })
}
fireEvent.keyDown(notes, { key: 'Enter', keyCode: 13 })
expect(updateWorktreeMeta).not.toHaveBeenCalled()
expect(useAppStore.getState().activeModal).toBe('edit-meta')
@@ -4,8 +4,6 @@
* input sources on the same device do run, non-Hangul scripts, desktop
* platforms, and anything else that writes to the PTY.
*/
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { writeTerminalPastePtyInput } from './terminal-pty-paste-writer'
import {
@@ -185,22 +183,6 @@ describe('the iPadOS Hangul path alongside everything else', () => {
expect(rig.emitted).toEqual(['ㅎ'])
}
})
it('reads the platform once, so no second gate can drift from it', () => {
const lifecycle = readFileSync(
resolve(
process.cwd(),
'src/renderer/src/components/terminal-pane/terminal-pane-pane-input.ts'
),
'utf8'
)
expect(lifecycle.match(/const isIosWeb =/g)).toHaveLength(1)
expect(lifecycle.match(/isCurrentPlatformIosWeb\(/g)).toHaveLength(1)
// And that the one gate is what installs the controller.
expect(lifecycle).toMatch(
/const iosHangulPreedit = isIosWeb\s*\?\s*installTerminalIosHangulPreedit\(/
)
})
})
it('composes Hangul after a paste written straight to the transport', async () => {
@@ -36,6 +36,7 @@ export function useImeEnterGestureOwnership(): {
isComposing: () => boolean
ownsKeyDown: (event: ImeEnterGestureEvent) => boolean
onKeyUp: (event: ImeEnterGestureEvent) => void
onCompositionEnd: () => void
reset: () => void
setComposing: (active: boolean) => void
} {
@@ -48,6 +49,16 @@ export function useImeEnterGestureOwnership(): {
const reset = (): void => {
stateRef.current = { composing: false, pendingEnter: null }
}
const expirePendingEnter = (): void => {
const pendingEnter = stateRef.current.pendingEnter
if (pendingEnter) {
requestAnimationFrame(() => {
if (stateRef.current.pendingEnter === pendingEnter) {
stateRef.current.pendingEnter = null
}
})
}
}
// Shift+Enter is a newline, never a submit — it must never be owned or swallowed.
const isPlainEnter = (event: ImeEnterGestureEvent): boolean =>
event.key === 'Enter' && event.keyCode === 13 && !event.shiftKey
@@ -93,13 +104,15 @@ export function useImeEnterGestureOwnership(): {
},
onKeyUp: (): void => {
// Any keyup can precede the redispatch, including an IME-owned Process/229 release.
const pendingEnter = stateRef.current.pendingEnter
if (pendingEnter) {
requestAnimationFrame(() => {
if (stateRef.current.pendingEnter === pendingEnter) {
stateRef.current.pendingEnter = null
}
})
expirePendingEnter()
},
onCompositionEnd: () => {
const compositionWasActive = stateRef.current.composing
stateRef.current.composing = false
if (compositionWasActive && !stateRef.current.pendingEnter) {
// IBus can finish composition before its only unmarked confirming Enter.
stateRef.current.pendingEnter = {}
expirePendingEnter()
}
},
reset,
@@ -46,6 +46,28 @@ describe.each([Input, Textarea, ImeInput, ImeTextarea])('IME text field %s', (Fi
}
)
it.each(['confirm', 'selection'])(
'releases completion-first composition ownership after %s',
(finish) => {
const action = vi.fn()
const { getByRole } = render(<Field onKeyDown={action} />)
const field = getByRole('textbox')
fireEvent.compositionStart(field)
fireEvent.change(field, { target: { value: '한글' } })
fireEvent.keyUp(field, { key: 'f', keyCode: 70, isComposing: true })
fireEvent.compositionEnd(field)
fireEvent.input(field, { data: '글', inputType: 'insertText', isComposing: false })
if (finish === 'confirm') {
expect(fireEvent.keyDown(field, enter)).toBe(false)
expect(action).not.toHaveBeenCalled()
} else {
vi.advanceTimersByTime(20)
}
expect(fireEvent.keyDown(field, enter)).toBe(true)
expect(action).toHaveBeenCalledOnce()
}
)
it('guards active composition, preserves lifecycle callbacks, and releases on blur', () => {
const action = vi.fn()
const start = vi.fn(),
+1 -1
View File
@@ -26,7 +26,7 @@ export function useImeTextFieldProps<T extends HTMLInputElement | HTMLTextAreaEl
props.onCompositionStart?.(event)
},
onCompositionEnd(event) {
ime.setComposing(false)
ime.onCompositionEnd()
props.onCompositionEnd?.(event)
},
onBlur(event) {
@@ -1,111 +0,0 @@
import { existsSync, readFileSync } from 'node:fs'
import path from 'node:path'
import { describe, expect, it } from 'vitest'
/**
* The renderer runs sandboxed with contextIsolation: `node:*` builtins do not resolve and even a
* bare `process` read throws. A module that reaches one is not a degraded feature — the chunk
* fails at evaluation, React never mounts, and the window stays blank with `workspaceSessionReady`
* stuck false (#18742 did exactly this by importing one constant out of a `node:child_process`
* module). Bundling hides it: the offending module can sit in a shared chunk far from the import
* that pulled it in.
*
* So walk the import graph from every renderer entry — lazy routes included, since a `node:`
* builtin behind one is just a blank route instead of a blank app — and refuse any builtin.
*/
const RENDERER_SRC = import.meta.dirname
const REPO_SRC = path.resolve(RENDERER_SRC, '../..')
const ENTRIES = ['main.tsx', 'popout.tsx', 'web/main.tsx']
const EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx']
/** `import`/`export ... from` and `import(...)` specifiers, minus type-only ones, which erase. */
function collectValueImportSpecifiers(source: string): string[] {
const specifiers: string[] = []
const pattern =
/(?:^|[\s;}])(?:import|export)(\s+type\s|\s*\{[^}]*\}|[^'"]*?)?\s*from\s*['"]([^'"]+)['"]|(?:^|[\s;}])import\s*['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)/g
for (const match of source.matchAll(pattern)) {
const clause = match[1] ?? ''
const specifier = match[2] ?? match[3] ?? match[4]
if (!specifier || /^\s*type\s/.test(clause)) {
continue
}
// A brace clause whose every binding is `type`-prefixed also erases entirely.
const bindings = clause.trim().startsWith('{') ? clause.trim().slice(1, -1).split(',') : null
if (bindings && bindings.some((b) => b.trim()) && bindings.every((b) => /^\s*type\s/.test(b))) {
continue
}
specifiers.push(specifier)
}
return specifiers
}
function resolveModule(specifier: string, fromFile: string): string | null {
let base: string
if (specifier.startsWith('@renderer/')) {
base = path.join(RENDERER_SRC, specifier.slice('@renderer/'.length))
} else if (specifier.startsWith('@/')) {
base = path.join(RENDERER_SRC, specifier.slice(2))
} else if (specifier.startsWith('.')) {
base = path.resolve(path.dirname(fromFile), specifier)
} else {
// Bare package specifiers are npm dependencies, not first-party source.
return null
}
for (const candidate of [
...EXTENSIONS.map((ext) => `${base}${ext}`),
...EXTENSIONS.map((ext) => path.join(base, `index${ext}`))
]) {
if (existsSync(candidate)) {
return candidate
}
}
return null
}
function walkRendererImportGraph(): Map<string, string[]> {
/** file -> the chain of first-party importers that reached it, entry first. */
const pathToFile = new Map<string, string[]>()
const queue: string[] = []
for (const entry of ENTRIES) {
const file = path.join(RENDERER_SRC, entry)
pathToFile.set(file, [file])
queue.push(file)
}
while (queue.length > 0) {
const file = queue.shift() as string
const chain = pathToFile.get(file) as string[]
for (const specifier of collectValueImportSpecifiers(readFileSync(file, 'utf8'))) {
const resolved = resolveModule(specifier, file)
if (!resolved || pathToFile.has(resolved)) {
continue
}
pathToFile.set(resolved, [...chain, resolved])
queue.push(resolved)
}
}
return pathToFile
}
describe('renderer node-builtin boundary', () => {
it('reaches no module that imports a node: builtin', () => {
const graph = walkRendererImportGraph()
const offenders: string[] = []
for (const [file, chain] of graph) {
const builtins = collectValueImportSpecifiers(readFileSync(file, 'utf8')).filter(
(specifier) => specifier.startsWith('node:')
)
if (builtins.length === 0) {
continue
}
const relativeChain = chain.map((step) => path.relative(REPO_SRC, step)).join('\n -> ')
offenders.push(`${builtins.join(', ')} via\n ${relativeChain}`)
}
expect(offenders.join('\n\n')).toBe('')
})
it('walks a real graph, so an empty offender list means something', () => {
const graph = walkRendererImportGraph()
expect(graph.size).toBeGreaterThan(3_000)
expect(graph.has(path.join(REPO_SRC, 'shared/process-table-snapshot.ts'))).toBe(true)
})
})
+3 -7
View File
@@ -239,11 +239,12 @@ Fix dispatch fallback preview for normalized status prompts`
})
it('accepts custom non-empty agentType values', () => {
const result = parseAgentStatusPayload('{"state":"working","agentType":"cursor"}')
const agentType: AgentType = 'some-in-house-agent'
const result = parseAgentStatusPayload(JSON.stringify({ state: 'working', agentType }))
expect(result).toEqual({
state: 'working',
prompt: '',
agentType: 'cursor'
agentType
})
})
@@ -731,11 +732,6 @@ describe('WellKnownAgentType', () => {
'rovo'
])
})
it('keeps AgentType open to custom agent names', () => {
const custom: AgentType = 'some-in-house-agent'
expect(custom).toBe('some-in-house-agent')
})
})
describe('the main agent field on a status payload', () => {
@@ -1,26 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
BROWSER_CLIENT_HOST_RUNTIME_CAPABILITY,
BROWSER_NETWORK_EXECUTION_HOSTS_RUNTIME_CAPABILITY,
BROWSER_NETWORK_TUNNEL_RUNTIME_CAPABILITY,
BROWSER_SCREENCAST_RUNTIME_CAPABILITY,
RUNTIME_CAPABILITIES
} from './protocol-version'
describe('browser network capabilities', () => {
it('keeps client hosting, network tunneling, and screencast independent', () => {
expect(BROWSER_CLIENT_HOST_RUNTIME_CAPABILITY).toBe('browser.clientHost.v1')
expect(BROWSER_NETWORK_TUNNEL_RUNTIME_CAPABILITY).toBe('network.browserTunnel.v1')
expect(BROWSER_NETWORK_EXECUTION_HOSTS_RUNTIME_CAPABILITY).toBe(
'network.browserTunnel.executionHosts.v1'
)
expect(BROWSER_SCREENCAST_RUNTIME_CAPABILITY).toBe('browser.screencast.v1')
})
it('advertises the registered client-host and tunnel implementations independently', () => {
expect(RUNTIME_CAPABILITIES).toContain(BROWSER_CLIENT_HOST_RUNTIME_CAPABILITY)
expect(RUNTIME_CAPABILITIES).toContain(BROWSER_NETWORK_TUNNEL_RUNTIME_CAPABILITY)
expect(RUNTIME_CAPABILITIES).toContain(BROWSER_NETWORK_EXECUTION_HOSTS_RUNTIME_CAPABILITY)
expect(RUNTIME_CAPABILITIES).toContain(BROWSER_SCREENCAST_RUNTIME_CAPABILITY)
})
})