Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions packages/host-service/src/terminal/terminal.adoption.node-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,42 @@ describe("createTerminalSessionInternal — host-service restart adoption", () =
await disposeSessionAndWait(terminalId, db);
});

test("initialCommand runs promptly even when OSC 133;A never fires", async () => {
// Regression guard against reintroducing the SHELL_READY_TIMEOUT_MS
// stall: bash with no Superset wrapper on disk never emits OSC 133;A,
// but the preset command should still run as soon as the shell reads.
__setAccountShellForTesting("/bin/bash");
try {
const terminalId = `e2e-no-marker-${randomUUID().slice(0, 8)}`;
const sentinelFile = path.join(TEST_HOME, `no-marker-${terminalId}`);

const start = Date.now();
const result = await createTerminalSessionInternal({
terminalId,
workspaceId,
db,
listed: true,
initialCommand: `echo ok > ${sentinelFile}`,
});
assert.ok(!("error" in result));
if ("error" in result) return;

await waitFor(() => fs.existsSync(sentinelFile), 10_000);
const elapsed = Date.now() - start;
console.log(`[repro] initialCommand executed in ${elapsed}ms`);
// Pre-fix: SHELL_READY_TIMEOUT_MS forced this to 15 s. 5 s leaves
// generous headroom for CI overhead while still catching regression.
assert.ok(
elapsed < 5000,
`expected initialCommand to run promptly, took ${elapsed}ms`,
);

await disposeSessionAndWait(terminalId, db);
} finally {
__setAccountShellForTesting("/bin/sh");
}
});

test("rejects reusing a live terminal id from another workspace", async () => {
const terminalId = `e2e-cross-live-${randomUUID().slice(0, 8)}`;

Expand Down
17 changes: 5 additions & 12 deletions packages/host-service/src/terminal/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,12 +207,8 @@ type TerminalSocket = {
// Scanner logic lives in @superset/shared/shell-ready-scanner.
// ---------------------------------------------------------------------------

/**
* How long to wait for the shell-ready marker before unblocking writes.
* 15 s covers heavy setups like Nix-based devenv via direnv. On timeout
* buffered writes flush immediately (same behaviour as before this feature).
*/
const SHELL_READY_TIMEOUT_MS = 15_000;
/** Flush partial OSC 133;A prefix bytes the scanner is holding if a full marker never arrives. */
const SHELL_READY_TIMEOUT_MS = 3_000;

/**
* Shell readiness lifecycle:
Expand Down Expand Up @@ -793,11 +789,9 @@ function queueInitialCommand(
const cmd = initialCommand.endsWith("\n")
? initialCommand
: `${initialCommand}\n`;
session.shellReadyPromise.then(() => {
if (!session.exited) {
session.pty.write(cmd);
}
});
// Don't gate on OSC 133;A: PTY stdin buffers until the shell reads it,
// and gating turned broken/missing markers into a guaranteed stall.
session.pty.write(cmd);
}

interface DaemonCloseResult {
Expand Down Expand Up @@ -1001,7 +995,6 @@ interface CreateTerminalSessionOptions {
themeType?: "dark" | "light";
db: HostDb;
eventBus?: EventBus;
/** Command to run after the shell is ready. Queued behind shellReadyPromise. */
initialCommand?: string;
cwd?: string;
/** Hidden sessions are process-internal and should not appear in user pickers. */
Expand Down
Loading