import * as NodeServices from "@effect/platform-node/NodeServices";import { assert, describe, it } from "@effect/vitest";import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId, type OrchestrationV2ThreadProjection, type Project, type ProjectScript,} from "@t3tools/contracts";import * as Effect from "effect/Effect";import * as Deferred from "effect/Deferred";import * as FileSystem from "effect/FileSystem";import * as Fiber from "effect/Fiber";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Path from "effect/Path";import { expect } from "vite-plus/test"; import * as ProcessRunner from "../processRunner.ts";import { makeKeyedSerialExecutor } from "../orchestration-v2/KeyedSerialExecutor.ts";import { ThreadCommandExecutor } from "../orchestration-v2/ThreadCommandExecutor.ts";import { ThreadManagementService, ThreadManagementThreadNotFoundError,} from "../orchestration-v2/ThreadManagementService.ts";import * as ProjectService from "../project/ProjectService.ts";import * as TerminalManager from "../terminal/Manager.ts";import * as PtyAdapter from "../terminal/PtyAdapter.ts";import type { McpInvocationScope } from "./McpInvocationContext.ts";import * as ProjectScriptMcpService from "./ProjectScriptMcpService.ts";import * as TerminalMcpService from "./TerminalMcpService.ts"; class FakePtyProcess implements PtyAdapter.PtyProcess { readonly writes: string[] = []; readonly pid: number; readonly failWrite: boolean; killCalls = 0; private readonly dataListeners = new Set<(data: string) => void>(); private readonly exitListeners = new Set<(event: PtyAdapter.PtyExitEvent) => void>(); constructor(pid: number, failWrite: boolean) { this.pid = pid; this.failWrite = failWrite; } write(data: string): void { if (this.failWrite) throw new Error("script PTY write failed"); this.writes.push(data); } resize(): void {} kill(): void { this.killCalls += 1; this.emitExit({ exitCode: 0, signal: null }); } onData(listener: (data: string) => void): () => void { this.dataListeners.add(listener); return () => this.dataListeners.delete(listener); } onExit(listener: (event: PtyAdapter.PtyExitEvent) => void): () => void { this.exitListeners.add(listener); return () => this.exitListeners.delete(listener); } emitExit(event: PtyAdapter.PtyExitEvent): void { for (const listener of this.exitListeners) listener(event); }} class FakePtyAdapter { readonly spawnInputs: PtyAdapter.PtySpawnInput[] = []; readonly processes: FakePtyProcess[] = []; failNextWrite = false; spawn(input: PtyAdapter.PtySpawnInput): Effect.Effect<PtyAdapter.PtyProcess> { this.spawnInputs.push(input); const process = new FakePtyProcess(14_000 + this.processes.length, this.failNextWrite); this.failNextWrite = false; this.processes.push(process); return Effect.succeed(process); }} function projection(input: { readonly threadId: ThreadId; readonly projectId: ProjectId; readonly runtimeMode?: "approval-required" | "full-access"; readonly interactionMode?: "plan" | "default"; readonly worktreePath?: string | null; readonly executionCwd?: string;}): OrchestrationV2ThreadProjection { const providerSessionId = `provider-session:${input.threadId}`; const providerThreadId = `provider-thread:${input.threadId}`; return { thread: { id: input.threadId, projectId: input.projectId, runtimeMode: input.runtimeMode ?? "full-access", interactionMode: input.interactionMode ?? "default", worktreePath: input.worktreePath ?? null, activeProviderThreadId: input.executionCwd === undefined ? null : providerThreadId, deletedAt: null, }, providerThreads: input.executionCwd === undefined ? [] : [{ id: providerThreadId, providerSessionId }], providerSessions: input.executionCwd === undefined ? [] : [{ id: providerSessionId, cwd: input.executionCwd, status: "ready" }], } as unknown as OrchestrationV2ThreadProjection;} function project(input: { readonly projectId: ProjectId; readonly workspaceRoot: string; readonly scripts: ReadonlyArray<ProjectScript>;}): Project { return { id: input.projectId, title: "Script project", workspaceRoot: input.workspaceRoot, repositoryIdentity: null, faviconPath: null, defaultModelSelection: null, scripts: [...input.scripts], createdAt: "2026-08-29T00:00:00.000Z", updatedAt: "2026-08-29T00:00:00.000Z", deletedAt: null, };} describe("ProjectScriptMcpService", () => { it.effect("runs only saved scripts in dedicated managed terminals", () => Effect.scoped( Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-project-script-mcp-" }); const workspaceRoot = path.join(baseDir, "workspace"); const worktreePath = path.join(baseDir, "worktree"); const nestedExecutionCwd = path.join(worktreePath, "packages", "app"); yield* fs.makeDirectory(workspaceRoot, { recursive: true }); yield* fs.makeDirectory(nestedExecutionCwd, { recursive: true }); const projectId = ProjectId.make("project:script-mcp"); const missingProjectId = ProjectId.make("project:script-mcp-missing"); const callerThreadId = ThreadId.make("thread:script-mcp-caller"); const targetThreadId = ThreadId.make("thread:script-mcp-target"); const restrictedThreadId = ThreadId.make("thread:script-mcp-restricted"); const missingProjectThreadId = ThreadId.make("thread:script-mcp-missing-project"); const projections = new Map([ [callerThreadId, projection({ threadId: callerThreadId, projectId })], [ targetThreadId, projection({ threadId: targetThreadId, projectId, worktreePath, executionCwd: nestedExecutionCwd, }), ], [ restrictedThreadId, projection({ threadId: restrictedThreadId, projectId, runtimeMode: "approval-required", interactionMode: "plan", }), ], [ missingProjectThreadId, projection({ threadId: missingProjectThreadId, projectId: missingProjectId }), ], ]); let savedScripts: ReadonlyArray<ProjectScript> = [ { id: "dev", name: "Development server", command: "pnpm run dev", icon: "play", runOnWorktreeCreate: false, previewUrl: "http://localhost:3000", autoOpenPreview: true, }, { id: "test", name: "Tests", command: "pnpm test", icon: "test", runOnWorktreeCreate: false, }, ]; const ptyAdapter = new FakePtyAdapter(); const manager = yield* TerminalManager.makeWithOptions({ logsDir: path.join(baseDir, "terminal-history"), ptyAdapter, processKillGraceMs: 1, }); const replacementAdmissionEntered = yield* Deferred.make<void>(); const replacementAdmissionRelease = yield* Deferred.make<void>(); const exitAdmissionEntered = yield* Deferred.make<void>(); const exitAdmissionRelease = yield* Deferred.make<void>(); let replacementAdmissionAttempts = 0; const gatedManager = TerminalManager.TerminalManager.of({ ...manager, admitRunningSessionHandle: (input, onAdmitted) => { if ( input.terminalId === "term-script-registration-replacement" && replacementAdmissionAttempts++ === 0 ) { return Deferred.succeed(replacementAdmissionEntered, undefined).pipe( Effect.andThen(Deferred.await(replacementAdmissionRelease)), Effect.andThen(manager.admitRunningSessionHandle(input, onAdmitted)), ); } if (input.terminalId === "term-script-registration-exit") { return Deferred.succeed(exitAdmissionEntered, undefined).pipe( Effect.andThen(Deferred.await(exitAdmissionRelease)), Effect.andThen(manager.admitRunningSessionHandle(input, onAdmitted)), ); } return manager.admitRunningSessionHandle(input, onAdmitted); }, }); const threadDispatch = yield* makeKeyedSerialExecutor<ThreadId>(); const threads = ThreadManagementService.of({ getThreadProjection: (threadId: ThreadId) => projections.has(threadId) ? Effect.succeed(projections.get(threadId)!) : Effect.fail(new Error("missing projection") as never), getProjectThread: ({ projectId: expectedProjectId, threadId, }: { readonly projectId: ProjectId; readonly threadId: ThreadId; }) => { const target = projections.get(threadId); return target !== undefined && target.thread.projectId === expectedProjectId ? Effect.succeed(target) : Effect.fail( new ThreadManagementThreadNotFoundError({ projectId: expectedProjectId, threadId, }), ); }, } as unknown as ThreadManagementService["Service"]); const projects = ProjectService.ProjectService.of({ getById: (requestedProjectId: ProjectId) => Effect.succeed( requestedProjectId === projectId ? Option.some(project({ projectId, workspaceRoot, scripts: savedScripts })) : Option.none(), ), } as unknown as ProjectService.ProjectService["Service"]); const terminalService = yield* TerminalMcpService.make.pipe( Effect.provideService(ThreadManagementService, threads), Effect.provideService(ProjectService.ProjectService, projects), Effect.provideService(TerminalManager.TerminalManager, gatedManager), Effect.provideService(ThreadCommandExecutor, threadDispatch), ); let afterCloseOwned: (() => Effect.Effect<void>) | null = null; const terminalServiceWithCloseGate = TerminalMcpService.TerminalMcpService.of({ ...terminalService, closeOwned: (scope, input) => terminalService.closeOwned(scope, input).pipe( Effect.tap((closed) => { const action = afterCloseOwned; afterCloseOwned = null; return closed && action !== null ? action() : Effect.void; }), ), }); const service = yield* ProjectScriptMcpService.make.pipe( Effect.provideService( TerminalMcpService.TerminalMcpService, terminalServiceWithCloseGate, ), Effect.provideService(TerminalManager.TerminalManager, gatedManager), ); const scope: McpInvocationScope = { environmentId: EnvironmentId.make("environment:script-mcp"), threadId: callerThreadId, providerSessionId: "provider-session:script-mcp", providerInstanceId: ProviderInstanceId.make("codex"), capabilities: new Set(["orchestration"]), issuedAt: 1, }; const page = yield* service.list(scope, { threadId: targetThreadId, limit: 1, commandPreviewChars: 5, }); expect(page).toMatchObject({ threadId: targetThreadId, projectId, total: 2, nextCursor: 1, scripts: [ { scriptId: "dev", commandPreview: "pnpm ", commandCharacters: 12, commandTruncated: true, previewUrl: "http://localhost:3000", autoOpenPreview: true, }, ], }); assert.equal(ptyAdapter.spawnInputs.length, 0); const unknownScript = yield* service .run(scope, { threadId: targetThreadId, scriptId: "missing", terminalId: "term-script-missing", }) .pipe(Effect.flip); assert.equal(unknownScript.code, "script_not_found"); const unknownThread = yield* service .list(scope, { threadId: ThreadId.make("thread:script-mcp-unknown") }) .pipe(Effect.flip); assert.equal(unknownThread.code, "thread_not_found"); const missingProject = yield* service .list({ ...scope, threadId: missingProjectThreadId }, {}) .pipe(Effect.flip); assert.equal(missingProject.code, "project_not_found"); const denied = yield* service .run(scope, { threadId: restrictedThreadId, scriptId: "dev", terminalId: "term-script-denied", }) .pipe(Effect.flip); assert.equal(denied.code, "execution_policy_denied"); assert.equal(ptyAdapter.spawnInputs.length, 0); const first = yield* service.run(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: "term-script-first", }); assert.equal(first.outcome, "input_accepted"); assert.equal(first.terminalId, "term-script-first"); assert.equal(first.previewAutoOpened, false); assert.equal(first.terminal.worktreePath, worktreePath); assert.equal(ptyAdapter.spawnInputs[0]?.cwd, nestedExecutionCwd); assert.equal(ptyAdapter.spawnInputs[0]?.env?.T3CODE_PROJECT_ROOT, workspaceRoot); assert.equal(ptyAdapter.spawnInputs[0]?.env?.T3CODE_WORKTREE_PATH, worktreePath); expect(ptyAdapter.processes[0]?.writes).toEqual(["pnpm run dev\r"]); const duplicate = yield* service .run(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: first.terminalId, }) .pipe(Effect.flip); assert.equal(duplicate.code, "terminal_already_exists"); assert.equal(ptyAdapter.spawnInputs.length, 1); const second = yield* service.run(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: "term-script-second", }); assert.notEqual(second.terminalId, first.terminalId); assert.equal(ptyAdapter.spawnInputs.length, 2); expect(ptyAdapter.processes[1]?.writes).toEqual(["pnpm run dev\r"]); const unrelatedStop = yield* service .stop(scope, { threadId: targetThreadId, scriptId: "test", terminalId: second.terminalId, }) .pipe(Effect.flip); assert.equal(unrelatedStop.code, "script_run_not_found"); const stopped = yield* service.stop(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: first.terminalId, }); assert.isTrue(stopped.stopped); assert.equal( yield* manager.inspectSession({ threadId: targetThreadId, terminalId: first.terminalId }), null, ); const stopRaceA = yield* service.run(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: "term-script-stop-reopen-race", }); afterCloseOwned = () => service .run(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: stopRaceA.terminalId, }) .pipe(Effect.orDie, Effect.asVoid); assert.isTrue( (yield* service.stop(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: stopRaceA.terminalId, })).stopped, ); assert.isTrue( (yield* service.stop(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: stopRaceA.terminalId, })).stopped, ); savedScripts = savedScripts.filter((script) => script.id !== "dev"); const stoppedAfterDefinitionRemoval = yield* service.stop(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: second.terminalId, }); assert.isTrue(stoppedAfterDefinitionRemoval.stopped); savedScripts = [ { id: "dev", name: "Development server", command: "pnpm run dev", icon: "play", runOnWorktreeCreate: false, previewUrl: "http://localhost:3000", autoOpenPreview: true, }, ...savedScripts, ]; const externallyClosed = yield* service.run(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: "term-script-external-close", }); yield* manager.close({ threadId: targetThreadId, terminalId: externallyClosed.terminalId, }); yield* manager.openFresh({ threadId: targetThreadId, terminalId: externallyClosed.terminalId, cwd: nestedExecutionCwd, worktreePath, cols: 120, rows: 30, }); const staleClose = yield* service .stop(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: externallyClosed.terminalId, }) .pipe(Effect.flip); assert.equal(staleClose.code, "script_run_not_found"); assert.equal( (yield* manager.inspectSession({ threadId: targetThreadId, terminalId: externallyClosed.terminalId, }))?.status, "running", ); yield* manager.close({ threadId: targetThreadId, terminalId: externallyClosed.terminalId, }); const externallyRestarted = yield* service.run(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: "term-script-external-restart", }); yield* manager.restart({ threadId: targetThreadId, terminalId: externallyRestarted.terminalId, cwd: nestedExecutionCwd, worktreePath, cols: 120, rows: 30, }); const staleRestart = yield* service .stop(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: externallyRestarted.terminalId, })Lines 1–500 of 739. Load more
import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, describe, it } from "@effect/vitest"; import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId, type OrchestrationV2ThreadProjection, type Project, type ProjectScript, } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import * as Deferred from "effect/Deferred"; import * as FileSystem from "effect/FileSystem"; import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import { expect } from "vite-plus/test"; import * as ProcessRunner from "../processRunner.ts"; import { makeKeyedSerialExecutor } from "../orchestration-v2/KeyedSerialExecutor.ts"; import { ThreadCommandExecutor } from "../orchestration-v2/ThreadCommandExecutor.ts"; import { ThreadManagementService, ThreadManagementThreadNotFoundError, } from "../orchestration-v2/ThreadManagementService.ts"; import * as ProjectService from "../project/ProjectService.ts"; import * as TerminalManager from "../terminal/Manager.ts"; import * as PtyAdapter from "../terminal/PtyAdapter.ts"; import type { McpInvocationScope } from "./McpInvocationContext.ts"; import * as ProjectScriptMcpService from "./ProjectScriptMcpService.ts"; import * as TerminalMcpService from "./TerminalMcpService.ts"; class FakePtyProcess implements PtyAdapter.PtyProcess { readonly writes: string[] = []; readonly pid: number; readonly failWrite: boolean; killCalls = 0; private readonly dataListeners = new Set<(data: string) => void>(); private readonly exitListeners = new Set<(event: PtyAdapter.PtyExitEvent) => void>(); constructor(pid: number, failWrite: boolean) { this.pid = pid; this.failWrite = failWrite; } write(data: string): void { if (this.failWrite) throw new Error("script PTY write failed"); this.writes.push(data); } resize(): void {} kill(): void { this.killCalls += 1; this.emitExit({ exitCode: 0, signal: null }); } onData(listener: (data: string) => void): () => void { this.dataListeners.add(listener); return () => this.dataListeners.delete(listener); } onExit(listener: (event: PtyAdapter.PtyExitEvent) => void): () => void { this.exitListeners.add(listener); return () => this.exitListeners.delete(listener); } emitExit(event: PtyAdapter.PtyExitEvent): void { for (const listener of this.exitListeners) listener(event); } } class FakePtyAdapter { readonly spawnInputs: PtyAdapter.PtySpawnInput[] = []; readonly processes: FakePtyProcess[] = []; failNextWrite = false; spawn(input: PtyAdapter.PtySpawnInput): Effect.Effect<PtyAdapter.PtyProcess> { this.spawnInputs.push(input); const process = new FakePtyProcess(14_000 + this.processes.length, this.failNextWrite); this.failNextWrite = false; this.processes.push(process); return Effect.succeed(process); } } function projection(input: { readonly threadId: ThreadId; readonly projectId: ProjectId; readonly runtimeMode?: "approval-required" | "full-access"; readonly interactionMode?: "plan" | "default"; readonly worktreePath?: string | null; readonly executionCwd?: string; }): OrchestrationV2ThreadProjection { const providerSessionId = `provider-session:${input.threadId}`; const providerThreadId = `provider-thread:${input.threadId}`; return { thread: { id: input.threadId, projectId: input.projectId, runtimeMode: input.runtimeMode ?? "full-access", interactionMode: input.interactionMode ?? "default", worktreePath: input.worktreePath ?? null, activeProviderThreadId: input.executionCwd === undefined ? null : providerThreadId, deletedAt: null, }, providerThreads: input.executionCwd === undefined ? [] : [{ id: providerThreadId, providerSessionId }], providerSessions: input.executionCwd === undefined ? [] : [{ id: providerSessionId, cwd: input.executionCwd, status: "ready" }], } as unknown as OrchestrationV2ThreadProjection; } function project(input: { readonly projectId: ProjectId; readonly workspaceRoot: string; readonly scripts: ReadonlyArray<ProjectScript>; }): Project { return { id: input.projectId, title: "Script project", workspaceRoot: input.workspaceRoot, repositoryIdentity: null, faviconPath: null, defaultModelSelection: null, scripts: [...input.scripts], createdAt: "2026-08-29T00:00:00.000Z", updatedAt: "2026-08-29T00:00:00.000Z", deletedAt: null, }; } describe("ProjectScriptMcpService", () => { it.effect("runs only saved scripts in dedicated managed terminals", () => Effect.scoped( Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-project-script-mcp-" }); const workspaceRoot = path.join(baseDir, "workspace"); const worktreePath = path.join(baseDir, "worktree"); const nestedExecutionCwd = path.join(worktreePath, "packages", "app"); yield* fs.makeDirectory(workspaceRoot, { recursive: true }); yield* fs.makeDirectory(nestedExecutionCwd, { recursive: true }); const projectId = ProjectId.make("project:script-mcp"); const missingProjectId = ProjectId.make("project:script-mcp-missing"); const callerThreadId = ThreadId.make("thread:script-mcp-caller"); const targetThreadId = ThreadId.make("thread:script-mcp-target"); const restrictedThreadId = ThreadId.make("thread:script-mcp-restricted"); const missingProjectThreadId = ThreadId.make("thread:script-mcp-missing-project"); const projections = new Map([ [callerThreadId, projection({ threadId: callerThreadId, projectId })], [ targetThreadId, projection({ threadId: targetThreadId, projectId, worktreePath, executionCwd: nestedExecutionCwd, }), ], [ restrictedThreadId, projection({ threadId: restrictedThreadId, projectId, runtimeMode: "approval-required", interactionMode: "plan", }), ], [ missingProjectThreadId, projection({ threadId: missingProjectThreadId, projectId: missingProjectId }), ], ]); let savedScripts: ReadonlyArray<ProjectScript> = [ { id: "dev", name: "Development server", command: "pnpm run dev", icon: "play", runOnWorktreeCreate: false, previewUrl: "http://localhost:3000", autoOpenPreview: true, }, { id: "test", name: "Tests", command: "pnpm test", icon: "test", runOnWorktreeCreate: false, }, ]; const ptyAdapter = new FakePtyAdapter(); const manager = yield* TerminalManager.makeWithOptions({ logsDir: path.join(baseDir, "terminal-history"), ptyAdapter, processKillGraceMs: 1, }); const replacementAdmissionEntered = yield* Deferred.make<void>(); const replacementAdmissionRelease = yield* Deferred.make<void>(); const exitAdmissionEntered = yield* Deferred.make<void>(); const exitAdmissionRelease = yield* Deferred.make<void>(); let replacementAdmissionAttempts = 0; const gatedManager = TerminalManager.TerminalManager.of({ ...manager, admitRunningSessionHandle: (input, onAdmitted) => { if ( input.terminalId === "term-script-registration-replacement" && replacementAdmissionAttempts++ === 0 ) { return Deferred.succeed(replacementAdmissionEntered, undefined).pipe( Effect.andThen(Deferred.await(replacementAdmissionRelease)), Effect.andThen(manager.admitRunningSessionHandle(input, onAdmitted)), ); } if (input.terminalId === "term-script-registration-exit") { return Deferred.succeed(exitAdmissionEntered, undefined).pipe( Effect.andThen(Deferred.await(exitAdmissionRelease)), Effect.andThen(manager.admitRunningSessionHandle(input, onAdmitted)), ); } return manager.admitRunningSessionHandle(input, onAdmitted); }, }); const threadDispatch = yield* makeKeyedSerialExecutor<ThreadId>(); const threads = ThreadManagementService.of({ getThreadProjection: (threadId: ThreadId) => projections.has(threadId) ? Effect.succeed(projections.get(threadId)!) : Effect.fail(new Error("missing projection") as never), getProjectThread: ({ projectId: expectedProjectId, threadId, }: { readonly projectId: ProjectId; readonly threadId: ThreadId; }) => { const target = projections.get(threadId); return target !== undefined && target.thread.projectId === expectedProjectId ? Effect.succeed(target) : Effect.fail( new ThreadManagementThreadNotFoundError({ projectId: expectedProjectId, threadId, }), ); }, } as unknown as ThreadManagementService["Service"]); const projects = ProjectService.ProjectService.of({ getById: (requestedProjectId: ProjectId) => Effect.succeed( requestedProjectId === projectId ? Option.some(project({ projectId, workspaceRoot, scripts: savedScripts })) : Option.none(), ), } as unknown as ProjectService.ProjectService["Service"]); const terminalService = yield* TerminalMcpService.make.pipe( Effect.provideService(ThreadManagementService, threads), Effect.provideService(ProjectService.ProjectService, projects), Effect.provideService(TerminalManager.TerminalManager, gatedManager), Effect.provideService(ThreadCommandExecutor, threadDispatch), ); let afterCloseOwned: (() => Effect.Effect<void>) | null = null; const terminalServiceWithCloseGate = TerminalMcpService.TerminalMcpService.of({ ...terminalService, closeOwned: (scope, input) => terminalService.closeOwned(scope, input).pipe( Effect.tap((closed) => { const action = afterCloseOwned; afterCloseOwned = null; return closed && action !== null ? action() : Effect.void; }), ), }); const service = yield* ProjectScriptMcpService.make.pipe( Effect.provideService( TerminalMcpService.TerminalMcpService, terminalServiceWithCloseGate, ), Effect.provideService(TerminalManager.TerminalManager, gatedManager), ); const scope: McpInvocationScope = { environmentId: EnvironmentId.make("environment:script-mcp"), threadId: callerThreadId, providerSessionId: "provider-session:script-mcp", providerInstanceId: ProviderInstanceId.make("codex"), capabilities: new Set(["orchestration"]), issuedAt: 1, }; const page = yield* service.list(scope, { threadId: targetThreadId, limit: 1, commandPreviewChars: 5, }); expect(page).toMatchObject({ threadId: targetThreadId, projectId, total: 2, nextCursor: 1, scripts: [ { scriptId: "dev", commandPreview: "pnpm ", commandCharacters: 12, commandTruncated: true, previewUrl: "http://localhost:3000", autoOpenPreview: true, }, ], }); assert.equal(ptyAdapter.spawnInputs.length, 0); const unknownScript = yield* service .run(scope, { threadId: targetThreadId, scriptId: "missing", terminalId: "term-script-missing", }) .pipe(Effect.flip); assert.equal(unknownScript.code, "script_not_found"); const unknownThread = yield* service .list(scope, { threadId: ThreadId.make("thread:script-mcp-unknown") }) .pipe(Effect.flip); assert.equal(unknownThread.code, "thread_not_found"); const missingProject = yield* service .list({ ...scope, threadId: missingProjectThreadId }, {}) .pipe(Effect.flip); assert.equal(missingProject.code, "project_not_found"); const denied = yield* service .run(scope, { threadId: restrictedThreadId, scriptId: "dev", terminalId: "term-script-denied", }) .pipe(Effect.flip); assert.equal(denied.code, "execution_policy_denied"); assert.equal(ptyAdapter.spawnInputs.length, 0); const first = yield* service.run(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: "term-script-first", }); assert.equal(first.outcome, "input_accepted"); assert.equal(first.terminalId, "term-script-first"); assert.equal(first.previewAutoOpened, false); assert.equal(first.terminal.worktreePath, worktreePath); assert.equal(ptyAdapter.spawnInputs[0]?.cwd, nestedExecutionCwd); assert.equal(ptyAdapter.spawnInputs[0]?.env?.T3CODE_PROJECT_ROOT, workspaceRoot); assert.equal(ptyAdapter.spawnInputs[0]?.env?.T3CODE_WORKTREE_PATH, worktreePath); expect(ptyAdapter.processes[0]?.writes).toEqual(["pnpm run dev\r"]); const duplicate = yield* service .run(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: first.terminalId, }) .pipe(Effect.flip); assert.equal(duplicate.code, "terminal_already_exists"); assert.equal(ptyAdapter.spawnInputs.length, 1); const second = yield* service.run(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: "term-script-second", }); assert.notEqual(second.terminalId, first.terminalId); assert.equal(ptyAdapter.spawnInputs.length, 2); expect(ptyAdapter.processes[1]?.writes).toEqual(["pnpm run dev\r"]); const unrelatedStop = yield* service .stop(scope, { threadId: targetThreadId, scriptId: "test", terminalId: second.terminalId, }) .pipe(Effect.flip); assert.equal(unrelatedStop.code, "script_run_not_found"); const stopped = yield* service.stop(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: first.terminalId, }); assert.isTrue(stopped.stopped); assert.equal( yield* manager.inspectSession({ threadId: targetThreadId, terminalId: first.terminalId }), null, ); const stopRaceA = yield* service.run(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: "term-script-stop-reopen-race", }); afterCloseOwned = () => service .run(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: stopRaceA.terminalId, }) .pipe(Effect.orDie, Effect.asVoid); assert.isTrue( (yield* service.stop(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: stopRaceA.terminalId, })).stopped, ); assert.isTrue( (yield* service.stop(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: stopRaceA.terminalId, })).stopped, ); savedScripts = savedScripts.filter((script) => script.id !== "dev"); const stoppedAfterDefinitionRemoval = yield* service.stop(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: second.terminalId, }); assert.isTrue(stoppedAfterDefinitionRemoval.stopped); savedScripts = [ { id: "dev", name: "Development server", command: "pnpm run dev", icon: "play", runOnWorktreeCreate: false, previewUrl: "http://localhost:3000", autoOpenPreview: true, }, ...savedScripts, ]; const externallyClosed = yield* service.run(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: "term-script-external-close", }); yield* manager.close({ threadId: targetThreadId, terminalId: externallyClosed.terminalId, }); yield* manager.openFresh({ threadId: targetThreadId, terminalId: externallyClosed.terminalId, cwd: nestedExecutionCwd, worktreePath, cols: 120, rows: 30, }); const staleClose = yield* service .stop(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: externallyClosed.terminalId, }) .pipe(Effect.flip); assert.equal(staleClose.code, "script_run_not_found"); assert.equal( (yield* manager.inspectSession({ threadId: targetThreadId, terminalId: externallyClosed.terminalId, }))?.status, "running", ); yield* manager.close({ threadId: targetThreadId, terminalId: externallyClosed.terminalId, }); const externallyRestarted = yield* service.run(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: "term-script-external-restart", }); yield* manager.restart({ threadId: targetThreadId, terminalId: externallyRestarted.terminalId, cwd: nestedExecutionCwd, worktreePath, cols: 120, rows: 30, }); const staleRestart = yield* service .stop(scope, { threadId: targetThreadId, scriptId: "dev", terminalId: externallyRestarted.terminalId, })