Files
wehub-resource-sync e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

374 lines
11 KiB
TypeScript

import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import {
create,
resolveCreateProjectDirectory,
resolvePresetUrl,
resolveProject,
resolveScaffoldSelector,
PROJECT_METADATA,
} from "../../src/commands/create";
describe("create command", () => {
it("exposes --preset option", () => {
const presetOption = create.options.find(
(option) => option.long === "--preset",
);
expect(presetOption).toBeDefined();
});
it("exposes --template option", () => {
const templateOption = create.options.find(
(option) => option.long === "--template",
);
expect(templateOption).toBeDefined();
});
it("exposes --example option", () => {
const exampleOption = create.options.find(
(option) => option.long === "--example",
);
expect(exampleOption).toBeDefined();
});
it("exposes --debug-source-root as a hidden option", () => {
const debugSourceRootOption = create.options.find(
(option) => option.long === "--debug-source-root",
);
expect(debugSourceRootOption).toBeDefined();
expect(debugSourceRootOption?.hidden).toBe(true);
expect(create.helpInformation()).not.toContain("--debug-source-root");
});
});
describe("resolveProject", () => {
it("returns template metadata when --template is provided", async () => {
const result = await resolveProject({
template: "cloud",
stdinIsTTY: true,
});
expect(result).toEqual(
expect.objectContaining({
name: "cloud",
category: "template",
hasLocalComponents: false,
}),
);
});
it("returns example metadata when --example is provided", async () => {
const result = await resolveProject({
example: "with-langgraph",
stdinIsTTY: true,
});
expect(result).toEqual(
expect.objectContaining({
name: "with-langgraph",
category: "example",
hasLocalComponents: false,
}),
);
});
it("supports the cloud-clerk template", async () => {
const result = await resolveProject({
template: "cloud-clerk",
stdinIsTTY: true,
});
expect(result).toEqual(
expect.objectContaining({
name: "cloud-clerk",
category: "template",
}),
);
});
it("defaults to default template in non-interactive shells", async () => {
const result = await resolveProject({
stdinIsTTY: false,
});
expect(result).toEqual(
expect.objectContaining({
name: "default",
category: "template",
}),
);
});
it("uses selected project in interactive mode", async () => {
const select = vi.fn().mockResolvedValue("with-ai-sdk-v7");
const isCancel = vi.fn().mockReturnValue(false);
const result = await resolveProject({
stdinIsTTY: true,
select,
isCancel,
});
expect(result).toEqual(
expect.objectContaining({
name: "with-ai-sdk-v7",
category: "example",
}),
);
});
it("returns null when selection is cancelled", async () => {
const select = vi.fn().mockResolvedValue(Symbol("cancel"));
const isCancel = vi.fn().mockReturnValue(true);
const result = await resolveProject({
stdinIsTTY: true,
select,
isCancel,
});
expect(result).toBeNull();
});
});
describe("resolveScaffoldSelector", () => {
it("returns an empty selector when no scaffold selector is provided", () => {
expect(resolveScaffoldSelector({})).toEqual({});
});
it("maps --native to the Expo example", () => {
expect(resolveScaffoldSelector({ native: true })).toEqual({
example: "with-expo",
});
});
it("maps --ink to the React Ink example", () => {
expect(resolveScaffoldSelector({ ink: true })).toEqual({
example: "with-react-ink",
});
});
it("uses the default template when only --preset is provided", () => {
expect(resolveScaffoldSelector({ preset: "chatgpt" })).toEqual({
template: "default",
preset: "chatgpt",
});
});
it("rejects --native with --ink", () => {
expect(() => resolveScaffoldSelector({ native: true, ink: true })).toThrow(
"Only one scaffold selector can be provided (--native, --ink). Choose one scaffold selector: --template <name>, --example <name>, --native, or --ink. --preset <name-or-url> can be used with --template or by itself.",
);
});
it("rejects --native with --example", () => {
expect(() =>
resolveScaffoldSelector({ native: true, example: "with-tanstack" }),
).toThrow(
"Only one scaffold selector can be provided (--example, --native). Choose one scaffold selector: --template <name>, --example <name>, --native, or --ink. --preset <name-or-url> can be used with --template or by itself.",
);
});
it("rejects --template with --example", () => {
expect(() =>
resolveScaffoldSelector({
template: "default",
example: "with-ai-sdk-v7",
}),
).toThrow(
"Only one scaffold selector can be provided (--template, --example). Choose one scaffold selector: --template <name>, --example <name>, --native, or --ink. --preset <name-or-url> can be used with --template or by itself.",
);
});
it("rejects --ink with --template", () => {
expect(() =>
resolveScaffoldSelector({ ink: true, template: "default" }),
).toThrow(
"Only one scaffold selector can be provided (--template, --ink). Choose one scaffold selector: --template <name>, --example <name>, --native, or --ink. --preset <name-or-url> can be used with --template or by itself.",
);
});
it("allows --preset with --template", () => {
expect(
resolveScaffoldSelector({ preset: "chatgpt", template: "minimal" }),
).toEqual({
template: "minimal",
preset: "chatgpt",
});
});
it("rejects --preset with --example", () => {
expect(() =>
resolveScaffoldSelector({
preset: "chatgpt",
example: "with-ai-sdk-v7",
}),
).toThrow(
"Cannot use --preset with --example. Choose one scaffold selector: --template <name>, --example <name>, --native, or --ink. --preset <name-or-url> can be used with --template or by itself.",
);
});
it("rejects --preset with --native", () => {
expect(() =>
resolveScaffoldSelector({
preset: "chatgpt",
native: true,
}),
).toThrow(
"Cannot use --preset with --native. Choose one scaffold selector: --template <name>, --example <name>, --native, or --ink. --preset <name-or-url> can be used with --template or by itself.",
);
});
it("rejects --preset with --ink", () => {
expect(() =>
resolveScaffoldSelector({
preset: "chatgpt",
ink: true,
}),
).toThrow(
"Cannot use --preset with --ink. Choose one scaffold selector: --template <name>, --example <name>, --native, or --ink. --preset <name-or-url> can be used with --template or by itself.",
);
});
});
describe("resolveProject error handling", () => {
let exitSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {
throw new Error("process.exit");
});
});
afterEach(() => {
exitSpy.mockRestore();
});
it("--template rejects an example name", async () => {
await expect(
resolveProject({ template: "with-langgraph", stdinIsTTY: true }),
).rejects.toThrow("process.exit");
expect(exitSpy).toHaveBeenCalledWith(1);
});
it("--template rejects an empty name", async () => {
await expect(
resolveProject({ template: "", stdinIsTTY: true }),
).rejects.toThrow("process.exit");
expect(exitSpy).toHaveBeenCalledWith(1);
});
it("--example rejects a template name", async () => {
await expect(
resolveProject({ example: "cloud", stdinIsTTY: true }),
).rejects.toThrow("process.exit");
expect(exitSpy).toHaveBeenCalledWith(1);
});
it("--example rejects an empty name", async () => {
await expect(
resolveProject({ example: "", stdinIsTTY: true }),
).rejects.toThrow("process.exit");
expect(exitSpy).toHaveBeenCalledWith(1);
});
it("exits when picker returns separator value", async () => {
const select = vi.fn().mockResolvedValue("_separator");
const isCancel = vi.fn().mockReturnValue(false);
await expect(
resolveProject({ stdinIsTTY: true, select, isCancel }),
).rejects.toThrow("process.exit");
expect(exitSpy).toHaveBeenCalledWith(1);
});
});
describe("PROJECT_METADATA", () => {
it("contains all 7 templates", () => {
const templates = PROJECT_METADATA.filter((m) => m.category === "template");
expect(templates).toHaveLength(7);
expect(templates.map((t) => t.name)).toEqual([
"default",
"minimal",
"cloud",
"cloud-clerk",
"langchain",
"mcp",
"eve",
]);
});
it("only the minimal template ships local components", () => {
const templates = PROJECT_METADATA.filter((m) => m.category === "template");
expect(
templates.filter((t) => t.hasLocalComponents).map((t) => t.name),
).toEqual(["minimal"]);
});
it("examples have correct hasLocalComponents values", () => {
const examples = PROJECT_METADATA.filter((m) => m.category === "example");
const withLocalComponents = examples.filter((e) => e.hasLocalComponents);
// These examples ship their own components (no shadcn)
expect(withLocalComponents.map((e) => e.name)).toEqual([
"with-elevenlabs-conversational",
"with-livekit",
"with-expo",
"with-interactables",
"with-react-ink",
]);
});
it("every entry has a path", () => {
for (const m of PROJECT_METADATA) {
expect(m.path).toBeTruthy();
expect(
m.path.startsWith("templates/") || m.path.startsWith("examples/"),
).toBe(true);
}
});
});
describe("resolveCreateProjectDirectory", () => {
it("defaults project directory in non-interactive mode", () => {
expect(
resolveCreateProjectDirectory({
stdinIsTTY: false,
}),
).toBe("my-aui-app");
});
it("does not force a project directory in interactive mode", () => {
expect(
resolveCreateProjectDirectory({
stdinIsTTY: true,
}),
).toBeUndefined();
});
it("keeps provided project directory in non-interactive mode", () => {
expect(
resolveCreateProjectDirectory({
projectDirectory: "custom-app",
stdinIsTTY: false,
}),
).toBe("custom-app");
});
});
describe("resolvePresetUrl", () => {
it("passes through full https URLs unchanged", () => {
const url = "https://www.assistant-ui.com/playground/init?preset=chatgpt";
expect(resolvePresetUrl(url)).toBe(url);
});
it("passes through http URLs unchanged", () => {
const url = "http://localhost:3000/preset";
expect(resolvePresetUrl(url)).toBe(url);
});
it("expands a bare preset name to the playground URL", () => {
expect(resolvePresetUrl("chatgpt")).toBe(
"https://www.assistant-ui.com/playground/init?preset=chatgpt",
);
});
it("encodes special characters in preset names", () => {
expect(resolvePresetUrl("my preset")).toBe(
"https://www.assistant-ui.com/playground/init?preset=my%20preset",
);
});
});