bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
206 lines
6.9 KiB
Go
206 lines
6.9 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package core
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
)
|
|
|
|
// saveAndRestoreWorkspace ensures package-level currentWorkspace is reset
|
|
// between subtests so cross-test pollution can't make assertions pass by
|
|
// accident.
|
|
func saveAndRestoreWorkspace(t *testing.T) {
|
|
t.Helper()
|
|
prev := CurrentWorkspace()
|
|
t.Cleanup(func() { SetCurrentWorkspace(prev) })
|
|
}
|
|
|
|
func TestNotConfiguredError_Local(t *testing.T) {
|
|
saveAndRestoreWorkspace(t)
|
|
SetCurrentWorkspace(WorkspaceLocal)
|
|
|
|
err := NotConfiguredError()
|
|
var cfgErr *errs.ConfigError
|
|
if !errors.As(err, &cfgErr) {
|
|
t.Fatalf("error type = %T, want *errs.ConfigError", err)
|
|
}
|
|
if cfgErr.Category != errs.CategoryConfig || cfgErr.Subtype != errs.SubtypeNotConfigured {
|
|
t.Errorf("category/subtype = %q/%q, want config/not_configured", cfgErr.Category, cfgErr.Subtype)
|
|
}
|
|
if cfgErr.Message != "not configured" {
|
|
t.Errorf("message = %q, want %q", cfgErr.Message, "not configured")
|
|
}
|
|
if !strings.Contains(cfgErr.Hint, "config init --new") {
|
|
t.Errorf("local hint should suggest config init --new; got %q", cfgErr.Hint)
|
|
}
|
|
if strings.Contains(cfgErr.Hint, "config bind") {
|
|
t.Errorf("local hint must not mention config bind; got %q", cfgErr.Hint)
|
|
}
|
|
}
|
|
|
|
func TestNotConfiguredError_OpenClaw(t *testing.T) {
|
|
saveAndRestoreWorkspace(t)
|
|
SetCurrentWorkspace(WorkspaceOpenClaw)
|
|
|
|
err := NotConfiguredError()
|
|
var cfgErr *errs.ConfigError
|
|
if !errors.As(err, &cfgErr) {
|
|
t.Fatalf("error type = %T, want *errs.ConfigError", err)
|
|
}
|
|
// The wire subtype stays not_configured; the workspace name only appears
|
|
// in the message, never in the typed taxonomy.
|
|
if cfgErr.Subtype != errs.SubtypeNotConfigured {
|
|
t.Errorf("subtype = %q, want not_configured", cfgErr.Subtype)
|
|
}
|
|
if !strings.Contains(cfgErr.Message, "openclaw") {
|
|
t.Errorf("message must name the openclaw workspace; got %q", cfgErr.Message)
|
|
}
|
|
// Hint must point at --help (read first, confirm with user, then bind),
|
|
// NOT a directly-executable bind command — binding is policy-laden
|
|
// (identity preset, may overwrite existing binding).
|
|
if !strings.Contains(cfgErr.Hint, "config bind --help") {
|
|
t.Errorf("agent hint must point to `config bind --help`; got %q", cfgErr.Hint)
|
|
}
|
|
if strings.Contains(cfgErr.Hint, "config init") {
|
|
t.Errorf("agent hint must NOT mention config init (would cause AI to create a new app); got %q", cfgErr.Hint)
|
|
}
|
|
}
|
|
|
|
func TestNotConfiguredError_Hermes(t *testing.T) {
|
|
saveAndRestoreWorkspace(t)
|
|
SetCurrentWorkspace(WorkspaceHermes)
|
|
|
|
err := NotConfiguredError()
|
|
var cfgErr *errs.ConfigError
|
|
if !errors.As(err, &cfgErr) {
|
|
t.Fatalf("error type = %T, want *errs.ConfigError", err)
|
|
}
|
|
if cfgErr.Subtype != errs.SubtypeNotConfigured {
|
|
t.Errorf("subtype = %q, want not_configured", cfgErr.Subtype)
|
|
}
|
|
if !strings.Contains(cfgErr.Message, "hermes") {
|
|
t.Errorf("message must name the hermes workspace; got %q", cfgErr.Message)
|
|
}
|
|
if !strings.Contains(cfgErr.Hint, "config bind --help") {
|
|
t.Errorf("hermes hint must point to `config bind --help`; got %q", cfgErr.Hint)
|
|
}
|
|
}
|
|
|
|
func TestNoActiveProfileError_Local(t *testing.T) {
|
|
saveAndRestoreWorkspace(t)
|
|
SetCurrentWorkspace(WorkspaceLocal)
|
|
|
|
err := NoActiveProfileError()
|
|
var cfgErr *errs.ConfigError
|
|
if !errors.As(err, &cfgErr) {
|
|
t.Fatalf("error type = %T, want *errs.ConfigError", err)
|
|
}
|
|
if cfgErr.Message != "no active profile" {
|
|
t.Errorf("message = %q, want %q", cfgErr.Message, "no active profile")
|
|
}
|
|
}
|
|
|
|
func TestNoActiveProfileError_AgentSuggestsBind(t *testing.T) {
|
|
saveAndRestoreWorkspace(t)
|
|
SetCurrentWorkspace(WorkspaceOpenClaw)
|
|
|
|
err := NoActiveProfileError()
|
|
var cfgErr *errs.ConfigError
|
|
if !errors.As(err, &cfgErr) {
|
|
t.Fatalf("error type = %T, want *errs.ConfigError", err)
|
|
}
|
|
if !strings.Contains(cfgErr.Hint, "config bind --help") {
|
|
t.Errorf("agent hint must point to `config bind --help`; got %q", cfgErr.Hint)
|
|
}
|
|
}
|
|
|
|
func TestReconfigureHint_Local(t *testing.T) {
|
|
saveAndRestoreWorkspace(t)
|
|
SetCurrentWorkspace(WorkspaceLocal)
|
|
|
|
got := reconfigureHint()
|
|
if !strings.Contains(got, "config init") {
|
|
t.Errorf("local reconfigure hint must mention config init; got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestReconfigureHint_Agent(t *testing.T) {
|
|
saveAndRestoreWorkspace(t)
|
|
SetCurrentWorkspace(WorkspaceHermes)
|
|
|
|
got := reconfigureHint()
|
|
if !strings.Contains(got, "config bind --help") {
|
|
t.Errorf("agent reconfigure hint must point to `config bind --help`; got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestLoadOrNotConfigured_FileMissing_ReturnsNotConfigured(t *testing.T) {
|
|
saveAndRestoreWorkspace(t)
|
|
SetCurrentWorkspace(WorkspaceLocal)
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
_, err := LoadOrNotConfigured()
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
var cfgErr *errs.ConfigError
|
|
if !errors.As(err, &cfgErr) {
|
|
t.Fatalf("error type = %T, want *errs.ConfigError", err)
|
|
}
|
|
if cfgErr.Subtype != errs.SubtypeNotConfigured {
|
|
t.Errorf("subtype = %q, want not_configured", cfgErr.Subtype)
|
|
}
|
|
if cfgErr.Message != "not configured" {
|
|
t.Errorf("message = %q, want \"not configured\"", cfgErr.Message)
|
|
}
|
|
if !strings.Contains(cfgErr.Hint, "config init --new") {
|
|
t.Errorf("missing-file in local must hint `config init --new`; got %q", cfgErr.Hint)
|
|
}
|
|
}
|
|
|
|
// TestLoadOrNotConfigured_CorruptFile_PreservesCause is the regression guard
|
|
// for the previous "every load error → not configured" coercion: a malformed
|
|
// config.json must surface its real failure cause so the user can fix it,
|
|
// not get sent in circles by an init/bind hint that wouldn't help here.
|
|
func TestLoadOrNotConfigured_CorruptFile_PreservesCause(t *testing.T) {
|
|
dir := t.TempDir()
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", dir)
|
|
// Write garbage that will fail JSON parsing.
|
|
if err := os.WriteFile(dir+"/config.json", []byte("{not valid json"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err := LoadOrNotConfigured()
|
|
if err == nil {
|
|
t.Fatal("expected error for corrupt config")
|
|
}
|
|
var cfgErr *errs.ConfigError
|
|
if !errors.As(err, &cfgErr) {
|
|
t.Fatalf("error type = %T, want *errs.ConfigError", err)
|
|
}
|
|
// A malformed file maps to invalid_config, not not_configured.
|
|
if cfgErr.Subtype != errs.SubtypeInvalidConfig {
|
|
t.Errorf("subtype = %q, want invalid_config", cfgErr.Subtype)
|
|
}
|
|
if !strings.Contains(cfgErr.Message, "failed to load config") {
|
|
t.Errorf("corrupt-file message must say 'failed to load config'; got %q", cfgErr.Message)
|
|
}
|
|
// And it must NOT pretend the user just hasn't initialised yet.
|
|
if cfgErr.Message == "not configured" {
|
|
t.Errorf("corrupt-file must not be coerced to 'not configured'")
|
|
}
|
|
if strings.Contains(cfgErr.Hint, "config init") || strings.Contains(cfgErr.Hint, "config bind") {
|
|
t.Errorf("corrupt-file hint must not redirect to init/bind; got %q", cfgErr.Hint)
|
|
}
|
|
// The underlying parse failure stays reachable through the unwrap chain.
|
|
if cfgErr.Cause == nil {
|
|
t.Error("Cause must wrap the underlying load error for errors.Is/Unwrap")
|
|
}
|
|
}
|