Skip to content

Commit 784e94e

Browse files
author
goweft
committed
feat: app ingestion Step 1 — MCP server as workspace\n\nApp ingestion Step 1: connect an MCP server and materialize it as a\nfirst-class workspace with a bound MCPAgent operating under contract.\n\ninternal/mcp/client.go (new)\n MCP client layer wrapping mark3labs/mcp-go.\n Connection: Connect(), discoverTools(), Call(), ToolSummary(), Close()\n Tool and ToolResult types.\n SSE transport; initialization handshake; tool discovery on connect.\n\ninternal/agent/mcp_agent.go (new)\n MCPAgent owns all KindIngest LLM interactions and MCP tool calls.\n Autonomy dial: suggest | confirm | run\n suggest — plan produced, tool not called; user confirms before execution\n confirm — tool called, every action surfaced to user\n run — tool called freely within workspace scope\n planToolCall: LLM selects tool + args from tool summary; JSON response\n parsed to MCPToolCall; plain-text fallback if model doesn't call a tool\n Contract:\n Pre: instruction non-empty, connection present, server has tools,\n autonomy is a valid value\n Post: selected tool name (if any) must exist on the server\n\ninternal/intent/detect.go\n KindIngest kind added\n ingestPatterns: matches 'ingest <url>', 'connect (to) <url>',\n 'add (mcp) server/source <url>'\n extractIngestURL: pulls the URL into TitleHint for shell routing\n\ninternal/contract/contract.go\n DefaultWorkspaceContract now accepts 'mcp' workspace type\n\ninternal/shell/shell.go\n Shell holds mcpAgent + mcpConns (wsID → *mcp.Connection)\n handleIngest: Connect → materialize 'mcp' workspace → bind connection\n HandleMCPAction: public method for UI to dispatch tool-call instructions\n CloseMCPWorkspace: closes connection when workspace is closed\n formatMCPWorkspace: produces initial panel content (title + tool list)\n KindIngest routed in ProcessMessage and StreamMessage\n\nTests:\n agent: +5 MCPAgent contract tests (empty instruction, nil connection,\n no tools, invalid autonomy, autonomy constant distinctness)\n intent: +TestIngestDetection (6 URL patterns), +TestIngestDoesNotMatchNormal\n\nDependency: github.com/mark3labs/mcp-go v0.54.0
1 parent ffd23fe commit 784e94e

9 files changed

Lines changed: 673 additions & 47 deletions

File tree

‎go.mod‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
module github.com/goweft/cas
22

3-
go 1.25.0
3+
go 1.25.5
44

55
require (
66
github.com/charmbracelet/bubbles v1.0.0
77
github.com/charmbracelet/bubbletea v1.3.10
88
github.com/charmbracelet/glamour v1.0.0
99
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
10+
github.com/mark3labs/mcp-go v0.54.0
11+
github.com/yuin/gopher-lua v1.1.2
1012
modernc.org/sqlite v1.48.1
1113
)
1214

