bastion 0.54.0
ci / personal-data (push) Successful in 4s
ci / fuzz (push) Successful in 2m44s
ci / test (push) Successful in 2m11s

This commit is contained in:
Privatae
2026-10-21 22:29:10 +00:00
parent e4c9b69b46
commit d62da8adb4
58 changed files with 6944 additions and 124 deletions
+461
View File
@@ -0,0 +1,461 @@
package main
import (
"encoding/json"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)
// Every fixture here is invented: server names, packages, hosts, paths and
// the secret-shaped values, which are built from repeated letters so no real
// credential can be in this file.
var (
fakeAnthropicKey = "sk-ant-api03-" + strings.Repeat("Q", 40)
fakeGitHubToken = "ghp_" + strings.Repeat("Z", 36)
fakeLiteral = "plain-" + strings.Repeat("k", 12)
)
func entriesByName(t *testing.T, format, body string) map[string]mcpEntry {
t.Helper()
es, _, err := mcpParse(format, []byte(body))
if err != nil {
t.Fatalf("%s: %v", format, err)
}
out := map[string]mcpEntry{}
for _, e := range es {
out[e.name] = e
}
return out
}
func TestMCPParseClaudeDesktopShape(t *testing.T) {
es := entriesByName(t, mcpFormatServers, `{"mcpServers": {
"files": {"command": "npx", "args": ["-y", "@demo-scope/server-files@1.4.2", "/srv/demo"]},
"search": {"command": "uvx", "args": ["demo-search"], "env": {"DEMO_API_KEY": "`+fakeLiteral+`"}}
}, "otherSetting": true}`)
if len(es) != 2 || es["files"].command != "npx" || es["search"].env["DEMO_API_KEY"] == "" {
t.Fatalf("entries: %+v", es)
}
}
func TestMCPParseClaudeCodeUserAndProjects(t *testing.T) {
body := `{"numStartups": 3, "mcpServers": {"u1": {"type": "stdio", "command": "demo-tool"}},
"projects": {
"/home/demo/proj-a": {"mcpServers": {"p1": {"type": "http", "url": "https://tools.example.com/x"}}},
"/home/demo/proj-b": {"allowedTools": []}
}}`
es, projects, err := mcpParse(mcpFormatClaude, []byte(body))
if err != nil {
t.Fatal(err)
}
if len(es) != 2 || es[0].name != "u1" || es[0].projectScoped || es[1].name != "p1" || !es[1].projectScoped {
t.Fatalf("entries: %+v", es)
}
want := []string{filepath.FromSlash("/home/demo/proj-a"), filepath.FromSlash("/home/demo/proj-b")}
if !reflect.DeepEqual(projects, want) {
t.Fatalf("projects: %v", projects)
}
}
func TestMCPParseVSCodeJSONC(t *testing.T) {
es := entriesByName(t, mcpFormatVSCode, `{
// a comment the file is allowed to carry
"inputs": [{"id": "tok", "type": "promptString", "password": true}],
"servers": {
"remote": {"type": "sse", "url": "https://tools.example.com/sse", "headers": {"Authorization": "Bearer ${input:tok}"},},
/* a block comment, "with a quote" */
"local": {"command": "node", "args": ["/opt/demo/server.js"],},
},
}`)
if es["remote"].typ != "sse" || es["local"].command != "node" || len(es) != 2 {
t.Fatalf("entries: %+v", es)
}
// A // inside a string is data, not a comment.
es = entriesByName(t, mcpFormatVSCode, `{"servers": {"u": {"url": "https://h.example.com//x"}}}`)
if es["u"].url != "https://h.example.com//x" {
t.Fatalf("a URL lost its slashes: %q", es["u"].url)
}
}
func TestMCPParseZedBothCommandForms(t *testing.T) {
es := entriesByName(t, mcpFormatZed, `{
"theme": "One Dark", // Zed's settings carry everything else too
"context_servers": {
"flat": {"source": "custom", "command": "demo-mcp-server", "args": ["--stdio"], "env": {"DEMO_TOKEN": "`+fakeLiteral+`"}},
"nested": {"command": {"path": "/usr/local/bin/demo", "args": ["serve"], "env": {"LOG_LEVEL": "info"}}},
"remote": {"url": "https://zed-tools.example.com/mcp", "headers": {"X-Api-Key": "`+fakeLiteral+`"}}
}
}`)
if es["flat"].command != "demo-mcp-server" || es["nested"].command != "/usr/local/bin/demo" ||
!reflect.DeepEqual(es["nested"].args, []string{"serve"}) || es["nested"].env["LOG_LEVEL"] != "info" ||
es["remote"].headers["X-Api-Key"] == "" {
t.Fatalf("entries: %+v", es)
}
}
func TestMCPParseWindsurfServerURL(t *testing.T) {
es := entriesByName(t, mcpFormatServers, `{"mcpServers": {"w": {"serverUrl": "https://ws-tools.example.com/mcp"}}}`)
if es["w"].url != "https://ws-tools.example.com/mcp" {
t.Fatalf("serverUrl not read: %+v", es)
}
}
func TestMCPParseCodexTOML(t *testing.T) {
body := `# invented Codex config
model = "demo-model"
[mcp_servers.docs]
command = "npx"
args = [
"-y", # a comment inside an array
"demo-docs-server@2.0.1",
]
env = { "DEMO_KEY" = "` + fakeLiteral + `", PLAIN = 'x' }
env_vars = ["HOME_HINT", { name = "FORWARDED", source = "local" }]
startup_timeout_sec = 20
[mcp_servers."remote tools"]
url = "https://codex-tools.example.com/mcp"
bearer_token_env_var = "DEMO_BEARER"
enabled = false
[mcp_servers.docs.tools.search]
approval_mode = "approve"
[[profiles_list]]
command = "not a server"
`
es := entriesByName(t, mcpFormatCodex, body)
d := es["docs"]
if d.command != "npx" || !reflect.DeepEqual(d.args, []string{"-y", "demo-docs-server@2.0.1"}) ||
d.env["DEMO_KEY"] == "" || d.env["PLAIN"] != "x" ||
!reflect.DeepEqual(d.envNames, []string{"HOME_HINT", "FORWARDED"}) {
t.Fatalf("docs: %+v", d)
}
r := es["remote tools"]
if r.url == "" || !r.authHint || !r.disabled || !reflect.DeepEqual(r.envNames, []string{"DEMO_BEARER"}) {
t.Fatalf("remote: %+v", r)
}
if len(es) != 2 {
t.Fatalf("an [[array of tables]] leaked in, or a server was lost: %v", es)
}
if _, err := parseTOMLSubset("[mcp_servers.x]\ncommand = \"unterminated\n"); err == nil {
t.Fatal("a broken line must fail the file, not half-read it")
}
if v, _ := parseTOMLSubset(`k = "a\tb\u00e9\"q"`); v["k"] != "a\tbé\"q" {
t.Fatalf("escapes: %q", v["k"])
}
}
// THE VALUE NEVER LEAVES. A secret in env, a header, an argument or the URL
// sets one bool; the marshalled record carries the variable's NAME and
// nothing of the value.
func TestMCPAnalyzeKeepsNamesAndDropsValues(t *testing.T) {
cases := []struct {
name string
e mcpEntry
}{
{"env, known prefix", mcpEntry{name: "a", command: "npx", args: []string{"demo"},
env: map[string]string{"ANTHROPIC_API_KEY": fakeAnthropicKey}}},
{"env, known prefix under a plain name", mcpEntry{name: "a2", command: "demo",
env: map[string]string{"DEMO_SETTING": fakeAnthropicKey}}},
{"env, literal under a secret name", mcpEntry{name: "b", command: "demo",
env: map[string]string{"SERVICE_TOKEN": fakeLiteral}}},
{"header", mcpEntry{name: "c", url: "https://h.example.com/mcp",
headers: map[string]string{"Authorization": "Bearer " + fakeLiteral}}},
{"argument after a flag", mcpEntry{name: "d", command: "demo", args: []string{"--api-key", fakeLiteral}}},
{"argument with =", mcpEntry{name: "e", command: "demo", args: []string{"--token=" + fakeLiteral}}},
{"bare token argument", mcpEntry{name: "f", command: "demo", args: []string{fakeGitHubToken}}},
{"docker -e NAME=value", mcpEntry{name: "g", command: "docker",
args: []string{"run", "-i", "--rm", "-e", "GITHUB_TOKEN=" + fakeLiteral, "ghcr.io/demo/server"}}},
{"URL query", mcpEntry{name: "h", url: "https://h.example.com/mcp?api_key=" + fakeLiteral}},
{"URL password", mcpEntry{name: "i", url: "https://u:" + fakeLiteral + "@h.example.com/mcp"}},
}
for _, c := range cases {
o := mcpAnalyze(c.e)
if !o.SecretInPlaintext {
t.Errorf("%s: the secret was not seen", c.name)
}
b, _ := json.Marshal(o)
for _, v := range []string{fakeAnthropicKey, fakeLiteral, fakeGitHubToken} {
if strings.Contains(string(b), v) {
t.Errorf("%s: a value left in the record: %s", c.name, b)
}
}
}
o := mcpAnalyze(mcpEntry{name: "a", command: "npx", args: []string{"demo"},
env: map[string]string{"B_KEY": "${env:B_KEY}", "A_URL": "https://h.example.com", "LOG": "debug",
"KEY_FILE": "/etc/demo/key.pem"}, envNames: []string{"FORWARDED", "LOG"}})
if o.SecretInPlaintext {
t.Fatal("a reference, a URL, a path and a short value are not secrets in plain text")
}
if !reflect.DeepEqual(o.EnvNames, []string{"A_URL", "B_KEY", "FORWARDED", "KEY_FILE", "LOG"}) {
t.Fatalf("env names: %v", o.EnvNames)
}
if b, _ := json.Marshal(o); strings.Contains(string(b), "debug") || strings.Contains(string(b), "key.pem") {
t.Fatalf("an env value travelled: %s", b)
}
}
func TestMCPPackageAndPinned(t *testing.T) {
cases := []struct {
cmd string
args []string
source, name, pinned string
}{
{"npx", []string{"-y", "@demo-scope/server-files@1.4.2", "/srv"}, "npx", "@demo-scope/server-files", "yes"},
{"npx", []string{"-y", "@demo-scope/server-files"}, "npx", "@demo-scope/server-files", "no"},
{"npx", []string{"demo-server@latest"}, "npx", "demo-server", "no"},
{"npx", []string{"demo-server@^1.2.0"}, "npx", "demo-server", "no"},
{"npx", []string{"--package", "demo-pkg@3.0.0", "demo-bin"}, "npx", "demo-pkg", "yes"},
{"C:\\Program Files\\nodejs\\npx.cmd", []string{"-y", "demo-win@0.1.0"}, "npx", "demo-win", "yes"},
{"pnpm", []string{"dlx", "demo-dlx@1.0.0"}, "npx", "demo-dlx", "yes"},
{"npx", []string{"github:demo/repo"}, "npx", "github:demo/repo", "unknown"},
{"uvx", []string{"demo-py==0.3.1"}, "uvx", "demo-py", "yes"},
{"uvx", []string{"demo-py@0.3.1"}, "uvx", "demo-py", "yes"},
{"uvx", []string{"--from", "demo-py>=0.3", "demo-cli"}, "uvx", "demo-py", "no"},
{"uvx", []string{"demo-py"}, "uvx", "demo-py", "no"},
{"uv", []string{"tool", "run", "demo-py==1.0"}, "uvx", "demo-py", "yes"},
{"pipx", []string{"run", "demo-pipx==2.1"}, "pip", "demo-pipx", "yes"},
{"docker", []string{"run", "-i", "--rm", "-e", "X", "ghcr.io/demo/srv:1.2"}, "docker", "ghcr.io/demo/srv", "no"},
{"docker", []string{"run", "--rm", "localhost:5000/demo/srv@sha256:" + strings.Repeat("a", 64)}, "docker", "localhost:5000/demo/srv", "yes"},
{"docker", []string{"run", "-i", "demo/srv"}, "docker", "demo/srv", "no"},
{"node", []string{"/opt/demo/server.js"}, "unknown", "", "unknown"},
{"/usr/local/bin/demo-mcp-server", []string{"stdio"}, "binary", "", "unknown"},
}
for _, c := range cases {
s, n, p := mcpPackage(c.cmd, c.args)
if s != c.source || n != c.name || p != c.pinned {
t.Errorf("%s %v: got %s %q %s, want %s %q %s", c.cmd, c.args, s, n, p, c.source, c.name, c.pinned)
}
}
}
func TestMCPTransportLocalityAuth(t *testing.T) {
cases := []struct {
e mcpEntry
transport, locality, auth, urlHst string
}{
{mcpEntry{command: "demo"}, "stdio", "local_process", "not_applicable", ""},
{mcpEntry{typ: "streamable-http", url: "https://tools.example.com/mcp"}, "http", "remote", "unknown", "tools.example.com"},
{mcpEntry{url: "https://tools.example.com/sse"}, "sse", "remote", "unknown", "tools.example.com"},
{mcpEntry{url: "wss://tools.example.com/x"}, "websocket", "remote", "unknown", "tools.example.com"},
{mcpEntry{typ: "ws", url: "ws://127.0.0.1:9000"}, "websocket", "loopback", "unknown", "127.0.0.1"},
{mcpEntry{url: "http://localhost:3333/mcp"}, "http", "loopback", "unknown", "localhost"},
{mcpEntry{url: "http://10.20.30.40:80/mcp"}, "http", "lan", "unknown", "10.20.30.40"},
{mcpEntry{url: "http://toolbox.internal/mcp"}, "http", "lan", "unknown", "toolbox.internal"},
{mcpEntry{url: "http://toolbox/mcp"}, "http", "lan", "unknown", "toolbox"},
{mcpEntry{url: "https://t.example.com/mcp", headers: map[string]string{"Authorization": "Bearer ${env:T}"}}, "http", "remote", "yes", "t.example.com"},
{mcpEntry{url: "https://t.example.com/mcp", authHint: true}, "http", "remote", "yes", "t.example.com"},
{mcpEntry{typ: "carrier-pigeon", url: "https://t.example.com"}, "unknown", "remote", "unknown", "t.example.com"},
}
for _, c := range cases {
o := mcpAnalyze(c.e)
if o.Transport != c.transport || o.Locality != c.locality || o.AuthConfigured != c.auth || o.URLHost != c.urlHst {
t.Errorf("%+v: got %s %s %s %q", c.e, o.Transport, o.Locality, o.AuthConfigured, o.URLHost)
}
if c.transport == "stdio" && o.BindClass != "not_applicable" || c.transport != "stdio" && o.BindClass != "unknown" {
t.Errorf("%+v: bind %s", c.e, o.BindClass)
}
}
// The command is the cmdline handling's: joined, and cut at 200.
long := mcpAnalyze(mcpEntry{command: "demo", args: []string{strings.Repeat("a", 300)}})
if len(long.Command) != 200 {
t.Fatalf("command not truncated like a cmdline: %d", len(long.Command))
}
}
func TestCorrelateMCPStates(t *testing.T) {
declared := []mcpServerObs{
mcpAnalyze(mcpEntry{name: "files", command: "npx", args: []string{"-y", "@demo-scope/server-files@1.4.2"}}),
mcpAnalyze(mcpEntry{name: "idle", command: "uvx", args: []string{"demo-idle"}}),
mcpAnalyze(mcpEntry{name: "bin", command: "/opt/demo/bin/demo-tool", args: []string{"serve"}}),
mcpAnalyze(mcpEntry{name: "saas", url: "https://tools.example.com/mcp"}),
mcpAnalyze(mcpEntry{name: "local-url", url: "http://127.0.0.1:7000/mcp"}),
}
procs := []mcpProc{
{PID: 10, PPID: 1, Argv: []string{"demo-client"}},
{PID: 20, PPID: 10, Argv: []string{"npm", "exec", "-y", "@demo-scope/server-files@1.4.2"}},
// npx's child: no package name in its own line, inherits the match.
{PID: 21, PPID: 20, Argv: []string{"node", "/home/demo/.npm/_npx/abc/node_modules/.bin/mcp-server-files"}},
{PID: 30, PPID: 10, Argv: []string{"/opt/demo/bin/demo-tool", "serve"}},
// Shadow: a server nobody declared, and its own child (collapsed).
{PID: 40, PPID: 1, Argv: []string{"node", "/opt/x/node_modules/@modelcontextprotocol/server-memory/dist/index.js"}},
{PID: 41, PPID: 40, Argv: []string{"node", "/opt/x/mcp-worker.js"}},
{PID: 50, PPID: 1, Argv: []string{"/usr/bin/github-mcp-server", "stdio", "--token=" + fakeGitHubToken}},
// Not servers: an agent run with an MCP flag, and a grep.
{PID: 60, PPID: 1, Argv: []string{"claude", "--mcp-config", "/tmp/mcp.json"}},
{PID: 61, PPID: 1, Argv: []string{"grep", "-r", "mcp-server", "/srv"}},
{PID: 70, PPID: 1}, // a process whose command line was not read
}
got := correlateMCP(declared, procs, true)
state := map[string]mcpServerObs{}
var shadows []mcpServerObs
for _, o := range got {
if o.RunningState == mcpStateShadow {
shadows = append(shadows, o)
continue
}
state[o.Name] = o
}
if s := state["files"]; s.RunningState != mcpStateRunning || !reflect.DeepEqual(s.PIDs, []int{20, 21}) {
t.Errorf("files: %s %v", s.RunningState, s.PIDs)
}
if s := state["idle"]; s.RunningState != mcpStateIdle {
t.Errorf("idle: %s", s.RunningState)
}
if s := state["bin"]; s.RunningState != mcpStateRunning || !reflect.DeepEqual(s.PIDs, []int{30}) {
t.Errorf("bin: %s %v", s.RunningState, s.PIDs)
}
if s := state["saas"]; s.RunningState != mcpStateNotLocal {
t.Errorf("saas: %s", s.RunningState)
}
if s := state["local-url"]; s.RunningState != mcpStateUnknown {
t.Errorf("local-url: %s", s.RunningState)
}
if len(shadows) != 2 || !reflect.DeepEqual(shadows[0].PIDs, []int{40}) || !reflect.DeepEqual(shadows[1].PIDs, []int{50}) {
t.Fatalf("shadow servers: %+v", shadows)
}
if !shadows[1].SecretInPlaintext || strings.Contains(shadows[1].Command, fakeGitHubToken) {
t.Fatalf("a running server's secret argument travelled: %+v", shadows[1])
}
// No process list: nothing is guessed, and no shadow is invented.
for _, o := range correlateMCP(declared, nil, false) {
if o.RunningState == mcpStateShadow || (o.Transport == "stdio" && o.RunningState != mcpStateUnknown) {
t.Errorf("with no process list %s read %s", o.Name, o.RunningState)
}
}
}
// The whole reader over invented homes: the per-OS path table, the projects
// Claude Code lists, the system file, and an unreadable file that must be
// counted rather than read as empty.
func TestReadMCPConfigsOverFixtureHomes(t *testing.T) {
root := t.TempDir()
home := filepath.Join(root, "home", "demo")
proj := filepath.Join(root, "work", "proj")
write := func(p, body string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(body), 0o600); err != nil {
t.Fatal(err)
}
}
h := mcpHome{Dir: home, ConfigDir: filepath.Join(home, ".config")}
projJSON, _ := json.Marshal(proj)
write(filepath.Join(home, ".claude.json"), `{"mcpServers": {"u": {"command": "demo-u"}},
"projects": {`+string(projJSON)+`: {}}}`)
write(filepath.Join(home, ".cursor", "mcp.json"), `{"mcpServers": {"c": {"url": "https://c.example.com/mcp"}}}`)
write(filepath.Join(home, ".codex", "config.toml"), "[mcp_servers.x]\ncommand = \"demo-x\"\n")
write(filepath.Join(proj, ".mcp.json"), `{"mcpServers": {"p": {"command": "demo-p"}}}`)
write(filepath.Join(proj, ".vscode", "mcp.json"), `{"servers": {"v": {"command": "demo-v"},},}`)
write(filepath.Join(home, ".config", "zed", "settings.json"), `{"context_servers": {"z": {"command": "demo-z"}`) // broken
// A directory where a file should be: present, and unreadable as a file.
if err := os.MkdirAll(filepath.Join(home, ".codeium", "windsurf", "mcp_config.json"), 0o755); err != nil {
t.Fatal(err)
}
sys := filepath.Join(root, "etc", "managed-mcp.json")
write(sys, `{"mcpServers": {"s": {"command": "demo-s"}}}`)
oldH, oldS, oldOS := mcpHomesFn, mcpSystemFn, mcpGOOS
t.Cleanup(func() { mcpHomesFn, mcpSystemFn, mcpGOOS = oldH, oldS, oldOS })
mcpGOOS = "linux"
mcpHomesFn = func() ([]mcpHome, bool) { return []mcpHome{h}, true }
mcpSystemFn = func() []mcpSource { return []mcpSource{{mcpClientCode, mcpScopeSystem, sys, mcpFormatServers}} }
got, scan := readMCPConfigs()
where := map[string]string{}
for _, o := range got {
where[o.Name] = o.Client + "/" + o.Scope
}
want := map[string]string{"s": "claude-code/system", "u": "claude-code/user", "c": "cursor/user",
"x": "codex/user", "p": "claude-code/project", "v": "vscode/project"}
if !reflect.DeepEqual(where, want) {
t.Fatalf("servers: %v", where)
}
if scan.Accounts != 1 || scan.Projects != 1 || scan.Unparsed != 1 || scan.Unreadable != 1 || scan.FilesRead != 7 {
t.Fatalf("scan: %+v", scan)
}
mcpHomesFn = func() ([]mcpHome, bool) { return nil, false }
if got, scan := readMCPConfigs(); got != nil || scan.Supported {
t.Fatal("an unsupported platform must say so, not report an empty machine")
}
}
// The path table names each client's file on each platform.
func TestMCPConfigSourcesPerPlatform(t *testing.T) {
find := func(srcs []mcpSource, client, suffix string) bool {
for _, s := range srcs {
if s.Client == client && strings.HasSuffix(filepath.ToSlash(s.Path), suffix) {
return true
}
}
return false
}
mac := mcpConfigSources("darwin", mcpHome{Dir: "/Users/demo", ConfigDir: "/Users/demo/Library/Application Support"})
win := mcpConfigSources("windows", mcpHome{Dir: `C:\Users\demo`, ConfigDir: `C:\Users\demo\AppData\Roaming`})
lin := mcpConfigSources("linux", mcpHome{Dir: "/home/demo", ConfigDir: "/home/demo/.config"})
for _, c := range []struct {
srcs []mcpSource
client, suffix string
}{
{mac, mcpClientDesktop, "Library/Application Support/Claude/claude_desktop_config.json"},
{win, mcpClientDesktop, "Roaming/Claude/claude_desktop_config.json"},
{lin, mcpClientCode, "/home/demo/.claude.json"},
{lin, mcpClientCursor, ".cursor/mcp.json"},
{mac, mcpClientVSCode, "Application Support/Code/User/mcp.json"},
{lin, mcpClientWindsurf, ".config/devin/mcp_config.json"},
{lin, mcpClientWindsurf, ".codeium/windsurf/mcp_config.json"},
{lin, mcpClientCodex, ".codex/config.toml"},
{mac, mcpClientZed, "/Users/demo/.config/zed/settings.json"},
{win, mcpClientZed, "Roaming/Zed/settings.json"},
} {
if !find(c.srcs, c.client, filepath.ToSlash(c.suffix)) {
t.Errorf("%s: no source ending %s", c.client, c.suffix)
}
}
if s := mcpSystemSources("windows", `C:\Program Files`); len(s) != 1 || !strings.HasSuffix(filepath.ToSlash(s[0].Path), "ClaudeCode/managed-mcp.json") {
t.Errorf("windows system sources: %v", s)
}
}
// The heartbeat's report: a process walk that failed says so, and every
// local server then reads unknown rather than "not running".
func TestMCPServersReportWhenTheProcessWalkFails(t *testing.T) {
home := t.TempDir()
if err := os.WriteFile(filepath.Join(home, ".claude.json"),
[]byte(`{"mcpServers": {"u": {"command": "demo-u"}}}`), 0o600); err != nil {
t.Fatal(err)
}
oldH, oldS, oldP := mcpHomesFn, mcpSystemFn, scanMCPProcessesFn
t.Cleanup(func() { mcpHomesFn, mcpSystemFn, scanMCPProcessesFn = oldH, oldS, oldP })
resetMCPConfigCache()
t.Cleanup(resetMCPConfigCache)
mcpHomesFn = func() ([]mcpHome, bool) { return []mcpHome{{Dir: home, ConfigDir: home}}, true }
mcpSystemFn = func() []mcpSource { return nil }
scanMCPProcessesFn = func(map[string]bool) ([]mcpProc, error) { return nil, os.ErrPermission }
got, scan := mcpServersReport()
if len(got) != 1 || got[0].RunningState != mcpStateUnknown || scan.ProcessScan == "ok" {
t.Fatalf("failed walk: %+v %+v", got, scan)
}
scanMCPProcessesFn = func(want map[string]bool) ([]mcpProc, error) {
if !want["demo-u"] {
t.Error("the declared program was not in the set whose command lines are read")
}
return []mcpProc{{PID: 9, PPID: 1, Argv: []string{"/usr/bin/demo-u"}}}, nil
}
got, scan = mcpServersReport()
if len(got) != 1 || got[0].RunningState != mcpStateRunning || scan.ProcessScan != "ok" {
t.Fatalf("walk: %+v %+v", got, scan)
}
}