diff --git a/README.md b/README.md index e3a0db1..9ef8337 100644 --- a/README.md +++ b/README.md @@ -134,11 +134,10 @@ console.log(access.hostname, access.port, access.username); ### Presigned URLs -Create a temporary public URL for a sandbox port. The optional second argument to -`createPresignedUrl(port, expiresIn)` is `expiresIn` in seconds. +Create a temporary public URL for a sandbox port. `expiresIn` is in seconds. ```ts -const presigned = await sandbox.createPresignedUrl(8080, 900); // 15 minutes +const presigned = await sandbox.createPresignedUrl({ port: 8080, expiresIn: 900 }); // 15 minutes console.log(presigned.url); await sandbox.deletePresignedUrl(presigned.id); diff --git a/examples/desktop.ts b/examples/desktop.ts index b18c574..e41d877 100644 --- a/examples/desktop.ts +++ b/examples/desktop.ts @@ -8,16 +8,16 @@ async function main(): Promise { try { const sandbox = await client.sandboxes.create({ templateName: DEFAULT_DESKTOP_TEMPLATE_NAME }); try { - await sandbox.desktop.waitUntilReady(60); + await sandbox.desktop.waitUntilReady({ timeout: 60 }); console.log("Desktop:", sandbox.desktop.desktopUrl()); const display = await sandbox.desktop.displayInfo(); console.log("Display:", display); - await sandbox.desktop.movePointer( - Math.floor(display.width / 2), - Math.floor(display.height / 2), - ); + await sandbox.desktop.movePointer({ + x: Math.floor(display.width / 2), + y: Math.floor(display.height / 2), + }); await sandbox.desktop.click({ button: 1 }); const screenshot = await sandbox.desktop.screenshot(); diff --git a/examples/ssh.ts b/examples/ssh.ts index 2ef5e86..dccae97 100644 --- a/examples/ssh.ts +++ b/examples/ssh.ts @@ -9,10 +9,10 @@ async function main(): Promise { const access = await sandbox.ssh.createAccess(); console.log("ssh command:", access.sshCommand); - const validation = await sandbox.ssh.validateAccess(access.id, access.password); + const validation = await sandbox.ssh.validateAccess({ id: access.id, password: access.password }); console.log("ssh valid:", validation.valid); - const rotated = await sandbox.ssh.regenerateAccess(access.id); + const rotated = await sandbox.ssh.regenerateAccess({ id: access.id }); console.log("rotated ssh command:", rotated.sshCommand); } finally { await sandbox.delete(); diff --git a/src/client/sandbox.ts b/src/client/sandbox.ts index 685e361..f4fc5f1 100644 --- a/src/client/sandbox.ts +++ b/src/client/sandbox.ts @@ -212,17 +212,15 @@ export class Sandbox implements SandboxData { /** * Creates a temporary public URL for a specific sandbox port. * - * @param port Sandbox port to expose. - * @param expiresIn Optional expiration in seconds. + * @param params Presigned URL creation parameters. * @param options Optional request settings. * @returns The created presigned URL. */ async createPresignedUrl( - port: number, - expiresIn?: number, + params: { port: number; expiresIn?: number }, options?: { timeout?: number }, ): Promise { - return this.client.sandboxes.createPresignedUrl(this.id, { port, expiresIn }, options); + return this.client.sandboxes.createPresignedUrl(this.id, params, options); } /** diff --git a/src/services/desktop.ts b/src/services/desktop.ts index f9d8ec8..70699d6 100644 --- a/src/services/desktop.ts +++ b/src/services/desktop.ts @@ -216,22 +216,20 @@ export class DesktopClient { * Moves the desktop pointer to a coordinate. * * @param sandbox Sandbox ID or sandbox-like object. - * @param x Horizontal coordinate. - * @param y Vertical coordinate. + * @param params Pointer move parameters. * @param options Optional request settings such as timeout and query params. * @returns The updated pointer position. */ async movePointer( sandbox: SandboxRef, - x: number, - y: number, + params: { x: number; y: number }, options?: RequestOptions, ): Promise { return this.requestJson( sandbox, desktopPointerPositionSchema, "/api/input/move", - { method: "POST", body: jsonBody({ x, y }) }, + { method: "POST", body: jsonBody({ x: params.x, y: params.y }) }, options, ); } @@ -640,7 +638,7 @@ export class DesktopClient { * Waits until the desktop reports a running state or all tracked processes are up. * * @param sandbox Sandbox ID or sandbox-like object. - * @param timeout Timeout in seconds. + * @param params Wait parameters. * @param options Optional request settings such as timeout and query params. * @throws {Leap0Error} If the desktop never becomes ready or a non-retryable error occurs. * @@ -649,7 +647,12 @@ export class DesktopClient { * await sandbox.desktop.waitUntilReady(); * ``` */ - async waitUntilReady(sandbox: SandboxRef, timeout = 60, options: RequestOptions = {}): Promise { + async waitUntilReady( + sandbox: SandboxRef, + params: { timeout?: number } = {}, + options: RequestOptions = {}, + ): Promise { + const timeout = params.timeout ?? 60; const deadline = Date.now() + timeout * 1000; let lastError: unknown; diff --git a/src/services/filesystem.ts b/src/services/filesystem.ts index 5077830..bdba9f0 100644 --- a/src/services/filesystem.ts +++ b/src/services/filesystem.ts @@ -332,19 +332,17 @@ export class FilesystemClient { * Deletes a file or directory. Set `recursive` for non-empty directories. * * @param sandbox Sandbox ID or sandbox-like object. - * @param path File or directory path to delete. - * @param recursive Whether to delete directories recursively. + * @param params Delete parameters. * @param options Optional request settings such as timeout and query params. */ async delete( sandbox: SandboxRef, - path: string, - recursive = false, + params: { path: string; recursive?: boolean }, options: RequestOptions = {}, ): Promise { await this.transport.request( this.fsPath(sandbox, "delete"), - { method: "POST", body: jsonBody({ path, recursive }) }, + { method: "POST", body: jsonBody({ path: params.path, recursive: params.recursive ?? false }) }, options, ); } @@ -488,21 +486,24 @@ export class FilesystemClient { * Moves or renames a file or directory. * * @param sandbox Sandbox ID or sandbox-like object. - * @param srcPath Source path. - * @param dstPath Destination path. - * @param overwrite Whether to overwrite an existing destination. + * @param params Move parameters. * @param options Optional request settings such as timeout and query params. */ async move( sandbox: SandboxRef, - srcPath: string, - dstPath: string, - overwrite = false, + params: { srcPath: string; dstPath: string; overwrite?: boolean }, options: RequestOptions = {}, ): Promise { await this.transport.request( this.fsPath(sandbox, "move"), - { method: "POST", body: jsonBody({ src_path: srcPath, dst_path: dstPath, overwrite }) }, + { + method: "POST", + body: jsonBody({ + src_path: params.srcPath, + dst_path: params.dstPath, + overwrite: params.overwrite ?? false, + }), + }, options, ); } diff --git a/src/services/lsp.ts b/src/services/lsp.ts index 9ded77e..4837af4 100644 --- a/src/services/lsp.ts +++ b/src/services/lsp.ts @@ -7,6 +7,14 @@ import type { import { Leap0Transport, jsonBody } from "@/core/transport.js"; import { sandboxIdOf, toFileUri } from "@/core/utils.js"; +export type LspRequestParams = { + languageId: string; + pathToProject: string; + uri: string; + text?: string; + version?: number; +}; + /** * Starts and interacts with language servers inside a sandbox. * @@ -34,39 +42,23 @@ export class LspClient { return result; } - private normalizeDidOpenArgs( - textOrOptions?: string | RequestOptions, - versionOrOptions?: number | RequestOptions, - options?: RequestOptions, - ): { text: string | undefined; version: number; options: RequestOptions | undefined } { - if (textOrOptions && typeof textOrOptions === "object") { - return { text: undefined, version: 1, options: textOrOptions }; - } - if (versionOrOptions && typeof versionOrOptions === "object") { - return { text: textOrOptions, version: 1, options: versionOrOptions }; - } - return { text: textOrOptions, version: versionOrOptions ?? 1, options }; - } - /** * Starts a language server for a project path inside the sandbox. * * @param sandbox Sandbox ID or sandbox-like object. - * @param languageId Language server identifier. - * @param pathToProject Project path inside the sandbox. + * @param params Language server start parameters. * @param options Optional request settings such as timeout and query params. * @returns The language server response payload. */ async start( sandbox: SandboxRef, - languageId: string, - pathToProject: string, + params: Pick, options?: RequestOptions, ): Promise { return await this.json( sandbox, "start", - { language_id: languageId, path_to_project: pathToProject }, + { language_id: params.languageId, path_to_project: params.pathToProject }, options, ); } @@ -74,21 +66,19 @@ export class LspClient { * Stops a previously started language server. * * @param sandbox Sandbox ID or sandbox-like object. - * @param languageId Language server identifier. - * @param pathToProject Project path inside the sandbox. + * @param params Language server stop parameters. * @param options Optional request settings such as timeout and query params. * @returns The language server response payload. */ async stop( sandbox: SandboxRef, - languageId: string, - pathToProject: string, + params: Pick, options?: RequestOptions, ): Promise { return await this.json( sandbox, "stop", - { language_id: languageId, path_to_project: pathToProject }, + { language_id: params.languageId, path_to_project: params.pathToProject }, options, ); } @@ -96,78 +86,59 @@ export class LspClient { * Opens a document in the language server. * * @param sandbox Sandbox ID or sandbox-like object. - * @param languageId Language server identifier. - * @param pathToProject Project path inside the sandbox. - * @param uri File URI to open. - * @param textOrOptions Optional in-memory document text, or request options when opening from disk. - * @param versionOrOptions Optional document version, or request options when no explicit version is needed. + * @param params Document open parameters. * @param options Optional request options. * * @example * ```ts - * await sandbox.lsp.didOpen( - * "typescript", - * "/workspace/app", - * "file:///workspace/app/src/index.ts", - * "const x = 1;", - * 1, - * ); + * await sandbox.lsp.didOpen({ + * languageId: "typescript", + * pathToProject: "/workspace/app", + * uri: "file:///workspace/app/src/index.ts", + * text: "const x = 1;", + * version: 1, + * }); * ``` */ async didOpen( sandbox: SandboxRef, - languageId: string, - pathToProject: string, - uri: string, - textOrOptions?: string | RequestOptions, - versionOrOptions?: number | RequestOptions, + params: LspRequestParams, options?: RequestOptions, ): Promise { - const { - text, - version, - options: normalizedOptions, - } = this.normalizeDidOpenArgs(textOrOptions, versionOrOptions, options); const payload: Record = { - language_id: languageId, - path_to_project: pathToProject, - uri, - version, + language_id: params.languageId, + path_to_project: params.pathToProject, + uri: params.uri, + version: params.version ?? 1, }; - if (text !== undefined) payload.text = text; + if (params.text !== undefined) payload.text = params.text; await this.transport.request( `/v1/sandbox/${sandboxIdOf(sandbox)}/lsp/did-open`, { method: "POST", body: jsonBody(payload) }, - normalizedOptions, + options, ); } /** * Opens a document by filesystem path instead of a file URI. * * @param sandbox Sandbox ID or sandbox-like object. - * @param languageId Language server identifier. - * @param pathToProject Project path inside the sandbox. - * @param path Filesystem path to open. - * @param textOrOptions Optional in-memory document text, or request options when opening from disk. - * @param versionOrOptions Optional document version, or request options when no explicit version is needed. + * @param params Document open parameters using a filesystem path. * @param options Optional request options. */ async didOpenPath( sandbox: SandboxRef, - languageId: string, - pathToProject: string, - path: string, - textOrOptions?: string | RequestOptions, - versionOrOptions?: number | RequestOptions, + params: Omit & { path: string }, options?: RequestOptions, ): Promise { await this.didOpen( sandbox, - languageId, - pathToProject, - toFileUri(path), - textOrOptions, - versionOrOptions, + { + languageId: params.languageId, + pathToProject: params.pathToProject, + uri: toFileUri(params.path), + text: params.text, + version: params.version, + }, options, ); } @@ -175,23 +146,23 @@ export class LspClient { * Closes a previously opened document. * * @param sandbox Sandbox ID or sandbox-like object. - * @param languageId Language server identifier. - * @param pathToProject Project path inside the sandbox. - * @param uri File URI to close. + * @param params Document close parameters. * @param options Optional request settings such as timeout and query params. */ async didClose( sandbox: SandboxRef, - languageId: string, - pathToProject: string, - uri: string, + params: Pick, options?: RequestOptions, ): Promise { await this.transport.request( `/v1/sandbox/${sandboxIdOf(sandbox)}/lsp/did-close`, { method: "POST", - body: jsonBody({ language_id: languageId, path_to_project: pathToProject, uri }), + body: jsonBody({ + language_id: params.languageId, + path_to_project: params.pathToProject, + uri: params.uri, + }), }, options, ); @@ -200,49 +171,48 @@ export class LspClient { * Closes a document by filesystem path instead of a file URI. * * @param sandbox Sandbox ID or sandbox-like object. - * @param languageId Language server identifier. - * @param pathToProject Project path inside the sandbox. - * @param path Filesystem path to close. + * @param params Document close parameters using a filesystem path. * @param options Optional request settings such as timeout and query params. */ async didClosePath( sandbox: SandboxRef, - languageId: string, - pathToProject: string, - path: string, + params: Pick & { path: string }, options?: RequestOptions, ): Promise { - await this.didClose(sandbox, languageId, pathToProject, toFileUri(path), options); + await this.didClose( + sandbox, + { + languageId: params.languageId, + pathToProject: params.pathToProject, + uri: toFileUri(params.path), + }, + options, + ); } /** * Requests completion items at a line/character position. * * @param sandbox Sandbox ID or sandbox-like object. - * @param languageId Language server identifier. - * @param pathToProject Project path inside the sandbox. - * @param uri File URI to inspect. - * @param line Zero-based line number. - * @param character Zero-based character offset. + * @param params Completion request parameters. * @param options Optional request settings such as timeout and query params. * @returns The raw JSON-RPC response from the language server. */ async completions( sandbox: SandboxRef, - languageId: string, - pathToProject: string, - uri: string, - line: number, - character: number, + params: Pick & { + line: number; + character: number; + }, options?: RequestOptions, ): Promise { return await this.json( sandbox, "completions", { - language_id: languageId, - path_to_project: pathToProject, - uri, - position: { line, character }, + language_id: params.languageId, + path_to_project: params.pathToProject, + uri: params.uri, + position: { line: params.line, character: params.character }, }, options, ); @@ -251,30 +221,28 @@ export class LspClient { * Requests completion items for a filesystem path. * * @param sandbox Sandbox ID or sandbox-like object. - * @param languageId Language server identifier. - * @param pathToProject Project path inside the sandbox. - * @param path Filesystem path to inspect. - * @param line Zero-based line number. - * @param character Zero-based character offset. + * @param params Completion request parameters using a filesystem path. * @param options Optional request settings such as timeout and query params. * @returns The raw JSON-RPC response from the language server. */ async completionsPath( sandbox: SandboxRef, - languageId: string, - pathToProject: string, - path: string, - line: number, - character: number, + params: Pick & { + path: string; + line: number; + character: number; + }, options?: RequestOptions, ): Promise { return await this.completions( sandbox, - languageId, - pathToProject, - toFileUri(path), - line, - character, + { + languageId: params.languageId, + pathToProject: params.pathToProject, + uri: toFileUri(params.path), + line: params.line, + character: params.character, + }, options, ); } @@ -282,23 +250,23 @@ export class LspClient { * Returns document symbols for an open document. * * @param sandbox Sandbox ID or sandbox-like object. - * @param languageId Language server identifier. - * @param pathToProject Project path inside the sandbox. - * @param uri File URI to inspect. + * @param params Document symbol request parameters. * @param options Optional request settings such as timeout and query params. * @returns The raw JSON-RPC response from the language server. */ async documentSymbols( sandbox: SandboxRef, - languageId: string, - pathToProject: string, - uri: string, + params: Pick, options?: RequestOptions, ): Promise { return await this.json( sandbox, "document-symbols", - { language_id: languageId, path_to_project: pathToProject, uri }, + { + language_id: params.languageId, + path_to_project: params.pathToProject, + uri: params.uri, + }, options, ); } @@ -306,19 +274,23 @@ export class LspClient { * Returns document symbols for a filesystem path. * * @param sandbox Sandbox ID or sandbox-like object. - * @param languageId Language server identifier. - * @param pathToProject Project path inside the sandbox. - * @param path Filesystem path to inspect. + * @param params Document symbol request parameters using a filesystem path. * @param options Optional request settings such as timeout and query params. * @returns The raw JSON-RPC response from the language server. */ async documentSymbolsPath( sandbox: SandboxRef, - languageId: string, - pathToProject: string, - path: string, + params: Pick & { path: string }, options?: RequestOptions, ): Promise { - return await this.documentSymbols(sandbox, languageId, pathToProject, toFileUri(path), options); + return await this.documentSymbols( + sandbox, + { + languageId: params.languageId, + pathToProject: params.pathToProject, + uri: toFileUri(params.path), + }, + options, + ); } } diff --git a/src/services/process.ts b/src/services/process.ts index dbef9d9..b709cc5 100644 --- a/src/services/process.ts +++ b/src/services/process.ts @@ -19,7 +19,7 @@ export class ProcessClient { * @param params Command execution parameters. * @param params.command Command line to execute. * @param params.cwd Optional working directory. - * @param params.timeout Optional command timeout in milliseconds. + * @param params.timeout Optional command timeout in seconds. * @param params.env Optional environment variables applied to the spawned process. * @param options Optional request settings such as timeout and query params. * @returns The process exit code and collected stdout/stderr output. diff --git a/src/services/pty.ts b/src/services/pty.ts index 1fbcc2e..1686bd0 100644 --- a/src/services/pty.ts +++ b/src/services/pty.ts @@ -176,22 +176,18 @@ export class PtyClient { * Resizes an existing PTY session. * * @param sandbox Sandbox ID or sandbox-like object. - * @param sessionId PTY session identifier. - * @param cols Terminal column count. - * @param rows Terminal row count. + * @param params Resize parameters. * @param options Optional request settings such as timeout and query params. * @returns The updated PTY session. */ async resize( sandbox: SandboxRef, - sessionId: string, - cols: number, - rows: number, + params: { sessionId: string; cols: number; rows: number }, options: RequestOptions = {}, ): Promise { const data = await this.transport.requestJson( - `/v1/sandbox/${sandboxIdOf(sandbox)}/pty/${encodeURIComponent(sessionId)}/resize`, - { method: "POST", body: jsonBody({ cols, rows }) }, + `/v1/sandbox/${sandboxIdOf(sandbox)}/pty/${encodeURIComponent(params.sessionId)}/resize`, + { method: "POST", body: jsonBody({ cols: params.cols, rows: params.rows }) }, options, ); return normalize(ptySessionSchema, data); diff --git a/src/services/ssh.ts b/src/services/ssh.ts index 6db7f3c..119f5f9 100644 --- a/src/services/ssh.ts +++ b/src/services/ssh.ts @@ -32,12 +32,16 @@ export class SshClient { * Deletes a specific SSH credential for a sandbox. * * @param sandbox Sandbox ID or sandbox-like object. - * @param id The SSH credential ID to delete. + * @param params SSH credential deletion parameters. * @param options Optional request settings such as timeout and query params. */ - async deleteAccess(sandbox: SandboxRef, id: string, options: RequestOptions = {}): Promise { + async deleteAccess( + sandbox: SandboxRef, + params: { id: string }, + options: RequestOptions = {}, + ): Promise { await this.transport.request( - `/v1/sandbox/${sandboxIdOf(sandbox)}/ssh/${id}`, + `/v1/sandbox/${sandboxIdOf(sandbox)}/ssh/${params.id}`, { method: "DELETE" }, options, ); @@ -47,20 +51,18 @@ export class SshClient { * Verifies a specific previously issued SSH credential pair. * * @param sandbox Sandbox ID or sandbox-like object. - * @param id The SSH credential ID to validate. - * @param password The password returned when the access credential was created. + * @param params SSH credential validation parameters. * @param options Optional request settings such as timeout and query params. * @returns The validation result. */ async validateAccess( sandbox: SandboxRef, - id: string, - password: string, + params: { id: string; password: string }, options: RequestOptions = {}, ): Promise { const data = await this.transport.requestJson( - `/v1/sandbox/${sandboxIdOf(sandbox)}/ssh/${id}/validate`, - { method: "POST", body: jsonBody({ password }) }, + `/v1/sandbox/${sandboxIdOf(sandbox)}/ssh/${params.id}/validate`, + { method: "POST", body: jsonBody({ password: params.password }) }, options, ); return normalize(sshValidationSchema, data); @@ -70,13 +72,17 @@ export class SshClient { * Rotates a specific SSH credential for a sandbox. * * @param sandbox Sandbox ID or sandbox-like object. - * @param id The SSH credential ID to rotate. + * @param params SSH credential rotation parameters. * @param options Optional request settings such as timeout and query params. * @returns The newly generated SSH access payload. */ - async regenerateAccess(sandbox: SandboxRef, id: string, options: RequestOptions = {}): Promise { + async regenerateAccess( + sandbox: SandboxRef, + params: { id: string }, + options: RequestOptions = {}, + ): Promise { const data = await this.transport.requestJson( - `/v1/sandbox/${sandboxIdOf(sandbox)}/ssh/${id}/regen`, + `/v1/sandbox/${sandboxIdOf(sandbox)}/ssh/${params.id}/regen`, { method: "POST" }, options, ); diff --git a/tests/client/client-sandbox.test.ts b/tests/client/client-sandbox.test.ts index 71318bb..bda7565 100644 --- a/tests/client/client-sandbox.test.ts +++ b/tests/client/client-sandbox.test.ts @@ -134,7 +134,7 @@ test("Sandbox binds service methods to itself", async () => { assert.equal(sandbox.invokeUrl("/healthz", 3000), "invoke:sb-1:/healthz:3000"); assert.equal(await sandbox.getUserHomeDir(), "home:sb-1"); assert.equal(await sandbox.getWorkdir(), "workdir:sb-1"); - assert.equal((await sandbox.createPresignedUrl(8080, 15)).url, "https://tok_1.leap0.app"); + assert.equal((await sandbox.createPresignedUrl({ port: 8080, expiresIn: 15 })).url, "https://tok_1.leap0.app"); await sandbox.deletePresignedUrl("psu-1"); }); @@ -192,13 +192,13 @@ test("client and sandbox helpers stay strongly typed", () => { [params: { command: string; cwd?: string; timeout?: number; env?: Record }, options?: RequestOptions] >(); expectTypeOf().parameters.toEqualTypeOf< - [id: string, password: string, options?: RequestOptions] + [params: { id: string; password: string }, options?: RequestOptions] >(); expectTypeOf().parameters.toEqualTypeOf< - [id: string, options?: RequestOptions] + [params: { id: string }, options?: RequestOptions] >(); expectTypeOf().parameters.toEqualTypeOf< - [id: string, options?: RequestOptions] + [params: { id: string }, options?: RequestOptions] >(); expectTypeOf>().toEqualTypeOf>(); expectTypeOf>().toEqualTypeOf>(); diff --git a/tests/services/desktop.test.ts b/tests/services/desktop.test.ts index fd2425e..3b7f6e5 100644 --- a/tests/services/desktop.test.ts +++ b/tests/services/desktop.test.ts @@ -69,7 +69,7 @@ test("desktop client sends expected request shapes", async () => { const client = new DesktopClient(transport as never); await client.displayInfo("sb-1"); await client.resizeScreen("sb-1", { width: 1280, height: 720 }); - await client.movePointer("sb-1", 10, 20); + await client.movePointer("sb-1", { x: 10, y: 20 }); await client.typeText("sb-1", "hello"); await client.getProcess("sb-1", "x11vnc"); await client.drag("sb-1", { fromX: 1, fromY: 2, toX: 3, toY: 4 }); @@ -170,7 +170,7 @@ test("desktop waitUntilReady treats count-only updates as ready", async () => { }); const client = new DesktopClient(transport as never); - await client.waitUntilReady("sb-1", 1); + await client.waitUntilReady("sb-1", { timeout: 1 }); }); test("desktop waitUntilReady forwards request timeout options", async () => { @@ -195,7 +195,7 @@ test("desktop waitUntilReady forwards request timeout options", async () => { }); const client = new DesktopClient(transport as never); - await client.waitUntilReady("sb-1", 5, { timeout: 2 }); + await client.waitUntilReady("sb-1", { timeout: 5 }, { timeout: 2 }); assert.equal(calls.length, 1); assert.equal(calls[0]?.options.timeout, 2); @@ -233,7 +233,7 @@ test("desktop waitUntilReady ignores zero total count updates", async () => { }); const client = new DesktopClient(transport as never); - await client.waitUntilReady("sb-1", 1); + await client.waitUntilReady("sb-1", { timeout: 1 }); }); test("desktop client validates request payloads before transport", async () => { diff --git a/tests/services/filesystem.test.ts b/tests/services/filesystem.test.ts index 5efa519..bad38c8 100644 --- a/tests/services/filesystem.test.ts +++ b/tests/services/filesystem.test.ts @@ -39,7 +39,7 @@ test("filesystem client sends expected request shapes", async () => { await client.writeBytes("sb-1", "/tmp/b.bin", new Uint8Array([1, 2])); await client.readFile("sb-1", "/tmp/a.txt", { head: 10 }); await client.readBytes("sb-1", "/tmp/b.bin"); - await client.delete("sb-1", "/tmp/a.txt"); + await client.delete("sb-1", { path: "/tmp/a.txt" }); await client.setPermissions("sb-1", "/tmp/a.txt", { mode: "0755" }); await client.setPermissions("sb-1", "/tmp/b.txt", { owner: "alice", group: "staff" }); await client.glob("sb-1", "/workspace", "**/*.ts"); @@ -47,7 +47,7 @@ test("filesystem client sends expected request shapes", async () => { await client.editFile("sb-1", "/tmp/a.txt", [{ find: "a", replace: "b" }]); await client.editFiles("sb-1", { paths: ["/tmp/a.txt"], find: "a", replace: "b" }); await client.editFiles("sb-1", { paths: ["/tmp/b.txt"], find: "x" }); - await client.move("sb-1", "/tmp/a", "/tmp/b"); + await client.move("sb-1", { srcPath: "/tmp/a", dstPath: "/tmp/b" }); await client.copy("sb-1", "/tmp/b", "/tmp/c"); const fileExists = await client.exists("sb-1", "/tmp/c"); await client.tree("sb-1", "/workspace", { maxDepth: 2 }); diff --git a/tests/services/lsp.test.ts b/tests/services/lsp.test.ts index 72e3c22..9aa012f 100644 --- a/tests/services/lsp.test.ts +++ b/tests/services/lsp.test.ts @@ -7,21 +7,39 @@ import { createRecordedTransport, jsonOf } from "@tests/utils/helpers.ts"; test("lsp client sends expected request shapes", async () => { const { transport, calls } = createRecordedTransport(); const client = new LspClient(transport as never); - await client.start("sb-1", "typescript", "/workspace"); - await client.stop("sb-1", "typescript", "/workspace"); + await client.start("sb-1", { languageId: "typescript", pathToProject: "/workspace" }); + await client.stop("sb-1", { languageId: "typescript", pathToProject: "/workspace" }); await client.didOpenPath( "sb-1", - "typescript", - "/workspace", - "/workspace/a.ts", - "const x = 1;", - 1, + { + languageId: "typescript", + pathToProject: "/workspace", + path: "/workspace/a.ts", + text: "const x = 1;", + version: 1, + }, ); - await client.didClosePath("sb-1", "typescript", "/workspace", "/workspace/a.ts"); - await client.completionsPath("sb-1", "typescript", "/workspace", "/workspace/a.ts", 1, 2); - await client.documentSymbolsPath("sb-1", "typescript", "/workspace", "/workspace/a.ts"); + await client.didClosePath("sb-1", { + languageId: "typescript", + pathToProject: "/workspace", + path: "/workspace/a.ts", + }); + await client.completionsPath("sb-1", { + languageId: "typescript", + pathToProject: "/workspace", + path: "/workspace/a.ts", + line: 1, + character: 2, + }); + await client.documentSymbolsPath("sb-1", { + languageId: "typescript", + pathToProject: "/workspace", + path: "/workspace/a.ts", + }); assert.equal(calls[0]?.path, "/v1/sandbox/sb-1/lsp/start"); assert.deepEqual(jsonOf(calls[0]!), { language_id: "typescript", path_to_project: "/workspace" }); + assert.equal(calls[1]?.path, "/v1/sandbox/sb-1/lsp/stop"); + assert.deepEqual(jsonOf(calls[1]!), { language_id: "typescript", path_to_project: "/workspace" }); assert.deepEqual(jsonOf(calls[2]!), { language_id: "typescript", path_to_project: "/workspace", @@ -29,12 +47,24 @@ test("lsp client sends expected request shapes", async () => { text: "const x = 1;", version: 1, }); + assert.equal(calls[3]?.path, "/v1/sandbox/sb-1/lsp/did-close"); + assert.deepEqual(jsonOf(calls[3]!), { + language_id: "typescript", + path_to_project: "/workspace", + uri: "file:///workspace/a.ts", + }); assert.deepEqual(jsonOf(calls[4]!), { language_id: "typescript", path_to_project: "/workspace", uri: "file:///workspace/a.ts", position: { line: 1, character: 2 }, }); + assert.equal(calls[5]?.path, "/v1/sandbox/sb-1/lsp/document-symbols"); + assert.deepEqual(jsonOf(calls[5]!), { + language_id: "typescript", + path_to_project: "/workspace", + uri: "file:///workspace/a.ts", + }); }); test("lsp client throws a clear error for empty json responses", async () => { @@ -44,16 +74,20 @@ test("lsp client throws a clear error for empty json responses", async () => { const client = new LspClient(transport as never); await assert.rejects( - () => client.start("sb-1", "typescript", "/workspace"), + () => client.start("sb-1", { languageId: "typescript", pathToProject: "/workspace" }), /Empty response from \/v1\/sandbox\/sb-1\/lsp\/start/, ); }); -test("lsp didOpenPath keeps compatibility with options in the old slot", async () => { +test("lsp didOpenPath accepts request options separately", async () => { const { transport, calls } = createRecordedTransport(); const client = new LspClient(transport as never); - await client.didOpenPath("sb-1", "typescript", "/workspace", "/workspace/a.ts", { timeout: 5 }); + await client.didOpenPath( + "sb-1", + { languageId: "typescript", pathToProject: "/workspace", path: "/workspace/a.ts" }, + { timeout: 5 }, + ); assert.deepEqual(jsonOf(calls[0]!), { language_id: "typescript", diff --git a/tests/services/pty.test.ts b/tests/services/pty.test.ts index 2667617..e677fbf 100644 --- a/tests/services/pty.test.ts +++ b/tests/services/pty.test.ts @@ -69,7 +69,7 @@ test("pty client sends expected request shapes", async () => { await client.list("sb-1"); await client.create("sb-1", { sessionId: "sess-1", cols: 80, rows: 24, cwd: "/workspace" }); await client.get("sb-1", "sess-1"); - await client.resize("sb-1", "sess-1", 120, 40); + await client.resize("sb-1", { sessionId: "sess-1", cols: 120, rows: 40 }); await client.delete("sb-1", "sess-1"); assert.equal(calls[0]?.path, "/v1/sandbox/sb-1/pty"); assert.equal(calls[0]?.init.method, "GET"); diff --git a/tests/services/ssh.test.ts b/tests/services/ssh.test.ts index ec9700c..032548c 100644 --- a/tests/services/ssh.test.ts +++ b/tests/services/ssh.test.ts @@ -22,9 +22,9 @@ test("ssh client sends expected request shapes", async () => { }); const client = new SshClient(transport as never); await client.createAccess("sb-1"); - await client.validateAccess("sb-1", "ssh-1", "secret"); - await client.regenerateAccess("sb-1", "ssh-1"); - await client.deleteAccess("sb-1", "ssh-1"); + await client.validateAccess("sb-1", { id: "ssh-1", password: "secret" }); + await client.regenerateAccess("sb-1", { id: "ssh-1" }); + await client.deleteAccess("sb-1", { id: "ssh-1" }); assert.equal(calls.length, 4); assert.equal(calls[0]?.path, "/v1/sandbox/sb-1/ssh/access"); assert.equal(calls[1]?.path, "/v1/sandbox/sb-1/ssh/ssh-1/validate");