@@ -26,6 +28,7 @@ require (
2628
github.com/dlclark/regexp2 v1.11.5 // indirect
2729
github.com/dustin/go-humanize v1.0.1 // indirect
2830
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
31+
github.com/google/jsonschema-go v0.4.2 // indirect
2932
github.com/google/uuid v1.6.0 // indirect
3033
github.com/gorilla/css v1.0.1 // indirect
3134
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
@@ -40,10 +43,12 @@ require (
4043
github.com/ncruces/go-strftime v1.0.0 // indirect
4144
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
4245
github.com/rivo/uniseg v0.4.7 // indirect
46+
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
47+
github.com/spf13/cast v1.7.1 // indirect
4348
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
49+
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
4450
github.com/yuin/goldmark v1.7.13 // indirect
4551
github.com/yuin/goldmark-emoji v1.0.6 // indirect
46-
github.com/yuin/gopher-lua v1.1.2 // indirect
4752
golang.org/x/net v0.38.0 // indirect
4853
golang.org/x/sys v0.42.0 // indirect
4954
golang.org/x/term v0.36.0 // indirect

‎go.sum‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,20 @@ github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfa
4040
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
4141
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
4242
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
43+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
44+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4345
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
4446
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
4547
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
4648
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
4749
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
4850
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
51+
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
52+
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
53+
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
54+
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
55+
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
56+
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
4957
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
5058
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
5159
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -56,8 +64,14 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
5664
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
5765
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
5866
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
67+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
68+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
69+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
70+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
5971
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
6072
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
73+
github.com/mark3labs/mcp-go v0.54.0 h1:PZhQvd+5xrT43cUoiaKn/hDcvLUhcLc1twSEKYPTcTA=
74+
github.com/mark3labs/mcp-go v0.54.0/go.mod h1:+8WclSK1ZUweCP3hvktSji8n8ABG/95QaEkeVE/Uwas=
6175
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
6276
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
6377
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
@@ -77,14 +91,26 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc
7791
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
7892
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
7993
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
94+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
95+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
8096
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
8197
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
8298
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
8399
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
84100
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
85101
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
102+
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
103+
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
104+
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
105+
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
106+
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
107+
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
108+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
109+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
86110
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
87111
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
112+
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
113+
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
88114
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
89115
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
90116
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
@@ -109,6 +135,8 @@ golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
109135
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
110136
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
111137
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
138+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
139+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
112140
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
113141
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
114142
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=

‎internal/agent/agent_test.go‎

Lines changed: 99 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"testing"
77

88
"github.com/goweft/cas/internal/agent"
9+
mcpclient "github.com/goweft/cas/internal/mcp"
910
"github.com/goweft/cas/internal/llm"
1011
)
1112

@@ -14,7 +15,7 @@ import (
1415
func TestGenerationAgentContractBadType(t *testing.T) {
1516
a := agent.NewGenerationAgent()
1617
_, err := a.Generate(context.Background(), agent.GenerationRequest{
17-
WSType: "spreadsheet", // invalid
18+
WSType: "spreadsheet",
1819
Title: "Test",
1920
Prompt: "write something",
2021
})
@@ -31,7 +32,7 @@ func TestGenerationAgentContractEmptyPrompt(t *testing.T) {
3132
_, err := a.Generate(context.Background(), agent.GenerationRequest{
3233
WSType: "document",
3334
Title: "Test",
34-
Prompt: " ", // whitespace only
35+
Prompt: " ",
3536
})
3637
if err == nil {
3738
t.Fatal("expected contract violation for empty prompt, got nil")
@@ -58,20 +59,6 @@ func TestGenerationAgentContractEmptyTitle(t *testing.T) {
5859

5960
func TestGenerationAgentValidTypes(t *testing.T) {
6061
t.Skip("skipped: requires live LLM endpoint")
61-
// Just check that contract pre-checks pass for valid types.
62-
// We don't make a real LLM call — we expect a network/auth error, not a contract error.
63-
a := agent.NewGenerationAgent()
64-
for _, wsType := range []string{"document", "code", "list"} {
65-
_, err := a.Generate(context.Background(), agent.GenerationRequest{
66-
WSType: wsType,
67-
Title: "Test",
68-
Prompt: "write something",
69-
})
70-
// Should not be a contract violation
71-
if err != nil && strings.Contains(err.Error(), "contract violation") {
72-
t.Errorf("wsType %q should pass preconditions, got contract violation: %v", wsType, err)
73-
}
74-
}
7562
}
7663

7764
// ── EditAgent ─────────────────────────────────────────────────────
@@ -126,16 +113,6 @@ func TestEditAgentContractBadType(t *testing.T) {
126113

127114
func TestEditAgentValidPreconditions(t *testing.T) {
128115
t.Skip("skipped: requires live LLM endpoint")
129-
a := agent.NewEditAgent()
130-
_, err := a.Edit(context.Background(), agent.EditRequest{
131-
WSType: "code",
132-
Title: "Test",
133-
CurrentContent: "package main\n\nfunc main() {}",
134-
EditRequest: "add a print statement",
135-
})
136-
if err != nil && strings.Contains(err.Error(), "contract violation") {
137-
t.Errorf("valid request should pass preconditions, got: %v", err)
138-
}
139116
}
140117

141118
// ── CombineAgent ─────────────────────────────────────────────────
@@ -161,7 +138,7 @@ func TestCombineAgentContractEmptySource(t *testing.T) {
161138
_, err := a.Combine(context.Background(), agent.CombineRequest{
162139
Sources: []struct{ Title, Type, Content string }{
163140
{Title: "One", Type: "document", Content: "some content"},
164-
{Title: "Two", Type: "document", Content: " "}, // empty
141+
{Title: "Two", Type: "document", Content: " "},
165142
},
166143
Instruction: "combine them",
167144
})
@@ -175,17 +152,6 @@ func TestCombineAgentContractEmptySource(t *testing.T) {
175152

176153
func TestCombineAgentValidPreconditions(t *testing.T) {
177154
t.Skip("skipped: requires live LLM endpoint")
178-
a := agent.NewCombineAgent()
179-
_, err := a.Combine(context.Background(), agent.CombineRequest{
180-
Sources: []struct{ Title, Type, Content string }{
181-
{Title: "One", Type: "document", Content: "content one"},
182-
{Title: "Two", Type: "document", Content: "content two"},
183-
},
184-
Instruction: "combine them",
185-
})
186-
if err != nil && strings.Contains(err.Error(), "contract violation") {
187-
t.Errorf("valid request should pass preconditions, got: %v", err)
188-
}
189155
}
190156

191157
// ── ChatAgent ─────────────────────────────────────────────────────
@@ -223,12 +189,101 @@ func TestChatAgentContractExcessiveHistory(t *testing.T) {
223189

224190
func TestChatAgentValidPreconditions(t *testing.T) {
225191
t.Skip("skipped: requires live LLM endpoint")
226-
a := agent.NewChatAgent()
227-
_, err := a.Chat(context.Background(), agent.ChatRequest{
228-
Message: "hello",
229-
Temperature: 0.7,
192+
}
193+
194+
// ── MCPAgent ──────────────────────────────────────────────────────
195+
196+
func TestMCPAgentContractEmptyInstruction(t *testing.T) {
197+
a := agent.NewMCPAgent()
198+
conn := &mcpclient.Connection{
199+
ServerURL: "http://localhost:3000",
200+
Tools: []mcpclient.Tool{{Name: "ping", Description: "ping the server"}},
201+
}
202+
_, err := a.Act(context.Background(), agent.MCPRequest{
203+
Instruction: " ",
204+
Connection: conn,
205+
Autonomy: agent.AutonomySuggest,
206+
})
207+
if err == nil {
208+
t.Fatal("expected contract violation for empty instruction, got nil")
209+
}
210+
if !strings.Contains(err.Error(), "instruction_not_empty") {
211+
t.Errorf("expected instruction_not_empty violation, got: %v", err)
212+
}
213+
}
214+
215+
func TestMCPAgentContractNilConnection(t *testing.T) {
216+
a := agent.NewMCPAgent()
217+
_, err := a.Act(context.Background(), agent.MCPRequest{
218+
Instruction: "list issues",
219+
Connection: nil,
220+
Autonomy: agent.AutonomySuggest,
221+
})
222+
if err == nil {
223+
t.Fatal("expected contract violation for nil connection, got nil")
224+
}
225+
if !strings.Contains(err.Error(), "connection_present") {
226+
t.Errorf("expected connection_present violation, got: %v", err)
227+
}
228+
}
229+
230+
func TestMCPAgentContractNoTools(t *testing.T) {
231+
a := agent.NewMCPAgent()
232+
conn := &mcpclient.Connection{
233+
ServerURL: "http://localhost:3000",
234+
Tools: []mcpclient.Tool{},
235+
}
236+
_, err := a.Act(context.Background(), agent.MCPRequest{
237+
Instruction: "do something",
238+
Connection: conn,
239+
Autonomy: agent.AutonomySuggest,
240+
})
241+
if err == nil {
242+
t.Fatal("expected contract violation for no tools, got nil")
243+
}
244+
if !strings.Contains(err.Error(), "connection_has_tools") {
245+
t.Errorf("expected connection_has_tools violation, got: %v", err)
246+
}
247+
}
248+
249+
func TestMCPAgentContractInvalidAutonomy(t *testing.T) {
250+
a := agent.NewMCPAgent()
251+
conn := &mcpclient.Connection{
252+
ServerURL: "http://localhost:3000",
253+
Tools: []mcpclient.Tool{{Name: "ping", Description: "ping"}},
254+
}
255+
_, err := a.Act(context.Background(), agent.MCPRequest{
256+
Instruction: "do something",
257+
Connection: conn,
258+
Autonomy: agent.Autonomy("full-auto"),
230259
})
231-
if err != nil && strings.Contains(err.Error(), "contract violation") {
232-
t.Errorf("valid request should pass preconditions, got: %v", err)
260+
if err == nil {
261+
t.Fatal("expected contract violation for invalid autonomy, got nil")
262+
}
263+
if !strings.Contains(err.Error(), "autonomy_valid") {
264+
t.Errorf("expected autonomy_valid violation, got: %v", err)
233265
}
234266
}
267+
268+
func TestMCPAgentAutonomyConstants(t *testing.T) {
269+
if agent.AutonomySuggest == "" || agent.AutonomyConfirm == "" || agent.AutonomyRun == "" {
270+
t.Error("autonomy constants must not be empty")
271+
}
272+
if agent.AutonomySuggest == agent.AutonomyConfirm ||
273+
agent.AutonomyConfirm == agent.AutonomyRun ||
274+
agent.AutonomySuggest == agent.AutonomyRun {
275+
t.Error("autonomy constants must be distinct")
276+
}
277+
}
278+
279+
func contains(s, sub string) bool {
280+
return len(s) >= len(sub) && (s == sub || len(sub) == 0 ||
281+
func() bool {
282+
for i := 0; i <= len(s)-len(sub); i++ {
283+
if s[i:i+len(sub)] == sub {
284+
return true
285+
}
286+
}
287+
return false
288+
}())
289+
}

0 commit comments

Comments
 (0)