Files
04931df162 MUL-8174: fix OpenCode resume after host clock rollback (#9460)
* fix(opencode): retire future-dated resume sessions (MUL-8174)

Co-authored-by: multica-agent <[email protected]>

* fix(opencode): check resume clocks across CLI versions

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-23 14:41:13 +08:00

101 lines
4.7 KiB
Go

package handler
import (
"context"
"testing"
"github.com/jackc/pgx/v5/pgtype"
"github.com/multica-ai/multica/server/internal/testutil"
db "github.com/multica-ai/multica/server/pkg/db/generated"
)
func TestOpenCodeClockSkewResumeQueries(t *testing.T) {
if testHandler == nil {
t.Skip("database not available")
}
ctx := context.Background()
agentID, runtimeID, _ := createRuntimeGuardAgent(t, ctx)
issueID := dbfx.Issue(t, "clock-skew resume filtering")
chatID := dbfx.ChatSession(t, agentID)
for _, scope := range []struct{ name, column, id string }{
{"issue", "issue_id", issueID}, {"chat", "chat_session_id", chatID},
} {
t.Run(scope.name, func(t *testing.T) {
for _, history := range []struct{ session, status, reason, age string }{
{"healthy", "completed", "", "4 minutes"},
{"skewed", "completed", "", "3 minutes"},
{"skewed", "failed", "opencode_clock_skew", "2 minutes"},
} {
dbfx.Task(t, agentID, testutil.Cols{scope.column: scope.id, "runtime_id": runtimeID, "session_id": history.session, "status": history.status, "failure_reason": history.reason, "work_dir": "/clock-workdir", "completed_at": testutil.Raw("now() - interval '" + history.age + "'")})
}
if scope.name == "issue" {
got, err := testHandler.Queries.GetLastTaskSession(ctx, db.GetLastTaskSessionParams{AgentID: parseUUID(agentID), IssueID: parseUUID(issueID)})
if err != nil || got.SessionID.String != "healthy" {
t.Fatalf("issue resumed poisoned history: %+v, %v", got, err)
}
} else {
got, err := testHandler.Queries.GetLastChatTaskSession(ctx, db.GetLastChatTaskSessionParams{ChatSessionID: parseUUID(chatID)})
if err != nil || got.SessionID.String != "healthy" {
t.Fatalf("chat resumed poisoned history: %+v, %v", got, err)
}
}
})
}
}
func TestOpenCodeClockSkewFailureClearsChatPointer(t *testing.T) {
if testHandler == nil {
t.Skip("database not available")
}
ctx := context.Background()
agentID, runtimeID, _ := createRuntimeGuardAgent(t, ctx)
chatID := dbfx.ChatSession(t, agentID, testutil.Cols{"session_id": "skewed", "work_dir": "/clock-workdir", "runtime_id": runtimeID})
taskID := dbfx.Task(t, agentID, testutil.Cols{"chat_session_id": chatID, "runtime_id": runtimeID, "status": "running", "session_id": "skewed"})
_, err := testHandler.TaskService.FailTask(ctx, parseUUID(taskID), "opencode clock skew: session ordering is unreliable", "skewed", "/clock-workdir", "", "opencode_clock_skew", false, "skewed", "")
if err != nil {
t.Fatal(err)
}
var pointer pgtype.Text
dbfx.QueryRow(t, `SELECT session_id FROM chat_session WHERE id = $1`, chatID).Scan(&pointer)
if pointer.Valid {
t.Fatalf("failed callback restored retired chat pointer: %+v", pointer)
}
}
func TestOpenCodeClockRetirementSurvivesManualRetryOfOldTask(t *testing.T) {
if testHandler == nil {
t.Skip("database not available")
}
ctx := context.Background()
agentID, runtimeID, daemonID := createRuntimeGuardAgent(t, ctx)
issueID := dbfx.Issue(t, "manual retry after clock preflight retirement")
sourceID := dbfx.Task(t, agentID, testutil.Cols{"issue_id": issueID, "runtime_id": runtimeID, "status": "completed", "session_id": "ses-old", "work_dir": "/clock-workdir"})
dbfx.Task(t, agentID, testutil.Cols{"issue_id": issueID, "runtime_id": runtimeID, "status": "completed", "session_id": "ses-new", "retired_session_id": "ses-old"})
dbfx.Task(t, agentID, testutil.Cols{"issue_id": issueID, "runtime_id": runtimeID, "rerun_of_task_id": sourceID, "force_fresh_session": true, "priority": 1000})
got := claimTaskForRuntimeGuard(t, runtimeID, daemonID)
if got.PriorSessionID != "" || got.PriorWorkDir != "/clock-workdir" {
t.Fatalf("manual retry bypassed retirement: %+v", got)
}
}
func TestOpenCodeClockFailureReleasesIssueQueue(t *testing.T) {
if testHandler == nil {
t.Skip("database not available")
}
ctx := context.Background()
agentID, runtimeID, daemonID := createRuntimeGuardAgent(t, ctx)
issueID := dbfx.Issue(t, "clock-skew terminal releases queue")
taskID := dbfx.Task(t, agentID, testutil.Cols{"issue_id": issueID, "runtime_id": runtimeID, "status": "running", "session_id": "skewed"})
nextID := dbfx.Task(t, agentID, testutil.Cols{"issue_id": issueID, "runtime_id": runtimeID, "priority": 1000})
_, err := testHandler.TaskService.FailTask(ctx, parseUUID(taskID), "fresh OpenCode launch failed", "", "/clock-workdir", "", "agent_error.process_failure", false, "skewed", "")
if err != nil {
t.Fatal(err)
}
got := claimTaskForRuntimeGuard(t, runtimeID, daemonID)
var status string
dbfx.QueryRow(t, `SELECT status FROM agent_task_queue WHERE id = $1`, nextID).Scan(&status)
if status != "dispatched" || got.PriorSessionID != "" {
t.Fatalf("queue did not recover with a clean session: %+v", got)
}
}