@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,213 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
// Twins of the src/index.ts behavior (terminal login entrypoint, stdio wiring, shutdown) and of
|
|
4
|
+
// test/unit/stdout-destroyed-does-not-crash.test.ts at the process level.
|
|
5
|
+
|
|
6
|
+
import (
|
|
7
|
+
"bytes"
|
|
8
|
+
"encoding/json"
|
|
9
|
+
"fmt"
|
|
10
|
+
"io"
|
|
11
|
+
"os"
|
|
12
|
+
"path/filepath"
|
|
13
|
+
"strings"
|
|
14
|
+
"testing"
|
|
15
|
+
"time"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
// TestMain doubles as the fake pig: with PIGACP_FAKE_PIG set it prints its arguments and exits
|
|
19
|
+
// with the requested code.
|
|
20
|
+
func TestMain(m *testing.M) {
|
|
21
|
+
if code := os.Getenv("PIGACP_FAKE_PIG"); code != "" {
|
|
22
|
+
fmt.Printf("fake-pig args=%q\n", os.Args[1:])
|
|
23
|
+
var n int
|
|
24
|
+
fmt.Sscanf(code, "%d", &n)
|
|
25
|
+
os.Exit(n)
|
|
26
|
+
}
|
|
27
|
+
root, _ := os.MkdirTemp("", "pigacp-main-*")
|
|
28
|
+
for _, k := range []string{"HOME", "PIG_HOME", "PIG_CODING_AGENT_DIR", "PI_CODING_AGENT_DIR", "XDG_CONFIG_HOME"} {
|
|
29
|
+
os.Setenv(k, filepath.Join(root, k))
|
|
30
|
+
}
|
|
31
|
+
for _, k := range []string{"PIG_ACP_PIG_COMMAND", "PI_ACP_PI_COMMAND", "PIG_USE_PI_DIRS"} {
|
|
32
|
+
os.Unsetenv(k)
|
|
33
|
+
}
|
|
34
|
+
code := m.Run()
|
|
35
|
+
os.RemoveAll(root)
|
|
36
|
+
os.Exit(code)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func fakePig(t *testing.T, exit int) string {
|
|
40
|
+
t.Helper()
|
|
41
|
+
exe, err := os.Executable()
|
|
42
|
+
if err != nil {
|
|
43
|
+
t.Fatal(err)
|
|
44
|
+
}
|
|
45
|
+
script := filepath.Join(t.TempDir(), "pig")
|
|
46
|
+
body := fmt.Sprintf("#!/bin/sh\nPIGACP_FAKE_PIG=%d exec %q \"$@\"\n", exit, exe)
|
|
47
|
+
if err := os.WriteFile(script, []byte(body), 0o755); err != nil {
|
|
48
|
+
t.Fatal(err)
|
|
49
|
+
}
|
|
50
|
+
return script
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func TestVersionAndUsage(t *testing.T) {
|
|
54
|
+
t.Run("--version names the tool and the pinned ACP protocol version", func(t *testing.T) {
|
|
55
|
+
var out, errw bytes.Buffer
|
|
56
|
+
if code := run([]string{"--version"}, strings.NewReader(""), &out, &errw); code != 0 {
|
|
57
|
+
t.Fatalf("exit %d: %s", code, errw.String())
|
|
58
|
+
}
|
|
59
|
+
if !strings.HasPrefix(out.String(), "pig-acp ") || !strings.Contains(out.String(), "ACP protocol version 1") {
|
|
60
|
+
t.Errorf("out = %q", out.String())
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
t.Run("--help lists the flags and the environment variables", func(t *testing.T) {
|
|
64
|
+
var out bytes.Buffer
|
|
65
|
+
if code := run([]string{"--help"}, strings.NewReader(""), &out, io.Discard); code != 0 {
|
|
66
|
+
t.Fatalf("exit %d", code)
|
|
67
|
+
}
|
|
68
|
+
for _, want := range []string{"--pig", "--piglet", "--pig-arg", "--terminal-login", "PIG_ACP_PIG_COMMAND", "PIG_ACP_ENABLE_EMBEDDED_CONTEXT"} {
|
|
69
|
+
if !strings.Contains(out.String(), want) {
|
|
70
|
+
t.Errorf("help lacks %q:\n%s", want, out.String())
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
t.Run("an unknown flag exits 2 with a message on stderr and nothing on stdout", func(t *testing.T) {
|
|
75
|
+
var out, errw bytes.Buffer
|
|
76
|
+
if code := run([]string{"--bogus"}, strings.NewReader(""), &out, &errw); code != 2 || out.Len() != 0 || !strings.Contains(errw.String(), "--bogus") {
|
|
77
|
+
t.Errorf("code=%d out=%q err=%q", code, out.String(), errw.String())
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func TestTerminalLogin(t *testing.T) {
|
|
83
|
+
t.Run("--terminal-login starts pig interactively and returns its exit status", func(t *testing.T) {
|
|
84
|
+
var out, errw bytes.Buffer
|
|
85
|
+
code := run([]string{"--pig", fakePig(t, 3), "--terminal-login"}, strings.NewReader(""), &out, &errw)
|
|
86
|
+
if code != 3 {
|
|
87
|
+
t.Fatalf("exit %d, stderr %s", code, errw.String())
|
|
88
|
+
}
|
|
89
|
+
if !strings.Contains(out.String(), "fake-pig args=[]") {
|
|
90
|
+
t.Errorf("pig was not started without arguments: %q", out.String())
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
t.Run("--terminal-login with a missing pig explains how to install it and exits 1", func(t *testing.T) {
|
|
94
|
+
var errw bytes.Buffer
|
|
95
|
+
code := run([]string{"--pig", "pig-does-not-exist-12345", "--terminal-login"}, strings.NewReader(""), io.Discard, &errw)
|
|
96
|
+
if code != 1 || !strings.Contains(errw.String(), "pig-acp: could not start pig (command not found: pig-does-not-exist-12345)") {
|
|
97
|
+
t.Errorf("code=%d stderr=%q", code, errw.String())
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func TestPigCommandSelection(t *testing.T) {
|
|
103
|
+
t.Run("--pig beats PIG_ACP_PIG_COMMAND beats PI_ACP_PI_COMMAND", func(t *testing.T) {
|
|
104
|
+
a, b, c := fakePig(t, 11), fakePig(t, 12), fakePig(t, 13)
|
|
105
|
+
var out bytes.Buffer
|
|
106
|
+
t.Setenv("PI_ACP_PI_COMMAND", c)
|
|
107
|
+
t.Setenv("PIG_ACP_PIG_COMMAND", b)
|
|
108
|
+
if code := run([]string{"--pig", a, "--terminal-login"}, strings.NewReader(""), &out, io.Discard); code != 11 {
|
|
109
|
+
t.Errorf("flag: exit %d", code)
|
|
110
|
+
}
|
|
111
|
+
if code := run([]string{"--terminal-login"}, strings.NewReader(""), &out, io.Discard); code != 12 {
|
|
112
|
+
t.Errorf("PIG_ACP_PIG_COMMAND: exit %d", code)
|
|
113
|
+
}
|
|
114
|
+
t.Setenv("PIG_ACP_PIG_COMMAND", "")
|
|
115
|
+
if code := run([]string{"--terminal-login"}, strings.NewReader(""), &out, io.Discard); code != 13 {
|
|
116
|
+
t.Errorf("PI_ACP_PI_COMMAND: exit %d", code)
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
t.Run("--piglet and --pig-arg are forwarded to the pig child after the RPC flags", func(t *testing.T) {
|
|
120
|
+
// Checked through the launch arguments the terminal login does not use; see pirpc.BuildArgs.
|
|
121
|
+
got := buildPigArgs([]string{"--piglet", "acp"}, "")
|
|
122
|
+
want := []string{"--mode", "rpc", "--no-themes", "--piglet", "acp"}
|
|
123
|
+
if strings.Join(got, "|") != strings.Join(want, "|") {
|
|
124
|
+
t.Errorf("args = %v, want %v", got, want)
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
func TestServing(t *testing.T) {
|
|
130
|
+
t.Run("serves ACP on stdin and stdout and exits 0 when stdin closes", func(t *testing.T) {
|
|
131
|
+
req, _ := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": map[string]any{"protocolVersion": 1}})
|
|
132
|
+
var out bytes.Buffer
|
|
133
|
+
code := run(nil, strings.NewReader(string(req)+"\n"), &out, io.Discard)
|
|
134
|
+
if code != 0 {
|
|
135
|
+
t.Fatalf("exit %d", code)
|
|
136
|
+
}
|
|
137
|
+
var resp map[string]any
|
|
138
|
+
if err := json.Unmarshal(bytes.TrimSpace(out.Bytes()), &resp); err != nil {
|
|
139
|
+
t.Fatalf("stdout %q is not one JSON line: %v", out.String(), err)
|
|
140
|
+
}
|
|
141
|
+
if resp["id"] != float64(1) || resp["result"].(map[string]any)["protocolVersion"] != float64(1) {
|
|
142
|
+
t.Errorf("response = %v", resp)
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
t.Run("writes nothing but protocol messages to stdout", func(t *testing.T) {
|
|
146
|
+
var out, errw bytes.Buffer
|
|
147
|
+
run(nil, strings.NewReader("garbage\n"), &out, &errw)
|
|
148
|
+
if out.Len() != 0 {
|
|
149
|
+
t.Errorf("stdout has %q", out.String())
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
t.Run("stdout writer: a destroyed stdout ends the run with exit 0 and no crash", func(t *testing.T) {
|
|
153
|
+
req, _ := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": map[string]any{"protocolVersion": 1}})
|
|
154
|
+
pr, pw := io.Pipe()
|
|
155
|
+
pr.Close()
|
|
156
|
+
done := make(chan int, 1)
|
|
157
|
+
go func() { done <- run(nil, strings.NewReader(string(req)+"\n"), pw, io.Discard) }()
|
|
158
|
+
select {
|
|
159
|
+
case code := <-done:
|
|
160
|
+
if code != 0 {
|
|
161
|
+
t.Errorf("exit %d", code)
|
|
162
|
+
}
|
|
163
|
+
case <-time.After(5 * time.Second):
|
|
164
|
+
t.Fatal("run blocked on a destroyed stdout")
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func TestPigletAndPigArgReachPig(t *testing.T) {
|
|
170
|
+
t.Run("--piglet and --pig-arg are the arguments pig is started with", func(t *testing.T) {
|
|
171
|
+
var out bytes.Buffer
|
|
172
|
+
code := run([]string{"--pig", fakePig(t, 0), "--piglet", "acp", "--pig-arg", "-e", "--pig-arg", "/x y", "--terminal-login"}, strings.NewReader(""), &out, io.Discard)
|
|
173
|
+
if code != 0 || !strings.Contains(out.String(), `args=["--piglet" "acp" "-e" "/x y"]`) {
|
|
174
|
+
t.Errorf("code=%d out=%q", code, out.String())
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// The auth method the editor runs for "sign in" must start the same agent the sessions use: a
|
|
180
|
+
// session started with --piglet and --pig-arg must not get a plain-pig login. (Found by review: the
|
|
181
|
+
// advertised arguments kept only --pig.)
|
|
182
|
+
func TestTerminalAuthMethodKeepsTheAgentSelection(t *testing.T) {
|
|
183
|
+
req, _ := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": map[string]any{
|
|
184
|
+
"protocolVersion": 1, "clientCapabilities": map[string]any{"_meta": map[string]any{"terminal-auth": true}}}})
|
|
185
|
+
var out bytes.Buffer
|
|
186
|
+
args := []string{"--pig", "/opt/pig", "--piglet", "acp", "--pig-arg", "-e", "--pig-arg", "/x y"}
|
|
187
|
+
if code := run(args, strings.NewReader(string(req)+"\n"), &out, io.Discard); code != 0 {
|
|
188
|
+
t.Fatalf("exit %d", code)
|
|
189
|
+
}
|
|
190
|
+
var resp struct {
|
|
191
|
+
Result struct {
|
|
192
|
+
AuthMethods []struct {
|
|
193
|
+
Args []string `json:"args"`
|
|
194
|
+
Meta struct {
|
|
195
|
+
TerminalAuth struct {
|
|
196
|
+
Args []string `json:"args"`
|
|
197
|
+
} `json:"terminal-auth"`
|
|
198
|
+
} `json:"_meta"`
|
|
199
|
+
} `json:"authMethods"`
|
|
200
|
+
} `json:"result"`
|
|
201
|
+
}
|
|
202
|
+
if err := json.Unmarshal(bytes.TrimSpace(out.Bytes()), &resp); err != nil || len(resp.Result.AuthMethods) != 1 {
|
|
203
|
+
t.Fatalf("response %q: %v", out.String(), err)
|
|
204
|
+
}
|
|
205
|
+
want := `["--pig" "/opt/pig" "--piglet" "acp" "--pig-arg" "-e" "--pig-arg" "/x y" "--terminal-login"]`
|
|
206
|
+
m := resp.Result.AuthMethods[0]
|
|
207
|
+
if got := fmt.Sprintf("%q", m.Args); got != want {
|
|
208
|
+
t.Errorf("args = %s, want %s", got, want)
|
|
209
|
+
}
|
|
210
|
+
if got := fmt.Sprintf("%q", m.Meta.TerminalAuth.Args); got != want {
|
|
211
|
+
t.Errorf("_meta terminal-auth args = %s, want %s", got, want)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Package ask is an end-to-end test fixture: the ask tool opens a select dialog and reports the choice.
|
|
2
|
+
package ask
|
|
3
|
+
|
|
4
|
+
import (
|
|
5
|
+
sdk "github.com/MichaelKinsy/PiG/extensions/sdk"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// Extension registers the ask tool.
|
|
9
|
+
func Extension() *sdk.Extension {
|
|
10
|
+
e := sdk.New("ask")
|
|
11
|
+
e.RegisterTool(sdk.ToolDefinition{
|
|
12
|
+
Name: "ask", Label: "Ask", Description: "Ask the user where to deploy",
|
|
13
|
+
Parameters: sdk.Schema{"type": "object", "properties": map[string]any{}},
|
|
14
|
+
Execute: func(ctx sdk.Context, _ map[string]any) (any, error) {
|
|
15
|
+
choice, ok, err := ctx.Select("Deploy where?", []string{"staging", "production"})
|
|
16
|
+
if err != nil {
|
|
17
|
+
return nil, err
|
|
18
|
+
}
|
|
19
|
+
if !ok {
|
|
20
|
+
ctx.Notify("no choice", "warning")
|
|
21
|
+
return "the user made no choice", nil
|
|
22
|
+
}
|
|
23
|
+
ctx.Notify("chose "+choice, "info")
|
|
24
|
+
return "the user chose " + choice, nil
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
return e
|
|
28
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Package gate is an end-to-end test fixture: pig runs its tool_call hook after it has announced
|
|
2
|
+
// the tool (tool_execution_start) and before it runs the tool, so a hook that blocks holds the tool
|
|
3
|
+
// while the adapter reacts to the announcement.
|
|
4
|
+
package gate
|
|
5
|
+
|
|
6
|
+
import (
|
|
7
|
+
"net"
|
|
8
|
+
"os"
|
|
9
|
+
"path/filepath"
|
|
10
|
+
"strings"
|
|
11
|
+
|
|
12
|
+
sdk "github.com/MichaelKinsy/PiG/extensions/sdk"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// Extension holds every write until the test closes the connection it opened to the address in
|
|
16
|
+
// <cwd>/gate.addr.
|
|
17
|
+
func Extension() *sdk.Extension {
|
|
18
|
+
e := sdk.New("gate")
|
|
19
|
+
e.OnEvent(sdk.EventToolCall, func(ctx sdk.Context, data map[string]any) (any, error) {
|
|
20
|
+
if data["toolName"] != "write" {
|
|
21
|
+
return nil, nil
|
|
22
|
+
}
|
|
23
|
+
addr, err := os.ReadFile(filepath.Join(ctx.Cwd(), "gate.addr"))
|
|
24
|
+
if err != nil {
|
|
25
|
+
return nil, err
|
|
26
|
+
}
|
|
27
|
+
conn, err := net.Dial("tcp", strings.TrimSpace(string(addr)))
|
|
28
|
+
if err != nil {
|
|
29
|
+
return nil, err
|
|
30
|
+
}
|
|
31
|
+
defer conn.Close()
|
|
32
|
+
// The test closes its end to release the write; Read returns then.
|
|
33
|
+
_, _ = conn.Read(make([]byte, 1))
|
|
34
|
+
return nil, nil
|
|
35
|
+
})
|
|
36
|
+
return e
|
|
37
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
module gatefixture
|
|
2
|
+
|
|
3
|
+
go 1.26
|
|
4
|
+
|
|
5
|
+
// Test fixture only (not part of the Package): a Go extension that holds the write tool until the
|
|
6
|
+
// end-to-end test lets it run, so the test decides whether the adapter or the write comes first.
|
|
7
|
+
require github.com/MichaelKinsy/PiG/extensions/sdk v0.4.1
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
// Twin helpers for a port whose original has tests. Every upstream test case gets one
|
|
4
|
+
// tw(...) call carrying the upstream title unchanged, or one tskip(...) with the reason it
|
|
5
|
+
// cannot be ported. `pigeq twins check` reads these calls and fails on a missing title, an
|
|
6
|
+
// unknown one (a misspelling) or one reason shared by many skips.
|
|
7
|
+
//
|
|
8
|
+
// pigeq twins list --tests <upstream>/test > port/upstream-tests.json
|
|
9
|
+
// pigeq twins check --ledger port/upstream-tests.json --go extensions/<name> [--files a,b]
|
|
10
|
+
//
|
|
11
|
+
// Copy this file into the port as twin_test.go and replace main with the package name (in
|
|
12
|
+
// an external test package use `main_test`). A parametrized upstream title (it contains
|
|
13
|
+
// `${`) is twinned once, with the template text unchanged.
|
|
14
|
+
|
|
15
|
+
import "testing"
|
|
16
|
+
|
|
17
|
+
// tw runs one exact twin: file is the upstream test file (its name without .test.mjs), title
|
|
18
|
+
// the upstream test title, unchanged.
|
|
19
|
+
func tw(t *testing.T, file, title string, fn func(t *testing.T)) {
|
|
20
|
+
t.Helper()
|
|
21
|
+
t.Run(title, fn)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// tskip records a named gap: the upstream case has no Go twin, with its own reason.
|
|
25
|
+
func tskip(t *testing.T, file, title, reason string) {
|
|
26
|
+
t.Helper()
|
|
27
|
+
t.Run(title, func(t *testing.T) { t.Skip(reason) })
|
|
28
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Package acp is the PiG-side extension of the ACP Package. The editor entrypoint itself is the
|
|
2
|
+
// companion executable in cmd/pig-acp: an extension may not write protocol bytes to the host's
|
|
3
|
+
// stdout, so it cannot speak ACP. This extension is the in-session part: `/acp` reports how to
|
|
4
|
+
// point an editor at this agent, and what the adapter does and does not support.
|
|
5
|
+
package acp
|
|
6
|
+
|
|
7
|
+
import (
|
|
8
|
+
"encoding/json"
|
|
9
|
+
"os"
|
|
10
|
+
"strings"
|
|
11
|
+
|
|
12
|
+
sdk "github.com/MichaelKinsy/PiG/extensions/sdk"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// Extension returns the extension.
|
|
16
|
+
func Extension() *sdk.Extension {
|
|
17
|
+
e := sdk.New("acp")
|
|
18
|
+
e.Command("acp", "show how to use this agent from an ACP editor such as Zed", func(ctx sdk.Context, args string) error {
|
|
19
|
+
switch strings.TrimSpace(args) {
|
|
20
|
+
case "":
|
|
21
|
+
ctx.Notify(zedSnippet()+"\n\n"+limits(), "info")
|
|
22
|
+
case "zed":
|
|
23
|
+
ctx.Notify(zedSnippet(), "info")
|
|
24
|
+
default:
|
|
25
|
+
ctx.Notify("Usage: /acp [zed]", "warning")
|
|
26
|
+
}
|
|
27
|
+
return nil
|
|
28
|
+
})
|
|
29
|
+
return e
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Executable is the path the editor configuration names for --pig: the running agent. Fused into a
|
|
33
|
+
// Piglet Binary the extension runs inside the agent, so that is this process. Installed as a Package it
|
|
34
|
+
// runs in its own extension process (a cell runner, not an agent); PiG then names the agent that
|
|
35
|
+
// started it in PIG_HARNESS_BINARY, the variable its extension processes use to re-launch the harness.
|
|
36
|
+
// That variable is not part of the public SDK: if a PiG release drops it, /acp names the runner again.
|
|
37
|
+
func Executable() string {
|
|
38
|
+
if host := os.Getenv("PIG_HARNESS_BINARY"); host != "" {
|
|
39
|
+
return host
|
|
40
|
+
}
|
|
41
|
+
if exe, err := os.Executable(); err == nil && exe != "" {
|
|
42
|
+
return exe
|
|
43
|
+
}
|
|
44
|
+
return "pig"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func zedSnippet() string {
|
|
48
|
+
cfg := map[string]any{"agent_servers": map[string]any{"PiG": map[string]any{
|
|
49
|
+
"type": "custom", "command": "pig-acp", "args": []string{"--pig", Executable()}, "env": map[string]any{},
|
|
50
|
+
}}}
|
|
51
|
+
b, _ := json.MarshalIndent(cfg, "", " ")
|
|
52
|
+
return "Agent Client Protocol (ACP): use this agent from an editor.\n" +
|
|
53
|
+
"Install pig-acp (the companion executable of the acp Package), then add this to Zed's settings.json:\n\n" +
|
|
54
|
+
string(b) +
|
|
55
|
+
"\n\nOther ACP clients run: pig-acp --pig " + Executable()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func limits() string {
|
|
59
|
+
return "Supported: sessions (new, load, list, delete), streaming text and thinking, tool calls with diffs and terminal output, " +
|
|
60
|
+
"cancel, model and thinking-level selection, context usage, permission requests for extension select and confirm dialogs.\n" +
|
|
61
|
+
"Not supported: file system and terminal delegation to the editor, MCP servers passed by the editor " +
|
|
62
|
+
"(accepted, never started), extension slash commands, input and editor dialogs (cancelled with a notice)."
|
|
63
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
package acp_test
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"strings"
|
|
6
|
+
"testing"
|
|
7
|
+
|
|
8
|
+
acpext "github.com/MichaelKinsy/pigpen/acp"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// The extension is the in-session half of the ACP Package (the protocol entrypoint is the
|
|
12
|
+
// companion executable, see cmd/pig-acp). Layer-1 cases through the fake host.
|
|
13
|
+
|
|
14
|
+
// runCommand runs a slash command through the fake host (the template's Host.Command).
|
|
15
|
+
func runCommand(h *Host, name, args string) string {
|
|
16
|
+
h.t.Helper()
|
|
17
|
+
return h.Command(name, args)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func notifications(h *Host) []HostCall { return h.CallsTo("ui.notify") }
|
|
21
|
+
|
|
22
|
+
func TestRegistersOnlyTheAcpCommand(t *testing.T) {
|
|
23
|
+
h := StartHost(t, acpext.Extension(), HostOptions{})
|
|
24
|
+
if h.Registered("session_start") || h.Registered("session_shutdown") || h.Registered("tool_call") {
|
|
25
|
+
t.Error("registered an event handler: the extension must not touch the session")
|
|
26
|
+
}
|
|
27
|
+
if code := runCommand(h, "acp", ""); code != "" {
|
|
28
|
+
t.Fatalf("/acp failed: %s", code)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func TestAcpCommandExplainsSetup(t *testing.T) {
|
|
33
|
+
h := StartHost(t, acpext.Extension(), HostOptions{})
|
|
34
|
+
if code := runCommand(h, "acp", ""); code != "" {
|
|
35
|
+
t.Fatalf("/acp failed: %s", code)
|
|
36
|
+
}
|
|
37
|
+
notes := notifications(h)
|
|
38
|
+
if len(notes) != 1 {
|
|
39
|
+
t.Fatalf("%d notifications, want 1", len(notes))
|
|
40
|
+
}
|
|
41
|
+
msg, _ := notes[0].Args["message"].(string)
|
|
42
|
+
for _, want := range []string{
|
|
43
|
+
"pig-acp", "--pig", `"agent_servers"`, "Agent Client Protocol",
|
|
44
|
+
"Supported:", "Not supported:", "file system and terminal delegation", "MCP",
|
|
45
|
+
} {
|
|
46
|
+
if !strings.Contains(msg, want) {
|
|
47
|
+
t.Errorf("message lacks %q:\n%s", want, msg)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if notes[0].Args["level"] != "info" {
|
|
51
|
+
t.Errorf("level = %v", notes[0].Args["level"])
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func TestAcpCommandNamesThisExecutable(t *testing.T) {
|
|
56
|
+
h := StartHost(t, acpext.Extension(), HostOptions{})
|
|
57
|
+
runCommand(h, "acp", "")
|
|
58
|
+
msg, _ := notifications(h)[0].Args["message"].(string)
|
|
59
|
+
exe := acpext.Executable()
|
|
60
|
+
if exe == "" || !strings.Contains(msg, exe) {
|
|
61
|
+
t.Errorf("message does not name the running executable %q:\n%s", exe, msg)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Installed as a Package (not fused into a Piglet Binary), the extension runs in its own process: an
|
|
66
|
+
// extension cell under PiG's cache, which is not an agent and cannot be pig-acp's --pig target. PiG hands
|
|
67
|
+
// every extension process the executable of the agent that started it in PIG_HARNESS_BINARY; /acp must
|
|
68
|
+
// name that one. (Found by review: `pig -e components/acp/extensions/acp` printed the cell runner.)
|
|
69
|
+
func TestAcpCommandNamesTheHostAgentWhenItRunsAsASubprocess(t *testing.T) {
|
|
70
|
+
t.Setenv("PIG_HARNESS_BINARY", "/opt/pig/bin/pig")
|
|
71
|
+
if got := acpext.Executable(); got != "/opt/pig/bin/pig" {
|
|
72
|
+
t.Errorf("Executable() = %q, want the host agent /opt/pig/bin/pig", got)
|
|
73
|
+
}
|
|
74
|
+
h := StartHost(t, acpext.Extension(), HostOptions{})
|
|
75
|
+
runCommand(h, "acp", "zed")
|
|
76
|
+
msg, _ := notifications(h)[0].Args["message"].(string)
|
|
77
|
+
if !strings.Contains(msg, `"/opt/pig/bin/pig"`) || !strings.Contains(msg, "pig-acp --pig /opt/pig/bin/pig") {
|
|
78
|
+
t.Errorf("the snippet does not name the host agent:\n%s", msg)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func TestAcpCommandRejectsUnknownArguments(t *testing.T) {
|
|
83
|
+
h := StartHost(t, acpext.Extension(), HostOptions{})
|
|
84
|
+
runCommand(h, "acp", "bogus")
|
|
85
|
+
notes := notifications(h)
|
|
86
|
+
if len(notes) != 1 || notes[0].Args["level"] != "warning" || !strings.Contains(notes[0].Args["message"].(string), "Usage: /acp") {
|
|
87
|
+
t.Errorf("notifications = %+v", notes)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func TestAcpCommandZedSnippetOnly(t *testing.T) {
|
|
92
|
+
h := StartHost(t, acpext.Extension(), HostOptions{})
|
|
93
|
+
runCommand(h, "acp", "zed")
|
|
94
|
+
msg, _ := notifications(h)[0].Args["message"].(string)
|
|
95
|
+
if !strings.Contains(msg, `"agent_servers"`) || strings.Contains(msg, "Not supported:") {
|
|
96
|
+
t.Errorf("message:\n%s", msg)
|
|
97
|
+
}
|
|
98
|
+
// The snippet names this executable as the --pig target (a JSON string, so escaped as JSON).
|
|
99
|
+
quoted, _ := json.Marshal(acpext.Executable())
|
|
100
|
+
if !strings.Contains(msg, string(quoted)) {
|
|
101
|
+
t.Errorf("the snippet does not name the executable %s:\n%s", quoted, msg)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
func TestAcpCommandStaysQuietWithoutUI(t *testing.T) {
|
|
106
|
+
no := false
|
|
107
|
+
h := StartHost(t, acpext.Extension(), HostOptions{Mode: "print", HasUI: &no})
|
|
108
|
+
if code := runCommand(h, "acp", ""); code != "" {
|
|
109
|
+
t.Fatalf("/acp failed headless: %s", code)
|
|
110
|
+
}
|
|
111
|
+
}
|