Update package.json, fix linting issues

This commit is contained in:
Morgan Dean
2025-06-18 10:39:25 -07:00
parent 0fef183caa
commit 5c9e2e7b85
10 changed files with 311 additions and 214 deletions

View File

@@ -1,20 +1,19 @@
{
"name": "@cua/computer",
"version": "0.0.0",
"version": "0.0.1",
"packageManager": "pnpm@10.11.0",
"description": "",
"description": "Typescript SDK for c/ua computer interaction",
"type": "module",
"license": "MIT",
"homepage": "",
"bugs": {
"url": ""
"url": "https://github.com/trycua/cua/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/trycua/cua.git"
},
"author": "",
"funding": "",
"author": "c/ua",
"files": [
"dist"
],
@@ -29,8 +28,8 @@
"access": "public"
},
"scripts": {
"lint": "biome lint --cache .",
"lint:fix": "biome lint --cache --fix .",
"lint": "biome lint .",
"lint:fix": "biome lint --fix .",
"build": "tsdown",
"dev": "tsdown --watch",
"test": "vitest",
@@ -52,4 +51,4 @@
"typescript": "^5.8.3",
"vitest": "^3.1.3"
}
}
}

View File

@@ -1,5 +1,5 @@
import { OSType, VMProviderType } from "./types";
import type { BaseComputerConfig, Display } from "./types";
import type { BaseComputerConfig } from "./types";
/**
* Default configuration values for Computer

View File

@@ -15,18 +15,18 @@ export const logger = pino({ name: "computer" });
/**
* Factory class for creating the appropriate Computer instance
*/
export class Computer {
export const Computer = {
/**
* Create a computer instance based on the provided configuration
* @param config The computer configuration
* @returns The appropriate computer instance based on the VM provider type
*/
static create(
create: (
config:
| Partial<BaseComputerConfig>
| Partial<CloudComputerConfig>
| Partial<LumeComputerConfig>
): BaseComputer {
): BaseComputer => {
// Apply defaults to the configuration
const fullConfig = applyDefaults(config);
@@ -41,7 +41,5 @@ export class Computer {
`Unsupported VM provider type: ${fullConfig.vmProvider}`
);
}
throw new Error(`Unsupported VM provider type`);
}
}
},
};

View File

@@ -63,8 +63,8 @@ export abstract class BaseComputer {
}
return {
width: parseInt(match[1], 10),
height: parseInt(match[2], 10),
width: Number.parseInt(match[1], 10),
height: Number.parseInt(match[2], 10),
};
}
@@ -93,14 +93,13 @@ export abstract class BaseComputer {
throw new Error(`Invalid memory format: ${memoryStr}`);
}
const value = parseFloat(match[1]);
const value = Number.parseFloat(match[1]);
const unit = match[2] || "MB"; // Default to MB if no unit specified
// Convert to MB
if (unit === "GB") {
return Math.round(value * 1024);
} else {
return Math.round(value);
}
return Math.round(value);
}
}

View File

