mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
Pass workspace ownership when creating terminals
This commit is contained in:
@@ -53,7 +53,7 @@ export interface SeedDaemonClient {
|
|||||||
cwd: string,
|
cwd: string,
|
||||||
name?: string,
|
name?: string,
|
||||||
requestId?: string,
|
requestId?: string,
|
||||||
options?: { agentId?: string; command?: string; args?: string[] },
|
options?: { agentId?: string; command?: string; args?: string[]; workspaceId?: string },
|
||||||
): Promise<{
|
): Promise<{
|
||||||
terminal: { id: string; name: string; cwd: string; activity?: TerminalActivity | null } | null;
|
terminal: { id: string; name: string; cwd: string; activity?: TerminalActivity | null } | null;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
|||||||
@@ -67,8 +67,9 @@ export class TerminalE2EHarness {
|
|||||||
? {
|
? {
|
||||||
command: input.command,
|
command: input.command,
|
||||||
args: input.args,
|
args: input.args,
|
||||||
|
workspaceId: this.workspaceId,
|
||||||
}
|
}
|
||||||
: undefined;
|
: { workspaceId: this.workspaceId };
|
||||||
const result = await this.client.createTerminal(
|
const result = await this.client.createTerminal(
|
||||||
this.tempRepo.path,
|
this.tempRepo.path,
|
||||||
input.name,
|
input.name,
|
||||||
@@ -90,7 +91,9 @@ export class TerminalE2EHarness {
|
|||||||
const timeoutMs = input.timeoutMs ?? 10_000;
|
const timeoutMs = input.timeoutMs ?? 10_000;
|
||||||
const deadline = Date.now() + timeoutMs;
|
const deadline = Date.now() + timeoutMs;
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
const result = await this.client.listTerminals(this.tempRepo.path);
|
const result = await this.client.listTerminals(this.tempRepo.path, undefined, {
|
||||||
|
workspaceId: this.workspaceId,
|
||||||
|
});
|
||||||
const terminal = result.terminals.find((entry) => entry.id === input.terminalId);
|
const terminal = result.terminals.find((entry) => entry.id === input.terminalId);
|
||||||
const activity = terminal?.activity ?? null;
|
const activity = terminal?.activity ?? null;
|
||||||
const attentionMatches =
|
const attentionMatches =
|
||||||
|
|||||||
@@ -108,7 +108,14 @@ test.describe("Terminal title propagation", () => {
|
|||||||
test.skip("terminal tab title updates from OSC title escape sequence", async ({ page }) => {
|
test.skip("terminal tab title updates from OSC title escape sequence", async ({ page }) => {
|
||||||
test.setTimeout(60_000);
|
test.setTimeout(60_000);
|
||||||
|
|
||||||
const result = await workspace.client.createTerminal(workspace.repoPath, "title-test");
|
const result = await workspace.client.createTerminal(
|
||||||
|
workspace.repoPath,
|
||||||
|
"title-test",
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
workspaceId: workspace.workspaceId,
|
||||||
|
},
|
||||||
|
);
|
||||||
if (!result.terminal) throw new Error(`Failed to create terminal: ${result.error}`);
|
if (!result.terminal) throw new Error(`Failed to create terminal: ${result.error}`);
|
||||||
const terminalId = result.terminal.id;
|
const terminalId = result.terminal.id;
|
||||||
|
|
||||||
@@ -138,7 +145,14 @@ test.describe("Terminal title propagation", () => {
|
|||||||
test.skip("title debouncing coalesces rapid changes", async ({ page }) => {
|
test.skip("title debouncing coalesces rapid changes", async ({ page }) => {
|
||||||
test.setTimeout(60_000);
|
test.setTimeout(60_000);
|
||||||
|
|
||||||
const result = await workspace.client.createTerminal(workspace.repoPath, "debounce-test");
|
const result = await workspace.client.createTerminal(
|
||||||
|
workspace.repoPath,
|
||||||
|
"debounce-test",
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
workspaceId: workspace.workspaceId,
|
||||||
|
},
|
||||||
|
);
|
||||||
if (!result.terminal) throw new Error(`Failed to create terminal: ${result.error}`);
|
if (!result.terminal) throw new Error(`Failed to create terminal: ${result.error}`);
|
||||||
const terminalId = result.terminal.id;
|
const terminalId = result.terminal.id;
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,18 @@ export async function runCreateCommand(
|
|||||||
const cwd = options.cwd ?? process.cwd();
|
const cwd = options.cwd ?? process.cwd();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = await client.createTerminal(cwd, options.name);
|
const opened = await client.openProject(cwd);
|
||||||
|
if (!opened.workspace) {
|
||||||
|
const error: CommandError = {
|
||||||
|
code: "WORKSPACE_OPEN_FAILED",
|
||||||
|
message: opened.error ?? "Failed to open workspace",
|
||||||
|
};
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = await client.createTerminal(cwd, options.name, undefined, {
|
||||||
|
workspaceId: opened.workspace.id,
|
||||||
|
});
|
||||||
if (!payload.terminal) {
|
if (!payload.terminal) {
|
||||||
const error: CommandError = {
|
const error: CommandError = {
|
||||||
code: "TERMINAL_CREATE_FAILED",
|
code: "TERMINAL_CREATE_FAILED",
|
||||||
|
|||||||
@@ -519,7 +519,18 @@ test("bootstraps configured worktree terminals after setup succeeds", async () =
|
|||||||
const setupPort = readFileSync(path.join(agent.cwd, "setup-port.txt"), "utf8").trim();
|
const setupPort = readFileSync(path.join(agent.cwd, "setup-port.txt"), "utf8").trim();
|
||||||
expect(setupPort.length).toBeGreaterThan(0);
|
expect(setupPort.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
const createdTerminal = await ctx.client.createTerminal(agent.cwd, "Manual Port Check");
|
const openedWorktree = await ctx.client.openProject(agent.cwd);
|
||||||
|
if (!openedWorktree.workspace) {
|
||||||
|
throw new Error(openedWorktree.error ?? `Failed to open worktree workspace for ${agent.cwd}`);
|
||||||
|
}
|
||||||
|
const createdTerminal = await ctx.client.createTerminal(
|
||||||
|
agent.cwd,
|
||||||
|
"Manual Port Check",
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
workspaceId: openedWorktree.workspace.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
expect(createdTerminal.error).toBeNull();
|
expect(createdTerminal.error).toBeNull();
|
||||||
expect(createdTerminal.terminal).toBeTruthy();
|
expect(createdTerminal.terminal).toBeTruthy();
|
||||||
const manualTerminalId = createdTerminal.terminal?.id;
|
const manualTerminalId = createdTerminal.terminal?.id;
|
||||||
|
|||||||
@@ -24,6 +24,26 @@ function tmpCwd(): string {
|
|||||||
return mkdtempSync(path.join(tmpdir(), "daemon-terminal-e2e-"));
|
return mkdtempSync(path.join(tmpdir(), "daemon-terminal-e2e-"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CreateTerminalInWorkspaceInput {
|
||||||
|
cwd: string;
|
||||||
|
name?: string;
|
||||||
|
options?: { command?: string; args?: string[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTerminalInWorkspace(
|
||||||
|
client: DaemonClient,
|
||||||
|
input: CreateTerminalInWorkspaceInput,
|
||||||
|
) {
|
||||||
|
const opened = await client.openProject(input.cwd);
|
||||||
|
if (!opened.workspace) {
|
||||||
|
throw new Error(opened.error ?? `Failed to open workspace for ${input.cwd}`);
|
||||||
|
}
|
||||||
|
return client.createTerminal(input.cwd, input.name, undefined, {
|
||||||
|
...input.options,
|
||||||
|
workspaceId: opened.workspace.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function createLogger() {
|
function createLogger() {
|
||||||
return {
|
return {
|
||||||
debug: () => {},
|
debug: () => {},
|
||||||
@@ -602,7 +622,11 @@ async function measureRepeatCadenceForTerminal(input: {
|
|||||||
options?: { command?: string; args?: string[] };
|
options?: { command?: string; args?: string[] };
|
||||||
setupInput?: string;
|
setupInput?: string;
|
||||||
}): Promise<TerminalRepeatCadenceMeasurement> {
|
}): Promise<TerminalRepeatCadenceMeasurement> {
|
||||||
const created = await ctx.client.createTerminal(input.cwd, input.name, undefined, input.options);
|
const created = await createTerminalInWorkspace(ctx.client, {
|
||||||
|
cwd: input.cwd,
|
||||||
|
name: input.name,
|
||||||
|
options: input.options,
|
||||||
|
});
|
||||||
const terminalId = created.terminal!.id;
|
const terminalId = created.terminal!.id;
|
||||||
const ws = await connectRawWebSocket(ctx.daemon.port);
|
const ws = await connectRawWebSocket(ctx.daemon.port);
|
||||||
|
|
||||||
@@ -742,7 +766,7 @@ test("lists terminals for a directory", async () => {
|
|||||||
|
|
||||||
test("client connects and receives a snapshot of the current terminal state", async () => {
|
test("client connects and receives a snapshot of the current terminal state", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
const created = await ctx.client.createTerminal(cwd);
|
const created = await createTerminalInWorkspace(ctx.client, { cwd });
|
||||||
const terminalId = created.terminal!.id;
|
const terminalId = created.terminal!.id;
|
||||||
|
|
||||||
ctx.client.sendTerminalInput(terminalId, {
|
ctx.client.sendTerminalInput(terminalId, {
|
||||||
@@ -764,7 +788,7 @@ test("client connects and receives a snapshot of the current terminal state", as
|
|||||||
|
|
||||||
test("live terminal restore skips the initial snapshot", async () => {
|
test("live terminal restore skips the initial snapshot", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
const created = await ctx.client.createTerminal(cwd);
|
const created = await createTerminalInWorkspace(ctx.client, { cwd });
|
||||||
const terminalId = created.terminal!.id;
|
const terminalId = created.terminal!.id;
|
||||||
const ws = await connectRawWebSocket(ctx.daemon.port);
|
const ws = await connectRawWebSocket(ctx.daemon.port);
|
||||||
|
|
||||||
@@ -809,12 +833,15 @@ test("live terminal restore skips the initial snapshot", async () => {
|
|||||||
|
|
||||||
test("visible terminal restore sends bounded ANSI history", async () => {
|
test("visible terminal restore sends bounded ANSI history", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
const created = await ctx.client.createTerminal(cwd, undefined, undefined, {
|
const created = await createTerminalInWorkspace(ctx.client, {
|
||||||
command: "/bin/sh",
|
cwd,
|
||||||
args: [
|
options: {
|
||||||
"-lc",
|
command: "/bin/sh",
|
||||||
"i=1; while [ $i -le 1200 ]; do printf 'restore-line-%04d\\n' $i; i=$((i+1)); done; sleep 30",
|
args: [
|
||||||
],
|
"-lc",
|
||||||
|
"i=1; while [ $i -le 1200 ]; do printf 'restore-line-%04d\\n' $i; i=$((i+1)); done; sleep 30",
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const terminalId = created.terminal!.id;
|
const terminalId = created.terminal!.id;
|
||||||
const ws = await connectRawWebSocket(ctx.daemon.port);
|
const ws = await connectRawWebSocket(ctx.daemon.port);
|
||||||
@@ -853,9 +880,12 @@ test("visible terminal restore sends bounded ANSI history", async () => {
|
|||||||
|
|
||||||
test("propagates debounced terminal titles through list responses and snapshots", async () => {
|
test("propagates debounced terminal titles through list responses and snapshots", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
const created = await ctx.client.createTerminal(cwd, undefined, undefined, {
|
const created = await createTerminalInWorkspace(ctx.client, {
|
||||||
command: "/bin/sh",
|
cwd,
|
||||||
args: ["-lc", "printf '\\033]0;Build Output\\007'; sleep 2"],
|
options: {
|
||||||
|
command: "/bin/sh",
|
||||||
|
args: ["-lc", "printf '\\033]0;Build Output\\007'; sleep 2"],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const terminalId = created.terminal!.id;
|
const terminalId = created.terminal!.id;
|
||||||
|
|
||||||
@@ -886,7 +916,7 @@ test("propagates debounced terminal titles through list responses and snapshots"
|
|||||||
|
|
||||||
test("subscribe response is sent before the initial snapshot frame", async () => {
|
test("subscribe response is sent before the initial snapshot frame", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
const created = await ctx.client.createTerminal(cwd);
|
const created = await createTerminalInWorkspace(ctx.client, { cwd });
|
||||||
const terminalId = created.terminal!.id;
|
const terminalId = created.terminal!.id;
|
||||||
const ws = await connectRawWebSocket(ctx.daemon.port);
|
const ws = await connectRawWebSocket(ctx.daemon.port);
|
||||||
|
|
||||||
@@ -982,7 +1012,7 @@ test("subscribe response is sent before the initial snapshot frame", async () =>
|
|||||||
|
|
||||||
test("client sends input and receives output as raw bytes", async () => {
|
test("client sends input and receives output as raw bytes", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
const created = await ctx.client.createTerminal(cwd);
|
const created = await createTerminalInWorkspace(ctx.client, { cwd });
|
||||||
const terminalId = created.terminal!.id;
|
const terminalId = created.terminal!.id;
|
||||||
|
|
||||||
const outputPromise = waitForTerminalOutput(ctx.client, terminalId, (text) =>
|
const outputPromise = waitForTerminalOutput(ctx.client, terminalId, (text) =>
|
||||||
@@ -1008,8 +1038,8 @@ test("default zsh terminal does not eagerly flush full state during repeat burst
|
|||||||
|
|
||||||
test("one client can stream two terminals concurrently", async () => {
|
test("one client can stream two terminals concurrently", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
const firstCreated = await ctx.client.createTerminal(cwd, "first");
|
const firstCreated = await createTerminalInWorkspace(ctx.client, { cwd, name: "first" });
|
||||||
const secondCreated = await ctx.client.createTerminal(cwd, "second");
|
const secondCreated = await createTerminalInWorkspace(ctx.client, { cwd, name: "second" });
|
||||||
const firstTerminalId = firstCreated.terminal!.id;
|
const firstTerminalId = firstCreated.terminal!.id;
|
||||||
const secondTerminalId = secondCreated.terminal!.id;
|
const secondTerminalId = secondCreated.terminal!.id;
|
||||||
|
|
||||||
@@ -1044,7 +1074,7 @@ test("one client can stream two terminals concurrently", async () => {
|
|||||||
|
|
||||||
test("disconnect and reconnect both receive the current snapshot", async () => {
|
test("disconnect and reconnect both receive the current snapshot", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
const created = await ctx.client.createTerminal(cwd);
|
const created = await createTerminalInWorkspace(ctx.client, { cwd });
|
||||||
const terminalId = created.terminal!.id;
|
const terminalId = created.terminal!.id;
|
||||||
|
|
||||||
await ctx.client.subscribeTerminal(terminalId);
|
await ctx.client.subscribeTerminal(terminalId);
|
||||||
@@ -1068,8 +1098,10 @@ test("disconnect and reconnect both receive the current snapshot", async () => {
|
|||||||
|
|
||||||
test("reconnected terminal streams replay active input modes after the snapshot", async () => {
|
test("reconnected terminal streams replay active input modes after the snapshot", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
const created = await ctx.client.createTerminal(cwd, "input-mode-probe", undefined, {
|
const created = await createTerminalInWorkspace(ctx.client, {
|
||||||
...createInputModeProbeOptions(),
|
cwd,
|
||||||
|
name: "input-mode-probe",
|
||||||
|
options: createInputModeProbeOptions(),
|
||||||
});
|
});
|
||||||
const terminalId = created.terminal!.id;
|
const terminalId = created.terminal!.id;
|
||||||
const firstWs = await connectRawWebSocket(ctx.daemon.port);
|
const firstWs = await connectRawWebSocket(ctx.daemon.port);
|
||||||
@@ -1125,8 +1157,10 @@ test("reconnected terminal streams replay active input modes after the snapshot"
|
|||||||
|
|
||||||
test("reconnected terminal streams do not replay input modes before they are enabled", async () => {
|
test("reconnected terminal streams do not replay input modes before they are enabled", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
const created = await ctx.client.createTerminal(cwd, "input-mode-inactive-probe", undefined, {
|
const created = await createTerminalInWorkspace(ctx.client, {
|
||||||
...createInputModeProbeOptions(),
|
cwd,
|
||||||
|
name: "input-mode-inactive-probe",
|
||||||
|
options: createInputModeProbeOptions(),
|
||||||
});
|
});
|
||||||
const terminalId = created.terminal!.id;
|
const terminalId = created.terminal!.id;
|
||||||
const ws = await connectRawWebSocket(ctx.daemon.port);
|
const ws = await connectRawWebSocket(ctx.daemon.port);
|
||||||
@@ -1146,7 +1180,7 @@ test("reconnected terminal streams do not replay input modes before they are ena
|
|||||||
|
|
||||||
test("fast output to a slow websocket client falls back to a snapshot", async () => {
|
test("fast output to a slow websocket client falls back to a snapshot", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
const created = await ctx.client.createTerminal(cwd);
|
const created = await createTerminalInWorkspace(ctx.client, { cwd });
|
||||||
const terminalId = created.terminal!.id;
|
const terminalId = created.terminal!.id;
|
||||||
const ws = await connectRawWebSocket(ctx.daemon.port);
|
const ws = await connectRawWebSocket(ctx.daemon.port);
|
||||||
|
|
||||||
@@ -1179,7 +1213,7 @@ test("fast output to a slow websocket client falls back to a snapshot", async ()
|
|||||||
|
|
||||||
test("multiple clients on the same terminal each receive output independently", async () => {
|
test("multiple clients on the same terminal each receive output independently", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
const created = await ctx.client.createTerminal(cwd);
|
const created = await createTerminalInWorkspace(ctx.client, { cwd });
|
||||||
const terminalId = created.terminal!.id;
|
const terminalId = created.terminal!.id;
|
||||||
const secondClient = await connectClient(
|
const secondClient = await connectClient(
|
||||||
ctx.daemon.port,
|
ctx.daemon.port,
|
||||||
@@ -1212,7 +1246,7 @@ test("multiple clients on the same terminal each receive output independently",
|
|||||||
|
|
||||||
test("resize updates server dimensions without sending a live snapshot", async () => {
|
test("resize updates server dimensions without sending a live snapshot", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
const created = await ctx.client.createTerminal(cwd);
|
const created = await createTerminalInWorkspace(ctx.client, { cwd });
|
||||||
const terminalId = created.terminal!.id;
|
const terminalId = created.terminal!.id;
|
||||||
await ctx.client.subscribeTerminal(terminalId);
|
await ctx.client.subscribeTerminal(terminalId);
|
||||||
ctx.client.sendTerminalInput(terminalId, {
|
ctx.client.sendTerminalInput(terminalId, {
|
||||||
@@ -1232,7 +1266,7 @@ test("resize updates server dimensions without sending a live snapshot", async (
|
|||||||
|
|
||||||
test("resize does not stall streamed output for an attached client", async () => {
|
test("resize does not stall streamed output for an attached client", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
const created = await ctx.client.createTerminal(cwd);
|
const created = await createTerminalInWorkspace(ctx.client, { cwd });
|
||||||
const terminalId = created.terminal!.id;
|
const terminalId = created.terminal!.id;
|
||||||
const secondClient = await connectClient(
|
const secondClient = await connectClient(
|
||||||
ctx.daemon.port,
|
ctx.daemon.port,
|
||||||
@@ -1271,7 +1305,7 @@ test("resize does not stall streamed output for an attached client", async () =>
|
|||||||
|
|
||||||
test("terminal exits notify the client", async () => {
|
test("terminal exits notify the client", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
const created = await ctx.client.createTerminal(cwd);
|
const created = await createTerminalInWorkspace(ctx.client, { cwd });
|
||||||
const terminalId = created.terminal!.id;
|
const terminalId = created.terminal!.id;
|
||||||
|
|
||||||
let sawExit = false;
|
let sawExit = false;
|
||||||
@@ -1296,7 +1330,7 @@ test("terminal exits notify the client", async () => {
|
|||||||
|
|
||||||
test("websocket terminate then new connection gets snapshot with all prior output", async () => {
|
test("websocket terminate then new connection gets snapshot with all prior output", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
const created = await ctx.client.createTerminal(cwd);
|
const created = await createTerminalInWorkspace(ctx.client, { cwd });
|
||||||
const terminalId = created.terminal!.id;
|
const terminalId = created.terminal!.id;
|
||||||
const firstSocket = await connectRawWebSocket(ctx.daemon.port);
|
const firstSocket = await connectRawWebSocket(ctx.daemon.port);
|
||||||
|
|
||||||
@@ -1359,7 +1393,7 @@ test("websocket terminate then new connection gets snapshot with all prior outpu
|
|||||||
|
|
||||||
test("two clients can both send input and each sees its own output", async () => {
|
test("two clients can both send input and each sees its own output", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
const created = await ctx.client.createTerminal(cwd);
|
const created = await createTerminalInWorkspace(ctx.client, { cwd });
|
||||||
const terminalId = created.terminal!.id;
|
const terminalId = created.terminal!.id;
|
||||||
const secondClient = await connectClient(
|
const secondClient = await connectClient(
|
||||||
ctx.daemon.port,
|
ctx.daemon.port,
|
||||||
@@ -1397,7 +1431,10 @@ test("two clients can both send input and each sees its own output", async () =>
|
|||||||
|
|
||||||
test("snapshot fidelity through websocket decode preserves dimensions and visible text", async () => {
|
test("snapshot fidelity through websocket decode preserves dimensions and visible text", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
const created = await ctx.client.createTerminal(cwd, "Snapshot Fidelity");
|
const created = await createTerminalInWorkspace(ctx.client, {
|
||||||
|
cwd,
|
||||||
|
name: "Snapshot Fidelity",
|
||||||
|
});
|
||||||
const terminalId = created.terminal!.id;
|
const terminalId = created.terminal!.id;
|
||||||
|
|
||||||
ctx.client.sendTerminalInput(terminalId, {
|
ctx.client.sendTerminalInput(terminalId, {
|
||||||
@@ -1435,7 +1472,7 @@ test("snapshot fidelity through websocket decode preserves dimensions and visibl
|
|||||||
|
|
||||||
test("terminal exit prevents resubscribe and sends no frames after exit", async () => {
|
test("terminal exit prevents resubscribe and sends no frames after exit", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
const created = await ctx.client.createTerminal(cwd);
|
const created = await createTerminalInWorkspace(ctx.client, { cwd });
|
||||||
const terminalId = created.terminal!.id;
|
const terminalId = created.terminal!.id;
|
||||||
const ws = await connectRawWebSocket(ctx.daemon.port);
|
const ws = await connectRawWebSocket(ctx.daemon.port);
|
||||||
|
|
||||||
@@ -1470,7 +1507,7 @@ test("terminal exit prevents resubscribe and sends no frames after exit", async
|
|||||||
|
|
||||||
test("empty input frame does not crash the server", async () => {
|
test("empty input frame does not crash the server", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
const created = await ctx.client.createTerminal(cwd);
|
const created = await createTerminalInWorkspace(ctx.client, { cwd });
|
||||||
const terminalId = created.terminal!.id;
|
const terminalId = created.terminal!.id;
|
||||||
const ws = await connectRawWebSocket(ctx.daemon.port);
|
const ws = await connectRawWebSocket(ctx.daemon.port);
|
||||||
|
|
||||||
@@ -1509,7 +1546,7 @@ test("empty input frame does not crash the server", async () => {
|
|||||||
|
|
||||||
test("1MB output burst keeps frames decodable and terminal usable afterward", async () => {
|
test("1MB output burst keeps frames decodable and terminal usable afterward", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
const created = await ctx.client.createTerminal(cwd);
|
const created = await createTerminalInWorkspace(ctx.client, { cwd });
|
||||||
const terminalId = created.terminal!.id;
|
const terminalId = created.terminal!.id;
|
||||||
const ws = await connectRawWebSocket(ctx.daemon.port);
|
const ws = await connectRawWebSocket(ctx.daemon.port);
|
||||||
|
|
||||||
@@ -1568,7 +1605,7 @@ describe("capture", () => {
|
|||||||
test("captures visible terminal output as plain text", async () => {
|
test("captures visible terminal output as plain text", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
tempDirs.push(cwd);
|
tempDirs.push(cwd);
|
||||||
const created = await ctx.client.createTerminal(cwd);
|
const created = await createTerminalInWorkspace(ctx.client, { cwd });
|
||||||
const terminalId = created.terminal!.id;
|
const terminalId = created.terminal!.id;
|
||||||
|
|
||||||
await ctx.client.subscribeTerminal(terminalId);
|
await ctx.client.subscribeTerminal(terminalId);
|
||||||
@@ -1592,7 +1629,7 @@ describe("capture", () => {
|
|||||||
test("captures with start/end line range", async () => {
|
test("captures with start/end line range", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
tempDirs.push(cwd);
|
tempDirs.push(cwd);
|
||||||
const created = await ctx.client.createTerminal(cwd);
|
const created = await createTerminalInWorkspace(ctx.client, { cwd });
|
||||||
const terminalId = created.terminal!.id;
|
const terminalId = created.terminal!.id;
|
||||||
|
|
||||||
await ctx.client.subscribeTerminal(terminalId);
|
await ctx.client.subscribeTerminal(terminalId);
|
||||||
@@ -1625,7 +1662,7 @@ describe("capture", () => {
|
|||||||
test("supports negative line indices", async () => {
|
test("supports negative line indices", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
tempDirs.push(cwd);
|
tempDirs.push(cwd);
|
||||||
const created = await ctx.client.createTerminal(cwd);
|
const created = await createTerminalInWorkspace(ctx.client, { cwd });
|
||||||
const terminalId = created.terminal!.id;
|
const terminalId = created.terminal!.id;
|
||||||
|
|
||||||
await ctx.client.subscribeTerminal(terminalId);
|
await ctx.client.subscribeTerminal(terminalId);
|
||||||
@@ -1655,7 +1692,7 @@ describe("capture", () => {
|
|||||||
test("strips ANSI by default", async () => {
|
test("strips ANSI by default", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
tempDirs.push(cwd);
|
tempDirs.push(cwd);
|
||||||
const created = await ctx.client.createTerminal(cwd);
|
const created = await createTerminalInWorkspace(ctx.client, { cwd });
|
||||||
const terminalId = created.terminal!.id;
|
const terminalId = created.terminal!.id;
|
||||||
|
|
||||||
await ctx.client.subscribeTerminal(terminalId);
|
await ctx.client.subscribeTerminal(terminalId);
|
||||||
@@ -1686,8 +1723,14 @@ describe("list terminals across directories", () => {
|
|||||||
const cwd2 = tmpCwd();
|
const cwd2 = tmpCwd();
|
||||||
tempDirs.push(cwd1, cwd2);
|
tempDirs.push(cwd1, cwd2);
|
||||||
|
|
||||||
const firstCreated = await ctx.client.createTerminal(cwd1, "first-terminal");
|
const firstCreated = await createTerminalInWorkspace(ctx.client, {
|
||||||
const secondCreated = await ctx.client.createTerminal(cwd2, "second-terminal");
|
cwd: cwd1,
|
||||||
|
name: "first-terminal",
|
||||||
|
});
|
||||||
|
const secondCreated = await createTerminalInWorkspace(ctx.client, {
|
||||||
|
cwd: cwd2,
|
||||||
|
name: "second-terminal",
|
||||||
|
});
|
||||||
|
|
||||||
const list = await ctx.client.listTerminals();
|
const list = await ctx.client.listTerminals();
|
||||||
|
|
||||||
@@ -1711,8 +1754,11 @@ describe("list terminals across directories", () => {
|
|||||||
const cwd2 = tmpCwd();
|
const cwd2 = tmpCwd();
|
||||||
tempDirs.push(cwd1, cwd2);
|
tempDirs.push(cwd1, cwd2);
|
||||||
|
|
||||||
const firstCreated = await ctx.client.createTerminal(cwd1, "cwd-one-terminal");
|
const firstCreated = await createTerminalInWorkspace(ctx.client, {
|
||||||
await ctx.client.createTerminal(cwd2, "cwd-two-terminal");
|
cwd: cwd1,
|
||||||
|
name: "cwd-one-terminal",
|
||||||
|
});
|
||||||
|
await createTerminalInWorkspace(ctx.client, { cwd: cwd2, name: "cwd-two-terminal" });
|
||||||
|
|
||||||
const list = await ctx.client.listTerminals(cwd1);
|
const list = await ctx.client.listTerminals(cwd1);
|
||||||
|
|
||||||
|
|||||||
@@ -231,11 +231,13 @@ function transition(input: {
|
|||||||
state: "working" | "idle" | "attention" | null;
|
state: "working" | "idle" | "attention" | null;
|
||||||
changedAt: number;
|
changedAt: number;
|
||||||
id?: string;
|
id?: string;
|
||||||
|
workspaceId?: string;
|
||||||
}): TerminalActivityTransitionEvent {
|
}): TerminalActivityTransitionEvent {
|
||||||
return {
|
return {
|
||||||
terminalId: input.id ?? "term-1",
|
terminalId: input.id ?? "term-1",
|
||||||
name: "bash",
|
name: "bash",
|
||||||
cwd: CWD,
|
cwd: CWD,
|
||||||
|
workspaceId: input.workspaceId ?? "ws-1",
|
||||||
activity: input.state ? { state: input.state, changedAt: input.changedAt } : null,
|
activity: input.state ? { state: input.state, changedAt: input.changedAt } : null,
|
||||||
previous: { state: input.previousState, changedAt: input.previousChangedAt },
|
previous: { state: input.previousState, changedAt: input.previousChangedAt },
|
||||||
};
|
};
|
||||||
@@ -266,7 +268,7 @@ describe("VoiceAssistantWebSocketServer terminal attention notifications", () =>
|
|||||||
const payload = readTerminalAttentionMessage(ws);
|
const payload = readTerminalAttentionMessage(ws);
|
||||||
expect(payload.terminalId).toBe("term-1");
|
expect(payload.terminalId).toBe("term-1");
|
||||||
expect(payload.cwd).toBe(CWD);
|
expect(payload.cwd).toBe(CWD);
|
||||||
expect(payload.workspaceId).toBeUndefined();
|
expect(payload.workspaceId).toBe("ws-1");
|
||||||
expect(payload.reason).toBe("finished");
|
expect(payload.reason).toBe("finished");
|
||||||
expect(payload.title).toBe("Terminal finished");
|
expect(payload.title).toBe("Terminal finished");
|
||||||
expect(payload.body).toBe("bash");
|
expect(payload.body).toBe("bash");
|
||||||
@@ -274,9 +276,9 @@ describe("VoiceAssistantWebSocketServer terminal attention notifications", () =>
|
|||||||
expect(payload.shouldNotify).toBe(false);
|
expect(payload.shouldNotify).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resolves the workspaceId for the terminal cwd into the payload", async () => {
|
it("forwards the terminal event workspaceId into the payload", async () => {
|
||||||
const { manager, emit } = createTerminalManager();
|
const { manager, emit } = createTerminalManager();
|
||||||
const { server } = createServer(manager, createWorkspaceRegistry([workspaceRecord()]));
|
const { server } = createServer(manager);
|
||||||
const ws = connectClient(server);
|
const ws = connectClient(server);
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
@@ -285,17 +287,18 @@ describe("VoiceAssistantWebSocketServer terminal attention notifications", () =>
|
|||||||
previousChangedAt: 1000,
|
previousChangedAt: 1000,
|
||||||
state: "idle",
|
state: "idle",
|
||||||
changedAt: 11001,
|
changedAt: 11001,
|
||||||
|
workspaceId: "ws-event",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await flushAsync();
|
await flushAsync();
|
||||||
|
|
||||||
expect(readTerminalAttentionMessage(ws).workspaceId).toBe("ws-1");
|
expect(readTerminalAttentionMessage(ws).workspaceId).toBe("ws-event");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resolves the parent workspaceId for a subdirectory terminal cwd", async () => {
|
it("keeps the event workspaceId for a subdirectory terminal cwd", async () => {
|
||||||
const { manager, emit } = createTerminalManager();
|
const { manager, emit } = createTerminalManager();
|
||||||
const { server } = createServer(manager, createWorkspaceRegistry([workspaceRecord()]));
|
const { server } = createServer(manager);
|
||||||
const ws = connectClient(server);
|
const ws = connectClient(server);
|
||||||
|
|
||||||
emit({
|
emit({
|
||||||
@@ -313,7 +316,7 @@ describe("VoiceAssistantWebSocketServer terminal attention notifications", () =>
|
|||||||
expect(readTerminalAttentionMessage(ws).workspaceId).toBe("ws-1");
|
expect(readTerminalAttentionMessage(ws).workspaceId).toBe("ws-1");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("omits workspaceId for archived or unregistered workspaces", async () => {
|
it("does not derive terminal attention ownership from the workspace registry", async () => {
|
||||||
const { manager, emit } = createTerminalManager();
|
const { manager, emit } = createTerminalManager();
|
||||||
const { server } = createServer(
|
const { server } = createServer(
|
||||||
manager,
|
manager,
|
||||||
@@ -335,7 +338,7 @@ describe("VoiceAssistantWebSocketServer terminal attention notifications", () =>
|
|||||||
|
|
||||||
await flushAsync();
|
await flushAsync();
|
||||||
|
|
||||||
expect(readTerminalAttentionMessage(ws).workspaceId).toBeUndefined();
|
expect(readTerminalAttentionMessage(ws).workspaceId).toBe("ws-1");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not broadcast on working -> working (no transition to idle)", async () => {
|
it("does not broadcast on working -> working (no transition to idle)", async () => {
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ interface DaemonClientLike {
|
|||||||
createTerminal(
|
createTerminal(
|
||||||
cwd: string,
|
cwd: string,
|
||||||
name?: string,
|
name?: string,
|
||||||
|
requestId?: string,
|
||||||
|
options?: { workspaceId?: string },
|
||||||
): Promise<{ terminal: { id: string } | null; error: string | null }>;
|
): Promise<{ terminal: { id: string } | null; error: string | null }>;
|
||||||
subscribeTerminal(terminalId: string): Promise<{ error: string | null }>;
|
subscribeTerminal(terminalId: string): Promise<{ error: string | null }>;
|
||||||
killTerminal(terminalId: string): Promise<unknown>;
|
killTerminal(terminalId: string): Promise<unknown>;
|
||||||
@@ -62,6 +64,7 @@ interface DaemonClientLike {
|
|||||||
createAgent(options: {
|
createAgent(options: {
|
||||||
provider: string;
|
provider: string;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
|
workspaceId?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
}): Promise<{ id: string }>;
|
}): Promise<{ id: string }>;
|
||||||
@@ -483,6 +486,7 @@ if (process.env.BENCH_INCLUDE_L3 === "1") {
|
|||||||
async function startLoad(
|
async function startLoad(
|
||||||
client: DaemonClientLike,
|
client: DaemonClientLike,
|
||||||
cwd: string,
|
cwd: string,
|
||||||
|
workspaceId: string,
|
||||||
spec: LevelSpec,
|
spec: LevelSpec,
|
||||||
): Promise<LoadController> {
|
): Promise<LoadController> {
|
||||||
const agentIds: string[] = [];
|
const agentIds: string[] = [];
|
||||||
@@ -490,6 +494,7 @@ async function startLoad(
|
|||||||
const agent = await client.createAgent({
|
const agent = await client.createAgent({
|
||||||
provider: "mock",
|
provider: "mock",
|
||||||
cwd,
|
cwd,
|
||||||
|
workspaceId,
|
||||||
model: "five-minute-stream",
|
model: "five-minute-stream",
|
||||||
title: `${spec.id}-load-${i}`,
|
title: `${spec.id}-load-${i}`,
|
||||||
});
|
});
|
||||||
@@ -511,7 +516,9 @@ async function startLoad(
|
|||||||
|
|
||||||
let noisyTerminalId: string | null = null;
|
let noisyTerminalId: string | null = null;
|
||||||
if (spec.secondNoisyTerminal) {
|
if (spec.secondNoisyTerminal) {
|
||||||
const created = await client.createTerminal(cwd, `${spec.id}-noisy`);
|
const created = await client.createTerminal(cwd, `${spec.id}-noisy`, undefined, {
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
if (created.terminal) {
|
if (created.terminal) {
|
||||||
noisyTerminalId = created.terminal.id;
|
noisyTerminalId = created.terminal.id;
|
||||||
await client.subscribeTerminal(noisyTerminalId);
|
await client.subscribeTerminal(noisyTerminalId);
|
||||||
@@ -563,11 +570,12 @@ interface LevelResult {
|
|||||||
async function runLevel(
|
async function runLevel(
|
||||||
client: DaemonClientLike,
|
client: DaemonClientLike,
|
||||||
cwd: string,
|
cwd: string,
|
||||||
|
workspaceId: string,
|
||||||
spec: LevelSpec,
|
spec: LevelSpec,
|
||||||
measuredTerminalId: string,
|
measuredTerminalId: string,
|
||||||
): Promise<LevelResult> {
|
): Promise<LevelResult> {
|
||||||
console.log(`\n=== ${spec.id} (${spec.description}) ===`);
|
console.log(`\n=== ${spec.id} (${spec.description}) ===`);
|
||||||
const load = await startLoad(client, cwd, spec);
|
const load = await startLoad(client, cwd, workspaceId, spec);
|
||||||
try {
|
try {
|
||||||
// Ping sampling runs concurrently with the echo + burst measurements so it
|
// Ping sampling runs concurrently with the echo + burst measurements so it
|
||||||
// observes the daemon under the same load the latency primitives see.
|
// observes the daemon under the same load the latency primitives see.
|
||||||
@@ -679,7 +687,11 @@ async function main(): Promise<void> {
|
|||||||
throw new Error(`Failed to open project: ${opened.error}`);
|
throw new Error(`Failed to open project: ${opened.error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const created = await client.createTerminal(workspaceDir, "measured");
|
const workspaceId = opened.workspace.id;
|
||||||
|
|
||||||
|
const created = await client.createTerminal(workspaceDir, "measured", undefined, {
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
if (!created.terminal) {
|
if (!created.terminal) {
|
||||||
throw new Error(`Failed to create terminal: ${created.error}`);
|
throw new Error(`Failed to create terminal: ${created.error}`);
|
||||||
}
|
}
|
||||||
@@ -691,7 +703,7 @@ async function main(): Promise<void> {
|
|||||||
|
|
||||||
const results: LevelResult[] = [];
|
const results: LevelResult[] = [];
|
||||||
for (const spec of LEVELS) {
|
for (const spec of LEVELS) {
|
||||||
const result = await runLevel(client, workspaceDir, spec, measuredTerminalId);
|
const result = await runLevel(client, workspaceDir, workspaceId, spec, measuredTerminalId);
|
||||||
results.push(result);
|
results.push(result);
|
||||||
// Settle between levels so a previous level's load fully drains.
|
// Settle between levels so a previous level's load fully drains.
|
||||||
await sleep(2_000);
|
await sleep(2_000);
|
||||||
|
|||||||
Reference in New Issue
Block a user