convex
Version:
Client for the Convex Cloud
1,098 lines (957 loc) • 32.5 kB
text/typescript
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
// @inquirer/testing/vitest must be imported before modules that use @inquirer/*
import { screen } from "@inquirer/testing/vitest";
import {
typedPlatformClient,
typedBigBrainClient,
selectRegion,
logNoDefaultRegionMessage,
} from "./lib/utils/utils.js";
import { PlatformProjectDetails } from "@convex-dev/platform/managementApi";
import {
getDeploymentSelection,
getProjectDetails,
deploymentNameFromSelection,
} from "./lib/deploymentSelection.js";
import { saveSelectedDeployment } from "./deploymentSelect.js";
import {
deploymentCreate,
resolveRegionDetails,
resolveClassDetails,
} from "./deploymentCreate.js";
import { ensureBackendBinaryDownloaded } from "./lib/localDeployment/download.js";
import {
loadProjectLocalConfig,
saveDeploymentConfig,
} from "./lib/localDeployment/filePaths.js";
import {
chooseLocalBackendPorts,
LOCAL_BACKEND_INSTANCE_SECRET,
} from "./lib/localDeployment/utils.js";
import { bigBrainStart } from "./lib/localDeployment/bigBrain.js";
vi.mock("@sentry/node", () => ({
captureException: vi.fn(),
close: vi.fn(),
}));
vi.mock("./lib/utils/utils.js", () => ({
typedPlatformClient: vi.fn(),
typedBigBrainClient: vi.fn(),
selectRegion: vi.fn(),
logNoDefaultRegionMessage: vi.fn(),
}));
vi.mock("./lib/deploymentSelection.js", () => ({
initializeBigBrainAuth: vi.fn(),
getDeploymentSelection: vi.fn(),
getProjectDetails: vi.fn(),
deploymentNameFromSelection: vi.fn().mockReturnValue(null),
}));
vi.mock("./deploymentSelect.js", () => ({
saveSelectedDeployment: vi.fn(),
}));
vi.mock("./lib/localDeployment/download.js", () => ({
ensureBackendBinaryDownloaded: vi.fn(),
}));
vi.mock("./lib/localDeployment/filePaths.js", () => ({
loadProjectLocalConfig: vi.fn(),
saveDeploymentConfig: vi.fn(),
}));
vi.mock("./lib/localDeployment/utils.js", () => ({
chooseLocalBackendPorts: vi.fn(),
LOCAL_BACKEND_INSTANCE_SECRET: "MockSecret123",
}));
vi.mock("./lib/localDeployment/bigBrain.js", () => ({
bigBrainStart: vi.fn(),
}));
const mockRegions = [
{
name: "aws-us-east-1" as const,
displayName: "US East (Virginia)",
available: true,
},
{
name: "aws-eu-west-1" as const,
displayName: "EU West (Ireland)",
available: true,
},
];
const mockClasses = [
{
type: "s16" as const,
available: true,
},
{
type: "s256" as const,
available: true,
},
{
type: "d1024" as const,
available: false,
},
];
const mockPlatformGet = vi.fn();
const mockPlatformPost = vi.fn();
const mockBigBrainGet = vi.fn();
function setupPlatformClient() {
vi.mocked(typedPlatformClient).mockReturnValue({
GET: mockPlatformGet,
POST: mockPlatformPost,
} as any);
vi.mocked(typedBigBrainClient).mockReturnValue({
GET: mockBigBrainGet,
} as any);
}
// Suppress process.exit and stderr
beforeEach(() => {
vi.spyOn(process, "exit").mockImplementation((() => {
throw new Error("process.exit called");
}) as any);
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
mockPlatformGet.mockReset();
mockPlatformPost.mockReset();
mockBigBrainGet.mockReset();
});
afterEach(() => {
vi.restoreAllMocks();
});
const fakeProject = {
id: 123,
teamId: 456,
slug: "my-project",
createTime: 0,
name: "My Project",
teamSlug: "my-team",
} satisfies PlatformProjectDetails;
const createdDeployment = {
kind: "cloud" as const,
reference: "dev/my-deployment",
deploymentType: "dev" as const,
isDefault: false,
};
function setupPlatformForCreate(overrides?: Record<string, unknown>) {
setupPlatformClient();
mockPlatformGet.mockImplementation((path: string) => {
if (path === "/teams/{team_id}/list_deployment_regions") {
return { data: { items: mockRegions } };
}
if (path === "/teams/{team_id}/list_deployment_classes") {
return { data: { items: mockClasses } };
}
throw new Error(`Unmocked GET route: ${path}`);
});
mockPlatformPost.mockResolvedValue({
data: { ...createdDeployment, ...overrides },
});
}
describe("non-interactive create flow", () => {
beforeEach(() => {
vi.mocked(getDeploymentSelection).mockReset();
vi.mocked(getProjectDetails).mockReset();
vi.mocked(saveSelectedDeployment).mockReset();
vi.mocked(deploymentNameFromSelection).mockReset();
vi.mocked(deploymentNameFromSelection).mockReturnValue(null);
});
describe("validation errors", () => {
test("crashes when no ref and no --default", async () => {
await expect(
deploymentCreate.parseAsync(["--type", "dev"], { from: "user" }),
).rejects.toThrow();
expect(process.stderr.write).toHaveBeenCalledWith(
expect.stringContaining("Specify a deployment ref"),
);
});
test("crashes when --type is missing", async () => {
await expect(
deploymentCreate.parseAsync(["my-deployment"], { from: "user" }),
).rejects.toThrow();
expect(process.stderr.write).toHaveBeenCalledWith(
expect.stringContaining("--type is required"),
);
});
test("creates a local deployment: downloads binary, chooses ports, registers with Big Brain, saves config", async () => {
vi.mocked(getDeploymentSelection).mockResolvedValue({
kind: "existingDeployment",
deploymentToActOn: {
url: "https://joyful-capybara-123.convex.cloud",
adminKey: "admin-key",
deploymentFields: {
deploymentName: "joyful-capybara-123",
deploymentType: "dev",
teamSlug: "my-team",
projectSlug: "my-project",
},
source: "deployKey" as const,
},
});
vi.mocked(getProjectDetails).mockResolvedValue(fakeProject);
vi.mocked(loadProjectLocalConfig).mockReturnValue(null);
vi.mocked(ensureBackendBinaryDownloaded).mockResolvedValue({
binaryPath: "/path",
version: "1.0.0",
});
vi.mocked(chooseLocalBackendPorts).mockResolvedValue({
cloudPort: 3210,
sitePort: 3211,
});
vi.mocked(bigBrainStart).mockResolvedValue({
deploymentName: "local-test-123",
adminKey: "test-key",
});
setupPlatformClient();
mockPlatformGet.mockResolvedValue({ data: { items: [] } });
await deploymentCreate.parseAsync(["local"], { from: "user" });
expect(saveDeploymentConfig).toHaveBeenCalledWith(
expect.anything(),
"local",
"local-test-123",
{
backendVersion: "1.0.0",
ports: { cloud: 3210, site: 3211 },
adminKey: "test-key",
instanceSecret: LOCAL_BACKEND_INSTANCE_SECRET,
},
);
expect(mockPlatformPost).not.toHaveBeenCalled();
});
test("creates a local deployment with --select and selects it", async () => {
vi.mocked(getDeploymentSelection).mockResolvedValue({
kind: "existingDeployment",
deploymentToActOn: {
url: "https://joyful-capybara-123.convex.cloud",
adminKey: "admin-key",
deploymentFields: {
deploymentName: "joyful-capybara-123",
deploymentType: "dev",
teamSlug: "my-team",
projectSlug: "my-project",
},
source: "deployKey" as const,
},
});
vi.mocked(getProjectDetails).mockResolvedValue(fakeProject);
vi.mocked(loadProjectLocalConfig).mockReturnValue(null);
vi.mocked(ensureBackendBinaryDownloaded).mockResolvedValue({
binaryPath: "/path",
version: "1.0.0",
});
vi.mocked(chooseLocalBackendPorts).mockResolvedValue({
cloudPort: 3210,
sitePort: 3211,
});
vi.mocked(bigBrainStart).mockResolvedValue({
deploymentName: "local-test-123",
adminKey: "test-key",
});
setupPlatformClient();
mockPlatformGet.mockResolvedValue({ data: { items: [] } });
await deploymentCreate.parseAsync(["local", "--select"], {
from: "user",
});
expect(saveSelectedDeployment).toHaveBeenCalledWith(
expect.anything(),
"local",
{
kind: "deploymentWithinProject",
targetProject: {
kind: "deploymentName",
deploymentName: "local-test-123",
deploymentType: "local",
},
selectionWithinProject: {
kind: "deploymentSelector",
selector: "local",
},
},
null,
);
});
test("crashes when creating a local deployment with --type", async () => {
await expect(
deploymentCreate.parseAsync(["local", "--type", "dev"], {
from: "user",
}),
).rejects.toThrow();
expect(process.stderr.write).toHaveBeenCalledWith(
expect.stringContaining(
"--type cannot be used when creating a local deployment",
),
);
expect(mockPlatformPost).not.toHaveBeenCalled();
});
test("errors when local deployment already exists", async () => {
vi.mocked(getDeploymentSelection).mockResolvedValue({
kind: "existingDeployment",
deploymentToActOn: {
url: "https://joyful-capybara-123.convex.cloud",
adminKey: "admin-key",
deploymentFields: {
deploymentName: "joyful-capybara-123",
deploymentType: "dev",
teamSlug: "my-team",
projectSlug: "my-project",
},
source: "deployKey" as const,
},
});
vi.mocked(loadProjectLocalConfig).mockReturnValue({
deploymentName: "existing-local-123",
config: {} as any,
});
await expect(
deploymentCreate.parseAsync(["local"], { from: "user" }),
).rejects.toThrow();
expect(process.stderr.write).toHaveBeenCalledWith(
expect.stringContaining("A local deployment already exists"),
);
});
test.each(["region", "class", "default", "expiration"] as const)(
"rejects --%s with local",
async (flag) => {
const args = ["local", `--${flag}`];
if (flag === "region") args.push("us");
if (flag === "class") args.push("s16");
if (flag === "expiration") args.push("none");
await expect(
deploymentCreate.parseAsync(args, { from: "user" }),
).rejects.toThrow();
expect(process.stderr.write).toHaveBeenCalledWith(
expect.stringContaining(
`--${flag} cannot be used when creating a local deployment`,
),
);
},
);
});
describe("with project configured", () => {
beforeEach(() => {
vi.mocked(getDeploymentSelection).mockResolvedValue({
kind: "existingDeployment",
deploymentToActOn: {
url: "https://joyful-capybara-123.convex.cloud",
adminKey: "admin-key",
deploymentFields: {
deploymentName: "joyful-capybara-123",
deploymentType: "dev",
teamSlug: "my-team",
projectSlug: "my-project",
},
source: "deployKey" as const,
},
});
vi.mocked(getProjectDetails).mockResolvedValue(fakeProject);
});
test("creates a dev deployment with ref and --type dev", async () => {
setupPlatformForCreate();
await deploymentCreate.parseAsync(["my-deployment", "--type", "dev"], {
from: "user",
});
expect(mockPlatformPost).toHaveBeenCalledWith(
"/projects/{project_id}/create_deployment",
expect.objectContaining({
params: { path: { project_id: 123 } },
body: {
type: "dev",
region: null,
reference: "my-deployment",
isDefault: null,
},
}),
);
});
test("creates a prod deployment with ref and --type prod", async () => {
setupPlatformForCreate({
deploymentType: "prod",
reference: "staging",
});
await deploymentCreate.parseAsync(["staging", "--type", "prod"], {
from: "user",
});
expect(mockPlatformPost).toHaveBeenCalledWith(
"/projects/{project_id}/create_deployment",
expect.objectContaining({
body: expect.objectContaining({
type: "prod",
reference: "staging",
}),
}),
);
});
test("creates a deployment with --default flag", async () => {
setupPlatformForCreate({ isDefault: true });
await deploymentCreate.parseAsync(
["my-deployment", "--type", "dev", "--default"],
{ from: "user" },
);
expect(mockPlatformPost).toHaveBeenCalledWith(
"/projects/{project_id}/create_deployment",
expect.objectContaining({
body: expect.objectContaining({
isDefault: true,
}),
}),
);
});
test("creates a default deployment without a ref", async () => {
setupPlatformForCreate({ isDefault: true, reference: null });
await deploymentCreate.parseAsync(["--type", "dev", "--default"], {
from: "user",
});
expect(mockPlatformPost).toHaveBeenCalledWith(
"/projects/{project_id}/create_deployment",
expect.objectContaining({
body: {
type: "dev",
region: null,
reference: null,
isDefault: true,
},
}),
);
});
test("creates a deployment with --region full name", async () => {
setupPlatformForCreate();
await deploymentCreate.parseAsync(
["my-deployment", "--type", "dev", "--region", "aws-eu-west-1"],
{ from: "user" },
);
expect(mockPlatformGet).toHaveBeenCalledWith(
"/teams/{team_id}/list_deployment_regions",
expect.objectContaining({
params: { path: { team_id: "456" } },
}),
);
expect(mockPlatformPost).toHaveBeenCalledWith(
"/projects/{project_id}/create_deployment",
expect.objectContaining({
body: expect.objectContaining({
region: "aws-eu-west-1",
}),
}),
);
});
test("creates a deployment with --region alias", async () => {
setupPlatformForCreate();
await deploymentCreate.parseAsync(
["my-deployment", "--type", "dev", "--region", "us"],
{ from: "user" },
);
expect(mockPlatformPost).toHaveBeenCalledWith(
"/projects/{project_id}/create_deployment",
expect.objectContaining({
body: expect.objectContaining({
region: "aws-us-east-1",
}),
}),
);
});
test("fails with invalid --region", async () => {
setupPlatformForCreate();
await expect(
deploymentCreate.parseAsync(
["my-deployment", "--type", "dev", "--region", "invalid-region"],
{ from: "user" },
),
).rejects.toThrow();
expect(process.stderr.write).toHaveBeenCalledWith(
expect.stringContaining('Invalid region "invalid-region"'),
);
});
test("creates a deployment with --class", async () => {
setupPlatformForCreate();
await deploymentCreate.parseAsync(
["my-deployment", "--type", "dev", "--class", "s256"],
{ from: "user" },
);
expect(mockPlatformGet).toHaveBeenCalledWith(
"/teams/{team_id}/list_deployment_classes",
expect.objectContaining({
params: { path: { team_id: "456" } },
}),
);
expect(mockPlatformPost).toHaveBeenCalledWith(
"/projects/{project_id}/create_deployment",
expect.objectContaining({
body: expect.objectContaining({
class: "s256",
}),
}),
);
});
test("fails with invalid --class", async () => {
setupPlatformForCreate();
await expect(
deploymentCreate.parseAsync(
["my-deployment", "--type", "dev", "--class", "invalid-class"],
{ from: "user" },
),
).rejects.toThrow();
expect(process.stderr.write).toHaveBeenCalledWith(
expect.stringContaining('Invalid class "invalid-class"'),
);
});
test("fails with unavailable --class", async () => {
setupPlatformForCreate();
await expect(
deploymentCreate.parseAsync(
["my-deployment", "--type", "dev", "--class", "d1024"],
{ from: "user" },
),
).rejects.toThrow();
expect(process.stderr.write).toHaveBeenCalledWith(
expect.stringContaining('Invalid class "d1024"'),
);
});
test("no --class flag does not include class in body", async () => {
setupPlatformForCreate();
await deploymentCreate.parseAsync(["my-deployment", "--type", "dev"], {
from: "user",
});
const call = mockPlatformPost.mock.calls[0];
expect(call[1].body).not.toHaveProperty("class");
});
test("creates a deployment with --select calls saveSelectedDeployment", async () => {
setupPlatformForCreate({
reference: "dev/my-deployment",
});
await deploymentCreate.parseAsync(
["my-deployment", "--type", "dev", "--select"],
{ from: "user" },
);
expect(saveSelectedDeployment).toHaveBeenCalledWith(
expect.anything(),
"dev/my-deployment",
{
kind: "deploymentWithinProject",
targetProject: {
kind: "teamAndProjectSlugs",
teamSlug: "my-team",
projectSlug: "my-project",
},
selectionWithinProject: {
kind: "deploymentSelector",
selector: "dev/my-deployment",
},
},
null,
);
});
});
describe("with team:project:ref syntax", () => {
test("uses getProjectDetails with teamAndProjectSlugs", async () => {
vi.mocked(getProjectDetails).mockResolvedValue({
...fakeProject,
slug: "other-project",
teamSlug: "other-team",
});
setupPlatformForCreate();
await deploymentCreate.parseAsync(
["other-team:other-project:my-deployment", "--type", "dev"],
{ from: "user" },
);
expect(getProjectDetails).toHaveBeenCalledWith(expect.anything(), {
kind: "teamAndProjectSlugs",
teamSlug: "other-team",
projectSlug: "other-project",
});
expect(mockPlatformPost).toHaveBeenCalledWith(
"/projects/{project_id}/create_deployment",
expect.objectContaining({
body: expect.objectContaining({
reference: "my-deployment",
}),
}),
);
});
});
describe("--expiration flag", () => {
beforeEach(() => {
vi.mocked(getDeploymentSelection).mockResolvedValue({
kind: "existingDeployment",
deploymentToActOn: {
url: "https://joyful-capybara-123.convex.cloud",
adminKey: "admin-key",
deploymentFields: {
deploymentName: "joyful-capybara-123",
deploymentType: "dev",
teamSlug: "my-team",
projectSlug: "my-project",
},
source: "deployKey" as const,
},
});
vi.mocked(getProjectDetails).mockResolvedValue(fakeProject);
});
test("--expiration none sends expiresAt: null", async () => {
setupPlatformForCreate();
await deploymentCreate.parseAsync(
["my-deployment", "--type", "dev", "--expiration", "none"],
{ from: "user" },
);
expect(mockPlatformPost).toHaveBeenCalledWith(
"/projects/{project_id}/create_deployment",
expect.objectContaining({
body: expect.objectContaining({
expiresAt: null,
}),
}),
);
});
test("--expiration with timestamp sends correct ms value", async () => {
setupPlatformForCreate();
// Use a timestamp far enough in the future to pass validation
const futureMs = Date.now() + 2 * 60 * 60 * 1000; // 2 hours from now
const futureSec = Math.floor(futureMs / 1000);
await deploymentCreate.parseAsync(
[
"my-deployment",
"--type",
"dev",
"--expiration",
futureSec.toString(),
],
{ from: "user" },
);
expect(mockPlatformPost).toHaveBeenCalledWith(
"/projects/{project_id}/create_deployment",
expect.objectContaining({
body: expect.objectContaining({
expiresAt: futureSec * 1000,
}),
}),
);
});
test("no --expiration flag does not include expiresAt in body", async () => {
setupPlatformForCreate();
await deploymentCreate.parseAsync(["my-deployment", "--type", "dev"], {
from: "user",
});
const call = mockPlatformPost.mock.calls[0];
expect(call[1].body).not.toHaveProperty("expiresAt");
});
});
describe("without project configured", () => {
beforeEach(() => {
vi.mocked(getDeploymentSelection).mockResolvedValue({
kind: "chooseProject",
} as any);
});
test("fails with bare ref when no project context", async () => {
await expect(
deploymentCreate.parseAsync(["my-deployment", "--type", "dev"], {
from: "user",
}),
).rejects.toThrow();
expect(process.stderr.write).toHaveBeenCalledWith(
expect.stringContaining("No project configured yet"),
);
});
test("succeeds with team:project:ref syntax", async () => {
vi.mocked(getProjectDetails).mockResolvedValue(fakeProject);
setupPlatformForCreate();
await deploymentCreate.parseAsync(
["my-team:my-project:my-deployment", "--type", "dev"],
{ from: "user" },
);
expect(getProjectDetails).toHaveBeenCalledWith(expect.anything(), {
kind: "teamAndProjectSlugs",
teamSlug: "my-team",
projectSlug: "my-project",
});
expect(mockPlatformPost).toHaveBeenCalled();
});
test("hint includes full team:project:ref when no project configured in current directory", async () => {
vi.mocked(getProjectDetails).mockResolvedValue(fakeProject);
setupPlatformForCreate();
await deploymentCreate.parseAsync(
["my-team:my-project:my-deployment", "--type", "dev"],
{ from: "user" },
);
expect(process.stderr.write).toHaveBeenCalledWith(
expect.stringContaining(
"npx convex deployment select my-team:my-project:dev/my-deployment",
),
);
});
});
});
describe("interactive create flow", () => {
beforeEach(() => {
process.stdin.isTTY = true;
vi.mocked(getDeploymentSelection).mockReset();
vi.mocked(getProjectDetails).mockReset();
vi.mocked(saveSelectedDeployment).mockReset();
vi.mocked(selectRegion).mockReset();
vi.mocked(deploymentNameFromSelection).mockReset();
vi.mocked(deploymentNameFromSelection).mockReturnValue(null);
// Default: project configured via deployment selection
vi.mocked(getDeploymentSelection).mockResolvedValue({
kind: "existingDeployment",
deploymentToActOn: {
url: "https://joyful-capybara-123.convex.cloud",
adminKey: "admin-key",
deploymentFields: {
deploymentName: "joyful-capybara-123",
deploymentType: "dev",
teamSlug: "my-team",
projectSlug: "my-project",
},
source: "deployKey" as const,
},
});
vi.mocked(getProjectDetails).mockResolvedValue(fakeProject);
});
afterEach(() => {
process.stdin.isTTY = false;
});
function setupPlatformRoutes(routes: Record<string, (args: any) => any>) {
setupPlatformClient();
mockPlatformGet.mockImplementation((path: string, args: any) => {
for (const [routePath, handler] of Object.entries(routes)) {
if (path === routePath || path.startsWith(routePath)) {
return { data: handler(args) };
}
}
throw new Error(`Unmocked GET route: ${path}`);
});
mockPlatformPost.mockResolvedValue({
data: { ...createdDeployment },
});
}
function setupDefaultRoutes() {
setupPlatformRoutes({
"/teams/{team_id}/list_deployment_regions": () => ({
items: mockRegions,
}),
"/teams/{team_id}/list_deployment_classes": () => ({
items: mockClasses,
}),
});
mockBigBrainGet.mockResolvedValue({
data: [{ id: 456, slug: "my-team", defaultRegion: "aws-us-east-1" }],
});
}
test.each([
{ deploymentType: "dev" as const, downPresses: 0 },
{ deploymentType: "preview" as const, downPresses: 1 },
{ deploymentType: "prod" as const, downPresses: 2 },
])(
"selecting $deploymentType calls endpoint with type=$deploymentType",
async ({ deploymentType, downPresses }) => {
setupDefaultRoutes();
const promise = deploymentCreate.parseAsync([], { from: "user" });
// Type prompt (select)
await screen.next();
expect(screen.getScreen()).toContain("Deployment type?");
for (let i = 0; i < downPresses; i++) {
screen.keypress("down");
}
screen.keypress("enter");
// Ref prompt (input)
await screen.next();
expect(screen.getScreen()).toContain(
"What do you want to call this deployment",
);
screen.type("my-feature");
screen.keypress("enter");
await promise;
expect(mockPlatformPost).toHaveBeenCalledWith(
"/projects/{project_id}/create_deployment",
expect.objectContaining({
body: expect.objectContaining({
type: deploymentType,
}),
}),
);
},
);
test("full prompt flow: select type, enter ref", async () => {
setupDefaultRoutes();
const promise = deploymentCreate.parseAsync([], { from: "user" });
// Type prompt (select) — "dev" is first choice, just press enter
await screen.next();
expect(screen.getScreen()).toContain("Deployment type?");
screen.keypress("enter");
// Ref prompt (input)
await screen.next();
expect(screen.getScreen()).toContain(
"What do you want to call this deployment",
);
screen.type("my-feature");
screen.keypress("enter");
await promise;
expect(mockPlatformPost).toHaveBeenCalledWith(
"/projects/{project_id}/create_deployment",
expect.objectContaining({
body: {
type: "dev",
region: "aws-us-east-1",
reference: "my-feature",
isDefault: null,
},
}),
);
});
test("partial flags: --type dev and ref provided, no prompts needed", async () => {
setupDefaultRoutes();
const promise = deploymentCreate.parseAsync(
["my-feature", "--type", "dev"],
{ from: "user" },
);
await promise;
expect(mockPlatformPost).toHaveBeenCalledWith(
"/projects/{project_id}/create_deployment",
expect.objectContaining({
body: expect.objectContaining({
type: "dev",
reference: "my-feature",
isDefault: null,
}),
}),
);
});
test("invalid ref retry: user enters invalid ref, then valid ref", async () => {
setupDefaultRoutes();
const promise = deploymentCreate.parseAsync(["--type", "dev"], {
from: "user",
});
// Ref prompt — enter invalid ref "dev"
await screen.next();
expect(screen.getScreen()).toContain(
"What do you want to call this deployment",
);
screen.type("dev");
screen.keypress("enter");
// Inline validation error, then edit input
await screen.next();
expect(screen.getScreen()).toContain(
'"dev" is reserved as an alias for your default dev deployment.',
);
screen.type("/my-feature");
screen.keypress("enter");
await promise;
expect(mockPlatformPost).toHaveBeenCalledWith(
"/projects/{project_id}/create_deployment",
expect.objectContaining({
body: expect.objectContaining({
reference: "dev/my-feature",
isDefault: null,
}),
}),
);
});
test("interactive ref prompt rejects 'local' as a deployment reference", async () => {
setupDefaultRoutes();
const promise = deploymentCreate.parseAsync(["--type", "dev"], {
from: "user",
});
// Ref prompt — enter "local"
await screen.next();
expect(screen.getScreen()).toContain(
"What do you want to call this deployment",
);
screen.type("local");
screen.keypress("enter");
// Inline validation error
await screen.next();
expect(screen.getScreen()).toContain(
'"local" is reserved as an alias for your local deployment',
);
// Fix the input
screen.type("ization-improvements");
screen.keypress("enter");
await promise;
expect(mockPlatformPost).toHaveBeenCalledWith(
"/projects/{project_id}/create_deployment",
expect.objectContaining({
body: expect.objectContaining({
reference: "localization-improvements",
}),
}),
);
});
test("--region invalid crashes", async () => {
setupDefaultRoutes();
await expect(
deploymentCreate.parseAsync(
["my-feature", "--type", "dev", "--region", "invalid-region"],
{ from: "user" },
),
).rejects.toThrow();
expect(process.stderr.write).toHaveBeenCalledWith(
expect.stringContaining('Invalid region "invalid-region"'),
);
});
test("no team default region: falls through to selectRegion", async () => {
setupDefaultRoutes();
// Override BigBrain to return team without defaultRegion
mockBigBrainGet.mockResolvedValue({
data: [{ id: 456, slug: "my-team" }],
});
vi.mocked(selectRegion).mockResolvedValue("aws-us-east-1");
await deploymentCreate.parseAsync(["my-feature", "--type", "dev"], {
from: "user",
});
expect(selectRegion).toHaveBeenCalledWith(expect.anything(), 456, "dev");
expect(logNoDefaultRegionMessage).toHaveBeenCalledWith("my-team");
});
test("uses team default region when no --region provided", async () => {
setupDefaultRoutes();
const promise = deploymentCreate.parseAsync(
["my-feature", "--type", "dev"],
{ from: "user" },
);
await promise;
expect(selectRegion).not.toHaveBeenCalled();
expect(mockPlatformPost).toHaveBeenCalledWith(
"/projects/{project_id}/create_deployment",
expect.objectContaining({
body: expect.objectContaining({
region: "aws-us-east-1",
}),
}),
);
});
});
const availableRegions = mockRegions.filter((r) => r.available);
describe("resolveRegionDetails", () => {
test("resolves region by alias", () => {
const result = resolveRegionDetails(availableRegions, "us");
expect(result).not.toBeNull();
expect(result!.name).toBe("aws-us-east-1");
expect(result!.displayName).toBe("US East (Virginia)");
});
test("resolves region by full name", () => {
const result = resolveRegionDetails(availableRegions, "aws-eu-west-1");
expect(result).not.toBeNull();
expect(result!.name).toBe("aws-eu-west-1");
expect(result!.displayName).toBe("EU West (Ireland)");
});
test("returns null on unknown region", () => {
const result = resolveRegionDetails(availableRegions, "invalid-region");
expect(result).toBeNull();
});
test("returns null on unavailable region", () => {
const result = resolveRegionDetails(availableRegions, "aws-ap-southeast-1");
expect(result).toBeNull();
});
});
const availableClasses = mockClasses.filter((c) => c.available);
describe("resolveClassDetails", () => {
test("resolves class by type", () => {
const result = resolveClassDetails(availableClasses, "s16");
expect(result).not.toBeNull();
expect(result!.type).toBe("s16");
});
test("resolves another class by type", () => {
const result = resolveClassDetails(availableClasses, "s256");
expect(result).not.toBeNull();
expect(result!.type).toBe("s256");
});
test("returns null on unknown class", () => {
const result = resolveClassDetails(availableClasses, "invalid-class");
expect(result).toBeNull();
});
test("returns null on unavailable class", () => {
const result = resolveClassDetails(availableClasses, "d1024");
expect(result).toBeNull();
});
});