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 сессии
75 lines
2.2 KiB
Go
75 lines
2.2 KiB
Go
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]
|
||
} |