v0.4.4 e5506c1

env_test.go RawHistoryFolder

1package main
2
3import (
4	"os"
5	"path/filepath"
6	"strings"
7	"testing"
8)
9
10func TestConfigEnvironmentExpansionWithProjectError(t *testing.T) {
11	for _, kind := range []string{"malformed", "unreadable"} {
12		t.Run(kind, func(t *testing.T) {
13			t.Setenv("ZOT_HOME", t.TempDir())
14			t.Setenv("MCP_TEST_VALUE", "synthetic-command")
15			const absent = "MCP_TEST_PROJECT_ERROR_UNSET"
16			// Setenv restores the original value after the test, including absence.
17			t.Setenv(absent, "")
18			if err := os.Unsetenv(absent); err != nil {
19				t.Fatal(err)
20			}
21			writeJSON(t, filepath.Join(os.Getenv("ZOT_HOME"), "mcp.json"), `{"mcpServers":{
22 "valid":{"command":"${MCP_TEST_VALUE}"},
23 "invalid":{"headers":{"Authorization":"secret-prefix ${MCP_TEST_PROJECT_ERROR_UNSET}"}}
24 }}`)
25			project := t.TempDir()
26			path := filepath.Join(project, ".zot", "mcp.json")
27			if kind == "malformed" {
28				writeJSON(t, path, `{`)
29			} else if err := os.MkdirAll(path, 0o755); err != nil {
30				// A directory produces a read error even when tests run as root.
31				t.Fatal(err)
32			}
33			cfg, err := loadConfig(project)
34			if err == nil {
35				t.Fatal("expected config error")
36			}
37			for _, want := range []string{"project config", path, "invalid", absent} {
38				if !strings.Contains(err.Error(), want) {
39					t.Errorf("diagnostic missing %q: %v", want, err)
40				}
41			}
42			if strings.Contains(err.Error(), "secret-prefix") {
43				t.Error("diagnostic exposed field value")
44			}
45			if got := cfg.MCPServers["valid"].Command; got != "synthetic-command" {
46				t.Errorf("global command = %q, want synthetic-command", got)
47			}
48			if _, ok := cfg.MCPServers["invalid"]; ok {
49				t.Error("server with missing variable must not be retained")
50			}
51		})
52	}
53}
54
55func TestConfigEnvironmentExpansion(t *testing.T) {
56	t.Setenv("ZOT_HOME", t.TempDir())
57	t.Setenv("MCP_TEST_VALUE", "synthetic\"\\token")
58	t.Setenv("MCP_TEST_EMPTY", "")
59	t.Setenv("MCP_TEST_LITERAL", "${MCP_TEST_VALUE}")
60	const absent = "MCP_TEST_UNSET"
61	old, set := os.LookupEnv(absent)
62	os.Unsetenv(absent)
63	t.Cleanup(func() {
64		if set {
65			os.Setenv(absent, old)
66		} else {
67			os.Unsetenv(absent)
68		}
69	})
70	project := t.TempDir()
71	writeJSON(t, filepath.Join(os.Getenv("ZOT_HOME"), "mcp.json"), `{"mcpServers":{"override":{"command":"${MCP_TEST_UNSET}"}}}`)
72	writeJSON(t, filepath.Join(project, ".zot", "mcp.json"), `{"mcpServers":{
73 "override":{"command":"ok"},
74 "valid":{"command":"${MCP_TEST_VALUE}","args":["${MCP_TEST_UNSET:-fallback}","${MCP_TEST_EMPTY:-fallback}","$HOME","${MCP_TEST_LITERAL}","${MCP_TEST_UNSET:-}"],"env":{"TOKEN":"${MCP_TEST_VALUE}"},"url":"${MCP_TEST_UNSET:-https://example.test}/mcp","headers":{"Authorization":"Bearer ${MCP_TEST_VALUE}"}},
75 "invalid":{"headers":{"Authorization":"secret-prefix ${MCP_TEST_UNSET}"}}
76 }}`)
77	cfg, err := loadConfig(project)
78	if err == nil || !strings.Contains(err.Error(), absent) || !strings.Contains(err.Error(), "invalid") || strings.Contains(err.Error(), "secret-prefix") {
79		t.Fatalf("unexpected diagnostic: %v", err)
80	}
81	if _, ok := cfg.MCPServers["invalid"]; ok {
82		t.Fatal("invalid server must not be started")
83	}
84	if cfg.MCPServers["override"].Command != "ok" {
85		t.Fatal("expand after merging")
86	}
87	s := cfg.MCPServers["valid"]
88	if s.Command != os.Getenv("MCP_TEST_VALUE") || s.Env["TOKEN"] != s.Command || s.Headers["Authorization"] != "Bearer "+s.Command {
89		t.Fatal("command/env/header expansion failed")
90	}
91	if s.URL != "https://example.test/mcp" || strings.Join(s.Args, "|") != "fallback||$HOME|${MCP_TEST_VALUE}|" {
92		t.Fatalf("unexpected URL/args: %s %v", s.URL, s.Args)
93	}
94}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close