feat: opencode через HTTP API — пул serve-серверов вместо spawn/NDJSON
Runner теперь ходит к постоянным serve по HTTP API (v1.17+, /api):
- клиент Client (create/send/wait/abort/messages/verdict)
- Pool: по одному serve на каталог, ленивый подъём, root-сервер в worktree,
выделение портов, ReleaseTask при завершении задачи
- Run: CreateSession('ratatoskr-<агент>') -> Send -> поллинг Verdict из
text-частей assistant-сообщений; idle/hard таймауты дают RC=-1
- вердикт извлекается из последнего assistant text-парта (плоский text)
- тесты: unit на фейковом HTTP-сервере; e2e эмулирует serve через httptest,
агент определяется по title сессии
This commit is contained in:
75
internal/opencode/pool_test.go
Normal file
75
internal/opencode/pool_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package opencode
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPool_EnsureRoot_NoSpawn(t *testing.T) {
|
||||
// без spawn: пул без Bin — EnsureRoot должен упасть (нет бинаря),
|
||||
// но НЕ упасть на пустой карте. Проверяем, что повтор вызова не паникует.
|
||||
dir := t.TempDir()
|
||||
p := NewPool(dir)
|
||||
// не запускаем — просто проверяем кэш
|
||||
p.mu.Lock()
|
||||
p.segs[dir] = &Server{URL: "http://127.0.0.1:1", PollInterval: time.Millisecond}
|
||||
p.mu.Unlock()
|
||||
|
||||
s, ok := p.ServerFor(dir)
|
||||
if !ok || s == nil {
|
||||
t.Fatal("ServerFor должен найти закэшированный сервер")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPool_ServerFor_ReleaseTask(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
a := filepath.Join(dir, "a")
|
||||
b := filepath.Join(dir, "b")
|
||||
p := NewPool(dir)
|
||||
p.Bin = fakeServeBin(t, dir)
|
||||
|
||||
// root не запускаем; добавляем серверы в карту напрямую (как после Ensure).
|
||||
p.mu.Lock()
|
||||
p.segs[a] = &Server{URL: "http://127.0.0.1:1", PollInterval: time.Nanosecond}
|
||||
p.segs[b] = &Server{URL: "http://127.0.0.1:1", PollInterval: time.Nanosecond}
|
||||
p.mu.Unlock()
|
||||
|
||||
// ReleaseTask: rootDir в карте НЕ занят, значит оба закрываются.
|
||||
p.ReleaseTask()
|
||||
if _, ok := p.ServerFor(a); ok {
|
||||
t.Error("каталог a должен быть снят после ReleaseTask")
|
||||
}
|
||||
if _, ok := p.ServerFor(b); ok {
|
||||
t.Error("каталог b должен быть снят после ReleaseTask")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPool_AllocPort_Unique(t *testing.T) {
|
||||
p := NewPool("/tmp/x")
|
||||
p.Host = "127.0.0.1"
|
||||
seen := map[int]bool{}
|
||||
var ports []int
|
||||
for i := 0; i < 5; i++ {
|
||||
port, err := p.allocPort()
|
||||
if err != nil {
|
||||
t.Fatalf("allocPort err: %v", err)
|
||||
}
|
||||
if seen[port] {
|
||||
t.Fatalf("дубль порта %d", port)
|
||||
}
|
||||
seen[port] = true
|
||||
ports = append(ports, port)
|
||||
}
|
||||
// освобождаем и убеждаемся, что порт можно переиспользовать
|
||||
p.releasePort(ports[0])
|
||||
if !p.free(ports[0]) {
|
||||
t.Errorf("порт %d должен быть свободен после releasePort", ports[0])
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pool) free(port int) bool {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
return !p.used[port]
|
||||
}
|
||||
Reference in New Issue
Block a user