feat: model selection at spawn (POST /api/spawn model -> LVMH_MODEL -> bridge createAgentSession); ops prepare resets ops session (fresh context per prepare)

This commit is contained in:
Raphael Westphal
2026-08-20 12:43:55 +02:00
parent 5292a69d66
commit 2d0839357d
11 changed files with 553 additions and 48 deletions
+45 -20
View File
@@ -29,7 +29,7 @@ func TestSpawnerStartHappyPath(t *testing.T) {
f := newFakeDocker()
sp, store := newTestSpawner(t, f)
res, err := sp.Start(context.Background(), "group/project", "main")
res, err := sp.Start(context.Background(), "group/project", "main", "")
if err != nil {
t.Fatalf("Start: %v", err)
}
@@ -148,7 +148,7 @@ func TestSpawnerBuildsImageWhenMissing(t *testing.T) {
f.images = 0 // image absent → ensureImage must build
sp, _ := newTestSpawner(t, f)
res, err := sp.Start(context.Background(), "group/project", "")
res, err := sp.Start(context.Background(), "group/project", "", "")
if err != nil {
t.Fatalf("Start: %v", err)
}
@@ -172,7 +172,7 @@ func TestSpawnerStartValidatesDockerAndDockerfile(t *testing.T) {
if err != nil {
t.Fatalf("NewSpawner: %v", err)
}
_, err = sp2.Start(context.Background(), "group/project", "")
_, err = sp2.Start(context.Background(), "group/project", "", "")
if err == nil || !strings.Contains(err.Error(), "no worker Dockerfile") {
t.Fatalf("Start without dockerfile err = %v", err)
}
@@ -247,7 +247,7 @@ func TestSpawnerRunJobErrorStates(t *testing.T) {
tc.setup(t, f)
}
sp, _ := newTestSpawner(t, f)
res, err := sp.Start(context.Background(), "group/project", "")
res, err := sp.Start(context.Background(), "group/project", "", "")
if err != nil {
t.Fatalf("Start: %v", err)
}
@@ -384,11 +384,11 @@ func TestSpawnerSameRepoSpawnsSerialize(t *testing.T) {
t.Fatal("slugLock must return distinct mutexes per slug")
}
res1, err := sp.Start(context.Background(), "group/project", "")
res1, err := sp.Start(context.Background(), "group/project", "", "")
if err != nil {
t.Fatalf("Start 1: %v", err)
}
res2, err := sp.Start(context.Background(), "group/project", "")
res2, err := sp.Start(context.Background(), "group/project", "", "")
if err != nil {
t.Fatalf("Start 2: %v", err)
}
@@ -436,7 +436,7 @@ func TestSpawnerCloneUsesHeaderAuthNotURLCredentials(t *testing.T) {
t.Fatalf("set token: %v", err)
}
res, err := sp.Start(context.Background(), "group/project", "")
res, err := sp.Start(context.Background(), "group/project", "", "")
if err != nil {
t.Fatalf("Start: %v", err)
}
@@ -491,7 +491,7 @@ func TestSpawnerFailedSeedRemovesRepoVolume(t *testing.T) {
f.failArchive = true // CopyToContainer fails → seed fails
sp, _ := newTestSpawner(t, f)
res, err := sp.Start(context.Background(), "group/project", "")
res, err := sp.Start(context.Background(), "group/project", "", "")
if err != nil {
t.Fatalf("Start: %v", err)
}
@@ -514,7 +514,7 @@ func TestSpawnerFailedSeedSurvivesVolumeRemoveFailure(t *testing.T) {
f.failVolumeDelete = true // cleanup itself fails; seed error still surfaces
sp, _ := newTestSpawner(t, f)
res, err := sp.Start(context.Background(), "group/project", "")
res, err := sp.Start(context.Background(), "group/project", "", "")
if err != nil {
t.Fatalf("Start: %v", err)
}
@@ -722,7 +722,7 @@ func TestSpawnerStartDockerUnavailable(t *testing.T) {
// because the client is already built; close the fake server instead.
sp.cli.Close()
closeDocker(t, sp)
if _, err := sp.Start(context.Background(), "group/project", ""); err == nil || !strings.Contains(err.Error(), "docker unavailable") {
if _, err := sp.Start(context.Background(), "group/project", "", ""); err == nil || !strings.Contains(err.Error(), "docker unavailable") {
t.Fatalf("Start with dead docker = %v, want docker unavailable", err)
}
}
@@ -799,7 +799,7 @@ func TestSpawnerSessionsVolumeCreateFails(t *testing.T) {
f.volume[volumeRepoPrefix+repoSlug("group/project")] = true // repo volume exists → skip seed
f.failVolumeCreate = true
sp, _ := newTestSpawner(t, f)
res, err := sp.Start(context.Background(), "group/project", "")
res, err := sp.Start(context.Background(), "group/project", "", "")
if err != nil {
t.Fatalf("Start: %v", err)
}
@@ -844,7 +844,7 @@ func TestSpawnerStoreFailures(t *testing.T) {
dead2 := openTestStore(t)
_ = dead2.Close()
sp2.store = dead2
res, err := sp2.Start(context.Background(), "group/project", "")
res, err := sp2.Start(context.Background(), "group/project", "", "")
if err != nil {
t.Fatalf("Start: %v", err)
}
@@ -877,7 +877,7 @@ func TestSpawnerWorkerCreateStartFailures(t *testing.T) {
sp, _ := newTestSpawner(t, f)
f.failCreate = true
res, err := sp.Start(context.Background(), "group/project", "")
res, err := sp.Start(context.Background(), "group/project", "", "")
if err != nil {
t.Fatalf("Start: %v", err)
}
@@ -887,7 +887,7 @@ func TestSpawnerWorkerCreateStartFailures(t *testing.T) {
f.failCreate = false
f.failStart = true
res2, err := sp.Start(context.Background(), "group/project", "")
res2, err := sp.Start(context.Background(), "group/project", "", "")
if err != nil {
t.Fatalf("Start 2: %v", err)
}
@@ -906,7 +906,7 @@ func TestSpawnerBuildHTTPErrors(t *testing.T) {
f.images = 0
f.failBuildHTTP = true // /build endpoint itself 500s
sp, _ := newTestSpawner(t, f)
res, err := sp.Start(context.Background(), "group/project", "")
res, err := sp.Start(context.Background(), "group/project", "", "")
if err != nil {
t.Fatalf("Start: %v", err)
}
@@ -1186,7 +1186,7 @@ func TestSpawnerStartDedupesImageList(t *testing.T) {
useFakeGit(t, fakeGitModeOK)
f := newFakeDocker()
sp, _ := newTestSpawner(t, f)
res, err := sp.Start(context.Background(), "group/project", "")
res, err := sp.Start(context.Background(), "group/project", "", "")
if err != nil {
t.Fatalf("Start: %v", err)
}
@@ -1209,7 +1209,7 @@ func TestSpawnerCustomImageUsed(t *testing.T) {
t.Fatalf("set repo image: %v", err)
}
res, err := sp.Start(context.Background(), "group/project", "")
res, err := sp.Start(context.Background(), "group/project", "", "")
if err != nil {
t.Fatalf("Start: %v", err)
}
@@ -1238,7 +1238,7 @@ func TestSpawnerCustomImageMissing(t *testing.T) {
t.Fatalf("set repo image: %v", err)
}
res, err := sp.Start(context.Background(), "group/project", "")
res, err := sp.Start(context.Background(), "group/project", "", "")
if err != nil {
t.Fatalf("Start: %v", err)
}
@@ -1266,7 +1266,7 @@ func TestSpawnerCustomImageDeleteFallsBack(t *testing.T) {
t.Fatalf("delete repo image: %v", err)
}
res, err := sp.Start(context.Background(), "group/project", "")
res, err := sp.Start(context.Background(), "group/project", "", "")
if err != nil {
t.Fatalf("Start: %v", err)
}
@@ -1287,8 +1287,33 @@ func TestSpawnerCustomImageCheckDockerDead(t *testing.T) {
t.Fatalf("set repo image: %v", err)
}
sp.images0AndDead(t)
_, _, err := sp.createAndStart(context.Background(), "group/project", repoSlug("group/project"), "s1")
_, _, err := sp.createAndStart(context.Background(), "group/project", repoSlug("group/project"), "", "s1")
if err == nil || !strings.Contains(err.Error(), "docker unavailable") {
t.Fatalf("createAndStart with dead docker = %v, want docker unavailable", err)
}
}
func TestSpawnerModelEnv(t *testing.T) {
useFakeGit(t, fakeGitModeOK)
f := newFakeDocker()
sp, _ := newTestSpawner(t, f)
res, err := sp.Start(context.Background(), "group/project", "", "zai-renaud/glm-5.2")
if err != nil {
t.Fatalf("Start: %v", err)
}
waitJobState(t, sp, res.SessionID, stateRunning)
creates := f.createsByName("lvmh-agent-")
if len(creates) != 1 {
t.Fatalf("creates = %d", len(creates))
}
found := false
for _, e := range creates[0].Env {
if e == "LVMH_MODEL=zai-renaud/glm-5.2" {
found = true
}
}
if !found {
t.Fatalf("LVMH_MODEL missing from %v", creates[0].Env)
}
}