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
10 changes: 5 additions & 5 deletions src/api/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ interface CliContext {
auth: CliAuth;
binPath: string;
workspace: Workspace;
writeEmitter: vscode.EventEmitter<string>;
write: (data: string) => void;
featureSet: FeatureSet;
}

Expand All @@ -70,13 +70,13 @@ function runCliCommand(ctx: CliContext, args: string[]): Promise<void> {
const proc = spawn(cmd, { shell: true });

proc.stdout.on("data", (data: Buffer) => {
ctx.writeEmitter.fire(data.toString().replace(/\r?\n/g, "\r\n"));
ctx.write(data.toString().replace(/\r?\n/g, "\r\n"));
});

let capturedStderr = "";
proc.stderr.on("data", (data: Buffer) => {
const text = data.toString();
ctx.writeEmitter.fire(text.replace(/\r?\n/g, "\r\n"));
ctx.write(text.replace(/\r?\n/g, "\r\n"));
capturedStderr += text;
});

Expand Down Expand Up @@ -126,15 +126,15 @@ export async function updateWorkspace(ctx: CliContext): Promise<Workspace> {

// REST API fallback for older CLIs.
if (ctx.workspace.latest_build.status === "running") {
ctx.writeEmitter.fire("Stopping workspace for update...\r\n");
ctx.write("Stopping workspace for update...\r\n");
const stopBuild = await ctx.restClient.stopWorkspace(ctx.workspace.id);
const stoppedJob = await ctx.restClient.waitForBuild(stopBuild);
if (stoppedJob?.status === "canceled") {
throw new Error("Workspace update canceled during stop");
}
}

ctx.writeEmitter.fire("Starting workspace with updated template...\r\n");
ctx.write("Starting workspace with updated template...\r\n");
await ctx.restClient.updateWorkspaceVersion(ctx.workspace);
return ctx.restClient.getWorkspace(ctx.workspace.id);
}
Expand Down
20 changes: 20 additions & 0 deletions src/remote/terminalOutputChannel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import stripAnsi from "strip-ansi";
import * as vscode from "vscode";

/** Adapts terminal-style output for a VS Code OutputChannel. Strips ANSI escape sequences and carriage returns. */
export class TerminalOutputChannel implements vscode.Disposable {
private readonly channel: vscode.OutputChannel;

constructor(name: string) {
this.channel = vscode.window.createOutputChannel(name);
this.channel.show(true);
}

write(data: string): void {
this.channel.append(stripAnsi(data).replace(/\r/g, ""));
}

dispose(): void {
this.channel.dispose();
}
}
39 changes: 0 additions & 39 deletions src/remote/terminalSession.ts

This file was deleted.

20 changes: 10 additions & 10 deletions src/remote/workspaceStateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import { maybeAskAgent } from "../promptUtils";
import { vscodeProposed } from "../vscodeProposed";

import { TerminalSession } from "./terminalSession";
import { TerminalOutputChannel } from "./terminalOutputChannel";

import type {
ProvisionerJobLog,
Expand All @@ -30,7 +30,7 @@ import type { AuthorityParts } from "../util";
* Streams build and agent logs, and handles socket lifecycle.
*/
export class WorkspaceStateMachine implements vscode.Disposable {
private readonly terminal: TerminalSession;
private readonly terminal: TerminalOutputChannel;
private readonly buildLogStream = new LazyStream<ProvisionerJobLog>();
private readonly agentLogStream = new LazyStream<WorkspaceAgentLog[]>();

Expand All @@ -45,7 +45,7 @@ export class WorkspaceStateMachine implements vscode.Disposable {
private readonly logger: Logger,
private readonly cliAuth: CliAuth,
) {
this.terminal = new TerminalSession("Workspace Build");
this.terminal = new TerminalOutputChannel("Coder: Workspace Build");
}

/**
Expand Down Expand Up @@ -102,12 +102,10 @@ export class WorkspaceStateMachine implements vscode.Disposable {
});
this.logger.info(`Waiting for ${workspaceName}`);

const write = (line: string) =>
this.terminal.writeEmitter.fire(line + "\r\n");
await this.buildLogStream.open(() =>
streamBuildLogs(
this.workspaceClient,
write,
(line) => this.terminal.write(line + "\r\n"),
workspace.latest_build.id,
),
);
Expand Down Expand Up @@ -183,10 +181,12 @@ export class WorkspaceStateMachine implements vscode.Disposable {
});
this.logger.debug(`Running agent ${agent.name} startup scripts`);

const writeAgent = (line: string) =>
this.terminal.writeEmitter.fire(line + "\r\n");
await this.agentLogStream.open(() =>
streamAgentLogs(this.workspaceClient, writeAgent, agent.id),
streamAgentLogs(
this.workspaceClient,
(line) => this.terminal.write(line + "\r\n"),
agent.id,
),
);
return false;
}
Expand Down Expand Up @@ -229,7 +229,7 @@ export class WorkspaceStateMachine implements vscode.Disposable {
auth: this.cliAuth,
binPath: this.binaryPath,
workspace,
writeEmitter: this.terminal.writeEmitter,
write: (data: string) => this.terminal.write(data),
featureSet: this.featureSet,
};
}
Expand Down
63 changes: 47 additions & 16 deletions test/mocks/testHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -946,43 +946,74 @@ export class MockContextManager {
readonly dispose = vi.fn();
}

/** Mock VS Code OutputChannel that captures all appended content. */
export class MockOutputChannel implements vscode.LogOutputChannel {
readonly name: string;
readonly logLevel = vscode.LogLevel.Info;
readonly onDidChangeLogLevel: vscode.Event<vscode.LogLevel> = vi.fn();

private _content: string[] = [];

constructor(name = "mock") {
this.name = name;
}

get content(): string[] {
return this._content;
}

append = vi.fn((value: string) => this._content.push(value));
appendLine = vi.fn((value: string) => this._content.push(value + "\n"));
replace = vi.fn((value: string) => {
this._content = [value];
});
clear = vi.fn(() => {
this._content = [];
});
dispose = vi.fn(() => {
this._content = [];
});
show = vi.fn();
hide = vi.fn();
trace = vi.fn();
debug = vi.fn();
info = vi.fn();
warn = vi.fn();
error = vi.fn();
}

/**
* Mock TerminalSession that captures all content written to the terminal.
* Mock TerminalOutputChannel that captures all written content.
* Use `lastInstance` to get the most recently created instance (set in the constructor),
* which is useful when the real TerminalSession is created inside the class under test.
* which is useful when the real class is created inside the class under test.
*/
export class MockTerminalSession {
static lastInstance: MockTerminalSession | undefined;
export class MockTerminalOutputChannel {
static lastInstance: MockTerminalOutputChannel | undefined;

private readonly _lines: string[] = [];

readonly writeEmitter = {
fire: vi.fn((data: string) => {
this._lines.push(data);
}),
event: vi.fn(),
dispose: vi.fn(),
};
readonly terminal = { show: vi.fn(), dispose: vi.fn() };
readonly write = vi.fn((data: string) => {
this._lines.push(data);
});
readonly dispose = vi.fn();

constructor(_name?: string) {
MockTerminalSession.lastInstance = this;
MockTerminalOutputChannel.lastInstance = this;
}

/** All lines written via writeEmitter.fire(). */
/** All lines written via write(). */
get lines(): readonly string[] {
return this._lines;
}

/** Concatenated terminal content. */
/** Concatenated content. */
get content(): string {
return this._lines.join("");
}

/** Reset captured content and mock call history. */
clear(): void {
this._lines.length = 0;
this.writeEmitter.fire.mockClear();
this.write.mockClear();
}
}
8 changes: 8 additions & 0 deletions test/mocks/vscode.runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ export const ColorThemeKind = E({
HighContrastLight: 4,
});
export const ExtensionMode = E({ Production: 1, Development: 2, Test: 3 });
export const LogLevel = E({
Off: 0,
Trace: 1,
Debug: 2,
Info: 3,
Warning: 4,
Error: 5,
});
export const UIKind = E({ Desktop: 1, Web: 2 });
export const InputBoxValidationSeverity = E({
Info: 1,
Expand Down
30 changes: 30 additions & 0 deletions test/unit/remote/terminalOutputChannel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, expect, it, vi } from "vitest";
import * as vscode from "vscode";

import { TerminalOutputChannel } from "@/remote/terminalOutputChannel";

import { MockOutputChannel } from "../../mocks/testHelpers";

vi.mocked(vscode.window.createOutputChannel).mockImplementation(
(name: string) => new MockOutputChannel(name),
);

function setup(input: string): MockOutputChannel {
const channel = new TerminalOutputChannel("test");
channel.write(input);
return vi.mocked(vscode.window.createOutputChannel).mock.results.at(-1)!
.value as MockOutputChannel;
}

describe("TerminalOutputChannel", () => {
it.each([
["converts \\r\\n to \\n", "hello\r\nworld\r\n", "hello\nworld\n"],
["strips bare \\r", "progress\r50%\r100%\n", "progress50%100%\n"],
["strips ANSI escape sequences", "\x1b[0;1mBold\x1b[0m text", "Bold text"],
["strips ANSI color codes", "\x1b[32m✔ Success\x1b[0m\r\n", "✔ Success\n"],
["passes plain text unchanged", "hello world", "hello world"],
["handles empty string", "", ""],
])("%s", (_label, input, expected) => {
expect(setup(input).content.join("")).toBe(expected);
});
});
8 changes: 4 additions & 4 deletions test/unit/remote/workspaceStateMachine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { WorkspaceStateMachine } from "@/remote/workspaceStateMachine";
import {
createMockLogger,
MockProgress,
MockTerminalSession,
MockTerminalOutputChannel,
MockUserInteraction,
} from "../../mocks/testHelpers";
import {
Expand Down Expand Up @@ -46,9 +46,9 @@ vi.mock("@/promptUtils", () => ({
maybeAskAgent: vi.fn(),
}));

vi.mock("@/remote/terminalSession", async () => {
vi.mock("@/remote/terminalOutputChannel", async () => {
const helpers = await import("../../mocks/testHelpers");
return { TerminalSession: helpers.MockTerminalSession };
return { TerminalOutputChannel: helpers.MockTerminalOutputChannel };
});

const DEFAULT_PARTS: Readonly<AuthorityParts> = {
Expand Down Expand Up @@ -92,7 +92,7 @@ function setup(startupMode: StartupMode = "start") {
describe("WorkspaceStateMachine", () => {
beforeEach(() => {
vi.clearAllMocks();
MockTerminalSession.lastInstance = undefined;
MockTerminalOutputChannel.lastInstance = undefined;
vi.mocked(maybeAskAgent).mockImplementation((agents) =>
Promise.resolve(agents.length > 0 ? agents[0] : undefined),
);
Expand Down