feat: opencode serve — постоянный сервер вместо разовых subprocess (вариант A)
Супервайзер Server spawn'ит opencode serve (или ходит на внешний URL),
Runner ходит к нему через 'opencode run --attach <url>'. По умолчанию
serve.enabled=false — историческая spawn-модель сохранена; наличие
serve.url переключает на внешний сервер.
- config: ServeCfg (enabled/hostname/port/url/password) + дефолты; лоадер
научился int/bool (раньше только string/Duration/struct).
- opencode: Server (Start/Run/Close, /global/health, рестарт упавшего,
reaper-горутина владеет Wait; .Exited() непригоден для SIGKILL).
- Runner.AttachURL: run --attach <url> при заданном URL, иначе как раньше.
- app: composition root — при включённом serve запускает супервайзер.
- тесты: TestRun_AttachMode, TestServer_{ExternalURL,OwnProcess,Restart}.
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -42,6 +43,13 @@ case "$mode" in
|
||||
echo '{"type":"text","part":{"text":"boom"}}'
|
||||
exit 7
|
||||
;;
|
||||
args)
|
||||
# печатаем аргументы в $FAKE_ARGS_FILE (тест читает) и успешно завершаемся
|
||||
printf '%s\n' "$@" > "${FAKE_ARGS_FILE:-/dev/null}"
|
||||
echo '{"type":"text","part":{"text":"ok"}}'
|
||||
echo '{"session_id":"sess-args"}'
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
`
|
||||
if err := os.WriteFile(bin, []byte(script), 0o755); err != nil {
|
||||
@@ -177,6 +185,61 @@ func TestResumeDev_Fallback(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestRun_AttachMode проверяет, что при заданном AttachURL команда opencode run
|
||||
// получает флаг `--attach <url>`, и что без AttachURL — не получает.
|
||||
func TestRun_AttachMode(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
bin := fakeOpenCode(t, dir)
|
||||
t.Setenv("FAKE_MODE", "args")
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
attach string
|
||||
wantFlag bool
|
||||
}{
|
||||
{"attach задан", "http://127.0.0.1:4096", true},
|
||||
{"attach пуст (spawn-модель)", "", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
argsFile := filepath.Join(dir, "args_"+strings.ReplaceAll(tc.name, " ", "_")+".txt")
|
||||
t.Setenv("FAKE_ARGS_FILE", argsFile)
|
||||
|
||||
r := &Runner{Bin: bin, AttachURL: tc.attach, PollInterval: 20 * time.Millisecond}
|
||||
res, err := r.Run(context.Background(), "task", dir, "dev", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Run err: %v", err)
|
||||
}
|
||||
if res.RC != 0 {
|
||||
t.Fatalf("RC = %d, want 0", res.RC)
|
||||
}
|
||||
data, err := os.ReadFile(argsFile)
|
||||
if err != nil {
|
||||
t.Fatalf("читать args-файл: %v", err)
|
||||
}
|
||||
args := strings.Fields(string(data))
|
||||
hasAttach := false
|
||||
for i, a := range args {
|
||||
if a == "--attach" {
|
||||
hasAttach = true
|
||||
if i+1 >= len(args) || args[i+1] != tc.attach {
|
||||
t.Fatalf("--attach URL = %q, want %q", args[min(i+1, len(args)-1)], tc.attach)
|
||||
}
|
||||
}
|
||||
}
|
||||
if hasAttach != tc.wantFlag {
|
||||
t.Errorf("--attach присутствует = %v, want %v; args=%v", hasAttach, tc.wantFlag, args)
|
||||
}
|
||||
if hasAttach && tc.attach != "" {
|
||||
// --dir должен идти следом за --attach
|
||||
if !contains(string(data), "--dir") {
|
||||
t.Errorf("ожидался --dir в args: %v", args)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s, sub string) bool {
|
||||
return len(s) >= len(sub) && (s == sub || len(s) > 0 && indexOf(s, sub) >= 0)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user