Files
Molecule AI Dev Engineer A (Kimi) 1823eb9348
Release Go binaries / test (pull_request) Successful in 41s
Release Go binaries / release (pull_request) Has been skipped
CI / Test / test (pull_request) Successful in 2m52s
test(migrate-provider): cover confirm, from==to, and non-AWS from-instance-id guards (#19 CR2 12092)
Adds explicit cmd-level validation tests for workspace migrate-provider:

- --confirm required

- --from and --to cannot be the same provider

- --from-instance-id required for non-AWS sources

- valid provider passes guard and fails later on missing CP env (post-guard)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-15 22:52:27 +00:00

343 lines
11 KiB
Go

package cmd
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/spf13/viper"
)
// TestBudgetFlagMapping verifies the budget flag→limits translation:
// unset flags are omitted, negative clears (nil), non-negative sets a pointer.
// Exercises the SHARED budgetLimitsFromFlags helper so prod and test agree.
func TestBudgetFlagMapping(t *testing.T) {
// All unset → empty map (show, not set).
if got := budgetLimitsFromFlags(budgetUnset, budgetUnset, budgetUnset, budgetUnset); len(got) != 0 {
t.Errorf("all-unset: want empty map, got %v", got)
}
// monthly=50000 set, daily=-1 clear, others unset.
got := budgetLimitsFromFlags(budgetUnset, -1, budgetUnset, 50000)
if len(got) != 2 {
t.Fatalf("want 2 entries, got %d (%v)", len(got), got)
}
if v, ok := got["daily"]; !ok || v != nil {
t.Errorf("daily: want present+nil (clear), got ok=%v v=%v", ok, v)
}
if v, ok := got["monthly"]; !ok || v == nil || *v != 50000 {
t.Errorf("monthly: want 50000, got ok=%v v=%v", ok, v)
}
if _, ok := got["hourly"]; ok {
t.Errorf("hourly should be absent when unset")
}
}
// TestBillingModeValidation walks the billing-mode arg validation branches via
// the SHARED resolveBillingMode helper (same code prod runs).
func TestBillingModeValidation(t *testing.T) {
cases := []struct {
in string
wantMode string // resolved mode passed to the client
wantErr bool
}{
{"platform_managed", "platform_managed", false},
{"byok", "byok", false},
{"disabled", "disabled", false},
{"clear", "", false},
{"null", "", false},
{"", "", false},
{"bogus", "", true},
}
for _, tc := range cases {
mode, err := resolveBillingMode(tc.in)
if (err != nil) != tc.wantErr {
t.Errorf("%q: err=%v want %v", tc.in, err, tc.wantErr)
}
if !tc.wantErr && mode != tc.wantMode {
t.Errorf("%q: resolved mode=%q want %q", tc.in, mode, tc.wantMode)
}
}
}
// TestReadFileMappings verifies template --file relpath=localpath parsing +
// file reads, including the error branches.
func TestReadFileMappings(t *testing.T) {
dir := t.TempDir()
good := filepath.Join(dir, "org.yaml")
if err := os.WriteFile(good, []byte("name: x\n"), 0o600); err != nil {
t.Fatal(err)
}
// Valid mapping.
out, err := readFileMappings([]string{"org.yaml=" + good})
if err != nil {
t.Fatalf("valid mapping: %v", err)
}
if out["org.yaml"] != "name: x\n" {
t.Errorf("contents = %q", out["org.yaml"])
}
// Missing '='.
if _, err := readFileMappings([]string{"justakey"}); err == nil {
t.Errorf("missing '=' should error")
}
// Empty relpath.
if _, err := readFileMappings([]string{"=" + good}); err == nil {
t.Errorf("empty relpath should error")
}
// Nonexistent local file.
if _, err := readFileMappings([]string{"a.yaml=" + filepath.Join(dir, "nope")}); err == nil {
t.Errorf("nonexistent local file should error")
}
}
// TestJSONFlagResolution confirms --json sets outputFormat=json via the
// persistent pre-run hook.
func TestJSONFlagResolution(t *testing.T) {
origFmt, origJSON := outputFormat, jsonOutput
defer func() { outputFormat, jsonOutput = origFmt, origJSON }()
outputFormat = "table"
jsonOutput = true
rootCmd.PersistentPreRun(rootCmd, nil)
if outputFormat != "json" {
t.Errorf("--json should set outputFormat=json, got %q", outputFormat)
}
}
// TestAuthHelpers confirms the credential loaders read the documented env vars.
func TestAuthHelpers(t *testing.T) {
t.Setenv("MOLECULE_API_KEY", "k1")
t.Setenv("MOLECULE_ORG_ID", "o1")
if authToken() != "k1" {
t.Errorf("authToken() = %q, want k1", authToken())
}
if orgID() != "o1" {
t.Errorf("orgID() = %q, want o1", orgID())
}
}
// TestApplyConfigDefaults confirms a config-file api_url/output value reaches
// the flag-backed globals (so `config set api_url` actually affects newClient),
// while an env var or an already-overridden global wins over the config file.
func TestApplyConfigDefaults(t *testing.T) {
origAPI, origFmt := apiURL, outputFormat
defer func() {
apiURL, outputFormat = origAPI, origFmt
viper.Reset()
}()
// 1) Config file provides api_url + output; no env, globals at default →
// config values are adopted.
viper.Reset()
viper.Set("api_url", "https://cfg.example")
viper.Set("output", "json")
t.Setenv("MOLECULE_API_URL", "")
t.Setenv("MOL_OUTPUT", "")
apiURL = "http://localhost:8080" // untouched flag default
outputFormat = "table" // untouched flag default
applyConfigDefaults()
if apiURL != "https://cfg.example" {
t.Errorf("apiURL = %q, want config value (config set api_url must flow to the client)", apiURL)
}
if outputFormat != "json" {
t.Errorf("outputFormat = %q, want config value", outputFormat)
}
// 2) Env var present → env wins, config file ignored.
viper.Reset()
viper.Set("api_url", "https://cfg.example")
t.Setenv("MOLECULE_API_URL", "https://env.example")
apiURL = "https://env.example" // flag default already folded the env in
applyConfigDefaults()
if apiURL != "https://env.example" {
t.Errorf("apiURL = %q, want env value (env must win over config file)", apiURL)
}
// 3) Global already overridden away from the default (e.g. explicit flag)
// → config file does not clobber it.
viper.Reset()
viper.Set("api_url", "https://cfg.example")
t.Setenv("MOLECULE_API_URL", "")
apiURL = "https://flag.example"
applyConfigDefaults()
if apiURL != "https://flag.example" {
t.Errorf("apiURL = %q, want flag value (explicit flag must win over config file)", apiURL)
}
}
// TestConfigSetMkdirsConfigDir confirms `config set` creates a missing config
// dir before writing (SafeWriteConfig/WriteConfig fail on a nonexistent dir).
func TestConfigSetMkdirsConfigDir(t *testing.T) {
// Point os.UserConfigDir at a temp HOME whose ~/.config does NOT exist yet.
tmp := t.TempDir()
t.Setenv("HOME", tmp) // darwin/linux
t.Setenv("XDG_CONFIG_HOME", "") // force ~/.config derivation on linux
t.Setenv("AppData", filepath.Join(tmp, "AppData", "Roaming")) // windows
if err := runConfigSet(nil, []string{"api_url", "https://written.example"}); err != nil {
t.Fatalf("runConfigSet on missing config dir: %v", err)
}
cd, _ := os.UserConfigDir()
if _, err := os.Stat(filepath.Join(cd, "molecule.yaml")); err != nil {
t.Errorf("config file not written: %v", err)
}
}
// TestCPURLNoTenantFallback confirms cpURL does NOT fall back to apiURL when
// MOLECULE_CP_URL is unset (it returns "" so the admin client can refuse to
// send the CP-admin bearer to a tenant host), and returns MOLECULE_CP_URL when
// set.
func TestCPURLNoTenantFallback(t *testing.T) {
origAPI := apiURL
defer func() { apiURL = origAPI }()
apiURL = "https://tenant.example"
t.Setenv("MOLECULE_CP_URL", "")
if got := cpURL(); got != "" {
t.Errorf("cpURL with MOLECULE_CP_URL unset = %q, want \"\" (no tenant fallback)", got)
}
t.Setenv("MOLECULE_CP_URL", "https://api.moleculesai.app")
if got := cpURL(); got != "https://api.moleculesai.app" {
t.Errorf("cpURL = %q, want CP url", got)
}
}
// TestCPAdminClientCredentialTargeting confirms the CP-admin client never sends
// the CP-admin bearer to the tenant apiURL: it requires an explicit
// MOLECULE_CP_URL and fails fast otherwise, even when MOLECULE_CP_ADMIN_TOKEN
// is set.
func TestCPAdminClientCredentialTargeting(t *testing.T) {
origAPI := apiURL
defer func() { apiURL = origAPI }()
apiURL = "https://tenant.example"
// Token present but no CP URL → must fail fast (NOT target the tenant).
t.Setenv("MOLECULE_CP_ADMIN_TOKEN", "cp-admin-secret")
t.Setenv("MOLECULE_CP_URL", "")
if _, err := cpAdminClient(); err == nil {
t.Fatal("cpAdminClient with no MOLECULE_CP_URL should fail fast, not target the tenant host")
}
// Token + explicit CP URL → client points at the CP, never the tenant.
t.Setenv("MOLECULE_CP_URL", "https://api.moleculesai.app")
cp, err := cpAdminClient()
if err != nil {
t.Fatalf("cpAdminClient with CP URL set: %v", err)
}
if cp.BaseURL != "https://api.moleculesai.app" {
t.Errorf("cpAdminClient BaseURL = %q, want CP url (must not be the tenant %q)", cp.BaseURL, apiURL)
}
if cp.BaseURL == apiURL {
t.Errorf("cpAdminClient targeted the tenant apiURL %q — CP-admin bearer would leak", apiURL)
}
// Missing token → fail fast regardless of CP URL.
t.Setenv("MOLECULE_CP_ADMIN_TOKEN", "")
if _, err := cpAdminClient(); err == nil {
t.Fatal("cpAdminClient with no MOLECULE_CP_ADMIN_TOKEN should fail fast")
}
}
// TestValidMigrationProvider walks the provider validation used by
// 'workspace migrate-provider' (shared helper, same code prod runs).
func TestValidMigrationProvider(t *testing.T) {
for _, p := range []string{"aws", "hetzner", "gcp"} {
if !validMigrationProvider(p) {
t.Errorf("%q should be a valid provider", p)
}
}
for _, p := range []string{"", "azure", "AWS", "do", "linode"} {
if validMigrationProvider(p) {
t.Errorf("%q should NOT be a valid provider", p)
}
}
}
// TestRunWorkspaceMigrateProviderValidation covers the client-side guards in
// runWorkspaceMigrateProvider: --confirm required, --from-instance-id required
// for non-AWS sources, and --from != --to. These fail fast before any CP call.
func TestRunWorkspaceMigrateProviderValidation(t *testing.T) {
orig := migrateFlags
defer func() { migrateFlags = orig }()
resetFlags := func() {
migrateFlags.to = ""
migrateFlags.from = ""
migrateFlags.fromInstanceID = ""
migrateFlags.orgID = ""
migrateFlags.runtime = ""
migrateFlags.confirm = false
}
cases := []struct {
name string
setup func()
wantErr string
}{
{
name: "confirm required",
setup: func() {
resetFlags()
migrateFlags.to = "hetzner"
migrateFlags.from = "aws"
},
wantErr: "refusing to migrate without --confirm",
},
{
name: "from==to rejected",
setup: func() {
resetFlags()
migrateFlags.to = "aws"
migrateFlags.from = "aws"
migrateFlags.confirm = true
},
wantErr: "--from and --to are the same provider",
},
{
name: "non-aws source requires from-instance-id",
setup: func() {
resetFlags()
migrateFlags.to = "aws"
migrateFlags.from = "gcp"
migrateFlags.confirm = true
},
wantErr: "--from-instance-id is required for a non-AWS",
},
{
name: "hetzner source with instance id passes guard",
setup: func() {
resetFlags()
migrateFlags.to = "aws"
migrateFlags.from = "hetzner"
migrateFlags.confirm = true
migrateFlags.fromInstanceID = "hz-box-1"
},
wantErr: "", // fails later at cpAdminClient because env is not set; guard is what we test
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
resetFlags()
tc.setup()
err := runWorkspaceMigrateProvider(nil, []string{"ws-123"})
if tc.wantErr == "" {
if err == nil {
t.Fatal("expected error after guard (cpAdminClient env missing), got nil")
}
if strings.Contains(err.Error(), "--confirm") || strings.Contains(err.Error(), "same provider") || strings.Contains(err.Error(), "--from-instance-id") {
t.Fatalf("expected post-guard error, got guard error: %v", err)
}
return
}
if err == nil {
t.Fatalf("expected error containing %q, got nil", tc.wantErr)
}
if !strings.Contains(err.Error(), tc.wantErr) {
t.Fatalf("expected error containing %q, got %v", tc.wantErr, err)
}
})
}
}