mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 22:37:03 +00:00
ba35908f2b
* fix(cli,web): link re-points a directory to another server and keeps .pad.toml's other keys; the move-to-Cloud guide is browser-only (TASK-3612) Verifying the guide's step 5 found it wrong for the people it is written for. A project linked to a self-hosted server pins that server's url in .pad.toml, and the pin wins over the global cloud mode, so `pad workspace link <slug>` talked to the OLD server, found the slug and printed "Already linked". With --url it still did, whenever Cloud had the same slug, which is the usual case after an import. - link is "already linked" only when the existing link's url equals the target. An explicit --url naming another server re-points it, and the output names the old and the new server. - WriteWorkspaceLink UPDATES .pad.toml instead of rewriting it: only workspace and url change, so agent_name, the [push] consent table and unknown keys survive every relink (link, switch and the three init paths). A file it cannot parse is refused and left as it was. - Step 5 now reads `pad workspace link <slug> --url https://app.getpad.dev`. - Lead ruling: the guide as a whole steers to Pad Cloud, so inside a mobile app's web view the route shows one line ("Open Pad in a browser to move a workspace") and both entry points are hidden. The entry points are one component carrying that gate, with a wiring test for each mount. Claude-Session: https://claude.ai/code/session_01FAVXA5mJ5BNhKmv95jxQkQ * fix(cli): link compares server URLs normalised, so case, a default port or a trailing slash is not a different server (TASK-3612, codex r1) Claude-Session: https://claude.ai/code/session_01FAVXA5mJ5BNhKmv95jxQkQ * fix(cli): server URL comparison keeps escaped path distinctions and strips one trailing slash only (TASK-3612, codex r2) Claude-Session: https://claude.ai/code/session_01FAVXA5mJ5BNhKmv95jxQkQ
104 lines
3.0 KiB
Go
104 lines
3.0 KiB
Go
package cli
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
)
|
|
|
|
// TASK-3612: WriteWorkspaceLink updates an existing .pad.toml instead of
|
|
// replacing it, so a relink keeps agent_name, the [push] consent table and
|
|
// any key a newer pad wrote.
|
|
|
|
func readPadTomlMap(t *testing.T, dir string) map[string]any {
|
|
t.Helper()
|
|
doc := map[string]any{}
|
|
if _, err := toml.DecodeFile(filepath.Join(dir, ".pad.toml"), &doc); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
return doc
|
|
}
|
|
|
|
func TestWriteWorkspaceLinkKeepsTheFilesOtherKeys(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
body := "workspace = \"old\"\nurl = \"https://pad.example.com\"\nagent_name = \"codex\"\nfuture_key = 7\n\n[push]\n auto_arm = true\n"
|
|
if err := os.WriteFile(filepath.Join(dir, ".pad.toml"), []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := WriteWorkspaceLink(dir, "new", "https://app.getpad.dev"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
doc := readPadTomlMap(t, dir)
|
|
if doc["workspace"] != "new" || doc["url"] != "https://app.getpad.dev" {
|
|
t.Errorf("link not updated: %v", doc)
|
|
}
|
|
if doc["agent_name"] != "codex" {
|
|
t.Errorf("agent_name = %v, want codex", doc["agent_name"])
|
|
}
|
|
if doc["future_key"] != int64(7) {
|
|
t.Errorf("future_key = %#v, want 7", doc["future_key"])
|
|
}
|
|
push, _ := doc["push"].(map[string]any)
|
|
if push == nil || push["auto_arm"] != true {
|
|
t.Errorf("[push] = %v, want auto_arm kept", doc["push"])
|
|
}
|
|
|
|
// And the typed reader still sees what it always did.
|
|
var typed PadToml
|
|
if _, err := toml.DecodeFile(filepath.Join(dir, ".pad.toml"), &typed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if typed.Workspace != "new" || typed.AgentName != "codex" || !typed.PadTomlAutoArm() {
|
|
t.Errorf("typed read = %+v", typed)
|
|
}
|
|
}
|
|
|
|
func TestWriteWorkspaceLinkDropsTheURLForALocalLink(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, ".pad.toml"), []byte("workspace = \"a\"\nurl = \"https://pad.example.com\"\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := WriteWorkspaceLink(dir, "a", ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if doc := readPadTomlMap(t, dir); doc["url"] != nil {
|
|
t.Errorf("url = %v, want it removed for a local link", doc["url"])
|
|
}
|
|
}
|
|
|
|
func TestWriteWorkspaceLinkCreatesAMissingFile(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
if err := WriteWorkspaceLink(dir, "fresh", ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
doc := readPadTomlMap(t, dir)
|
|
if doc["workspace"] != "fresh" || len(doc) != 1 {
|
|
t.Errorf("new file = %v, want only workspace", doc)
|
|
}
|
|
}
|
|
|
|
func TestWriteWorkspaceLinkRefusesAFileItCannotRead(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
broken := []byte("workspace = \"a\"\nthis is not toml\n")
|
|
path := filepath.Join(dir, ".pad.toml")
|
|
if err := os.WriteFile(path, broken, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := WriteWorkspaceLink(dir, "b", ""); err == nil {
|
|
t.Fatal("wrote over a .pad.toml it could not parse")
|
|
}
|
|
got, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != string(broken) {
|
|
t.Errorf("the unreadable file was changed:\n%s", got)
|
|
}
|
|
}
|