631 lines
22 KiB
Go
631 lines
22 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"image"
|
|
"image/color"
|
|
"image/png"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/openclaw/clawdex/internal/model"
|
|
)
|
|
|
|
func TestExecuteEndToEndLocalCommands(t *testing.T) {
|
|
cfg, data := testPaths(t)
|
|
run := func(args ...string) string {
|
|
t.Helper()
|
|
var out, errOut bytes.Buffer
|
|
full := append([]string{"--config", cfg}, args...)
|
|
if err := Execute(full, &out, &errOut); err != nil {
|
|
t.Fatalf("Execute(%v): %v stderr=%s stdout=%s", full, err, errOut.String(), out.String())
|
|
}
|
|
return out.String()
|
|
}
|
|
out := run("init", data, "--remote", "")
|
|
if !strings.Contains(out, "repo_path:") {
|
|
t.Fatalf("init out = %s", out)
|
|
}
|
|
out = run("person", "add", "Ada Lovelace", "--email", "ada@example.com", "--phone", "+1 555 0100", "--tag", "math")
|
|
if !strings.Contains(out, "Ada Lovelace") {
|
|
t.Fatalf("add out = %s", out)
|
|
}
|
|
out = run("person", "list", "--plain")
|
|
if !strings.Contains(out, "Ada Lovelace") {
|
|
t.Fatalf("list out = %s", out)
|
|
}
|
|
out = run("person", "show", "ada@example.com")
|
|
if !strings.Contains(out, "email: ada@example.com") {
|
|
t.Fatalf("show out = %s", out)
|
|
}
|
|
avatarPath := filepath.Join(t.TempDir(), "avatar.png")
|
|
writeCLITestPNG(t, avatarPath)
|
|
out = run("person", "avatar", "set", "ada@example.com", avatarPath)
|
|
if !strings.Contains(out, `"path": "avatars/avatar.png"`) {
|
|
t.Fatalf("avatar set out = %s", out)
|
|
}
|
|
out = run("person", "avatar", "show", "ada@example.com", "--path")
|
|
if !strings.Contains(out, "avatars/avatar.png") {
|
|
t.Fatalf("avatar show path = %s", out)
|
|
}
|
|
out = run("person", "avatar", "show", "ada@example.com")
|
|
if !strings.Contains(out, `"mime": "image/png"`) {
|
|
t.Fatalf("avatar show = %s", out)
|
|
}
|
|
out = run("--dry-run", "person", "avatar", "set", "ada@example.com", avatarPath)
|
|
if !strings.Contains(out, "would_set_avatar") {
|
|
t.Fatalf("avatar dry set out = %s", out)
|
|
}
|
|
out = run("note", "add", "ada", "--kind", "dm", "--source", "manual", "--text", "Analytical engine")
|
|
if !strings.Contains(out, "dm\tmanual") {
|
|
t.Fatalf("note out = %s", out)
|
|
}
|
|
out = run("note", "list", "ada")
|
|
if !strings.Contains(out, "Analytical engine") {
|
|
t.Fatalf("notes out = %s", out)
|
|
}
|
|
out = run("timeline", "ada")
|
|
if !strings.Contains(out, "Analytical engine") {
|
|
t.Fatalf("timeline out = %s", out)
|
|
}
|
|
out = run("search", "engine", "--plain")
|
|
if !strings.Contains(out, "note") {
|
|
t.Fatalf("search out = %s", out)
|
|
}
|
|
vcardPath := filepath.Join(t.TempDir(), "contacts.vcf")
|
|
out = run("export", "vcard", "--all", "--include-avatars", "-o", vcardPath)
|
|
if !strings.Contains(out, "exported: 1") {
|
|
t.Fatalf("export out = %s", out)
|
|
}
|
|
if data, err := os.ReadFile(vcardPath); err != nil || !strings.Contains(string(data), "BEGIN:VCARD") {
|
|
t.Fatalf("vcard data=%q err=%v", data, err)
|
|
}
|
|
out = run("person", "avatar", "clear", "ada@example.com")
|
|
if !strings.Contains(out, "Ada Lovelace") {
|
|
t.Fatalf("avatar clear out = %s", out)
|
|
}
|
|
out = run("--dry-run", "person", "avatar", "clear", "ada@example.com")
|
|
if !strings.Contains(out, "would_clear_avatar") {
|
|
t.Fatalf("avatar dry clear out = %s", out)
|
|
}
|
|
var noAvatarOut, noAvatarErr bytes.Buffer
|
|
if err := Execute([]string{"--config", cfg, "person", "avatar", "show", "ada@example.com"}, &noAvatarOut, &noAvatarErr); err == nil {
|
|
t.Fatal("expected no avatar error")
|
|
}
|
|
out = run("sync", "apple")
|
|
if !strings.Contains(out, "remote writes not implemented") {
|
|
t.Fatalf("sync out = %s", out)
|
|
}
|
|
out = run("sync", "google", "--account", "me@example.com")
|
|
if !strings.Contains(out, "me@example.com") {
|
|
t.Fatalf("sync google out = %s", out)
|
|
}
|
|
out = run("doctor")
|
|
if !strings.Contains(out, "people: 1") {
|
|
t.Fatalf("doctor out = %s", out)
|
|
}
|
|
out = run("git", "commit", "-m", "test: contacts")
|
|
if !strings.Contains(out, "committed: true") {
|
|
t.Fatalf("git commit out = %s", out)
|
|
}
|
|
out = run("git", "commit", "-m", "test: no changes")
|
|
if !strings.Contains(out, "committed: false") {
|
|
t.Fatalf("git commit clean out = %s", out)
|
|
}
|
|
}
|
|
|
|
func writeCLITestPNG(t *testing.T, path string) {
|
|
t.Helper()
|
|
img := image.NewRGBA(image.Rect(0, 0, 1, 1))
|
|
img.Set(0, 0, color.RGBA{R: 255, A: 255})
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
if err := png.Encode(f, img); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestExecuteConfigJSONAndUsage(t *testing.T) {
|
|
cfg, data := testPaths(t)
|
|
var out, errOut bytes.Buffer
|
|
if err := Execute([]string{"--config", cfg, "--json", "init", data, "--remote", ""}, &out, &errOut); err != nil {
|
|
t.Fatalf("init: %v %s", err, errOut.String())
|
|
}
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(out.Bytes(), &payload); err != nil {
|
|
t.Fatalf("json = %s err=%v", out.String(), err)
|
|
}
|
|
out.Reset()
|
|
if err := Execute([]string{"--config", cfg, "config", "set", "git.branch", "main"}, &out, &errOut); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out.Reset()
|
|
if err := Execute([]string{"--config", cfg, "--dry-run", "config", "set", "google.default_account", "me@example.com"}, &out, &errOut); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(out.String(), "me@example.com") {
|
|
t.Fatalf("dry config = %s", out.String())
|
|
}
|
|
out.Reset()
|
|
if err := Execute([]string{"--config", cfg, "--json", "config", "show"}, &out, &errOut); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(out.String(), `"branch": "main"`) {
|
|
t.Fatalf("config = %s", out.String())
|
|
}
|
|
if err := Execute([]string{"--config", cfg, "config", "set", "nope", "x"}, &out, &errOut); err == nil || ExitCode(err) != 2 {
|
|
t.Fatalf("expected usage err, got %v", err)
|
|
}
|
|
if err := Execute([]string{"--bogus"}, &out, &errOut); err == nil || ExitCode(err) != 2 {
|
|
t.Fatalf("expected parse usage err, got %v", err)
|
|
}
|
|
badCfg := filepath.Join(t.TempDir(), "bad.toml")
|
|
if err := os.WriteFile(badCfg, []byte("["), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := Execute([]string{"--config", badCfg, "config"}, &out, &errOut); err == nil {
|
|
t.Fatal("expected config parse error")
|
|
}
|
|
if ExitCode(nil) != 0 {
|
|
t.Fatal("nil exit code")
|
|
}
|
|
}
|
|
|
|
func TestExecuteImportAppleFromFileAndGoogleViaFakeGog(t *testing.T) {
|
|
cfg, data := testPaths(t)
|
|
var out, errOut bytes.Buffer
|
|
if err := Execute([]string{"--config", cfg, "init", data, "--remote", ""}, &out, &errOut); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
input := filepath.Join(t.TempDir(), "apple.ndjson")
|
|
if err := os.WriteFile(input, []byte("{\"identifier\":\"a1\",\"full_name\":\"Ada Apple\",\"emails\":[\"apple@example.com\"]}\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out.Reset()
|
|
if err := Execute([]string{"--config", cfg, "import", "apple", "--input", input}, &out, &errOut); err != nil {
|
|
t.Fatalf("apple import: %v %s", err, errOut.String())
|
|
}
|
|
if !strings.Contains(out.String(), "create\tAda Apple") {
|
|
t.Fatalf("apple import out = %s", out.String())
|
|
}
|
|
fakeGog := writeFakeGog(t, `[{"resourceName":"people/g1","name":"Grace Google","email":"grace@example.com"}]`)
|
|
t.Setenv("PATH", filepath.Dir(fakeGog)+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
out.Reset()
|
|
if err := Execute([]string{"--config", cfg, "import", "google", "--account", "me@example.com"}, &out, &errOut); err != nil {
|
|
t.Fatalf("google import: %v %s", err, errOut.String())
|
|
}
|
|
if !strings.Contains(out.String(), "create\tGrace Google") {
|
|
t.Fatalf("google import out = %s", out.String())
|
|
}
|
|
out.Reset()
|
|
if err := Execute([]string{"--config", cfg, "person", "show", "grace@example.com"}, &out, &errOut); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(out.String(), "Grace Google") {
|
|
t.Fatalf("show = %s", out.String())
|
|
}
|
|
fakeSQLite := writeFakeSQLite(t, `[{"channel_id":"dm1","name":"Discord Friend","messages":5,"counterpart_id":"user1"}]`)
|
|
t.Setenv("PATH", filepath.Dir(fakeSQLite)+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
out.Reset()
|
|
if err := Execute([]string{"--config", cfg, "import", "discrawl", "--db", filepath.Join(t.TempDir(), "discrawl.db"), "--min-messages", "4"}, &out, &errOut); err != nil {
|
|
t.Fatalf("discrawl import: %v %s", err, errOut.String())
|
|
}
|
|
if !strings.Contains(out.String(), "create\tDiscord Friend") {
|
|
t.Fatalf("discrawl import out = %s", out.String())
|
|
}
|
|
fakeBirdclaw := writeFakeSQLite(t, `[{"conversation_id":"1-2","profile_id":"2","handle":"bird","display_name":"Bird Person","messages":5}]`)
|
|
t.Setenv("PATH", filepath.Dir(fakeBirdclaw)+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
out.Reset()
|
|
if err := Execute([]string{"--config", cfg, "import", "birdclaw", "--db", filepath.Join(t.TempDir(), "birdclaw.sqlite"), "--min-messages", "4"}, &out, &errOut); err != nil {
|
|
t.Fatalf("birdclaw import: %v %s", err, errOut.String())
|
|
}
|
|
if !strings.Contains(out.String(), "create\tBird Person") {
|
|
t.Fatalf("birdclaw import out = %s", out.String())
|
|
}
|
|
}
|
|
|
|
func TestExecuteGitStatusAndDryRun(t *testing.T) {
|
|
cfg, data := testPaths(t)
|
|
var out, errOut bytes.Buffer
|
|
if err := Execute([]string{"--config", cfg, "init", data, "--remote", ""}, &out, &errOut); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out.Reset()
|
|
if err := Execute([]string{"--config", cfg, "--dry-run", "person", "add", "Dry Run"}, &out, &errOut); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(out.String(), "would_create: Dry Run") {
|
|
t.Fatalf("dry run = %s", out.String())
|
|
}
|
|
out.Reset()
|
|
if err := Execute([]string{"--config", cfg, "git"}, &out, &errOut); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(out.String(), "No commits yet") {
|
|
t.Fatalf("git status = %s", out.String())
|
|
}
|
|
}
|
|
|
|
func TestExecuteImportDiscrawlErrors(t *testing.T) {
|
|
cfg, data := testPaths(t)
|
|
var out, errOut bytes.Buffer
|
|
if err := Execute([]string{"--config", cfg, "init", data, "--remote", ""}, &out, &errOut); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fakeSQLite := filepath.Join(t.TempDir(), "sqlite3")
|
|
if err := os.WriteFile(fakeSQLite, []byte("#!/bin/sh\necho locked >&2\nexit 1\n"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("PATH", filepath.Dir(fakeSQLite)+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
if err := Execute([]string{"--config", cfg, "import", "discrawl", "--db", filepath.Join(t.TempDir(), "discrawl.db")}, &out, &errOut); err == nil {
|
|
t.Fatal("expected discrawl import error")
|
|
}
|
|
}
|
|
|
|
func TestExecuteImportBirdclawErrors(t *testing.T) {
|
|
cfg, data := testPaths(t)
|
|
var out, errOut bytes.Buffer
|
|
if err := Execute([]string{"--config", cfg, "init", data, "--remote", ""}, &out, &errOut); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fakeSQLite := filepath.Join(t.TempDir(), "sqlite3")
|
|
if err := os.WriteFile(fakeSQLite, []byte("#!/bin/sh\necho locked >&2\nexit 1\n"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("PATH", filepath.Dir(fakeSQLite)+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
if err := Execute([]string{"--config", cfg, "import", "birdclaw", "--db", filepath.Join(t.TempDir(), "birdclaw.sqlite")}, &out, &errOut); err == nil {
|
|
t.Fatal("expected birdclaw import error")
|
|
}
|
|
}
|
|
|
|
func TestExecuteJSONPlainAndStdoutBranches(t *testing.T) {
|
|
cfg, data := testPaths(t)
|
|
var out, errOut bytes.Buffer
|
|
must := func(args ...string) string {
|
|
t.Helper()
|
|
out.Reset()
|
|
errOut.Reset()
|
|
if err := Execute(append([]string{"--config", cfg}, args...), &out, &errOut); err != nil {
|
|
t.Fatalf("%v: %v stderr=%s", args, err, errOut.String())
|
|
}
|
|
return out.String()
|
|
}
|
|
must("init", data, "--remote", "")
|
|
must("person", "add", "Ada JSON", "--email", "json@example.com")
|
|
must("person", "add", "Empty Email")
|
|
if got := must("--json", "person", "show", "json@example.com"); !strings.Contains(got, `"name": "Ada JSON"`) {
|
|
t.Fatalf("json show = %s", got)
|
|
}
|
|
if got := must("--json", "person", "list", "--query", "Ada"); !strings.Contains(got, `"Ada JSON"`) {
|
|
t.Fatalf("json list = %s", got)
|
|
}
|
|
if got := must("--plain", "person", "show", "json@example.com"); !strings.Contains(got, "Ada JSON") {
|
|
t.Fatalf("plain show = %s", got)
|
|
}
|
|
if got := must("--plain", "person", "list", "--query", "NoMatch"); got != "" {
|
|
t.Fatalf("empty list = %s", got)
|
|
}
|
|
if got := must("person", "list", "--query", "Empty"); !strings.Contains(got, "Empty Email") {
|
|
t.Fatalf("no-email list = %s", got)
|
|
}
|
|
must("note", "add", "json@example.com", "--kind", "call", "--source", "manual", "--text", "Call body", "--occurred-at", "2026-05-08 10:00")
|
|
if got := must("--json", "note", "list", "json@example.com"); !strings.Contains(got, `"kind": "call"`) {
|
|
t.Fatalf("json notes = %s", got)
|
|
}
|
|
if got := must("export", "vcard", "--person", "json@example.com", "-o", "-"); !strings.Contains(got, "BEGIN:VCARD") {
|
|
t.Fatalf("stdout vcard = %s", got)
|
|
}
|
|
input := filepath.Join(t.TempDir(), "apple.ndjson")
|
|
if err := os.WriteFile(input, []byte("{\"identifier\":\"a1\",\"full_name\":\"Dry Apple\",\"emails\":[\"dry@example.com\"]}\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := must("--dry-run", "import", "apple", "--input", input); !strings.Contains(got, "create\tDry Apple") {
|
|
t.Fatalf("dry import = %s", got)
|
|
}
|
|
}
|
|
|
|
func TestPrintHelpersCoverPlainJSONAndWriteErrors(t *testing.T) {
|
|
var out bytes.Buffer
|
|
person := model.Person{
|
|
ID: "person_1",
|
|
Name: "Print Person",
|
|
Path: "/tmp/person.md",
|
|
Emails: []model.ContactValue{{Value: "print@example.com"}},
|
|
}
|
|
note := model.Note{
|
|
ID: "note_1",
|
|
Kind: "note",
|
|
Source: "manual",
|
|
OccurredAt: time.Date(2026, 5, 8, 10, 0, 0, 0, time.UTC),
|
|
Body: "line one\nline two",
|
|
}
|
|
hit := model.SearchHit{Kind: "note", ID: "note_1", Name: "Print Person", Snippet: "line", Path: "/tmp/note.md"}
|
|
|
|
r := &Runtime{stdout: &out, root: &CLI{}}
|
|
if err := r.printPeople([]model.Person{person}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(out.String(), "print@example.com") {
|
|
t.Fatalf("people out = %s", out.String())
|
|
}
|
|
out.Reset()
|
|
if err := r.printTimeline([]model.Note{note}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(out.String(), "\nline two") {
|
|
t.Fatalf("timeline did not flatten body = %s", out.String())
|
|
}
|
|
out.Reset()
|
|
if err := r.printHits([]model.SearchHit{hit}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(out.String(), "line") {
|
|
t.Fatalf("hits out = %s", out.String())
|
|
}
|
|
out.Reset()
|
|
r.root.Plain = true
|
|
if err := r.printPeople([]model.Person{{ID: "person_2", Name: "No Email", Path: "/tmp/no.md"}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := r.printHits([]model.SearchHit{hit}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r.root.JSON = true
|
|
if err := r.printTimeline([]model.Note{note}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
r.stdout = errWriter{}
|
|
r.root.JSON = false
|
|
if err := r.printPeople([]model.Person{person}); err == nil {
|
|
t.Fatal("expected printPeople write error")
|
|
}
|
|
if err := r.printTimeline([]model.Note{note}); err == nil {
|
|
t.Fatal("expected printTimeline write error")
|
|
}
|
|
if err := r.printHits([]model.SearchHit{hit}); err == nil {
|
|
t.Fatal("expected printHits write error")
|
|
}
|
|
if err := r.printPerson(person); err == nil {
|
|
t.Fatal("expected printPerson write error")
|
|
}
|
|
}
|
|
|
|
type errWriter struct{}
|
|
|
|
func (errWriter) Write([]byte) (int, error) {
|
|
return 0, errors.New("write failed")
|
|
}
|
|
|
|
func TestExecuteGitPushPullWithLocalRemote(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfg := filepath.Join(dir, "config.toml")
|
|
data := filepath.Join(dir, "contacts")
|
|
remote := filepath.Join(dir, "remote.git")
|
|
if err := os.Mkdir(remote, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
runShell(t, remote, "git", "init", "--bare")
|
|
var out, errOut bytes.Buffer
|
|
for _, args := range [][]string{
|
|
{"--config", cfg, "init", data, "--remote", remote},
|
|
{"--config", cfg, "person", "add", "Ada Remote"},
|
|
{"--config", cfg, "git", "commit", "-m", "test: remote"},
|
|
{"--config", cfg, "git", "push"},
|
|
{"--config", cfg, "git", "pull"},
|
|
} {
|
|
out.Reset()
|
|
errOut.Reset()
|
|
if err := Execute(args, &out, &errOut); err != nil {
|
|
t.Fatalf("%v: %v stderr=%s stdout=%s", args, err, errOut.String(), out.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExecuteEditorExportPersonAndRepair(t *testing.T) {
|
|
cfg, data := testPaths(t)
|
|
var out, errOut bytes.Buffer
|
|
if err := Execute([]string{"--config", cfg, "init", data, "--remote", ""}, &out, &errOut); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := Execute([]string{"--config", cfg, "person", "add", "Ada Edit", "--email", "edit@example.com"}, &out, &errOut); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
editor := filepath.Join(t.TempDir(), "editor")
|
|
if err := os.WriteFile(editor, []byte("#!/bin/sh\nprintf '%s' \"$1\" > \""+filepath.Join(t.TempDir(), "edited")+"\"\n"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("EDITOR", editor)
|
|
if err := Execute([]string{"--config", cfg, "person", "edit", "edit@example.com"}, &out, &errOut); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
vcardPath := filepath.Join(t.TempDir(), "one.vcf")
|
|
if err := Execute([]string{"--config", cfg, "export", "vcard", "--person", "edit@example.com", "-o", vcardPath}, &out, &errOut); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
personPath := filepath.Join(data, "people", "ada-edit", "person.md")
|
|
if err := os.WriteFile(personPath, []byte("---\nid: person_x\nname: Ada Edit\ntags: [broken\n---\n# Ada Edit\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out.Reset()
|
|
if err := Execute([]string{"--config", cfg, "--dry-run", "doctor", "--repair"}, &out, &errOut); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(out.String(), "repaired: 1") {
|
|
t.Fatalf("repair dry-run = %s", out.String())
|
|
}
|
|
out.Reset()
|
|
if err := Execute([]string{"--config", cfg, "doctor", "--repair"}, &out, &errOut); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(out.String(), "repaired: 1") {
|
|
t.Fatalf("repair = %s", out.String())
|
|
}
|
|
if err := os.WriteFile(personPath, []byte("---\nid: person_x\nname: Ada Edit\navatar:\n path: avatars/missing.png\n---\n# Ada Edit\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out.Reset()
|
|
if err := Execute([]string{"--config", cfg, "--dry-run", "doctor", "--repair"}, &out, &errOut); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(out.String(), "avatar_problems: 1") || !strings.Contains(out.String(), "avatar_repaired: 1") {
|
|
t.Fatalf("avatar repair dry-run = %s", out.String())
|
|
}
|
|
out.Reset()
|
|
if err := Execute([]string{"--config", cfg, "doctor", "--repair"}, &out, &errOut); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(out.String(), "avatar_repaired: 1") {
|
|
t.Fatalf("avatar repair = %s", out.String())
|
|
}
|
|
}
|
|
|
|
func TestExecuteUsageGuards(t *testing.T) {
|
|
cfg, data := testPaths(t)
|
|
var out, errOut bytes.Buffer
|
|
if err := Execute([]string{"--config", cfg, "init", data, "--remote", ""}, &out, &errOut); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, args := range [][]string{
|
|
{"--config", cfg, "note", "add", "nobody", "--kind", "note", "--source", "manual"},
|
|
{"--config", cfg, "note", "add", "nobody", "--kind", "note", "--source", "manual", "--text", "x", "--occurred-at", "bad"},
|
|
{"--config", cfg, "export", "vcard", "-o", filepath.Join(t.TempDir(), "x.vcf")},
|
|
{"--config", cfg, "person", "show", "missing"},
|
|
{"--config", cfg, "person", "avatar", "clear", "missing"},
|
|
{"--config", cfg, "--dry-run", "person", "avatar", "set", "nobody", filepath.Join(t.TempDir(), "missing.png")},
|
|
} {
|
|
out.Reset()
|
|
errOut.Reset()
|
|
if err := Execute(args, &out, &errOut); err == nil {
|
|
t.Fatalf("expected error for %v", args)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSmallCLIHelpers(t *testing.T) {
|
|
if got := firstNonEmpty("", " ", "x"); got != "x" {
|
|
t.Fatalf("firstNonEmpty = %q", got)
|
|
}
|
|
if got := firstNonEmpty("", " "); got != "" {
|
|
t.Fatalf("firstNonEmpty empty = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestExecuteErrorBranchesAndNoConfigInit(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfg := filepath.Join(dir, "config.toml")
|
|
data := filepath.Join(dir, "contacts")
|
|
var out, errOut bytes.Buffer
|
|
if err := Execute([]string{"--config", cfg, "init", data, "--remote", "", "--no-config"}, &out, &errOut); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := os.Stat(cfg); !os.IsNotExist(err) {
|
|
t.Fatalf("config unexpectedly exists: %v", err)
|
|
}
|
|
for _, args := range [][]string{
|
|
{"--config", cfg, "--repo", filepath.Join(dir, "missing"), "person", "add", "No Repo"},
|
|
{"--config", cfg, "--repo", filepath.Join(dir, "missing"), "person", "list"},
|
|
{"--config", cfg, "--repo", filepath.Join(dir, "missing"), "export", "vcard", "--all", "-o", "-"},
|
|
{"--config", cfg, "--repo", filepath.Join(dir, "missing"), "doctor"},
|
|
} {
|
|
out.Reset()
|
|
errOut.Reset()
|
|
if err := Execute(args, &out, &errOut); err == nil {
|
|
t.Fatalf("expected error for %v", args)
|
|
}
|
|
}
|
|
out.Reset()
|
|
errOut.Reset()
|
|
if err := Execute([]string{"--config", cfg, "init", data, "--remote", ""}, &out, &errOut); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := Execute([]string{"--config", cfg, "config", "set", "repo_path", data}, &out, &errOut); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := Execute([]string{"--config", cfg, "config", "set", "git.remote", ""}, &out, &errOut); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, args := range [][]string{
|
|
{"--config", cfg, "note", "list", "missing"},
|
|
{"--config", cfg, "timeline", "missing"},
|
|
{"--config", cfg, "search", ""},
|
|
{"--config", cfg, "export", "vcard", "--person", "missing", "-o", "-"},
|
|
{"--config", cfg, "export", "vcard", "--all", "-o", filepath.Join(dir, "nope", "x.vcf")},
|
|
{"--config", cfg, "import", "apple", "--input", filepath.Join(dir, "missing.ndjson")},
|
|
} {
|
|
out.Reset()
|
|
errOut.Reset()
|
|
if err := Execute(args, &out, &errOut); err == nil {
|
|
t.Fatalf("expected error for %v", args)
|
|
}
|
|
}
|
|
fakeGog := writeFakeGogExit(t)
|
|
t.Setenv("PATH", filepath.Dir(fakeGog)+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
if err := Execute([]string{"--config", cfg, "import", "google"}, &out, &errOut); err == nil {
|
|
t.Fatal("expected fake gog failure")
|
|
}
|
|
}
|
|
|
|
func testPaths(t *testing.T) (string, string) {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
return filepath.Join(dir, "config.toml"), filepath.Join(dir, "contacts")
|
|
}
|
|
|
|
func writeFakeGog(t *testing.T, output string) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
name := "gog"
|
|
if runtime.GOOS == "windows" {
|
|
name = "gog.bat"
|
|
}
|
|
path := filepath.Join(dir, name)
|
|
script := "#!/bin/sh\nprintf '%s\\n' '" + output + "'\n"
|
|
if err := os.WriteFile(path, []byte(script), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func writeFakeGogExit(t *testing.T) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "gog")
|
|
if err := os.WriteFile(path, []byte("#!/bin/sh\necho bad >&2\nexit 4\n"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func writeFakeSQLite(t *testing.T, output string) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "sqlite3")
|
|
if err := os.WriteFile(path, []byte("#!/bin/sh\ncat <<'JSON'\n"+output+"\nJSON\n"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func runShell(t *testing.T, dir string, name string, args ...string) {
|
|
t.Helper()
|
|
cmd := exec.CommandContext(t.Context(), name, args...)
|
|
cmd.Dir = dir
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
t.Fatalf("%s %v: %v\n%s", name, args, err, out)
|
|
}
|
|
}
|