refactor(server): extract the workspace-scripts feature into a deep module (#1716)

* refactor(server): extract the workspace-scripts feature into a deep module

The service-proxy-backed "workspace scripts" feature had no home: building a
workspace's scripts payload was duplicated between the descriptor builder and
buildWorkspaceScriptPayloadSnapshot (the same 9-input buildWorkspaceScriptPayloads
assembly), and the "scripts available on this daemon?" guard appeared a third time
in the start handler.

Move the feature into session/workspace-scripts/workspace-scripts-service.ts behind
createWorkspaceScriptsService(deps): { buildSnapshot, emitStatusUpdate, start }. The
payload assembly and availability guard now live in one place; the descriptor builder,
the status-emission path, and the start RPC all funnel through it. spawnWorkspaceScript
is injected as a port so the feature is testable without a real process. session.ts
drops ~98 lines; the two methods existing tests reach stay as thin delegators.

New zero-mock unit test covers the guard, status emission, and the start branch matrix
with injected fakes, the real service proxy + runtime store, and a fake launcher.

* refactor(server): address review — durable comment + documented test stand-in

- Drop the transient "#1714" PR reference from the buildWorkspaceScriptPayloadSnapshot
  accessor comment; state the durable reason instead.
- Hoist the opaque terminalManager test stand-in to a named const documenting the
  non-call guarantee, rather than an inline `as unknown as` cast.
This commit is contained in:
Mohamed Boudra
2026-06-25 12:20:55 +08:00
committed by GitHub
parent 0be5cc9dae
commit b561b108a7
4 changed files with 456 additions and 125 deletions

View File

@@ -52,14 +52,13 @@ import { respondToAgentPermission } from "./agent/permission-response.js";
import { experimental_createMCPClient } from "ai";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import type { VoiceCallerContext, VoiceSpeakHandler } from "./voice-types.js";
import {
buildWorkspaceScriptPayloads,
readPaseoConfigForProjection,
} from "./script-status-projection.js";
import { deriveProjectSlug } from "./workspace-git-metadata.js";
import type { ScriptHealthState } from "./script-health-monitor.js";
import { spawnWorkspaceScript } from "./worktree-bootstrap.js";
import type { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
import {
createWorkspaceScriptsService,
type WorkspaceScriptsService,
} from "./session/workspace-scripts/workspace-scripts-service.js";
import type { DaemonConfigStore } from "./daemon-config-store.js";
import { getErrorMessage, getErrorMessageOr } from "@getpaseo/protocol/error-utils";
import { getAgentStatusPriority } from "@getpaseo/protocol/agent-state-bucket";
@@ -589,6 +588,7 @@ export class Session {
private readonly agentConfigSession: AgentConfigSession;
private readonly projectConfigSession: ProjectConfigSession;
private readonly daemonSession: DaemonSession;
private readonly workspaceScripts: WorkspaceScriptsService;
private readonly createAgentLifecycleDispatch: CreateAgentLifecycleDispatch;
constructor(options: SessionOptions) {
@@ -848,6 +848,20 @@ export class Session {
this.getDaemonTcpHost = getDaemonTcpHost ?? null;
this.serviceProxyPublicBaseUrl = serviceProxyPublicBaseUrl ?? null;
this.resolveScriptHealth = resolveScriptHealth ?? null;
this.workspaceScripts = createWorkspaceScriptsService({
serviceProxy: this.serviceProxy,
scriptRuntimeStore: this.scriptRuntimeStore,
terminalManager: this.terminalManager,
workspaceRegistry: this.workspaceRegistry,
workspaceGitService: this.workspaceGitService,
getDaemonTcpPort: this.getDaemonTcpPort,
getDaemonTcpHost: this.getDaemonTcpHost,
serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl,
resolveScriptHealth: this.resolveScriptHealth,
logger: this.sessionLogger,
emit: (message) => this.emit(message),
spawnWorkspaceScript,
});
this.subscribeToOptionalManagers();
this.workspaceDirectory = new WorkspaceDirectory({
logger: this.sessionLogger,
@@ -3688,20 +3702,7 @@ export class Session {
statusEnteredAt: null,
activityAt: null,
diffStat,
scripts:
this.serviceProxy && this.scriptRuntimeStore
? buildWorkspaceScriptPayloads({
workspaceId: workspace.workspaceId,
workspaceDirectory: workspace.cwd,
paseoConfig: readPaseoConfigForProjection(workspace.cwd, this.sessionLogger),
serviceProxy: this.serviceProxy,
runtimeStore: this.scriptRuntimeStore,
daemonPort: this.getDaemonTcpPort?.() ?? null,
serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl,
gitMetadata: this.resolveWorkspaceScriptGitMetadata(workspace.cwd),
resolveHealth: this.resolveScriptHealth ?? undefined,
})
: [],
scripts: this.buildWorkspaceScriptPayloadSnapshot(workspace.workspaceId, workspace.cwd),
...(resolvedProjectRecord
? {
project: await this.buildProjectPlacementForWorkspace(workspace, resolvedProjectRecord),
@@ -4862,116 +4863,17 @@ export class Session {
}
}
// Named accessor: the workspace descriptor builder and the git-watch test both read a workspace's
// scripts snapshot through here; the workspace-scripts module owns the payload assembly.
private buildWorkspaceScriptPayloadSnapshot(
workspaceId: string,
workspaceDirectory: string,
): WorkspaceDescriptorPayload["scripts"] {
if (!this.serviceProxy || !this.scriptRuntimeStore) {
return [];
}
return buildWorkspaceScriptPayloads({
workspaceId,
workspaceDirectory,
paseoConfig: readPaseoConfigForProjection(workspaceDirectory, this.sessionLogger),
serviceProxy: this.serviceProxy,
runtimeStore: this.scriptRuntimeStore,
daemonPort: this.getDaemonTcpPort?.() ?? null,
serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl,
gitMetadata: this.resolveWorkspaceScriptGitMetadata(workspaceDirectory),
resolveHealth: this.resolveScriptHealth ?? undefined,
});
return this.workspaceScripts.buildSnapshot(workspaceId, workspaceDirectory);
}
private resolveWorkspaceScriptGitMetadata(
workspaceDirectory: string,
): { projectSlug: string; currentBranch: string | null } | undefined {
const snapshot = this.workspaceGitService.peekSnapshot(workspaceDirectory);
if (!snapshot) {
return undefined;
}
return {
projectSlug: deriveProjectSlug(
workspaceDirectory,
snapshot.git.isGit ? snapshot.git.remoteUrl : null,
),
currentBranch: snapshot.git.currentBranch,
};
}
private emitWorkspaceScriptStatusUpdate(workspaceId: string, workspaceDirectory: string): void {
this.emit({
type: "script_status_update",
payload: {
workspaceId,
scripts: this.buildWorkspaceScriptPayloadSnapshot(workspaceId, workspaceDirectory),
},
});
}
private async handleStartWorkspaceScriptRequest(
request: StartWorkspaceScriptRequest,
): Promise<void> {
try {
if (!this.terminalManager || !this.serviceProxy || !this.scriptRuntimeStore) {
throw new Error("Workspace scripts are not available on this daemon");
}
const workspace = await this.workspaceRegistry.get(request.workspaceId);
if (!workspace) {
throw new Error(`Workspace not found: ${request.workspaceId}`);
}
const gitMetadata = await this.workspaceGitService.getWorkspaceGitMetadata(workspace.cwd);
const serviceResult = await spawnWorkspaceScript({
repoRoot: workspace.cwd,
workspaceId: workspace.workspaceId,
projectSlug: gitMetadata.projectSlug,
branchName: gitMetadata.currentBranch,
scriptName: request.scriptName,
daemonPort: this.getDaemonTcpPort?.() ?? null,
daemonListenHost: this.getDaemonTcpHost?.() ?? null,
serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl,
serviceProxy: this.serviceProxy,
runtimeStore: this.scriptRuntimeStore,
terminalManager: this.terminalManager,
logger: this.sessionLogger,
onLifecycleChanged: () => {
this.emitWorkspaceScriptStatusUpdate(workspace.workspaceId, workspace.cwd);
},
});
this.emitWorkspaceScriptStatusUpdate(workspace.workspaceId, workspace.cwd);
this.emit({
type: "start_workspace_script_response",
payload: {
requestId: request.requestId,
workspaceId: request.workspaceId,
scriptName: request.scriptName,
terminalId: serviceResult.terminalId,
error: null,
},
});
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to start workspace script";
this.sessionLogger.error(
{
err: error,
workspaceId: request.workspaceId,
scriptName: request.scriptName,
},
"Failed to start workspace script",
);
this.emit({
type: "start_workspace_script_response",
payload: {
requestId: request.requestId,
workspaceId: request.workspaceId,
scriptName: request.scriptName,
terminalId: null,
error: message,
},
});
}
private handleStartWorkspaceScriptRequest(request: StartWorkspaceScriptRequest): Promise<void> {
return this.workspaceScripts.start(request);
}
// COMPAT(desktopEditorBridge): added in v0.1.88, remove after 2026-12-03 once old clients no longer call daemon editor RPCs.
@@ -5047,7 +4949,7 @@ export class Session {
getDaemonTcpHost: this.getDaemonTcpHost,
serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl,
onScriptsChanged: (workspaceId, workspaceDirectory) => {
this.emitWorkspaceScriptStatusUpdate(workspaceId, workspaceDirectory);
this.workspaceScripts.emitStatusUpdate(workspaceId, workspaceDirectory);
},
},
input,

View File

@@ -0,0 +1,244 @@
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pino } from "pino";
import { afterEach, describe, expect, test } from "vitest";
import type { SessionOutboundMessage, StartWorkspaceScriptRequest } from "../../messages.js";
import { createServiceProxySubsystem, type ServiceProxySubsystem } from "../../service-proxy.js";
import type { TerminalManager } from "../../../terminal/terminal-manager.js";
import type { PersistedWorkspaceRecord, WorkspaceRegistry } from "../../workspace-registry.js";
import type { WorkspaceGitMetadata } from "../../workspace-git-metadata.js";
import { WorkspaceScriptRuntimeStore } from "../../workspace-script-runtime-store.js";
import type {
SpawnWorkspaceScriptOptions,
WorktreeScriptResult,
} from "../../worktree-bootstrap.js";
import { createWorkspaceScriptsService } from "./workspace-scripts-service.js";
// The production module reads only WorkspaceGitService.{peekSnapshot,getWorkspaceGitMetadata},
// WorkspaceRegistry.get, and forwards the launcher + opaque managers to the injected
// spawnWorkspaceScript port. The fakes below implement exactly that slice; the service proxy and
// runtime store are the real in-memory implementations, and spawning is injected so no process runs.
const logger = pino({ level: "silent" });
const gitMetadata: WorkspaceGitMetadata = {
projectKind: "git",
projectDisplayName: "repo",
workspaceDisplayName: "repo",
gitRemote: null,
isWorktree: false,
projectSlug: "paseo",
repoRoot: "/tmp/repo",
currentBranch: "feature/scripts",
remoteUrl: null,
};
function fakeWorkspaceRegistry(
record: PersistedWorkspaceRecord | null,
): Pick<WorkspaceRegistry, "get"> {
return {
async get() {
return record;
},
};
}
function fakeGitService(metadata: WorkspaceGitMetadata = gitMetadata) {
return {
peekSnapshot() {
return null;
},
async getWorkspaceGitMetadata() {
return metadata;
},
};
}
// The service only truthiness-checks terminalManager in its availability guard and then forwards it
// opaquely to the injected spawnWorkspaceScript fake, which ignores it — an empty stand-in is enough.
const availableTerminalManager = {} as unknown as TerminalManager;
interface BuildOptions {
serviceProxy?: ServiceProxySubsystem | null;
scriptRuntimeStore?: WorkspaceScriptRuntimeStore | null;
terminalManager?: TerminalManager | null;
workspace?: PersistedWorkspaceRecord | null;
spawnThrows?: string;
}
function buildService(options: BuildOptions = {}) {
const emitted: SessionOutboundMessage[] = [];
const spawnCalls: SpawnWorkspaceScriptOptions[] = [];
const workspace =
options.workspace === undefined
? ({ workspaceId: "ws-1", cwd: "/tmp/repo" } as PersistedWorkspaceRecord)
: options.workspace;
const service = createWorkspaceScriptsService({
serviceProxy:
options.serviceProxy === undefined
? createServiceProxySubsystem({ logger })
: options.serviceProxy,
scriptRuntimeStore:
options.scriptRuntimeStore === undefined
? new WorkspaceScriptRuntimeStore()
: options.scriptRuntimeStore,
terminalManager:
options.terminalManager === undefined ? availableTerminalManager : options.terminalManager,
workspaceRegistry: fakeWorkspaceRegistry(workspace),
workspaceGitService: fakeGitService(),
getDaemonTcpPort: () => 6767,
getDaemonTcpHost: () => "127.0.0.1",
serviceProxyPublicBaseUrl: null,
resolveScriptHealth: null,
logger,
emit: (message) => emitted.push(message),
async spawnWorkspaceScript(spawnOptions): Promise<WorktreeScriptResult> {
spawnCalls.push(spawnOptions);
if (options.spawnThrows) {
throw new Error(options.spawnThrows);
}
spawnOptions.onLifecycleChanged?.();
return {
scriptName: spawnOptions.scriptName,
hostname: null,
port: null,
terminalId: "terminal-1",
};
},
});
return { service, emitted, spawnCalls };
}
const request: StartWorkspaceScriptRequest = {
type: "start_workspace_script_request",
workspaceId: "ws-1",
scriptName: "app",
requestId: "req-1",
};
const tempDirs: string[] = [];
afterEach(() => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) {
rmSync(dir, { recursive: true, force: true });
}
}
});
describe("buildSnapshot", () => {
test("returns no scripts when the service proxy is unavailable", () => {
const { service } = buildService({ serviceProxy: null });
expect(service.buildSnapshot("ws-1", "/tmp/repo")).toEqual([]);
});
test("returns no scripts when the runtime store is unavailable", () => {
const { service } = buildService({ scriptRuntimeStore: null });
expect(service.buildSnapshot("ws-1", "/tmp/repo")).toEqual([]);
});
test("returns no scripts for a workspace without a paseo.json", () => {
const dir = mkdtempSync(join(tmpdir(), "workspace-scripts-"));
tempDirs.push(dir);
const { service } = buildService();
expect(service.buildSnapshot("ws-1", dir)).toEqual([]);
});
});
describe("emitStatusUpdate", () => {
test("emits one script_status_update carrying the snapshot", () => {
const { service, emitted } = buildService();
service.emitStatusUpdate("ws-1", "/tmp/repo");
expect(emitted).toEqual([
{ type: "script_status_update", payload: { workspaceId: "ws-1", scripts: [] } },
]);
});
});
describe("start", () => {
test("reports an error when workspace scripts are unavailable", async () => {
const { service, emitted, spawnCalls } = buildService({ terminalManager: null });
await service.start(request);
expect(spawnCalls).toEqual([]);
expect(emitted).toEqual([
{
type: "start_workspace_script_response",
payload: {
requestId: "req-1",
workspaceId: "ws-1",
scriptName: "app",
terminalId: null,
error: "Workspace scripts are not available on this daemon",
},
},
]);
});
test("reports an error when the workspace is not found", async () => {
const { service, emitted, spawnCalls } = buildService({ workspace: null });
await service.start(request);
expect(spawnCalls).toEqual([]);
expect(emitted).toEqual([
{
type: "start_workspace_script_response",
payload: {
requestId: "req-1",
workspaceId: "ws-1",
scriptName: "app",
terminalId: null,
error: "Workspace not found: ws-1",
},
},
]);
});
test("spawns the script with resolved git metadata and reports success", async () => {
const { service, emitted, spawnCalls } = buildService();
await service.start(request);
expect(spawnCalls).toHaveLength(1);
expect(spawnCalls[0]).toMatchObject({
repoRoot: "/tmp/repo",
workspaceId: "ws-1",
projectSlug: "paseo",
branchName: "feature/scripts",
scriptName: "app",
daemonPort: 6767,
daemonListenHost: "127.0.0.1",
});
expect(emitted).toContainEqual({
type: "script_status_update",
payload: { workspaceId: "ws-1", scripts: [] },
});
expect(emitted).toContainEqual({
type: "start_workspace_script_response",
payload: {
requestId: "req-1",
workspaceId: "ws-1",
scriptName: "app",
terminalId: "terminal-1",
error: null,
},
});
});
test("reports the launcher error when spawning fails", async () => {
const { service, emitted } = buildService({ spawnThrows: "boom" });
await service.start(request);
expect(emitted).toEqual([
{
type: "start_workspace_script_response",
payload: {
requestId: "req-1",
workspaceId: "ws-1",
scriptName: "app",
terminalId: null,
error: "boom",
},
},
]);
});
});

View File

@@ -0,0 +1,185 @@
import type pino from "pino";
import type {
SessionOutboundMessage,
StartWorkspaceScriptRequest,
WorkspaceDescriptorPayload,
} from "../../messages.js";
import type { TerminalManager } from "../../../terminal/terminal-manager.js";
import type { ServiceProxySubsystem } from "../../service-proxy.js";
import type { WorkspaceScriptRuntimeStore } from "../../workspace-script-runtime-store.js";
import type { ScriptHealthState } from "../../script-health-monitor.js";
import type { WorkspaceGitService } from "../../workspace-git-service.js";
import type { WorkspaceRegistry } from "../../workspace-registry.js";
import type {
SpawnWorkspaceScriptOptions,
WorktreeScriptResult,
} from "../../worktree-bootstrap.js";
import {
buildWorkspaceScriptPayloads,
readPaseoConfigForProjection,
} from "../../script-status-projection.js";
import { deriveProjectSlug } from "../../workspace-git-metadata.js";
type WorkspaceScriptsPayload = WorkspaceDescriptorPayload["scripts"];
interface WorkspaceScriptGitMetadata {
projectSlug: string;
currentBranch: string | null;
}
/**
* The service-proxy-backed scripts a workspace exposes: build the scripts payload
* snapshot, emit a script_status_update to clients, and start a script.
*
* The workspace descriptor builder, the script-status emission path, and the
* start-script RPC all funnel through one assembly of buildWorkspaceScriptPayloads'
* inputs and one "scripts available on this daemon?" guard, instead of duplicating
* that assembly and guard across the session.
*/
export interface WorkspaceScriptsService {
buildSnapshot(workspaceId: string, workspaceDirectory: string): WorkspaceScriptsPayload;
emitStatusUpdate(workspaceId: string, workspaceDirectory: string): void;
start(request: StartWorkspaceScriptRequest): Promise<void>;
}
type WorkspaceScriptsGitSource = Pick<
WorkspaceGitService,
"peekSnapshot" | "getWorkspaceGitMetadata"
>;
export function createWorkspaceScriptsService(deps: {
serviceProxy: ServiceProxySubsystem | null;
scriptRuntimeStore: WorkspaceScriptRuntimeStore | null;
terminalManager: TerminalManager | null;
workspaceRegistry: Pick<WorkspaceRegistry, "get">;
workspaceGitService: WorkspaceScriptsGitSource;
getDaemonTcpPort: (() => number | null) | null;
getDaemonTcpHost: (() => string | null) | null;
serviceProxyPublicBaseUrl: string | null;
resolveScriptHealth: ((hostname: string) => ScriptHealthState | null) | null;
logger: pino.Logger;
emit: (message: SessionOutboundMessage) => void;
spawnWorkspaceScript: (options: SpawnWorkspaceScriptOptions) => Promise<WorktreeScriptResult>;
}): WorkspaceScriptsService {
const {
serviceProxy,
scriptRuntimeStore,
terminalManager,
workspaceRegistry,
workspaceGitService,
getDaemonTcpPort,
getDaemonTcpHost,
serviceProxyPublicBaseUrl,
resolveScriptHealth,
logger,
emit,
spawnWorkspaceScript,
} = deps;
function resolveGitMetadata(workspaceDirectory: string): WorkspaceScriptGitMetadata | undefined {
const snapshot = workspaceGitService.peekSnapshot(workspaceDirectory);
if (!snapshot) {
return undefined;
}
return {
projectSlug: deriveProjectSlug(
workspaceDirectory,
snapshot.git.isGit ? snapshot.git.remoteUrl : null,
),
currentBranch: snapshot.git.currentBranch,
};
}
function buildSnapshot(workspaceId: string, workspaceDirectory: string): WorkspaceScriptsPayload {
if (!serviceProxy || !scriptRuntimeStore) {
return [];
}
return buildWorkspaceScriptPayloads({
workspaceId,
workspaceDirectory,
paseoConfig: readPaseoConfigForProjection(workspaceDirectory, logger),
serviceProxy,
runtimeStore: scriptRuntimeStore,
daemonPort: getDaemonTcpPort?.() ?? null,
serviceProxyPublicBaseUrl,
gitMetadata: resolveGitMetadata(workspaceDirectory),
resolveHealth: resolveScriptHealth ?? undefined,
});
}
function emitStatusUpdate(workspaceId: string, workspaceDirectory: string): void {
emit({
type: "script_status_update",
payload: {
workspaceId,
scripts: buildSnapshot(workspaceId, workspaceDirectory),
},
});
}
async function start(request: StartWorkspaceScriptRequest): Promise<void> {
try {
if (!terminalManager || !serviceProxy || !scriptRuntimeStore) {
throw new Error("Workspace scripts are not available on this daemon");
}
const workspace = await workspaceRegistry.get(request.workspaceId);
if (!workspace) {
throw new Error(`Workspace not found: ${request.workspaceId}`);
}
const gitMetadata = await workspaceGitService.getWorkspaceGitMetadata(workspace.cwd);
const serviceResult = await spawnWorkspaceScript({
repoRoot: workspace.cwd,
workspaceId: workspace.workspaceId,
projectSlug: gitMetadata.projectSlug,
branchName: gitMetadata.currentBranch,
scriptName: request.scriptName,
daemonPort: getDaemonTcpPort?.() ?? null,
daemonListenHost: getDaemonTcpHost?.() ?? null,
serviceProxyPublicBaseUrl,
serviceProxy,
runtimeStore: scriptRuntimeStore,
terminalManager,
logger,
onLifecycleChanged: () => {
emitStatusUpdate(workspace.workspaceId, workspace.cwd);
},
});
emitStatusUpdate(workspace.workspaceId, workspace.cwd);
emit({
type: "start_workspace_script_response",
payload: {
requestId: request.requestId,
workspaceId: request.workspaceId,
scriptName: request.scriptName,
terminalId: serviceResult.terminalId,
error: null,
},
});
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to start workspace script";
logger.error(
{
err: error,
workspaceId: request.workspaceId,
scriptName: request.scriptName,
},
"Failed to start workspace script",
);
emit({
type: "start_workspace_script_response",
payload: {
requestId: request.requestId,
workspaceId: request.workspaceId,
scriptName: request.scriptName,
terminalId: null,
error: message,
},
});
}
}
return { buildSnapshot, emitStatusUpdate, start };
}

View File

@@ -694,7 +694,7 @@ export interface WorktreeScriptResult {
terminalId: string;
}
interface SpawnWorkspaceScriptOptions {
export interface SpawnWorkspaceScriptOptions {
repoRoot: string;
workspaceId: string;
projectSlug: string;