Files
larksuite-cli/internal/binding/reader_test.go
evandance ce80b3bc46 feat(config): add 'config bind' for per-Agent credential isolation (#515)
Give each AI Agent (OpenClaw, Hermes) its own lark-cli workspace so
its Feishu calls don't overwrite the developer's local config or
collide with other Agents.

    lark-cli config bind [--source openclaw|hermes] [--app-id <id>]
                         [--identity bot-only|user-default] [--force]

Key capabilities:

- Source auto-detected from OPENCLAW_* / HERMES_* env signals; config
  written to ~/.lark-cli/<agent>/, isolated per Agent.
- Two identity presets: 'bot-only' (flag-mode default) and
  'user-default'. Flag mode rejects silent bot→user escalation
  without --force; TUI prompts are exempt.
- Agent-friendly stdout JSON with 'identity' + 'message' for
  next-step branching.
- 'config show' and 'doctor' expose the bound 'workspace'.
- OpenClaw SecretRef resolution: plain / ${VAR} / file:+JSON Pointer
  / exec:.
2026-04-23 19:51:36 +08:00

183 lines
5.1 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package binding
import (
"os"
"path/filepath"
"testing"
)
func TestReadOpenClawConfig_ValidSingleAccount(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "openclaw.json")
data := `{"channels":{"feishu":{"appId":"cli_abc","appSecret":"plain_secret","domain":"feishu"}}}`
if err := os.WriteFile(p, []byte(data), 0o644); err != nil {
t.Fatalf("write temp file: %v", err)
}
root, err := ReadOpenClawConfig(p)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if root.Channels.Feishu == nil {
t.Fatal("expected Channels.Feishu to be non-nil")
}
if got := root.Channels.Feishu.AppID; got != "cli_abc" {
t.Errorf("AppID = %q, want %q", got, "cli_abc")
}
if got := root.Channels.Feishu.AppSecret.Plain; got != "plain_secret" {
t.Errorf("AppSecret.Plain = %q, want %q", got, "plain_secret")
}
if root.Channels.Feishu.AppSecret.Ref != nil {
t.Error("AppSecret.Ref should be nil for a plain string")
}
if got := root.Channels.Feishu.Brand; got != "feishu" {
t.Errorf("Brand = %q, want %q", got, "feishu")
}
}
func TestReadOpenClawConfig_ValidMultiAccount(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "openclaw.json")
data := `{
"channels": {
"feishu": {
"domain": "feishu",
"accounts": {
"work": {"appId": "cli_work", "appSecret": "secret_work", "domain": "feishu"},
"personal": {"appId": "cli_personal", "appSecret": "secret_personal", "domain": "lark"}
}
}
}
}`
if err := os.WriteFile(p, []byte(data), 0o644); err != nil {
t.Fatalf("write temp file: %v", err)
}
root, err := ReadOpenClawConfig(p)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if root.Channels.Feishu == nil {
t.Fatal("expected Channels.Feishu to be non-nil")
}
apps := ListCandidateApps(root.Channels.Feishu)
if len(apps) != 2 {
t.Fatalf("ListCandidateApps returned %d apps, want 2", len(apps))
}
byLabel := make(map[string]CandidateApp, len(apps))
for _, a := range apps {
byLabel[a.Label] = a
}
work, ok := byLabel["work"]
if !ok {
t.Fatal("missing account label 'work'")
}
if work.AppID != "cli_work" {
t.Errorf("work.AppID = %q, want %q", work.AppID, "cli_work")
}
personal, ok := byLabel["personal"]
if !ok {
t.Fatal("missing account label 'personal'")
}
if personal.AppID != "cli_personal" {
t.Errorf("personal.AppID = %q, want %q", personal.AppID, "cli_personal")
}
}
func TestReadOpenClawConfig_MissingFeishu(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "openclaw.json")
data := `{"channels":{}}`
if err := os.WriteFile(p, []byte(data), 0o644); err != nil {
t.Fatalf("write temp file: %v", err)
}
root, err := ReadOpenClawConfig(p)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if root.Channels.Feishu != nil {
t.Error("expected Channels.Feishu to be nil when not present in JSON")
}
}
func TestReadOpenClawConfig_InvalidJSON(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "openclaw.json")
if err := os.WriteFile(p, []byte(`{not valid json`), 0o644); err != nil {
t.Fatalf("write temp file: %v", err)
}
_, err := ReadOpenClawConfig(p)
if err == nil {
t.Fatal("expected error for invalid JSON, got nil")
}
}
func TestReadOpenClawConfig_FileNotFound(t *testing.T) {
_, err := ReadOpenClawConfig(filepath.Join(t.TempDir(), "nonexistent.json"))
if err == nil {
t.Fatal("expected error for non-existent file, got nil")
}
}
func TestReadOpenClawConfig_EnvTemplate(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "openclaw.json")
data := `{"channels":{"feishu":{"appId":"cli_env","appSecret":"${FEISHU_APP_SECRET}","domain":"feishu"}}}`
if err := os.WriteFile(p, []byte(data), 0o644); err != nil {
t.Fatalf("write temp file: %v", err)
}
root, err := ReadOpenClawConfig(p)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
secret := root.Channels.Feishu.AppSecret
if secret.Plain != "${FEISHU_APP_SECRET}" {
t.Errorf("SecretInput.Plain = %q, want %q", secret.Plain, "${FEISHU_APP_SECRET}")
}
if secret.Ref != nil {
t.Error("SecretInput.Ref should be nil for env template string")
}
}
func TestReadOpenClawConfig_SecretRefObject(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "openclaw.json")
data := `{"channels":{"feishu":{"appId":"cli_ref","appSecret":{"source":"file","provider":"fp","id":"/path"},"domain":"feishu"}}}`
if err := os.WriteFile(p, []byte(data), 0o644); err != nil {
t.Fatalf("write temp file: %v", err)
}
root, err := ReadOpenClawConfig(p)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
secret := root.Channels.Feishu.AppSecret
if secret.Plain != "" {
t.Errorf("SecretInput.Plain = %q, want empty for object form", secret.Plain)
}
if secret.Ref == nil {
t.Fatal("SecretInput.Ref should be non-nil for object form")
}
if secret.Ref.Source != "file" {
t.Errorf("Ref.Source = %q, want %q", secret.Ref.Source, "file")
}
if secret.Ref.Provider != "fp" {
t.Errorf("Ref.Provider = %q, want %q", secret.Ref.Provider, "fp")
}
if secret.Ref.ID != "/path" {
t.Errorf("Ref.ID = %q, want %q", secret.Ref.ID, "/path")
}
}