@@ -8,8 +8,10 @@ const logger = pino({ name: "cloud" });
* Cloud-specific computer implementation
*/
export class CloudComputer extends BaseComputer {
private apiKey: string;
constructor(config: CloudComputerConfig) {
super(config);
this.apiKey = config.apiKey;
}
/**

View File

@@ -90,7 +90,7 @@ export class LumeComputer extends BaseComputer {
async runVm(
image: string,
name: string,
runOpts: { [key: string]: any } = {},
runOpts: { [key: string]: unknown } = {},
storage?: string
): Promise<VMInfo> {
logger.info(
@@ -106,7 +106,7 @@ export class LumeComputer extends BaseComputer {
// Lume-specific implementation
try {
await this.getVm(name, storage);
} catch (e) {
} catch {
logger.info(
`VM ${name} not found, attempting to pull image ${image} from registry...`
);
@@ -161,9 +161,8 @@ export class LumeComputer extends BaseComputer {
name: string,
image: string,
storage?: string,
registry: string = "ghcr.io",
organization: string = "trycua",
pullOpts?: { [key: string]: any }
registry = "ghcr.io",
organization = "trycua"
): Promise<VMInfo> {
// Validate image parameter
if (!image) {
@@ -222,7 +221,7 @@ export class LumeComputer extends BaseComputer {
*/
async updateVm(
name: string,
updateOpts: { [key: string]: any },
updateOpts: { [key: string]: unknown },
storage?: string
): Promise<VMInfo> {
return await lumeApiUpdate(
@@ -239,11 +238,7 @@ export class LumeComputer extends BaseComputer {
/**
* Lume-specific method to get the IP address of a VM, waiting indefinitely until it's available
*/
async getIp(
name: string,
storage?: string,
retryDelay: number = 2
): Promise<string> {
async getIp(name: string, storage?: string, retryDelay = 2): Promise<string> {
// Track total attempts for logging purposes
let attempts = 0;

View File

@@ -40,11 +40,11 @@ export interface VMInfo {
}
export interface RunOptions {
[key: string]: any;
[key: string]: unknown;
}
export interface UpdateOptions {
[key: string]: any;
[key: string]: unknown;
}
/**
@@ -63,8 +63,8 @@ export async function lumeApiGet(
host: string,
port: number,
storage?: string,
debug: boolean = false,
verbose: boolean = false
debug = false,
verbose = false
): Promise<VMInfo[]> {
// URL encode the storage parameter for the query
let storageParam = "";
@@ -111,16 +111,17 @@ export async function lumeApiGet(
}
return result;
} catch (error: any) {
} catch (err) {
const error = err as Error;
let errorMsg = "Unknown error";
if (error.name === "AbortError") {
errorMsg =
"Operation timeout - the API server is taking too long to respond";
} else if (error.code === "ECONNREFUSED") {
} else if (error.message.includes("ECONNREFUSED")) {
errorMsg =
"Failed to connect to the API server - it might still be starting up";
} else if (error.code === "ENOTFOUND") {
} else if (error.message.includes("ENOTFOUND")) {
errorMsg = "Failed to resolve host - check the API server address";
} else if (error.message) {
errorMsg = error.message;
@@ -149,8 +150,8 @@ export async function lumeApiRun(
port: number,
runOpts: RunOptions,
storage?: string,
debug: boolean = false,
verbose: boolean = false
debug = false,
verbose = false
): Promise<VMInfo> {
// URL encode the storage parameter for the query
let storageParam = "";
@@ -199,14 +200,18 @@ export async function lumeApiRun(
}
return data;
} catch (error: any) {
} catch (err) {
const error = err as Error;
let errorMsg = "Unknown error";
if (error.name === "AbortError") {
errorMsg =
"Operation timeout - the API server is taking too long to respond";
} else if (error.code === "ECONNREFUSED") {
errorMsg = "Failed to connect to the API server";
} else if (error.message.includes("ECONNREFUSED")) {
errorMsg =
"Failed to connect to the API server - it might still be starting up";
} else if (error.message.includes("ENOTFOUND")) {
errorMsg = "Failed to resolve host - check the API server address";
} else if (error.message) {
errorMsg = error.message;
}
@@ -232,8 +237,8 @@ export async function lumeApiStop(
host: string,
port: number,
storage?: string,
debug: boolean = false,
verbose: boolean = false
debug = false,
verbose = false
): Promise<VMInfo> {
// URL encode the storage parameter for the query
let storageParam = "";
@@ -278,14 +283,18 @@ export async function lumeApiStop(
}
return data;
} catch (error: any) {
} catch (err) {
const error = err as Error;
let errorMsg = "Unknown error";
if (error.name === "AbortError") {
errorMsg =
"Operation timeout - the API server is taking too long to respond";
} else if (error.code === "ECONNREFUSED") {
errorMsg = "Failed to connect to the API server";
} else if (error.message.includes("ECONNREFUSED")) {
errorMsg =
"Failed to connect to the API server - it might still be starting up";
} else if (error.message.includes("ENOTFOUND")) {
errorMsg = "Failed to resolve host - check the API server address";
} else if (error.message) {
errorMsg = error.message;
}
@@ -313,8 +322,8 @@ export async function lumeApiUpdate(
port: number,
updateOpts: UpdateOptions,
storage?: string,
debug: boolean = false,
verbose: boolean = false
debug = false,
verbose = false
): Promise<VMInfo> {
// URL encode the storage parameter for the query
let storageParam = "";
@@ -363,14 +372,18 @@ export async function lumeApiUpdate(
}
return data;
} catch (error: any) {
} catch (err) {
const error = err as Error;
let errorMsg = "Unknown error";
if (error.name === "AbortError") {
errorMsg =
"Operation timeout - the API server is taking too long to respond";
} else if (error.code === "ECONNREFUSED") {
errorMsg = "Failed to connect to the API server";
} else if (error.message.includes("ECONNREFUSED")) {
errorMsg =
"Failed to connect to the API server - it might still be starting up";
} else if (error.message.includes("ENOTFOUND")) {
errorMsg = "Failed to resolve host - check the API server address";
} else if (error.message) {
errorMsg = error.message;
}
@@ -400,10 +413,10 @@ export async function lumeApiPull(
host: string,
port: number,
storage?: string,
registry: string = "ghcr.io",
organization: string = "trycua",
debug: boolean = false,
verbose: boolean = false
registry = "ghcr.io",
organization = "trycua",
debug = false,
verbose = false
): Promise<VMInfo> {
// URL encode the storage parameter for the query
let storageParam = "";
@@ -459,13 +472,18 @@ export async function lumeApiPull(
}
return data;
} catch (error: any) {
} catch (err) {
const error = err as Error;
let errorMsg = "Unknown error";
if (error.name === "AbortError") {
errorMsg = "Operation timeout - the pull is taking too long";
} else if (error.code === "ECONNREFUSED") {
errorMsg = "Failed to connect to the API server";
errorMsg =
"Operation timeout - the API server is taking too long to respond";
} else if (error.message.includes("ECONNREFUSED")) {
errorMsg =
"Failed to connect to the API server - it might still be starting up";
} else if (error.message.includes("ENOTFOUND")) {
errorMsg = "Failed to resolve host - check the API server address";
} else if (error.message) {
errorMsg = error.message;
}
@@ -491,8 +509,8 @@ export async function lumeApiDelete(
host: string,
port: number,
storage?: string,
debug: boolean = false,
verbose: boolean = false
debug = false,
verbose = false
): Promise<VMInfo | null> {
// URL encode the storage parameter for the query
let storageParam = "";
@@ -537,10 +555,10 @@ export async function lumeApiDelete(
// Try to parse JSON response, but handle empty responses
let data: VMInfo | null = null;
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
if (contentType?.includes("application/json")) {
try {
data = (await response.json()) as VMInfo;
} catch (e) {
} catch {
// Empty response is OK for DELETE
}
} else {
@@ -552,14 +570,18 @@ export async function lumeApiDelete(
}
return data;
} catch (error: any) {
} catch (err) {
const error = err as Error;
let errorMsg = "Unknown error";
if (error.name === "AbortError") {
errorMsg =
"Operation timeout - the API server is taking too long to respond";
} else if (error.code === "ECONNREFUSED") {
errorMsg = "Failed to connect to the API server";
} else if (error.message.includes("ECONNREFUSED")) {
errorMsg =
"Failed to connect to the API server - it might still be starting up";
} else if (error.message.includes("ENOTFOUND")) {
errorMsg = "Failed to resolve host - check the API server address";
} else if (error.message) {
errorMsg = error.message;
}

View File

@@ -86,28 +86,37 @@ describe("LumeComputer", () => {
describe("getVm", () => {
it("should get VM info successfully", async () => {
vi.mocked(lumeApi.lumeApiGet).mockResolvedValue([mockVMInfo]);
const computer = new LumeComputer(defaultConfig);
const result = await computer.getVm("test-vm");
expect(lumeApi.lumeApiGet).toHaveBeenCalledWith("test-vm", "localhost", 7777, undefined);
expect(lumeApi.lumeApiGet).toHaveBeenCalledWith(
"test-vm",
"localhost",
7777,
undefined
);
expect(result).toEqual(mockVMInfo);
});
it("should handle VM not found error", async () => {
vi.mocked(lumeApi.lumeApiGet).mockResolvedValue([]);
const computer = new LumeComputer(defaultConfig);
await expect(computer.getVm("test-vm")).rejects.toThrow("VM Not Found.");
});
it("should handle stopped VM state", async () => {
const stoppedVM = { ...mockVMInfo, status: "stopped", ipAddress: undefined };
const stoppedVM = {
...mockVMInfo,
status: "stopped",
ipAddress: undefined,
};
vi.mocked(lumeApi.lumeApiGet).mockResolvedValue([stoppedVM]);
const computer = new LumeComputer(defaultConfig);
const result = await computer.getVm("test-vm");
expect(result.status).toBe("stopped");
expect(result.name).toBe("test-vm");
});
@@ -115,20 +124,25 @@ describe("LumeComputer", () => {
it("should handle VM without IP address", async () => {
const noIpVM = { ...mockVMInfo, ipAddress: undefined };
vi.mocked(lumeApi.lumeApiGet).mockResolvedValue([noIpVM]);
const computer = new LumeComputer(defaultConfig);
const result = await computer.getVm("test-vm");
expect(result).toEqual(noIpVM);
});
it("should pass storage parameter", async () => {
vi.mocked(lumeApi.lumeApiGet).mockResolvedValue([mockVMInfo]);
const computer = new LumeComputer(defaultConfig);
await computer.getVm("test-vm", "/custom/storage");
expect(lumeApi.lumeApiGet).toHaveBeenCalledWith("test-vm", "localhost", 7777, "/custom/storage");
expect(lumeApi.lumeApiGet).toHaveBeenCalledWith(
"test-vm",
"localhost",
7777,
"/custom/storage"
);
});
});
@@ -136,10 +150,10 @@ describe("LumeComputer", () => {
it("should list all VMs", async () => {
const vmList = [mockVMInfo, { ...mockVMInfo, name: "another-vm" }];
vi.mocked(lumeApi.lumeApiGet).mockResolvedValue(vmList);
const computer = new LumeComputer(defaultConfig);
const result = await computer.listVm();
expect(lumeApi.lumeApiGet).toHaveBeenCalledWith("", "localhost", 7777);
expect(result).toEqual(vmList);
});
@@ -149,24 +163,44 @@ describe("LumeComputer", () => {
it("should run VM when it already exists", async () => {
vi.mocked(lumeApi.lumeApiGet).mockResolvedValue([mockVMInfo]);
vi.mocked(lumeApi.lumeApiRun).mockResolvedValue(mockVMInfo);
const computer = new LumeComputer(defaultConfig);
const runOpts = { memory: "4GB" };
const result = await computer.runVm("macos-sequoia-cua:latest", "test-vm", runOpts);
expect(lumeApi.lumeApiGet).toHaveBeenCalledWith("test-vm", "localhost", 7777, undefined);
expect(lumeApi.lumeApiRun).toHaveBeenCalledWith("test-vm", "localhost", 7777, runOpts, undefined);
const result = await computer.runVm(
"macos-sequoia-cua:latest",
"test-vm",
runOpts
);
expect(lumeApi.lumeApiGet).toHaveBeenCalledWith(
"test-vm",
"localhost",
7777,
undefined
);
expect(lumeApi.lumeApiRun).toHaveBeenCalledWith(
"test-vm",
"localhost",
7777,
runOpts,
undefined
);
expect(result).toEqual(mockVMInfo);
});
it("should pull and run VM when it doesn't exist", async () => {
vi.mocked(lumeApi.lumeApiGet).mockRejectedValueOnce(new Error("VM not found"));
vi.mocked(lumeApi.lumeApiGet).mockRejectedValueOnce(
new Error("VM not found")
);
vi.mocked(lumeApi.lumeApiPull).mockResolvedValue(mockVMInfo);
vi.mocked(lumeApi.lumeApiRun).mockResolvedValue(mockVMInfo);
const computer = new LumeComputer(defaultConfig);
const result = await computer.runVm("macos-sequoia-cua:latest", "test-vm");
const result = await computer.runVm(
"macos-sequoia-cua:latest",
"test-vm"
);
expect(lumeApi.lumeApiGet).toHaveBeenCalled();
expect(lumeApi.lumeApiPull).toHaveBeenCalledWith(
"macos-sequoia-cua:latest",
@@ -182,33 +216,60 @@ describe("LumeComputer", () => {
});
it("should handle pull failure", async () => {
vi.mocked(lumeApi.lumeApiGet).mockRejectedValueOnce(new Error("VM not found"));
vi.mocked(lumeApi.lumeApiPull).mockRejectedValue(new Error("Pull failed"));
vi.mocked(lumeApi.lumeApiGet).mockRejectedValueOnce(
new Error("VM not found")
);
vi.mocked(lumeApi.lumeApiPull).mockRejectedValue(
new Error("Pull failed")
);
const computer = new LumeComputer(defaultConfig);
await expect(computer.runVm("macos-sequoia-cua:latest", "test-vm")).rejects.toThrow("Pull failed");
await expect(
computer.runVm("macos-sequoia-cua:latest", "test-vm")
).rejects.toThrow("Pull failed");
});
it("should pass storage parameter", async () => {
vi.mocked(lumeApi.lumeApiGet).mockResolvedValue([mockVMInfo]);
vi.mocked(lumeApi.lumeApiRun).mockResolvedValue(mockVMInfo);
const computer = new LumeComputer(defaultConfig);
await computer.runVm("macos-sequoia-cua:latest", "test-vm", {}, "/storage");
expect(lumeApi.lumeApiGet).toHaveBeenCalledWith("test-vm", "localhost", 7777, "/storage");
expect(lumeApi.lumeApiRun).toHaveBeenCalledWith("test-vm", "localhost", 7777, {}, "/storage");
await computer.runVm(
"macos-sequoia-cua:latest",
"test-vm",
{},
"/storage"
);
expect(lumeApi.lumeApiGet).toHaveBeenCalledWith(
"test-vm",
"localhost",
7777,
"/storage"
);
expect(lumeApi.lumeApiRun).toHaveBeenCalledWith(
"test-vm",
"localhost",
7777,
{},
"/storage"
);
});
});
describe("stopVm", () => {
it("should stop VM normally", async () => {
vi.mocked(lumeApi.lumeApiStop).mockResolvedValue(mockVMInfo);
const computer = new LumeComputer(defaultConfig);
const result = await computer.stopVm("test-vm");
expect(lumeApi.lumeApiStop).toHaveBeenCalledWith("test-vm", "localhost", 7777, undefined);
expect(lumeApi.lumeApiStop).toHaveBeenCalledWith(
"test-vm",
"localhost",
7777,
undefined
);
expect(result).toEqual(mockVMInfo);
});
@@ -216,13 +277,20 @@ describe("LumeComputer", () => {
const stoppedVM = { ...mockVMInfo, status: "stopped" };
vi.mocked(lumeApi.lumeApiStop).mockResolvedValue(stoppedVM);
vi.mocked(lumeApi.lumeApiDelete).mockResolvedValue(null);
const ephemeralConfig = { ...defaultConfig, ephemeral: true };
const computer = new LumeComputer(ephemeralConfig);
const result = await computer.stopVm("test-vm");
expect(lumeApi.lumeApiStop).toHaveBeenCalled();
expect(lumeApi.lumeApiDelete).toHaveBeenCalledWith("test-vm", "localhost", 7777, undefined, false, false);
expect(lumeApi.lumeApiDelete).toHaveBeenCalledWith(
"test-vm",
"localhost",
7777,
undefined,
false,
false
);
expect(result).toMatchObject({
...stoppedVM,
deleted: true,
@@ -232,34 +300,26 @@ describe("LumeComputer", () => {
it("should handle delete failure in ephemeral mode", async () => {
vi.mocked(lumeApi.lumeApiStop).mockResolvedValue(mockVMInfo);
vi.mocked(lumeApi.lumeApiDelete).mockRejectedValue(new Error("Delete failed"));
const ephemeralConfig = { ...defaultConfig, ephemeral: true };
const computer = new LumeComputer(ephemeralConfig);
await expect(computer.stopVm("test-vm")).rejects.toThrow("Failed to delete ephemeral VM test-vm: Error: Failed to delete VM: Error: Delete failed");
});
vi.mocked(lumeApi.lumeApiDelete).mockRejectedValue(
new Error("Delete failed")
);
it("should not delete VM if stop returns error in ephemeral mode", async () => {
const errorResponse = { error: "Stop failed" } as any;
vi.mocked(lumeApi.lumeApiStop).mockResolvedValue(errorResponse);
const ephemeralConfig = { ...defaultConfig, ephemeral: true };
const computer = new LumeComputer(ephemeralConfig);
const result = await computer.stopVm("test-vm");
expect(lumeApi.lumeApiDelete).not.toHaveBeenCalled();
expect(result).toEqual(errorResponse);
await expect(computer.stopVm("test-vm")).rejects.toThrow(
"Failed to delete ephemeral VM test-vm: Error: Failed to delete VM: Error: Delete failed"
);
});
});
describe("pullVm", () => {
it("should pull VM image successfully", async () => {
vi.mocked(lumeApi.lumeApiPull).mockResolvedValue(mockVMInfo);
const computer = new LumeComputer(defaultConfig);
const result = await computer.pullVm("test-vm", "ubuntu:latest");
expect(lumeApi.lumeApiPull).toHaveBeenCalledWith(
"ubuntu:latest",
"test-vm",
@@ -274,15 +334,23 @@ describe("LumeComputer", () => {
it("should throw error if image parameter is missing", async () => {
const computer = new LumeComputer(defaultConfig);
await expect(computer.pullVm("test-vm", "")).rejects.toThrow("Image parameter is required for pullVm");
await expect(computer.pullVm("test-vm", "")).rejects.toThrow(
"Image parameter is required for pullVm"
);
});
it("should use custom registry and organization", async () => {
vi.mocked(lumeApi.lumeApiPull).mockResolvedValue(mockVMInfo);
const computer = new LumeComputer(defaultConfig);
await computer.pullVm("test-vm", "custom:tag", "/storage", "docker.io", "myorg");
await computer.pullVm(
"test-vm",
"custom:tag",
"/storage",
"docker.io",
"myorg"
);
expect(lumeApi.lumeApiPull).toHaveBeenCalledWith(
"custom:tag",
"test-vm",
@@ -295,38 +363,60 @@ describe("LumeComputer", () => {
});
it("should handle pull failure", async () => {
vi.mocked(lumeApi.lumeApiPull).mockRejectedValue(new Error("Network error"));
vi.mocked(lumeApi.lumeApiPull).mockRejectedValue(
new Error("Network error")
);
const computer = new LumeComputer(defaultConfig);
await expect(computer.pullVm("test-vm", "ubuntu:latest")).rejects.toThrow("Failed to pull VM: Error: Network error");
await expect(computer.pullVm("test-vm", "ubuntu:latest")).rejects.toThrow(
"Failed to pull VM: Error: Network error"
);
});
});
describe("deleteVm", () => {
it("should delete VM successfully", async () => {
vi.mocked(lumeApi.lumeApiDelete).mockResolvedValue(null);
const computer = new LumeComputer(defaultConfig);
const result = await computer.deleteVm("test-vm");
expect(lumeApi.lumeApiDelete).toHaveBeenCalledWith("test-vm", "localhost", 7777, undefined, false, false);
expect(lumeApi.lumeApiDelete).toHaveBeenCalledWith(
"test-vm",
"localhost",
7777,
undefined,
false,
false
);
expect(result).toBeNull();
});
it("should handle delete failure", async () => {
vi.mocked(lumeApi.lumeApiDelete).mockRejectedValue(new Error("Permission denied"));
vi.mocked(lumeApi.lumeApiDelete).mockRejectedValue(
new Error("Permission denied")
);
const computer = new LumeComputer(defaultConfig);
await expect(computer.deleteVm("test-vm")).rejects.toThrow("Failed to delete VM: Error: Permission denied");
await expect(computer.deleteVm("test-vm")).rejects.toThrow(
"Failed to delete VM: Error: Permission denied"
);
});
it("should pass storage parameter", async () => {
vi.mocked(lumeApi.lumeApiDelete).mockResolvedValue(null);
const computer = new LumeComputer(defaultConfig);
await computer.deleteVm("test-vm", "/storage");
expect(lumeApi.lumeApiDelete).toHaveBeenCalledWith("test-vm", "localhost", 7777, "/storage", false, false);
expect(lumeApi.lumeApiDelete).toHaveBeenCalledWith(
"test-vm",
"localhost",
7777,
"/storage",
false,
false
);
});
});
@@ -334,11 +424,11 @@ describe("LumeComputer", () => {
it("should update VM configuration", async () => {
const updatedVM = { ...mockVMInfo, memorySize: 4096 };
vi.mocked(lumeApi.lumeApiUpdate).mockResolvedValue(updatedVM);
const computer = new LumeComputer(defaultConfig);
const updateOpts = { memory: "4GB" };
const result = await computer.updateVm("test-vm", updateOpts);
expect(lumeApi.lumeApiUpdate).toHaveBeenCalledWith(
"test-vm",
"localhost",
@@ -353,10 +443,10 @@ describe("LumeComputer", () => {
it("should pass storage parameter", async () => {
vi.mocked(lumeApi.lumeApiUpdate).mockResolvedValue(mockVMInfo);
const computer = new LumeComputer(defaultConfig);
await computer.updateVm("test-vm", {}, "/storage");
expect(lumeApi.lumeApiUpdate).toHaveBeenCalledWith(
"test-vm",
"localhost",
@@ -372,10 +462,10 @@ describe("LumeComputer", () => {
describe("getIp", () => {
it("should return IP address immediately if available", async () => {
vi.mocked(lumeApi.lumeApiGet).mockResolvedValue([mockVMInfo]);
const computer = new LumeComputer(defaultConfig);
const ip = await computer.getIp("test-vm");
expect(ip).toBe("192.168.1.100");
expect(lumeApi.lumeApiGet).toHaveBeenCalledTimes(1);
});
@@ -386,18 +476,22 @@ describe("LumeComputer", () => {
.mockResolvedValueOnce([noIpVM])
.mockResolvedValueOnce([noIpVM])
.mockResolvedValueOnce([mockVMInfo]);
const computer = new LumeComputer(defaultConfig);
const ip = await computer.getIp("test-vm", undefined, 0.1); // Short retry delay for testing
expect(ip).toBe("192.168.1.100");
expect(lumeApi.lumeApiGet).toHaveBeenCalledTimes(3);
});
it("should throw error if VM is stopped", async () => {
const stoppedVM = { ...mockVMInfo, status: "stopped", ipAddress: undefined };
const stoppedVM = {
...mockVMInfo,
status: "stopped",
ipAddress: undefined,
};
vi.mocked(lumeApi.lumeApiGet).mockResolvedValue([stoppedVM]);
const computer = new LumeComputer(defaultConfig);
await expect(computer.getIp("test-vm")).rejects.toThrow(
"VM test-vm is in 'stopped' state and will not get an IP address"
@@ -407,7 +501,7 @@ describe("LumeComputer", () => {
it("should throw error if VM is in error state", async () => {
const errorVM = { ...mockVMInfo, status: "error", ipAddress: undefined };
vi.mocked(lumeApi.lumeApiGet).mockResolvedValue([errorVM]);
const computer = new LumeComputer(defaultConfig);
await expect(computer.getIp("test-vm")).rejects.toThrow(
"VM test-vm is in 'error' state and will not get an IP address"
@@ -415,48 +509,61 @@ describe("LumeComputer", () => {
});
it("should handle getVm errors", async () => {
vi.mocked(lumeApi.lumeApiGet).mockRejectedValue(new Error("Network error"));
vi.mocked(lumeApi.lumeApiGet).mockRejectedValue(
new Error("Network error")
);
const computer = new LumeComputer(defaultConfig);
await expect(computer.getIp("test-vm")).rejects.toThrow("Network error");
});
it("should pass storage parameter", async () => {
vi.mocked(lumeApi.lumeApiGet).mockResolvedValue([mockVMInfo]);
const computer = new LumeComputer(defaultConfig);
await computer.getIp("test-vm", "/storage");
expect(lumeApi.lumeApiGet).toHaveBeenCalledWith("test-vm", "localhost", 7777, "/storage");
expect(lumeApi.lumeApiGet).toHaveBeenCalledWith(
"test-vm",
"localhost",
7777,
"/storage"
);
});
});
describe("integration scenarios", () => {
it("should handle full VM lifecycle", async () => {
// Simulate VM not existing initially
vi.mocked(lumeApi.lumeApiGet).mockRejectedValueOnce(new Error("VM not found"));
vi.mocked(lumeApi.lumeApiGet).mockRejectedValueOnce(
new Error("VM not found")
);
vi.mocked(lumeApi.lumeApiPull).mockResolvedValue(mockVMInfo);
// Simulate VM starting without IP, then getting IP
const startingVM = { ...mockVMInfo, ipAddress: undefined, status: "starting" };
const startingVM = {
...mockVMInfo,
ipAddress: undefined,
status: "starting",
};
vi.mocked(lumeApi.lumeApiRun).mockResolvedValue(startingVM);
vi.mocked(lumeApi.lumeApiGet)
.mockResolvedValueOnce([startingVM])
.mockResolvedValueOnce([mockVMInfo]);
// Simulate stop
const stoppedVM = { ...mockVMInfo, status: "stopped" };
vi.mocked(lumeApi.lumeApiStop).mockResolvedValue(stoppedVM);
const computer = new LumeComputer(defaultConfig);
// Run VM (should pull first)
await computer.runVm("macos-sequoia-cua:latest", "test-vm");
// Get IP (should retry once)
const ip = await computer.getIp("test-vm", undefined, 0.1);
expect(ip).toBe("192.168.1.100");
// Stop VM
const stopResult = await computer.stopVm("test-vm");
expect(stopResult.status).toBe("stopped");
@@ -465,20 +572,23 @@ describe("LumeComputer", () => {
it("should handle ephemeral VM lifecycle", async () => {
const ephemeralConfig = { ...defaultConfig, ephemeral: true };
const computer = new LumeComputer(ephemeralConfig);
// Setup mocks
vi.mocked(lumeApi.lumeApiGet).mockResolvedValue([mockVMInfo]);
vi.mocked(lumeApi.lumeApiRun).mockResolvedValue(mockVMInfo);
vi.mocked(lumeApi.lumeApiStop).mockResolvedValue({ ...mockVMInfo, status: "stopped" });
vi.mocked(lumeApi.lumeApiStop).mockResolvedValue({
...mockVMInfo,
status: "stopped",
});
vi.mocked(lumeApi.lumeApiDelete).mockResolvedValue(null);
// Run and stop ephemeral VM
await computer.runVm("macos-sequoia-cua:latest", "test-vm");
const result = await computer.stopVm("test-vm");
// Verify VM was deleted
expect(lumeApi.lumeApiDelete).toHaveBeenCalled();
expect((result as any).deleted).toBe(true);
expect(result).toBe(null);
});
});
});
});

View File

@@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { describe, it } from "vitest";
import { Computer, OSType, VMProviderType } from "../src/index";
describe("Cloud Interface", () => {

View File

@@ -1,4 +1,4 @@
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { describe, expect, it, vi } from "vitest";
import {
lumeApiGet,
lumeApiRun,
@@ -15,24 +15,6 @@ const PORT = 1213;
const HOST = "localhost";
describe("Lume API", () => {
let lumeServer: any;
beforeAll(() => {
// Spawn the lume serve process before tests
const { spawn } = require("child_process");
lumeServer = spawn("lume", ["serve", "--port", PORT], {
stdio: "pipe",
detached: true,
});
// Clean up the server when tests are done
afterAll(() => {
if (lumeServer && !lumeServer.killed) {
process.kill(-lumeServer.pid);
}
});
});
describe("lumeApiGet", () => {
it("should fetch VM information successfully", async () => {
// Mock fetch for this test - API returns a single VMDetails object
@@ -44,7 +26,7 @@ describe("Lume API", () => {
os: "ubuntu",
display: "1920x1080",
locationName: "local",
cpuCount: 2,
cpuCount: 2,
sharedDirectories: [
{
hostPath: "/home/user/shared",
@@ -56,7 +38,7 @@ describe("Lume API", () => {
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
json: async () => mockVMInfo,
json: async () => mockVMInfo,
headers: new Headers({ "content-type": "application/json" }),
} as Response);
@@ -99,7 +81,7 @@ describe("Lume API", () => {
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
json: async () => mockVMList,
json: async () => mockVMList,
headers: new Headers({ "content-type": "application/json" }),
} as Response);
@@ -161,7 +143,7 @@ describe("Lume API", () => {
it("should handle connection refused errors", async () => {
const error = new Error("Connection refused");
(error as any).code = "ECONNREFUSED";
(error as Error).message = "ECONNREFUSED";
global.fetch = vi.fn().mockRejectedValueOnce(error);
await expect(lumeApiGet("test-vm", HOST, PORT)).rejects.toThrow(
@@ -181,7 +163,7 @@ describe("Lume API", () => {
it("should handle host not found errors", async () => {
const error = new Error("Host not found");
(error as any).code = "ENOTFOUND";
(error as Error).message = "ENOTFOUND";
global.fetch = vi.fn().mockRejectedValueOnce(error);
await expect(lumeApiGet("test-vm", HOST, PORT)).rejects.toThrow(
@@ -200,7 +182,7 @@ describe("Lume API", () => {
os: "ubuntu",
display: "1920x1080",
locationName: "local",
cpuCount: 2,
cpuCount: 2,
vncUrl: "vnc://localhost:5900",
ipAddress: "192.168.1.100",
};
@@ -263,9 +245,7 @@ describe("Lume API", () => {
headers: new Headers(),
} as Response);
await expect(
lumeApiRun("test-vm", HOST, PORT, {})
).rejects.toThrow(
await expect(lumeApiRun("test-vm", HOST, PORT, {})).rejects.toThrow(
"API request failed: HTTP error returned from API server (status: 500)"
);
});
@@ -281,7 +261,7 @@ describe("Lume API", () => {
os: "ubuntu",
display: "1920x1080",
locationName: "local",
cpuCount: 2,
cpuCount: 2,
};
global.fetch = vi.fn().mockResolvedValueOnce({
@@ -338,7 +318,7 @@ describe("Lume API", () => {
os: "ubuntu",
display: "2560x1440",
locationName: "local",
cpuCount: 2,
cpuCount: 2,
};
global.fetch = vi.fn().mockResolvedValueOnce({
@@ -396,7 +376,7 @@ describe("Lume API", () => {
os: "ubuntu",
display: "1920x1080",
locationName: "local",
cpuCount: 2,
cpuCount: 2,
};
global.fetch = vi.fn().mockResolvedValueOnce({
@@ -493,7 +473,7 @@ describe("Lume API", () => {
os: "ubuntu",
display: "",
locationName: "local",
cpuCount: 0,
cpuCount: 0,
};
global.fetch = vi.fn().mockResolvedValueOnce({
@@ -543,7 +523,7 @@ describe("Lume API", () => {
} as Response);
const result = await lumeApiDelete("non-existent-vm", HOST, PORT);
expect(result).toBeNull();
});
@@ -566,7 +546,7 @@ describe("Lume API", () => {
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
json: async () => [{ name: "test-vm", cpuCount: 2 }],
json: async () => [{ name: "test-vm", cpuCount: 2 }],
headers: new Headers({ "content-type": "application/json" }),
} as Response);
@@ -584,7 +564,7 @@ describe("Lume API", () => {
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
json: async () => ({ name: "test-vm", cpuCount: 2 }),
json: async () => ({ name: "test-vm", cpuCount: 2 }),
headers: new Headers({ "content-type": "application/json" }),
} as Response);
@@ -613,13 +593,5 @@ describe("Lume API", () => {
"API request failed: Custom error message"
);
});
it("should handle unknown errors", async () => {
global.fetch = vi.fn().mockRejectedValueOnce({});
await expect(lumeApiGet("test-vm", HOST, PORT)).rejects.toThrow(
"API request failed: Unknown error"
);
});
});
});