@pi-in-go/pigpen-acp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CREDITS.md +23 -0
- package/LICENSE +22 -0
- package/README.md +110 -0
- package/extensions/acp/cmd/pig-acp/e2e_llm_test.go +168 -0
- package/extensions/acp/cmd/pig-acp/e2e_test.go +541 -0
- package/extensions/acp/cmd/pig-acp/go.mod +6 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent.go +784 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_builtin_test.go +536 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_commands_test.go +199 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_config_test.go +530 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_helpers_test.go +215 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_load_test.go +333 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_prompt.go +289 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_session_test.go +429 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/auth.go +61 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/bench_test.go +47 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/helpers_test.go +421 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/main_test.go +28 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/mutation_extra_test.go +142 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/paths.go +77 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/pisessions.go +317 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/protocol.go +188 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/server.go +218 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/server_test.go +386 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/session.go +1068 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/session_diff_test.go +183 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/session_events_test.go +443 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/session_extra_test.go +547 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/session_usage_test.go +198 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/settings.go +79 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/slash.go +278 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/slash_test.go +128 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/startup.go +156 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/store.go +118 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/store_paths_test.go +466 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/translate.go +352 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/translate_test.go +105 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/twin_test.go +28 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/types.go +202 -0
- package/extensions/acp/cmd/pig-acp/internal/jsonrpc/bench_test.go +24 -0
- package/extensions/acp/cmd/pig-acp/internal/jsonrpc/jsonrpc.go +302 -0
- package/extensions/acp/cmd/pig-acp/internal/jsonrpc/jsonrpc_test.go +320 -0
- package/extensions/acp/cmd/pig-acp/internal/jsonrpc/twin_test.go +28 -0
- package/extensions/acp/cmd/pig-acp/internal/pirpc/kill_unix.go +19 -0
- package/extensions/acp/cmd/pig-acp/internal/pirpc/kill_windows.go +11 -0
- package/extensions/acp/cmd/pig-acp/internal/pirpc/process.go +530 -0
- package/extensions/acp/cmd/pig-acp/internal/pirpc/process_test.go +355 -0
- package/extensions/acp/cmd/pig-acp/internal/pirpc/twin_test.go +28 -0
- package/extensions/acp/cmd/pig-acp/main.go +177 -0
- package/extensions/acp/cmd/pig-acp/main_test.go +213 -0
- package/extensions/acp/cmd/pig-acp/sigpipe_unix.go +10 -0
- package/extensions/acp/cmd/pig-acp/sigpipe_windows.go +5 -0
- package/extensions/acp/cmd/pig-acp/testdata/ask/extension.go +28 -0
- package/extensions/acp/cmd/pig-acp/testdata/ask/go.mod +7 -0
- package/extensions/acp/cmd/pig-acp/testdata/ask/go.sum +2 -0
- package/extensions/acp/cmd/pig-acp/testdata/gate/extension.go +37 -0
- package/extensions/acp/cmd/pig-acp/testdata/gate/go.mod +7 -0
- package/extensions/acp/cmd/pig-acp/testdata/gate/go.sum +2 -0
- package/extensions/acp/cmd/pig-acp/twin_test.go +28 -0
- package/extensions/acp/extension.go +63 -0
- package/extensions/acp/extension_test.go +111 -0
- package/extensions/acp/fakehost_test.go +548 -0
- package/extensions/acp/go.mod +6 -0
- package/extensions/acp/go.sum +2 -0
- package/package.json +41 -0
- package/port/PORT.md +147 -0
- package/port/interop/package-lock.json +89 -0
- package/port/interop/package.json +13 -0
- package/port/interop/run.mjs +229 -0
- package/port/mutate.mjs +110 -0
- package/port/mutation-results.txt +107 -0
- package/port/mutations.json +748 -0
- package/port/oracle/pi-acp/LICENSE +21 -0
- package/port/oracle/pi-acp/README.md +209 -0
- package/port/oracle/pi-acp/package.json +65 -0
- package/port/oracle/pi-acp/src/acp/agent.ts +1661 -0
- package/port/oracle/pi-acp/src/acp/auth-required.ts +37 -0
- package/port/oracle/pi-acp/src/acp/auth.ts +59 -0
- package/port/oracle/pi-acp/src/acp/paths.ts +15 -0
- package/port/oracle/pi-acp/src/acp/pi-commands.ts +59 -0
- package/port/oracle/pi-acp/src/acp/pi-sessions.ts +333 -0
- package/port/oracle/pi-acp/src/acp/pi-settings.ts +75 -0
- package/port/oracle/pi-acp/src/acp/session-store.ts +68 -0
- package/port/oracle/pi-acp/src/acp/session.ts +1078 -0
- package/port/oracle/pi-acp/src/acp/slash-commands.ts +197 -0
- package/port/oracle/pi-acp/src/acp/translate/bash.ts +100 -0
- package/port/oracle/pi-acp/src/acp/translate/pi-messages.ts +17 -0
- package/port/oracle/pi-acp/src/acp/translate/pi-tools.ts +51 -0
- package/port/oracle/pi-acp/src/acp/translate/prompt.ts +71 -0
- package/port/oracle/pi-acp/src/index.ts +82 -0
- package/port/oracle/pi-acp/src/pi-rpc/command.ts +16 -0
- package/port/oracle/pi-acp/src/pi-rpc/process.ts +432 -0
- package/port/oracle/pi-acp/test/component/agent-steering-followup-modes.test.ts +143 -0
- package/port/oracle/pi-acp/test/component/session-diff.test.ts +214 -0
- package/port/oracle/pi-acp/test/component/session-events.test.ts +1102 -0
- package/port/oracle/pi-acp/test/component/session-list-and-load.test.ts +129 -0
- package/port/oracle/pi-acp/test/component/session-list-custom-session-dir.test.ts +52 -0
- package/port/oracle/pi-acp/test/component/session-list-scoped.test.ts +76 -0
- package/port/oracle/pi-acp/test/component/session-load-toolresult.test.ts +68 -0
- package/port/oracle/pi-acp/test/component/session-queue-cancel.test.ts +43 -0
- package/port/oracle/pi-acp/test/component/session-slash-commands.test.ts +37 -0
- package/port/oracle/pi-acp/test/component/session-thinking-modes.test.ts +22 -0
- package/port/oracle/pi-acp/test/component/session-title-long-session.test.ts +56 -0
- package/port/oracle/pi-acp/test/component/session-updatedAt-message-only.test.ts +57 -0
- package/port/oracle/pi-acp/test/helpers/fakes.ts +87 -0
- package/port/oracle/pi-acp/test/unit/auth-methods-terminal-auth-meta.test.ts +22 -0
- package/port/oracle/pi-acp/test/unit/builtin-commands.test.ts +60 -0
- package/port/oracle/pi-acp/test/unit/context-usage.test.ts +204 -0
- package/port/oracle/pi-acp/test/unit/merge-commands.test.ts +19 -0
- package/port/oracle/pi-acp/test/unit/model-thinking-levels.test.ts +268 -0
- package/port/oracle/pi-acp/test/unit/new-session-auth-required-when-no-models.test.ts +51 -0
- package/port/oracle/pi-acp/test/unit/new-session-pi-not-found.test.ts +26 -0
- package/port/oracle/pi-acp/test/unit/new-session-runtime-startup-errors.test.ts +148 -0
- package/port/oracle/pi-acp/test/unit/pi-command.test.ts +44 -0
- package/port/oracle/pi-acp/test/unit/pi-commands.test.ts +32 -0
- package/port/oracle/pi-acp/test/unit/pi-enable-embed-context-flag.test.ts +35 -0
- package/port/oracle/pi-acp/test/unit/pi-messages.test.ts +29 -0
- package/port/oracle/pi-acp/test/unit/pi-rpc-request-timeout.test.ts +137 -0
- package/port/oracle/pi-acp/test/unit/pi-rpc-session-path.test.ts +93 -0
- package/port/oracle/pi-acp/test/unit/pi-tools.test.ts +40 -0
- package/port/oracle/pi-acp/test/unit/prompt-to-pi-message.test.ts +74 -0
- package/port/oracle/pi-acp/test/unit/session-config-options.test.ts +217 -0
- package/port/oracle/pi-acp/test/unit/session-delete.test.ts +170 -0
- package/port/oracle/pi-acp/test/unit/session-restore.test.ts +239 -0
- package/port/oracle/pi-acp/test/unit/slash-commands.test.ts +36 -0
- package/port/oracle/pi-acp/test/unit/startup-info-env.test.ts +84 -0
- package/port/oracle/pi-acp/test/unit/startup-info-load-session.test.ts +53 -0
- package/port/oracle/pi-acp/test/unit/startup-info-project-packages.test.ts +74 -0
- package/port/oracle/pi-acp/test/unit/stdout-destroyed-does-not-crash.test.ts +32 -0
- package/port/oracle/pi-acp/test/unit/thinking-level-rpc.test.ts +43 -0
- package/port/schema/LICENSE +191 -0
- package/port/schema/schema.json +8015 -0
- package/port/upstream-tests.json +201 -0
- package/provenance.json +28 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
package acp
|
|
2
|
+
|
|
3
|
+
// Twins of the usage_update tests in test/component/session-events.test.ts, and of
|
|
4
|
+
// test/component/session-queue-cancel.test.ts, session-slash-commands.test.ts.
|
|
5
|
+
|
|
6
|
+
import (
|
|
7
|
+
"errors"
|
|
8
|
+
"math"
|
|
9
|
+
"testing"
|
|
10
|
+
"time"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
func usageUpdates(c *fakeConn) []Update {
|
|
14
|
+
var out []Update
|
|
15
|
+
for _, u := range c.ofKind("usage_update") {
|
|
16
|
+
out = append(out, u.Update)
|
|
17
|
+
}
|
|
18
|
+
return out
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func TestSessionUsage(t *testing.T) {
|
|
22
|
+
tw(t, "component/session-events", "PiAcpSession: emits usage_update from contextUsage before resolving prompt on agent_settled", func(t *testing.T) {
|
|
23
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
24
|
+
proc.sessionStats = SessionStats{"tokens": map[string]any{"total": 999999}, "contextUsage": map[string]any{"tokens": 12345, "contextWindow": 200000}}
|
|
25
|
+
s := newTestSession(cwdNow(t), proc, conn)
|
|
26
|
+
started := make(chan struct{})
|
|
27
|
+
release := make(chan struct{})
|
|
28
|
+
conn.sessionUpdateHook = func(u Update) {
|
|
29
|
+
if u["sessionUpdate"] == "usage_update" {
|
|
30
|
+
close(started)
|
|
31
|
+
<-release
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
p := s.Prompt("hello", nil)
|
|
35
|
+
proc.emit(Event{"type": "agent_start"})
|
|
36
|
+
proc.emit(Event{"type": "agent_end"})
|
|
37
|
+
proc.emit(Event{"type": "agent_settled"})
|
|
38
|
+
select {
|
|
39
|
+
case <-started:
|
|
40
|
+
case <-time.After(5 * time.Second):
|
|
41
|
+
t.Fatal("usage_update delivery never started")
|
|
42
|
+
}
|
|
43
|
+
select {
|
|
44
|
+
case <-p:
|
|
45
|
+
t.Fatal("prompt resolved before usage_update landed")
|
|
46
|
+
case <-time.After(50 * time.Millisecond):
|
|
47
|
+
}
|
|
48
|
+
close(release)
|
|
49
|
+
if r := wait(t, p); r.Reason != StopEndTurn {
|
|
50
|
+
t.Fatalf("reason = %v", r)
|
|
51
|
+
}
|
|
52
|
+
if proc.statsCount() != 1 {
|
|
53
|
+
t.Errorf("get_session_stats called %d times", proc.statsCount())
|
|
54
|
+
}
|
|
55
|
+
jsonEqual(t, usageUpdates(conn), []Update{{"sessionUpdate": "usage_update", "used": 12345, "size": 200000}})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
tw(t, "component/session-events", "PiAcpSession: skips usage_update when contextUsage tokens are null", func(t *testing.T) {
|
|
59
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
60
|
+
proc.sessionStats = SessionStats{"tokens": map[string]any{"total": 4000}, "contextUsage": map[string]any{"tokens": nil, "contextWindow": 200000}}
|
|
61
|
+
s := newTestSession(cwdNow(t), proc, conn)
|
|
62
|
+
p := s.Prompt("hello", nil)
|
|
63
|
+
proc.emit(Event{"type": "agent_settled"})
|
|
64
|
+
if r := wait(t, p); r.Reason != StopEndTurn {
|
|
65
|
+
t.Fatalf("reason = %v", r)
|
|
66
|
+
}
|
|
67
|
+
if len(usageUpdates(conn)) != 0 {
|
|
68
|
+
t.Error("usage_update emitted for null tokens")
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
tw(t, "component/session-events", "PiAcpSession: skips usage_update for invalid contextUsage values", func(t *testing.T) {
|
|
73
|
+
invalid := []struct {
|
|
74
|
+
name string
|
|
75
|
+
usage any
|
|
76
|
+
}{
|
|
77
|
+
{"missing contextUsage", nil},
|
|
78
|
+
{"negative tokens", map[string]any{"tokens": -1, "contextWindow": 100}},
|
|
79
|
+
{"fractional tokens", map[string]any{"tokens": 1.5, "contextWindow": 100}},
|
|
80
|
+
{"NaN tokens", map[string]any{"tokens": math.NaN(), "contextWindow": 100}},
|
|
81
|
+
{"zero contextWindow", map[string]any{"tokens": 10, "contextWindow": 0}},
|
|
82
|
+
{"negative contextWindow", map[string]any{"tokens": 10, "contextWindow": -1}},
|
|
83
|
+
{"fractional contextWindow", map[string]any{"tokens": 10, "contextWindow": 100.5}},
|
|
84
|
+
{"null contextWindow", map[string]any{"tokens": 10, "contextWindow": nil}},
|
|
85
|
+
{"infinite contextWindow", map[string]any{"tokens": 10, "contextWindow": math.Inf(1)}},
|
|
86
|
+
}
|
|
87
|
+
for _, tc := range invalid {
|
|
88
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
89
|
+
stats := SessionStats{}
|
|
90
|
+
if tc.usage != nil {
|
|
91
|
+
stats["contextUsage"] = tc.usage
|
|
92
|
+
}
|
|
93
|
+
proc.sessionStats = stats
|
|
94
|
+
s := newTestSession(cwdNow(t), proc, conn)
|
|
95
|
+
p := s.Prompt("hello", nil)
|
|
96
|
+
proc.emit(Event{"type": "agent_settled"})
|
|
97
|
+
if r := wait(t, p); r.Reason != StopEndTurn {
|
|
98
|
+
t.Fatalf("%s: reason = %v", tc.name, r)
|
|
99
|
+
}
|
|
100
|
+
if len(usageUpdates(conn)) != 0 {
|
|
101
|
+
t.Errorf("%s: usage_update emitted", tc.name)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
tw(t, "component/session-events", "PiAcpSession: get_session_stats rejection does not break the prompt", func(t *testing.T) {
|
|
107
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
108
|
+
proc.sessionStatsError = errors.New("pi get_session_stats failed: unsupported")
|
|
109
|
+
s := newTestSession(cwdNow(t), proc, conn)
|
|
110
|
+
p := s.Prompt("hello", nil)
|
|
111
|
+
proc.emit(Event{"type": "agent_settled"})
|
|
112
|
+
if r := wait(t, p); r.Reason != StopEndTurn {
|
|
113
|
+
t.Fatalf("reason = %v", r)
|
|
114
|
+
}
|
|
115
|
+
if proc.statsCount() != 1 || len(usageUpdates(conn)) != 0 {
|
|
116
|
+
t.Errorf("stats=%d updates=%v", proc.statsCount(), usageUpdates(conn))
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
tw(t, "component/session-events", "PiAcpSession: get_session_stats timeout does not block the prompt", func(t *testing.T) {
|
|
121
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
122
|
+
// The timeout lives in Process.Request; the session sees it as an error.
|
|
123
|
+
proc.sessionStatsError = errors.New("pi get_session_stats timed out after 1000ms")
|
|
124
|
+
s := newTestSession(cwdNow(t), proc, conn)
|
|
125
|
+
p := s.Prompt("hello", nil)
|
|
126
|
+
proc.emit(Event{"type": "agent_settled"})
|
|
127
|
+
if r := wait(t, p); r.Reason != StopEndTurn {
|
|
128
|
+
t.Fatalf("reason = %v", r)
|
|
129
|
+
}
|
|
130
|
+
if proc.statsCount() != 1 || len(usageUpdates(conn)) != 0 {
|
|
131
|
+
t.Errorf("stats=%d updates=%v", proc.statsCount(), usageUpdates(conn))
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
tw(t, "component/session-events", "PiAcpSession: cancelled turn still reports cancelled after usage publish", func(t *testing.T) {
|
|
136
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
137
|
+
proc.sessionStats = SessionStats{"contextUsage": map[string]any{"tokens": 42, "contextWindow": 100}}
|
|
138
|
+
s := newTestSession(cwdNow(t), proc, conn)
|
|
139
|
+
p := s.Prompt("hello", nil)
|
|
140
|
+
if err := s.Cancel(); err != nil {
|
|
141
|
+
t.Fatal(err)
|
|
142
|
+
}
|
|
143
|
+
proc.emit(Event{"type": "agent_settled"})
|
|
144
|
+
if r := wait(t, p); r.Reason != StopCancelled {
|
|
145
|
+
t.Fatalf("reason = %v", r)
|
|
146
|
+
}
|
|
147
|
+
jsonEqual(t, usageUpdates(conn), []Update{{"sessionUpdate": "usage_update", "used": 42, "size": 100}})
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// test/component/session-queue-cancel.test.ts
|
|
152
|
+
func TestSessionQueueCancel(t *testing.T) {
|
|
153
|
+
tw(t, "component/session-queue-cancel", "PiAcpSession: cancel clears queued prompts", func(t *testing.T) {
|
|
154
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
155
|
+
s := newTestSession(cwdNow(t), proc, conn)
|
|
156
|
+
first := s.Prompt("one", nil)
|
|
157
|
+
second := s.Prompt("two", nil)
|
|
158
|
+
third := s.Prompt("three", nil)
|
|
159
|
+
eventually(t, "the first prompt", func() bool { return len(proc.promptList()) == 1 })
|
|
160
|
+
if err := s.Cancel(); err != nil {
|
|
161
|
+
t.Fatal(err)
|
|
162
|
+
}
|
|
163
|
+
if proc.aborts() != 1 {
|
|
164
|
+
t.Errorf("abort count = %d", proc.aborts())
|
|
165
|
+
}
|
|
166
|
+
if r := wait(t, second); r.Reason != StopCancelled {
|
|
167
|
+
t.Errorf("second = %v", r)
|
|
168
|
+
}
|
|
169
|
+
if r := wait(t, third); r.Reason != StopCancelled {
|
|
170
|
+
t.Errorf("third = %v", r)
|
|
171
|
+
}
|
|
172
|
+
settledTurn(proc)
|
|
173
|
+
if r := wait(t, first); r.Reason != StopCancelled {
|
|
174
|
+
t.Errorf("first = %v", r)
|
|
175
|
+
}
|
|
176
|
+
// The queue was cleared, so no further prompt starts.
|
|
177
|
+
time.Sleep(20 * time.Millisecond)
|
|
178
|
+
if n := len(promptsOf(t, proc, 1)); n != 1 {
|
|
179
|
+
t.Errorf("%d prompts reached pi", n)
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// test/component/session-slash-commands.test.ts
|
|
185
|
+
func TestSessionSlashCommands(t *testing.T) {
|
|
186
|
+
tw(t, "component/session-slash-commands", "PiAcpSession: expands /command before sending to pi", func(t *testing.T) {
|
|
187
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
188
|
+
s := newTestSession(cwdNow(t), proc, conn, FileSlashCommand{Name: "hello", Description: "(user)", Content: "Expanded $1", Source: "(user)"})
|
|
189
|
+
p := s.Prompt("/hello world", nil)
|
|
190
|
+
settledTurn(proc)
|
|
191
|
+
if r := wait(t, p); r.Reason != StopEndTurn {
|
|
192
|
+
t.Fatalf("reason = %v", r)
|
|
193
|
+
}
|
|
194
|
+
if got := promptsOf(t, proc, 1); len(got) != 1 || got[0].Message != "Expanded world" {
|
|
195
|
+
t.Fatalf("prompts = %v", got)
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
package acp
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
func readJSONObject(path string) map[string]any {
|
|
10
|
+
raw, err := os.ReadFile(path)
|
|
11
|
+
if err != nil {
|
|
12
|
+
return map[string]any{}
|
|
13
|
+
}
|
|
14
|
+
var data any
|
|
15
|
+
if json.Unmarshal(raw, &data) != nil {
|
|
16
|
+
return map[string]any{}
|
|
17
|
+
}
|
|
18
|
+
if m, ok := data.(map[string]any); ok {
|
|
19
|
+
return m
|
|
20
|
+
}
|
|
21
|
+
return map[string]any{}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func deepMerge(a, b map[string]any) map[string]any {
|
|
25
|
+
out := make(map[string]any, len(a)+len(b))
|
|
26
|
+
for k, v := range a {
|
|
27
|
+
out[k] = v
|
|
28
|
+
}
|
|
29
|
+
for k, v := range b {
|
|
30
|
+
av, aok := out[k].(map[string]any)
|
|
31
|
+
bv, bok := v.(map[string]any)
|
|
32
|
+
if aok && bok {
|
|
33
|
+
out[k] = deepMerge(av, bv)
|
|
34
|
+
} else {
|
|
35
|
+
out[k] = v
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// mergedSettings mirrors pig's settings: the global file, overridden by the project file.
|
|
42
|
+
func mergedSettings(cwd string) map[string]any {
|
|
43
|
+
global := readJSONObject(filepath.Join(AgentDir(), "settings.json"))
|
|
44
|
+
project := readJSONObject(filepath.Join(absPathFrom(cwd), ProjectDirName(), "settings.json"))
|
|
45
|
+
return deepMerge(global, project)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func absPathFrom(cwd string) string {
|
|
49
|
+
if filepath.IsAbs(cwd) {
|
|
50
|
+
return filepath.Clean(cwd)
|
|
51
|
+
}
|
|
52
|
+
return absPath(cwd)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// GetEnableSkillCommands reads enableSkillCommands from the merged PiG settings.
|
|
56
|
+
func GetEnableSkillCommands(cwd string) bool {
|
|
57
|
+
m := mergedSettings(cwd)
|
|
58
|
+
if b, ok := m["enableSkillCommands"].(bool); ok {
|
|
59
|
+
return b
|
|
60
|
+
}
|
|
61
|
+
if skills, ok := m["skills"].(map[string]any); ok {
|
|
62
|
+
if b, ok := skills["enableSkillCommands"].(bool); ok {
|
|
63
|
+
return b
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// GetQuietStartup reads quietStartup from the merged PiG settings.
|
|
70
|
+
func GetQuietStartup(cwd string) bool {
|
|
71
|
+
m := mergedSettings(cwd)
|
|
72
|
+
if b, ok := m["quietStartup"].(bool); ok {
|
|
73
|
+
return b
|
|
74
|
+
}
|
|
75
|
+
if b, ok := m["quietStart"].(bool); ok {
|
|
76
|
+
return b
|
|
77
|
+
}
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
package acp
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"regexp"
|
|
7
|
+
"strconv"
|
|
8
|
+
"strings"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// FileSlashCommand is a prompt template file used as a slash command.
|
|
12
|
+
type FileSlashCommand struct {
|
|
13
|
+
Name string
|
|
14
|
+
Description string
|
|
15
|
+
Content string
|
|
16
|
+
Source string // "(user)", "(project)", "(project:frontend)"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// AvailableCommand is an ACP available command.
|
|
20
|
+
type AvailableCommand struct {
|
|
21
|
+
Name string `json:"name"`
|
|
22
|
+
Description string `json:"description"`
|
|
23
|
+
Input map[string]any `json:"input,omitempty"`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
var frontmatterLine = regexp.MustCompile(`^(\w+):\s*(.*)$`)
|
|
27
|
+
|
|
28
|
+
func parseFrontmatter(content string) (map[string]string, string) {
|
|
29
|
+
fm := map[string]string{}
|
|
30
|
+
if !strings.HasPrefix(content, "---") {
|
|
31
|
+
return fm, content
|
|
32
|
+
}
|
|
33
|
+
end := strings.Index(content[3:], "\n---")
|
|
34
|
+
if end == -1 {
|
|
35
|
+
return fm, content
|
|
36
|
+
}
|
|
37
|
+
end += 3
|
|
38
|
+
block := ""
|
|
39
|
+
if len(content) >= 4 {
|
|
40
|
+
block = content[4:end]
|
|
41
|
+
}
|
|
42
|
+
remaining := jsTrim(content[end+4:])
|
|
43
|
+
for _, line := range strings.Split(block, "\n") {
|
|
44
|
+
if m := frontmatterLine.FindStringSubmatch(line); m != nil {
|
|
45
|
+
fm[m[1]] = jsTrim(m[2])
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return fm, remaining
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
func loadCommandsFromDir(dir, source, subdir string) []FileSlashCommand {
|
|
52
|
+
var commands []FileSlashCommand
|
|
53
|
+
entries, err := os.ReadDir(dir)
|
|
54
|
+
if err != nil {
|
|
55
|
+
return nil
|
|
56
|
+
}
|
|
57
|
+
for _, e := range entries {
|
|
58
|
+
full := filepath.Join(dir, e.Name())
|
|
59
|
+
isDir, isFile := e.IsDir(), e.Type().IsRegular()
|
|
60
|
+
if e.Type()&os.ModeSymlink != 0 {
|
|
61
|
+
if st, err := os.Stat(full); err == nil {
|
|
62
|
+
isDir, isFile = st.IsDir(), st.Mode().IsRegular()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if isDir {
|
|
66
|
+
sub := e.Name()
|
|
67
|
+
if subdir != "" {
|
|
68
|
+
sub = subdir + ":" + e.Name()
|
|
69
|
+
}
|
|
70
|
+
commands = append(commands, loadCommandsFromDir(full, source, sub)...)
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
if !isFile || !strings.HasSuffix(e.Name(), ".md") {
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
raw, err := os.ReadFile(full)
|
|
77
|
+
if err != nil {
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
fm, content := parseFrontmatter(string(raw))
|
|
81
|
+
name := strings.TrimSuffix(e.Name(), ".md")
|
|
82
|
+
src := "(" + source + ")"
|
|
83
|
+
if subdir != "" {
|
|
84
|
+
src = "(" + source + ":" + subdir + ")"
|
|
85
|
+
}
|
|
86
|
+
desc := fm["description"]
|
|
87
|
+
if desc == "" {
|
|
88
|
+
for _, l := range strings.Split(content, "\n") {
|
|
89
|
+
if jsTrim(l) != "" {
|
|
90
|
+
desc = truncateRunes(l, 60)
|
|
91
|
+
if len([]rune(l)) > 60 {
|
|
92
|
+
desc += "..."
|
|
93
|
+
}
|
|
94
|
+
break
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if desc != "" {
|
|
99
|
+
desc += " " + src
|
|
100
|
+
} else {
|
|
101
|
+
desc = src
|
|
102
|
+
}
|
|
103
|
+
commands = append(commands, FileSlashCommand{Name: name, Description: desc, Content: content, Source: src})
|
|
104
|
+
}
|
|
105
|
+
return commands
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// LoadSlashCommands loads user then project prompt templates:
|
|
109
|
+
// <agent dir>/prompts and <cwd>/.pig/prompts.
|
|
110
|
+
func LoadSlashCommands(cwd string) []FileSlashCommand {
|
|
111
|
+
var commands []FileSlashCommand
|
|
112
|
+
commands = append(commands, loadCommandsFromDir(filepath.Join(AgentDir(), "prompts"), "user", "")...)
|
|
113
|
+
commands = append(commands, loadCommandsFromDir(filepath.Join(absPathFrom(cwd), ProjectDirName(), "prompts"), "project", "")...)
|
|
114
|
+
return commands
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ToAvailableCommands converts file commands, de-duplicating by name (first wins).
|
|
118
|
+
func ToAvailableCommands(cmds []FileSlashCommand) []AvailableCommand {
|
|
119
|
+
seen := map[string]bool{}
|
|
120
|
+
out := []AvailableCommand{}
|
|
121
|
+
for _, c := range cmds {
|
|
122
|
+
if seen[c.Name] {
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
seen[c.Name] = true
|
|
126
|
+
out = append(out, AvailableCommand{Name: c.Name, Description: c.Description})
|
|
127
|
+
}
|
|
128
|
+
return out
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ParseCommandArgs splits arguments with bash-style quotes.
|
|
132
|
+
func ParseCommandArgs(s string) []string {
|
|
133
|
+
var args []string
|
|
134
|
+
var cur strings.Builder
|
|
135
|
+
var quote rune
|
|
136
|
+
for _, ch := range s {
|
|
137
|
+
if quote != 0 {
|
|
138
|
+
if ch == quote {
|
|
139
|
+
quote = 0
|
|
140
|
+
} else {
|
|
141
|
+
cur.WriteRune(ch)
|
|
142
|
+
}
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
switch {
|
|
146
|
+
case ch == '"' || ch == '\'':
|
|
147
|
+
quote = ch
|
|
148
|
+
case ch == ' ' || ch == '\t':
|
|
149
|
+
if cur.Len() > 0 {
|
|
150
|
+
args = append(args, cur.String())
|
|
151
|
+
cur.Reset()
|
|
152
|
+
}
|
|
153
|
+
default:
|
|
154
|
+
cur.WriteRune(ch)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if cur.Len() > 0 {
|
|
158
|
+
args = append(args, cur.String())
|
|
159
|
+
}
|
|
160
|
+
return args
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
var digitsRE = regexp.MustCompile(`\$(\d+)`)
|
|
164
|
+
|
|
165
|
+
// SubstituteArgs replaces $1.. and $@.
|
|
166
|
+
func SubstituteArgs(content string, args []string) string {
|
|
167
|
+
result := strings.ReplaceAll(content, "$@", strings.Join(args, " "))
|
|
168
|
+
return digitsRE.ReplaceAllStringFunc(result, func(m string) string {
|
|
169
|
+
n, err := strconv.Atoi(m[1:])
|
|
170
|
+
if err != nil || n-1 < 0 || n-1 >= len(args) {
|
|
171
|
+
return ""
|
|
172
|
+
}
|
|
173
|
+
return args[n-1]
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ExpandSlashCommand expands a leading /command that names a file command.
|
|
178
|
+
func ExpandSlashCommand(text string, cmds []FileSlashCommand) string {
|
|
179
|
+
if !strings.HasPrefix(text, "/") {
|
|
180
|
+
return text
|
|
181
|
+
}
|
|
182
|
+
space := strings.Index(text, " ")
|
|
183
|
+
name, argStr := text[1:], ""
|
|
184
|
+
if space != -1 {
|
|
185
|
+
name, argStr = text[1:space], text[space+1:]
|
|
186
|
+
}
|
|
187
|
+
for _, c := range cmds {
|
|
188
|
+
if c.Name == name {
|
|
189
|
+
return SubstituteArgs(c.Content, ParseCommandArgs(argStr))
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return text
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// MergeCommands keeps order and de-duplicates by name (first wins).
|
|
196
|
+
func MergeCommands(a, b []AvailableCommand) []AvailableCommand {
|
|
197
|
+
seen := map[string]bool{}
|
|
198
|
+
out := []AvailableCommand{}
|
|
199
|
+
for _, list := range [][]AvailableCommand{a, b} {
|
|
200
|
+
for _, c := range list {
|
|
201
|
+
if seen[c.Name] {
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
seen[c.Name] = true
|
|
205
|
+
out = append(out, c)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return out
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// PiCommandsOptions select which `get_commands` entries are advertised.
|
|
212
|
+
type PiCommandsOptions struct {
|
|
213
|
+
EnableSkillCommands bool
|
|
214
|
+
IncludeExtensionCommands bool
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
func describeFallback(c map[string]any) string {
|
|
218
|
+
var parts []string
|
|
219
|
+
if s, _ := c["source"].(string); s != "" {
|
|
220
|
+
parts = append(parts, s)
|
|
221
|
+
}
|
|
222
|
+
if s, _ := c["location"].(string); s != "" {
|
|
223
|
+
parts = append(parts, s)
|
|
224
|
+
}
|
|
225
|
+
if len(parts) == 0 {
|
|
226
|
+
return "(command)"
|
|
227
|
+
}
|
|
228
|
+
return "(" + strings.Join(parts, ":") + ")"
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ToAvailableCommandsFromPiGetCommands converts `get_commands` data.
|
|
232
|
+
func ToAvailableCommandsFromPiGetCommands(data any, opts PiCommandsOptions) []AvailableCommand {
|
|
233
|
+
root := asObject(data)
|
|
234
|
+
raw, ok := root["commands"].([]any)
|
|
235
|
+
if !ok {
|
|
236
|
+
raw, _ = asObject(root["data"])["commands"].([]any)
|
|
237
|
+
}
|
|
238
|
+
out := []AvailableCommand{}
|
|
239
|
+
for _, item := range raw {
|
|
240
|
+
c := asObject(item)
|
|
241
|
+
name := ""
|
|
242
|
+
if s, ok := c["name"].(string); ok {
|
|
243
|
+
name = jsTrim(s)
|
|
244
|
+
}
|
|
245
|
+
if name == "" {
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
if src, _ := c["source"].(string); !opts.IncludeExtensionCommands && src == "extension" {
|
|
249
|
+
continue
|
|
250
|
+
}
|
|
251
|
+
if !opts.EnableSkillCommands && strings.HasPrefix(name, "skill:") {
|
|
252
|
+
continue
|
|
253
|
+
}
|
|
254
|
+
desc := ""
|
|
255
|
+
if s, ok := c["description"].(string); ok {
|
|
256
|
+
desc = jsTrim(s)
|
|
257
|
+
}
|
|
258
|
+
if desc == "" {
|
|
259
|
+
desc = describeFallback(c)
|
|
260
|
+
}
|
|
261
|
+
out = append(out, AvailableCommand{Name: name, Description: desc})
|
|
262
|
+
}
|
|
263
|
+
return out
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// BuiltinAvailableCommands are the commands the adapter handles itself; they never reach the model.
|
|
267
|
+
func BuiltinAvailableCommands() []AvailableCommand {
|
|
268
|
+
return []AvailableCommand{
|
|
269
|
+
{Name: "compact", Description: "Manually compact the session context", Input: map[string]any{"hint": "optional custom instructions"}},
|
|
270
|
+
{Name: "autocompact", Description: "Toggle automatic context compaction", Input: map[string]any{"hint": "on|off|toggle"}},
|
|
271
|
+
{Name: "export", Description: "Export session to an HTML file in the session cwd"},
|
|
272
|
+
{Name: "session", Description: "Show session stats (messages, tokens, cost, session file)"},
|
|
273
|
+
{Name: "name", Description: "Set session display name", Input: map[string]any{"hint": "<name>"}},
|
|
274
|
+
{Name: "steering", Description: "Get/set pig steering message delivery mode (how queued steering messages are delivered)", Input: map[string]any{"hint": "(no args to show) all | one-at-a-time"}},
|
|
275
|
+
{Name: "follow-up", Description: "Get/set pig follow-up message delivery mode (how queued follow-up messages are delivered)", Input: map[string]any{"hint": "(no args to show) all | one-at-a-time"}},
|
|
276
|
+
{Name: "changelog", Description: "Show pig changelog"},
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
package acp
|
|
2
|
+
|
|
3
|
+
// Twins of test/unit/slash-commands.test.ts, merge-commands.test.ts, pi-commands.test.ts,
|
|
4
|
+
// builtin command lists, and auth-methods-terminal-auth-meta.test.ts.
|
|
5
|
+
|
|
6
|
+
import (
|
|
7
|
+
"reflect"
|
|
8
|
+
"testing"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
func TestSlashCommands(t *testing.T) {
|
|
12
|
+
tw(t, "unit/slash-commands", "parseCommandArgs: handles quotes", func(t *testing.T) {
|
|
13
|
+
for in, want := range map[string][]string{"a b": {"a", "b"}, "'a b' c": {"a b", "c"}, `"a b" c`: {"a b", "c"}} {
|
|
14
|
+
if got := ParseCommandArgs(in); !reflect.DeepEqual(got, want) {
|
|
15
|
+
t.Errorf("%q: got %q, want %q", in, got, want)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
tw(t, "unit/slash-commands", "substituteArgs: replaces $1.. and $@", func(t *testing.T) {
|
|
20
|
+
if got := SubstituteArgs("x=$1 y=$2 all=$@", []string{"one", "two"}); got != "x=one y=two all=one two" {
|
|
21
|
+
t.Errorf("got %q", got)
|
|
22
|
+
}
|
|
23
|
+
if got := SubstituteArgs("$3", []string{"one"}); got != "" {
|
|
24
|
+
t.Errorf("got %q", got)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
tw(t, "unit/slash-commands", "expandSlashCommand: expands known command", func(t *testing.T) {
|
|
28
|
+
cmds := []FileSlashCommand{{Name: "hello", Description: "(user)", Content: "Say hi to $1", Source: "(user)"}}
|
|
29
|
+
for in, want := range map[string]string{"/hello world": "Say hi to world", "/unknown world": "/unknown world", "not a command": "not a command"} {
|
|
30
|
+
if got := ExpandSlashCommand(in, cmds); got != want {
|
|
31
|
+
t.Errorf("%q: got %q, want %q", in, got, want)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
tw(t, "unit/slash-commands", "toAvailableCommands: de-dupes by name (first wins)", func(t *testing.T) {
|
|
36
|
+
got := ToAvailableCommands([]FileSlashCommand{
|
|
37
|
+
{Name: "x", Description: "first", Content: "1", Source: "(user)"},
|
|
38
|
+
{Name: "x", Description: "second", Content: "2", Source: "(project)"}})
|
|
39
|
+
jsonEqual(t, got, []any{map[string]any{"name": "x", "description": "first"}})
|
|
40
|
+
})
|
|
41
|
+
tw(t, "unit/merge-commands", "mergeCommands: preserves order and de-dupes (first wins)", func(t *testing.T) {
|
|
42
|
+
mk := func(names ...string) []AvailableCommand {
|
|
43
|
+
var out []AvailableCommand
|
|
44
|
+
for _, n := range names {
|
|
45
|
+
out = append(out, AvailableCommand{Name: n})
|
|
46
|
+
}
|
|
47
|
+
return out
|
|
48
|
+
}
|
|
49
|
+
got := MergeCommands(mk("a", "b"), mk("b", "c"))
|
|
50
|
+
var names []string
|
|
51
|
+
for _, c := range got {
|
|
52
|
+
names = append(names, c.Name)
|
|
53
|
+
}
|
|
54
|
+
if !reflect.DeepEqual(names, []string{"a", "b", "c"}) {
|
|
55
|
+
t.Errorf("got %v", names)
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
tw(t, "unit/pi-commands", "toAvailableCommandsFromPiGetCommands: hides extension commands by default and filters skill commands", func(t *testing.T) {
|
|
59
|
+
data := map[string]any{"commands": []any{
|
|
60
|
+
map[string]any{"name": "x", "description": "X", "source": "extension"},
|
|
61
|
+
map[string]any{"name": "skill:foo", "description": "Foo", "source": "skill", "location": "user"},
|
|
62
|
+
map[string]any{"name": "y", "source": "prompt", "location": "project"},
|
|
63
|
+
}}
|
|
64
|
+
all := ToAvailableCommandsFromPiGetCommands(data, PiCommandsOptions{EnableSkillCommands: true})
|
|
65
|
+
jsonEqual(t, all, []any{
|
|
66
|
+
map[string]any{"name": "skill:foo", "description": "Foo"},
|
|
67
|
+
map[string]any{"name": "y", "description": "(prompt:project)"},
|
|
68
|
+
})
|
|
69
|
+
withExt := ToAvailableCommandsFromPiGetCommands(data, PiCommandsOptions{EnableSkillCommands: true, IncludeExtensionCommands: true})
|
|
70
|
+
jsonEqual(t, withExt, []any{
|
|
71
|
+
map[string]any{"name": "x", "description": "X"},
|
|
72
|
+
map[string]any{"name": "skill:foo", "description": "Foo"},
|
|
73
|
+
map[string]any{"name": "y", "description": "(prompt:project)"},
|
|
74
|
+
})
|
|
75
|
+
noSkills := ToAvailableCommandsFromPiGetCommands(data, PiCommandsOptions{EnableSkillCommands: false})
|
|
76
|
+
jsonEqual(t, noSkills, []any{map[string]any{"name": "y", "description": "(prompt:project)"}})
|
|
77
|
+
})
|
|
78
|
+
t.Run("builtin commands are the eight the adapter handles itself", func(t *testing.T) {
|
|
79
|
+
var names []string
|
|
80
|
+
for _, c := range BuiltinAvailableCommands() {
|
|
81
|
+
names = append(names, c.Name)
|
|
82
|
+
}
|
|
83
|
+
want := []string{"compact", "autocompact", "export", "session", "name", "steering", "follow-up", "changelog"}
|
|
84
|
+
if !reflect.DeepEqual(names, want) {
|
|
85
|
+
t.Errorf("got %v, want %v", names, want)
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func TestAuthMethods(t *testing.T) {
|
|
91
|
+
tw(t, "unit/auth-methods-terminal-auth-meta", "getAuthMethods: includes Zed terminal-auth metadata when enabled", func(t *testing.T) {
|
|
92
|
+
methods := AuthMethods(true)
|
|
93
|
+
if len(methods) != 1 {
|
|
94
|
+
t.Fatalf("%d methods", len(methods))
|
|
95
|
+
}
|
|
96
|
+
m := methods[0]
|
|
97
|
+
if m["id"] != PiSetupMethodID {
|
|
98
|
+
t.Errorf("id = %v", m["id"])
|
|
99
|
+
}
|
|
100
|
+
meta, _ := m["_meta"].(map[string]any)
|
|
101
|
+
ta, _ := meta["terminal-auth"].(map[string]any)
|
|
102
|
+
if ta == nil {
|
|
103
|
+
t.Fatal("no terminal-auth metadata")
|
|
104
|
+
}
|
|
105
|
+
if _, ok := ta["command"].(string); !ok {
|
|
106
|
+
t.Errorf("command = %v", ta["command"])
|
|
107
|
+
}
|
|
108
|
+
jsonEqual(t, ta["args"], []any{"--terminal-login"})
|
|
109
|
+
if ta["label"] != "Launch pig" {
|
|
110
|
+
t.Errorf("label = %v", ta["label"])
|
|
111
|
+
}
|
|
112
|
+
if m["type"] != "terminal" {
|
|
113
|
+
t.Errorf("type = %v", m["type"])
|
|
114
|
+
}
|
|
115
|
+
jsonEqual(t, m["args"], []any{"--terminal-login"})
|
|
116
|
+
})
|
|
117
|
+
tw(t, "unit/auth-methods-terminal-auth-meta", "getAuthMethods: omits Zed terminal-auth metadata when disabled", func(t *testing.T) {
|
|
118
|
+
methods := AuthMethods(false)
|
|
119
|
+
if len(methods) != 1 {
|
|
120
|
+
t.Fatalf("%d methods", len(methods))
|
|
121
|
+
}
|
|
122
|
+
m := methods[0]
|
|
123
|
+
meta, _ := m["_meta"].(map[string]any)
|
|
124
|
+
if _, ok := meta["terminal-auth"]; ok {
|
|
125
|
+
t.Error("terminal-auth present")
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
}
|