mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-16 14:49:09 +02:00
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai>
165 lines
5.6 KiB
Go
165 lines
5.6 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/multica-ai/multica/server/internal/util"
|
|
db "github.com/multica-ai/multica/server/pkg/db/generated"
|
|
)
|
|
|
|
// stubWakeup records every call so the test can assert that notify
|
|
// reaches the daemon hub and carries the right runtime / task IDs.
|
|
type stubWakeup struct {
|
|
calls []struct{ runtimeID, taskID string }
|
|
}
|
|
|
|
func (s *stubWakeup) NotifyTaskAvailable(runtimeID, taskID string) {
|
|
s.calls = append(s.calls, struct{ runtimeID, taskID string }{runtimeID, taskID})
|
|
}
|
|
|
|
// TestNotifyTaskAvailable_BumpsBeforeWakeup pins the contract noted in
|
|
// the EmptyClaimCache docs: the version Bump MUST run before the
|
|
// daemon WS wakeup, otherwise the wakeup-driven claim could read a
|
|
// still-current empty verdict and return null while the freshly
|
|
// queued task sits idle. The test (1) marks the runtime empty under
|
|
// the current version, (2) fires notifyTaskAvailable, then (3)
|
|
// asserts the prior verdict is rejected AND the wakeup hook saw the
|
|
// new task — proving every enqueue path (issue / mention /
|
|
// quick-create / chat / autopilot / retry) gets the same
|
|
// bump-then-notify behaviour for free.
|
|
func TestNotifyTaskAvailable_BumpsBeforeWakeup(t *testing.T) {
|
|
rdb := newRedisTestClient(t)
|
|
cache := NewEmptyClaimCache(rdb)
|
|
wakeup := &stubWakeup{}
|
|
|
|
svc := &TaskService{
|
|
EmptyClaim: cache,
|
|
Wakeup: wakeup,
|
|
}
|
|
|
|
runtimeID := testUUID(7)
|
|
taskID := testUUID(8)
|
|
runtimeKey := util.UUIDToString(runtimeID)
|
|
|
|
ctx := context.Background()
|
|
v0 := cache.CurrentVersion(ctx, runtimeKey)
|
|
cache.MarkEmpty(ctx, runtimeKey, v0)
|
|
if !cache.IsEmpty(ctx, runtimeKey) {
|
|
t.Fatal("precondition: cache should report empty after MarkEmpty under current version")
|
|
}
|
|
|
|
svc.notifyTaskAvailable(db.AgentTaskQueue{
|
|
ID: taskID,
|
|
RuntimeID: runtimeID,
|
|
})
|
|
|
|
if cache.IsEmpty(ctx, runtimeKey) {
|
|
t.Fatal("notifyTaskAvailable must Bump the version so the prior empty verdict is rejected")
|
|
}
|
|
if got := len(wakeup.calls); got != 1 {
|
|
t.Fatalf("expected 1 wakeup call, got %d", got)
|
|
}
|
|
if wakeup.calls[0].runtimeID != runtimeKey {
|
|
t.Fatalf("wakeup runtime mismatch: got %q want %q", wakeup.calls[0].runtimeID, runtimeKey)
|
|
}
|
|
if wakeup.calls[0].taskID != util.UUIDToString(taskID) {
|
|
t.Fatalf("wakeup task mismatch: got %q want %q", wakeup.calls[0].taskID, util.UUIDToString(taskID))
|
|
}
|
|
}
|
|
|
|
// TestNotifyTaskAvailable_InvalidWithoutRuntimeIsNoOp guards the
|
|
// no-RuntimeID early return — chat / quick-create / autopilot all set
|
|
// it on insert, but a buggy caller that forgot must not silently bump
|
|
// every workspace's version. The cache treats Bump("") as a no-op,
|
|
// but this test pins that the RuntimeID guard sits above the Bump
|
|
// call so a future refactor cannot drop the guard without test
|
|
// coverage.
|
|
func TestNotifyTaskAvailable_InvalidWithoutRuntimeIsNoOp(t *testing.T) {
|
|
rdb := newRedisTestClient(t)
|
|
cache := NewEmptyClaimCache(rdb)
|
|
wakeup := &stubWakeup{}
|
|
|
|
svc := &TaskService{
|
|
EmptyClaim: cache,
|
|
Wakeup: wakeup,
|
|
}
|
|
|
|
ctx := context.Background()
|
|
v0 := cache.CurrentVersion(ctx, "rt-stays")
|
|
cache.MarkEmpty(ctx, "rt-stays", v0)
|
|
|
|
svc.notifyTaskAvailable(db.AgentTaskQueue{
|
|
// RuntimeID intentionally invalid (zero value, Valid=false).
|
|
ID: testUUID(9),
|
|
})
|
|
|
|
if !cache.IsEmpty(ctx, "rt-stays") {
|
|
t.Fatal("notifyTaskAvailable with invalid RuntimeID must not touch cache")
|
|
}
|
|
if got := len(wakeup.calls); got != 0 {
|
|
t.Fatalf("expected 0 wakeup calls when RuntimeID is invalid, got %d", got)
|
|
}
|
|
}
|
|
|
|
// TestNotifyTaskFinished_BumpsBeforeRuntimeWakeup pins the terminal-transition
|
|
// half of the wakeup contract. A prior empty verdict must not hide queued work
|
|
// that becomes claimable when another task releases agent capacity or a
|
|
// per-(issue, agent) serialization key.
|
|
func TestNotifyTaskFinished_BumpsBeforeRuntimeWakeup(t *testing.T) {
|
|
rdb := newRedisTestClient(t)
|
|
cache := NewEmptyClaimCache(rdb)
|
|
wakeup := &stubWakeup{}
|
|
svc := &TaskService{EmptyClaim: cache, Wakeup: wakeup}
|
|
|
|
runtimeID := testUUID(10)
|
|
runtimeKey := util.UUIDToString(runtimeID)
|
|
ctx := context.Background()
|
|
version := cache.CurrentVersion(ctx, runtimeKey)
|
|
cache.MarkEmpty(ctx, runtimeKey, version)
|
|
if !cache.IsEmpty(ctx, runtimeKey) {
|
|
t.Fatal("precondition: cache should report empty")
|
|
}
|
|
|
|
svc.NotifyTaskFinished(db.AgentTaskQueue{ID: testUUID(11), RuntimeID: runtimeID})
|
|
|
|
if cache.IsEmpty(ctx, runtimeKey) {
|
|
t.Fatal("terminal wakeup must invalidate the prior empty verdict")
|
|
}
|
|
if got := len(wakeup.calls); got != 1 {
|
|
t.Fatalf("expected 1 terminal wakeup, got %d", got)
|
|
}
|
|
if wakeup.calls[0].runtimeID != runtimeKey {
|
|
t.Fatalf("wakeup runtime mismatch: got %q want %q", wakeup.calls[0].runtimeID, runtimeKey)
|
|
}
|
|
if wakeup.calls[0].taskID != "" {
|
|
t.Fatalf("terminal wakeup must omit completed task id, got %q", wakeup.calls[0].taskID)
|
|
}
|
|
}
|
|
|
|
func TestNotifyTasksFinished_CoalescesByRuntime(t *testing.T) {
|
|
wakeup := &stubWakeup{}
|
|
svc := &TaskService{Wakeup: wakeup}
|
|
runtimeA := testUUID(12)
|
|
runtimeB := testUUID(13)
|
|
|
|
svc.notifyTasksFinished([]db.AgentTaskQueue{
|
|
{ID: testUUID(14), RuntimeID: runtimeA},
|
|
{ID: testUUID(15), RuntimeID: runtimeA},
|
|
{ID: testUUID(16), RuntimeID: runtimeB},
|
|
{ID: testUUID(17)}, // Invalid runtime is ignored.
|
|
})
|
|
|
|
if got := len(wakeup.calls); got != 2 {
|
|
t.Fatalf("expected one wakeup per runtime, got %d", got)
|
|
}
|
|
if wakeup.calls[0].runtimeID != util.UUIDToString(runtimeA) || wakeup.calls[1].runtimeID != util.UUIDToString(runtimeB) {
|
|
t.Fatalf("unexpected runtime wakeups: %#v", wakeup.calls)
|
|
}
|
|
for _, call := range wakeup.calls {
|
|
if call.taskID != "" {
|
|
t.Fatalf("terminal wakeup must omit completed task id, got %q", call.taskID)
|
|
}
|
|
}
|
|
}
|