mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-02 10:05:41 +02:00
feat(cli): add workspace create command (#5062)
Adds `multica workspace create` (--name/--slug/--description/--context/--issue-prefix, JSON/table output). Creation does not switch the current workspace. Both --name and --slug are required to match the server contract, and the slug is immutable after creation. Docs (en/zh/ja/ko) and the CLI reference now show the executable command. Closes #5055
This commit is contained in:
@@ -38,6 +38,7 @@ CI やヘッドレス環境では、ブラウザフローをスキップでき
|
||||
| コマンド | 用途 |
|
||||
|---|---|
|
||||
| `multica workspace list` | アクセスできるすべてのワークスペースを一覧 |
|
||||
| `multica workspace create --name "..." --slug "..." [--issue-prefix "..."] [--description "..."] [--context "..."]` | ワークスペースを作成(作成者が owner になります)。`--name` と `--slug` は必須で、slug は作成後に変更できません。現在のワークスペースは切り替わりません。 |
|
||||
| `multica workspace get <slug>` | 1 つのワークスペースの詳細を表示 |
|
||||
| `multica workspace member list` | 現在のワークスペースのメンバーを一覧 |
|
||||
| `multica workspace update <id> --name "..." [--description "..."] [--context "..."] [--issue-prefix "..."]` | ワークスペースのメタデータを更新(admin/owner)。長いフィールドは `--description-stdin` / `--context-stdin` を使用できます。 |
|
||||
|
||||
@@ -38,6 +38,7 @@ CI나 headless 환경에서는 브라우저 플로우를 건너뛰세요. 웹
|
||||
| 명령어 | 용도 |
|
||||
|---|---|
|
||||
| `multica workspace list` | 접근할 수 있는 모든 워크스페이스 나열 |
|
||||
| `multica workspace create --name "..." --slug "..." [--issue-prefix "..."] [--description "..."] [--context "..."]` | 워크스페이스 생성(생성자가 owner가 됩니다). `--name`과 `--slug`는 필수이며 slug는 생성 후 변경할 수 없습니다. 현재 워크스페이스는 전환되지 않습니다. |
|
||||
| `multica workspace get <slug>` | 워크스페이스 하나의 상세 정보 표시 |
|
||||
| `multica workspace member list` | 현재 워크스페이스의 멤버 나열 |
|
||||
| `multica workspace update <id> --name "..." [--description "..."] [--context "..."] [--issue-prefix "..."]` | 워크스페이스 메타데이터 업데이트(admin/owner). 긴 필드는 `--description-stdin` / `--context-stdin`을 사용할 수 있습니다. |
|
||||
|
||||
@@ -38,6 +38,7 @@ For the difference between token types, see [Authentication and tokens](/auth-to
|
||||
| Command | Purpose |
|
||||
|---|---|
|
||||
| `multica workspace list` | List every workspace you can access |
|
||||
| `multica workspace create --name "..." --slug "..." [--issue-prefix "..."] [--description "..."] [--context "..."]` | Create a workspace (you become its owner). `--name` and `--slug` are required; the slug is permanent. Does not switch your current workspace. |
|
||||
| `multica workspace get <slug>` | Show details for one workspace |
|
||||
| `multica workspace member list` | List members of the current workspace |
|
||||
| `multica workspace update <id> --name "..." [--description "..."] [--context "..."] [--issue-prefix "..."]` | Update workspace metadata (admin/owner). Long fields accept `--description-stdin` / `--context-stdin`. |
|
||||
|
||||
@@ -38,6 +38,7 @@ Token 类型的详细区分见 [认证与令牌](/auth-tokens)。
|
||||
| 命令 | 用途 |
|
||||
|---|---|
|
||||
| `multica workspace list` | 列出你有权访问的所有工作区 |
|
||||
| `multica workspace create --name "..." --slug "..." [--issue-prefix "..."] [--description "..."] [--context "..."]` | 创建工作区(你会成为 owner)。`--name` 和 `--slug` 必填,slug 创建后不可修改;不会切换你当前的工作区。 |
|
||||
| `multica workspace get <slug>` | 查看一个工作区的详情 |
|
||||
| `multica workspace member list` | 列出当前工作区的成员 |
|
||||
| `multica workspace update <id> --name "..." [--description "..."] [--context "..."] [--issue-prefix "..."]` | 修改 workspace 元数据(admin/owner 权限)。长文本可用 `--description-stdin` / `--context-stdin`。 |
|
||||
|
||||
@@ -19,10 +19,10 @@ import { Callout } from "fumadocs-ui/components/callout";
|
||||
**イシュー接頭辞は変更しないでください。** イシュー番号は現在の接頭辞でレンダリングされるため、接頭辞を変更すると `MUL-5` がただちに `NEW-5` になります。すべての外部リンク、Slack のメンション、コメント内の過去の参照が古い番号と合わなくなります。イシュー接頭辞は「作成時に決め、決して触らない」値として扱ってください。
|
||||
</Callout>
|
||||
|
||||
ワークスペースは Web UI から作成することも、コマンドラインから作成することもできます。
|
||||
ワークスペースは Web UI から作成することも、コマンドラインから作成することもできます。`--name` と `--slug` はどちらも必須で、作成しても現在のワークスペースは**切り替わりません**。作成したワークスペースで作業したい場合は、続けて `multica workspace switch <slug>` を実行してください。
|
||||
|
||||
```bash
|
||||
multica workspace create
|
||||
multica workspace create --name "Support Team" --slug support-team --issue-prefix SUP
|
||||
```
|
||||
|
||||
## イシュー番号
|
||||
|
||||
@@ -19,10 +19,10 @@ import { Callout } from "fumadocs-ui/components/callout";
|
||||
**이슈 접두사는 변경하지 마세요.** 이슈 번호는 현재 접두사로 렌더링되므로, 접두사를 변경하면 `MUL-5`가 즉시 `NEW-5`가 됩니다. 모든 외부 링크, Slack 멘션, 댓글 속 과거 참조가 기존 번호와 맞지 않게 됩니다. 이슈 접두사는 "생성 시 정하고 절대 건드리지 않는" 값으로 다루세요.
|
||||
</Callout>
|
||||
|
||||
워크스페이스는 웹 UI에서 생성할 수도 있고, 커맨드 라인에서 생성할 수도 있습니다.
|
||||
워크스페이스는 웹 UI에서 생성할 수도 있고, 커맨드 라인에서 생성할 수도 있습니다. `--name`과 `--slug`는 모두 필수이며, 생성해도 현재 워크스페이스는 **전환되지 않습니다**. 생성한 워크스페이스에서 작업하려면 이어서 `multica workspace switch <slug>`를 실행하세요.
|
||||
|
||||
```bash
|
||||
multica workspace create
|
||||
multica workspace create --name "Support Team" --slug support-team --issue-prefix SUP
|
||||
```
|
||||
|
||||
## 이슈 번호
|
||||
|
||||
@@ -19,10 +19,10 @@ Three things get decided when you create a workspace:
|
||||
**Avoid changing the issue prefix.** Issue numbers are rendered with the current prefix — change it and `MUL-5` instantly becomes `NEW-5`. Every external link, Slack mention, and historical reference in comments breaks against the old number. Treat the issue prefix as "set at creation, never touched."
|
||||
</Callout>
|
||||
|
||||
You can create a workspace from the web UI or from the command line:
|
||||
You can create a workspace from the web UI, or from the command line. Both `--name` and `--slug` are required, and creating a workspace does **not** switch your current one — run `multica workspace switch <slug>` afterward if you want to work in it:
|
||||
|
||||
```bash
|
||||
multica workspace create
|
||||
multica workspace create --name "Support Team" --slug support-team --issue-prefix SUP
|
||||
```
|
||||
|
||||
## Issue numbers
|
||||
|
||||
@@ -19,10 +19,10 @@ import { Callout } from "fumadocs-ui/components/callout";
|
||||
**尽量不要修改 issue 前缀。** 系统在展示 issue 编号时会用当前的前缀——改了之后,`MUL-5` 会立刻变成 `NEW-5`。所有外部链接、Slack 提及、评论里的历史引用都会对不上旧编号。把 issue 前缀当成"创建后不改"的设计来对待。
|
||||
</Callout>
|
||||
|
||||
你可以通过 Web 界面创建工作区,也可以用命令行:
|
||||
你可以通过 Web 界面创建工作区,也可以用命令行。`--name` 和 `--slug` 都是必填项,创建后**不会**切换你当前的工作区——如果想进去操作,创建后再执行 `multica workspace switch <slug>`:
|
||||
|
||||
```bash
|
||||
multica workspace create
|
||||
multica workspace create --name "Support Team" --slug support-team --issue-prefix SUP
|
||||
```
|
||||
|
||||
## Issue 编号
|
||||
|
||||
@@ -24,6 +24,20 @@ var workspaceListCmd = &cobra.Command{
|
||||
RunE: runWorkspaceList,
|
||||
}
|
||||
|
||||
var workspaceCreateCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a workspace",
|
||||
Long: "Creates a new workspace and adds you as its owner. Both --name and " +
|
||||
"--slug are required; the slug is permanent (lowercase letters, digits, " +
|
||||
"and hyphens) and cannot be changed after creation.\n\n" +
|
||||
"Creating a workspace does NOT change the current default workspace for " +
|
||||
"this profile — run 'multica workspace switch <slug>' afterward if you " +
|
||||
"want subsequent commands to target the new workspace.",
|
||||
Example: " multica workspace create --name \"Support Team\" --slug support-team --issue-prefix SUP",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: runWorkspaceCreate,
|
||||
}
|
||||
|
||||
var workspaceGetCmd = &cobra.Command{
|
||||
Use: "get [workspace-id|slug|prefix]",
|
||||
Short: "Get workspace details",
|
||||
@@ -86,6 +100,7 @@ var workspaceSwitchCmd = &cobra.Command{
|
||||
|
||||
func init() {
|
||||
workspaceCmd.AddCommand(workspaceListCmd)
|
||||
workspaceCmd.AddCommand(workspaceCreateCmd)
|
||||
workspaceCmd.AddCommand(workspaceGetCmd)
|
||||
workspaceCmd.AddCommand(workspaceMemberCmd)
|
||||
workspaceMemberCmd.AddCommand(workspaceMemberListCmd)
|
||||
@@ -95,6 +110,14 @@ func init() {
|
||||
|
||||
workspaceListCmd.Flags().String("output", "table", "Output format: table or json")
|
||||
workspaceListCmd.Flags().Bool("full-id", false, "Show full UUIDs in table output")
|
||||
workspaceCreateCmd.Flags().String("name", "", "Workspace name")
|
||||
workspaceCreateCmd.Flags().String("slug", "", "Workspace slug")
|
||||
workspaceCreateCmd.Flags().String("description", "", "Workspace description (decodes \\n, \\r, \\t, \\\\; pipe via --description-stdin to preserve literal backslashes)")
|
||||
workspaceCreateCmd.Flags().Bool("description-stdin", false, "Read description from stdin (preserves multi-line content verbatim)")
|
||||
workspaceCreateCmd.Flags().String("context", "", "Workspace context (decodes \\n, \\r, \\t, \\\\; pipe via --context-stdin to preserve literal backslashes)")
|
||||
workspaceCreateCmd.Flags().Bool("context-stdin", false, "Read context from stdin (preserves multi-line content verbatim)")
|
||||
workspaceCreateCmd.Flags().String("issue-prefix", "", "Issue prefix (uppercased server-side)")
|
||||
workspaceCreateCmd.Flags().String("output", "json", "Output format: table or json")
|
||||
workspaceGetCmd.Flags().String("output", "json", "Output format: table or json")
|
||||
workspaceMemberListCmd.Flags().String("output", "table", "Output format: table or json")
|
||||
workspaceMemberInviteCmd.Flags().String("role", "member", "Member role to grant: member or admin (owner is not allowed)")
|
||||
@@ -178,6 +201,76 @@ func runWorkspaceList(cmd *cobra.Command, _ []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildWorkspaceCreateBody(cmd *cobra.Command) (map[string]any, error) {
|
||||
name, _ := cmd.Flags().GetString("name")
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return nil, fmt.Errorf("--name is required")
|
||||
}
|
||||
|
||||
// The server requires both name and slug (POST /api/workspaces returns 400
|
||||
// without them) and the slug is immutable after creation, so it must be
|
||||
// chosen explicitly here rather than silently omitted.
|
||||
slug, _ := cmd.Flags().GetString("slug")
|
||||
if strings.TrimSpace(slug) == "" {
|
||||
return nil, fmt.Errorf("--slug is required")
|
||||
}
|
||||
|
||||
// A single stdin stream cannot feed two fields: whichever field reads first
|
||||
// drains it and the other gets EOF. Reject the ambiguous combination up
|
||||
// front instead of surfacing a misleading "content is empty" error.
|
||||
descStdin, _ := cmd.Flags().GetBool("description-stdin")
|
||||
ctxStdin, _ := cmd.Flags().GetBool("context-stdin")
|
||||
if descStdin && ctxStdin {
|
||||
return nil, fmt.Errorf("--description-stdin and --context-stdin cannot be combined; a single stdin cannot feed both fields — pass one of them inline")
|
||||
}
|
||||
|
||||
body := map[string]any{"name": name, "slug": slug}
|
||||
if cmd.Flags().Changed("description") || cmd.Flags().Changed("description-stdin") {
|
||||
desc, _, err := resolveTextFlag(cmd, "description")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body["description"] = desc
|
||||
}
|
||||
if cmd.Flags().Changed("context") || cmd.Flags().Changed("context-stdin") {
|
||||
ctxText, _, err := resolveTextFlag(cmd, "context")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body["context"] = ctxText
|
||||
}
|
||||
if cmd.Flags().Changed("issue-prefix") {
|
||||
v, _ := cmd.Flags().GetString("issue-prefix")
|
||||
if strings.TrimSpace(v) == "" {
|
||||
return nil, fmt.Errorf("--issue-prefix cannot be empty; omit it to use the server-generated prefix")
|
||||
}
|
||||
body["issue_prefix"] = v
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func runWorkspaceCreate(cmd *cobra.Command, _ []string) error {
|
||||
body, err := buildWorkspaceCreateBody(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := newAPIClient(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client.WorkspaceID = ""
|
||||
|
||||
ctx, cancel := cli.APIContext(context.Background())
|
||||
defer cancel()
|
||||
|
||||
var ws map[string]any
|
||||
if err := client.PostJSON(ctx, "/api/workspaces", body, &ws); err != nil {
|
||||
return fmt.Errorf("create workspace: %w", err)
|
||||
}
|
||||
return printWorkspace(cmd, ws)
|
||||
}
|
||||
|
||||
// resolveWorkspaceByIDOrSlug looks up a workspace in the caller's accessible
|
||||
// list by full UUID, slug (case-insensitive), or short UUID prefix (≥4 hex
|
||||
// chars). The matching order is exact UUID → exact slug → prefix, so a slug
|
||||
@@ -327,6 +420,10 @@ func runWorkspaceGet(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("get workspace: %w", err)
|
||||
}
|
||||
|
||||
return printWorkspace(cmd, ws)
|
||||
}
|
||||
|
||||
func printWorkspace(cmd *cobra.Command, ws map[string]any) error {
|
||||
output, _ := cmd.Flags().GetString("output")
|
||||
if output == "table" {
|
||||
desc := strVal(ws, "description")
|
||||
@@ -421,31 +518,7 @@ func runWorkspaceUpdate(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("update workspace: %w", err)
|
||||
}
|
||||
|
||||
output, _ := cmd.Flags().GetString("output")
|
||||
if output == "table" {
|
||||
desc := strVal(ws, "description")
|
||||
if utf8.RuneCountInString(desc) > 60 {
|
||||
runes := []rune(desc)
|
||||
desc = string(runes[:57]) + "..."
|
||||
}
|
||||
wsContext := strVal(ws, "context")
|
||||
if utf8.RuneCountInString(wsContext) > 60 {
|
||||
runes := []rune(wsContext)
|
||||
wsContext = string(runes[:57]) + "..."
|
||||
}
|
||||
headers := []string{"ID", "NAME", "SLUG", "DESCRIPTION", "CONTEXT"}
|
||||
rows := [][]string{{
|
||||
strVal(ws, "id"),
|
||||
strVal(ws, "name"),
|
||||
strVal(ws, "slug"),
|
||||
desc,
|
||||
wsContext,
|
||||
}}
|
||||
cli.PrintTable(os.Stdout, headers, rows)
|
||||
return nil
|
||||
}
|
||||
|
||||
return cli.PrintJSON(os.Stdout, ws)
|
||||
return printWorkspace(cmd, ws)
|
||||
}
|
||||
|
||||
func runWorkspaceMembers(cmd *cobra.Command, args []string) error {
|
||||
|
||||
@@ -25,6 +25,247 @@ func newWorkspaceSwitchTestCmd() *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newWorkspaceCreateTestCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{Use: "create"}
|
||||
cmd.Flags().String("workspace-id", "", "")
|
||||
cmd.Flags().String("profile", "", "")
|
||||
cmd.Flags().String("server-url", "", "")
|
||||
cmd.Flags().String("name", "", "")
|
||||
cmd.Flags().String("slug", "", "")
|
||||
cmd.Flags().String("description", "", "")
|
||||
cmd.Flags().Bool("description-stdin", false, "")
|
||||
cmd.Flags().String("context", "", "")
|
||||
cmd.Flags().Bool("context-stdin", false, "")
|
||||
cmd.Flags().String("issue-prefix", "", "")
|
||||
cmd.Flags().String("output", "json", "")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// decodeWorkspaceCreateBody mirrors the real POST /api/workspaces contract:
|
||||
// it rejects a body missing name or slug with 400 (as CreateWorkspace does) so
|
||||
// a CLI regression that drops slug surfaces as a failing request instead of
|
||||
// being masked by a mock that fabricates the field.
|
||||
func decodeWorkspaceCreateBody(t *testing.T, w http.ResponseWriter, r *http.Request) (map[string]any, bool) {
|
||||
t.Helper()
|
||||
var body map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("decode request body: %v", err)
|
||||
}
|
||||
name, _ := body["name"].(string)
|
||||
slug, _ := body["slug"].(string)
|
||||
if strings.TrimSpace(name) == "" || strings.TrimSpace(slug) == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"error": "name and slug are required"})
|
||||
return nil, false
|
||||
}
|
||||
return body, true
|
||||
}
|
||||
|
||||
func TestRunWorkspaceCreatePostsWorkspaceAndDoesNotSwitchDefault(t *testing.T) {
|
||||
var gotBody map[string]any
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/api/workspaces" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if r.Header.Get("X-Workspace-ID") != "" {
|
||||
t.Fatalf("X-Workspace-ID = %q, want empty for workspace creation", r.Header.Get("X-Workspace-ID"))
|
||||
}
|
||||
body, ok := decodeWorkspaceCreateBody(t, w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
gotBody = body
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"id": "33333333-3333-3333-3333-333333333333",
|
||||
"name": "Growth Team",
|
||||
"slug": "growth-team",
|
||||
"description": "Handles GTM work",
|
||||
"context": "Launch notes",
|
||||
"issue_prefix": "GRO",
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
t.Setenv("MULTICA_SERVER_URL", srv.URL)
|
||||
t.Setenv("MULTICA_TOKEN", "test-token")
|
||||
t.Setenv("MULTICA_WORKSPACE_ID", "existing-workspace")
|
||||
if err := cli.SaveCLIConfig(cli.CLIConfig{WorkspaceID: "existing-workspace"}); err != nil {
|
||||
t.Fatalf("seed config: %v", err)
|
||||
}
|
||||
|
||||
cmd := newWorkspaceCreateTestCmd()
|
||||
for name, value := range map[string]string{
|
||||
"name": "Growth Team",
|
||||
"slug": "growth-team",
|
||||
"description": `Handles\nGTM work`,
|
||||
"context": "Launch notes",
|
||||
"issue-prefix": "GRO",
|
||||
} {
|
||||
if err := cmd.Flags().Set(name, value); err != nil {
|
||||
t.Fatalf("set --%s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
out, err := captureStdout(t, func() error {
|
||||
return runWorkspaceCreate(cmd, nil)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("runWorkspaceCreate: %v", err)
|
||||
}
|
||||
|
||||
if gotBody["name"] != "Growth Team" {
|
||||
t.Errorf("name = %v, want Growth Team", gotBody["name"])
|
||||
}
|
||||
if gotBody["slug"] != "growth-team" {
|
||||
t.Errorf("slug = %v, want growth-team", gotBody["slug"])
|
||||
}
|
||||
if gotBody["description"] != "Handles\nGTM work" {
|
||||
t.Errorf("description = %q, want decoded newline", gotBody["description"])
|
||||
}
|
||||
if gotBody["context"] != "Launch notes" {
|
||||
t.Errorf("context = %v, want Launch notes", gotBody["context"])
|
||||
}
|
||||
if gotBody["issue_prefix"] != "GRO" {
|
||||
t.Errorf("issue_prefix = %v, want GRO", gotBody["issue_prefix"])
|
||||
}
|
||||
|
||||
var printed map[string]any
|
||||
if err := json.Unmarshal([]byte(out), &printed); err != nil {
|
||||
t.Fatalf("decode stdout JSON %q: %v", out, err)
|
||||
}
|
||||
if printed["id"] != "33333333-3333-3333-3333-333333333333" {
|
||||
t.Errorf("printed id = %v, want created workspace id", printed["id"])
|
||||
}
|
||||
|
||||
cfg, err := cli.LoadCLIConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadCLIConfig: %v", err)
|
||||
}
|
||||
if cfg.WorkspaceID != "existing-workspace" {
|
||||
t.Errorf("workspace_id = %q, want existing-workspace (create must not auto-switch)", cfg.WorkspaceID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWorkspaceCreateRequiresName(t *testing.T) {
|
||||
cmd := newWorkspaceCreateTestCmd()
|
||||
err := runWorkspaceCreate(cmd, nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected missing --name error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "--name is required") {
|
||||
t.Fatalf("error = %q, want --name is required", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWorkspaceCreateRequiresSlug(t *testing.T) {
|
||||
cmd := newWorkspaceCreateTestCmd()
|
||||
if err := cmd.Flags().Set("name", "Growth Team"); err != nil {
|
||||
t.Fatalf("set --name: %v", err)
|
||||
}
|
||||
err := runWorkspaceCreate(cmd, nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected missing --slug error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "--slug is required") {
|
||||
t.Fatalf("error = %q, want --slug is required", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWorkspaceCreateRejectsDualStdin(t *testing.T) {
|
||||
cmd := newWorkspaceCreateTestCmd()
|
||||
for name, value := range map[string]string{
|
||||
"name": "Growth Team",
|
||||
"slug": "growth-team",
|
||||
"description-stdin": "true",
|
||||
"context-stdin": "true",
|
||||
} {
|
||||
if err := cmd.Flags().Set(name, value); err != nil {
|
||||
t.Fatalf("set --%s: %v", name, err)
|
||||
}
|
||||
}
|
||||
_, err := buildWorkspaceCreateBody(cmd)
|
||||
if err == nil {
|
||||
t.Fatal("expected mutually-exclusive stdin error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "cannot be combined") {
|
||||
t.Fatalf("error = %q, want stdin combination rejection", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWorkspaceCreateReadsDescriptionFromStdin(t *testing.T) {
|
||||
cmd := newWorkspaceCreateTestCmd()
|
||||
for name, value := range map[string]string{
|
||||
"name": "Growth Team",
|
||||
"slug": "growth-team",
|
||||
"description-stdin": "true",
|
||||
} {
|
||||
if err := cmd.Flags().Set(name, value); err != nil {
|
||||
t.Fatalf("set --%s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
var got map[string]any
|
||||
pipeStdin(t, "line1\nline2\n", func() {
|
||||
b, err := buildWorkspaceCreateBody(cmd)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
got = b
|
||||
})
|
||||
if got["description"] != "line1\nline2" {
|
||||
t.Errorf("description = %q, want stdin content", got["description"])
|
||||
}
|
||||
if got["slug"] != "growth-team" {
|
||||
t.Errorf("slug = %v, want growth-team (must still be sent)", got["slug"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWorkspaceCreatePrintsTable(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/api/workspaces" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if _, ok := decodeWorkspaceCreateBody(t, w, r); !ok {
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"id": "44444444-4444-4444-4444-444444444444",
|
||||
"name": "Support Team",
|
||||
"slug": "support-team",
|
||||
"description": "A workspace for support operations",
|
||||
"context": "Customer support workflows",
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
t.Setenv("MULTICA_SERVER_URL", srv.URL)
|
||||
t.Setenv("MULTICA_TOKEN", "test-token")
|
||||
|
||||
cmd := newWorkspaceCreateTestCmd()
|
||||
_ = cmd.Flags().Set("name", "Support Team")
|
||||
_ = cmd.Flags().Set("slug", "support-team")
|
||||
_ = cmd.Flags().Set("output", "table")
|
||||
|
||||
out, err := captureStdout(t, func() error {
|
||||
return runWorkspaceCreate(cmd, nil)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("runWorkspaceCreate: %v", err)
|
||||
}
|
||||
|
||||
for _, want := range []string{"ID", "NAME", "SLUG", "Support Team", "support-team"} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("table output %q does not contain %q", out, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWorkspaceSwitch(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/workspaces" {
|
||||
|
||||
Reference in New Issue
Block a user