|
9 | 9 | "os" |
10 | 10 | "path/filepath" |
11 | 11 | "testing" |
| 12 | + "testing/synctest" |
12 | 13 | "time" |
13 | 14 |
|
14 | 15 | "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" |
@@ -57,7 +58,7 @@ func capabilityEnvelope(t *testing.T, id string, request proto.RuntimePreparePay |
57 | 58 | func capabilityBegin(environment, session string, body []byte) proto.RuntimePreparePayload { |
58 | 59 | digest := sha256.Sum256(body) |
59 | 60 | return proto.RuntimePreparePayload{ |
60 | | - Step: "begin", Action: "skill", EnvironmentID: environment, SessionID: session, |
| 61 | + BudgetMS: 300000, Step: "begin", Action: "skill", EnvironmentID: environment, SessionID: session, |
61 | 62 | Skill: &agentskill.Metadata{Type: "inline", Name: "proof", Description: "A proof."}, |
62 | 63 | SizeBytes: len(body), SHA256: hex.EncodeToString(digest[:]), |
63 | 64 | } |
@@ -247,7 +248,7 @@ func TestRuntimePreparationCancellationKeepsOwnershipUntilApplyStops(t *testing. |
247 | 248 | r, sender, environment, session := capabilitiesTestRouter(t) |
248 | 249 | ctx, cancel := context.WithCancel(context.Background()) |
249 | 250 | id := uuid.NewString() |
250 | | - request := proto.RuntimePreparePayload{Step: "begin", Action: "finalize", EnvironmentID: environment, SessionID: session, Sources: &agentcapabilities.Input{}} |
| 251 | + request := proto.RuntimePreparePayload{BudgetMS: 300000, Step: "begin", Action: "finalize", EnvironmentID: environment, SessionID: session, Sources: &agentcapabilities.Input{}} |
251 | 252 | owner := &runtimePreparationTransfer{envelope: capabilityEnvelope(t, id, request), request: request, ready: make(chan struct{}), cancel: cancel, finished: true, apply: true} |
252 | 253 | close(owner.ready) |
253 | 254 | r.runtimePreparation = owner |
@@ -346,3 +347,79 @@ func TestRuntimePreparationResultCategoriesAndUnknownOwnership(t *testing.T) { |
346 | 347 | t.Fatal("shutdown claimed uncertain mutation settled") |
347 | 348 | } |
348 | 349 | } |
| 350 | + |
| 351 | +func TestRuntimePreparationStagingDeadlineRejectsWithoutApplying(t *testing.T) { |
| 352 | + synctest.Test(t, func(t *testing.T) { |
| 353 | + r, sender, environment, session := capabilitiesTestRouter(t) |
| 354 | + id := uuid.NewString() |
| 355 | + request := capabilityBegin(environment, session, []byte("abc")) |
| 356 | + if err := r.Handle(t.Context(), capabilityEnvelope(t, id, request)); err != nil { |
| 357 | + t.Fatal(err) |
| 358 | + } |
| 359 | + capabilitiesReceipt(t, sender, id, "ready") |
| 360 | + time.Sleep(121 * time.Second) |
| 361 | + synctest.Wait() |
| 362 | + capabilitiesReceipt(t, sender, id, "rejected") |
| 363 | + r.mu.Lock() |
| 364 | + pending := r.runtimePreparation != nil |
| 365 | + r.mu.Unlock() |
| 366 | + if pending { |
| 367 | + t.Fatal("uncommitted staging retained ownership") |
| 368 | + } |
| 369 | + shutdownCapabilitiesRouter(t, r) |
| 370 | + }) |
| 371 | +} |
| 372 | + |
| 373 | +func TestRuntimePreparationApplyOutlivesStagingAndHonorsBudget(t *testing.T) { |
| 374 | + synctest.Test(t, func(t *testing.T) { |
| 375 | + r, sender, environment, session := capabilitiesTestRouter(t) |
| 376 | + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Minute) |
| 377 | + id := uuid.NewString() |
| 378 | + request := capabilityBegin(environment, session, []byte("abc")) |
| 379 | + owner := &runtimePreparationTransfer{envelope: capabilityEnvelope(t, id, request), request: request, ready: make(chan struct{}), cancel: cancel, finished: true, apply: true, transferDeadline: time.Now().Add(2 * time.Minute)} |
| 380 | + close(owner.ready) |
| 381 | + r.runtimePreparation = owner |
| 382 | + r.shutdownWG.Add(1) |
| 383 | + stopped := make(chan struct{}) |
| 384 | + go r.runRuntimePreparationTransfer(ctx, owner, func(ctx context.Context, _ proto.RuntimePreparePayload, _ []byte) error { |
| 385 | + <-ctx.Done() |
| 386 | + close(stopped) |
| 387 | + return ctx.Err() |
| 388 | + }) |
| 389 | + time.Sleep(121 * time.Second) |
| 390 | + synctest.Wait() |
| 391 | + select { |
| 392 | + case <-stopped: |
| 393 | + t.Fatal("staging timeout killed apply") |
| 394 | + default: |
| 395 | + } |
| 396 | + time.Sleep(180 * time.Second) |
| 397 | + synctest.Wait() |
| 398 | + <-stopped |
| 399 | + capabilitiesReceipt(t, sender, id, "unknown") |
| 400 | + // Unknown effects remain retained; this test must not claim safe replay. |
| 401 | + r.mu.Lock() |
| 402 | + unknown := r.runtimePreparation == owner && owner.uncertain |
| 403 | + r.mu.Unlock() |
| 404 | + if !unknown { |
| 405 | + t.Fatal("unknown apply lost ownership") |
| 406 | + } |
| 407 | + }) |
| 408 | +} |
| 409 | + |
| 410 | +func TestRuntimePreparationHonorsBeginBudgetDuringStaging(t *testing.T) { |
| 411 | + synctest.Test(t, func(t *testing.T) { |
| 412 | + r, sender, environment, session := capabilitiesTestRouter(t) |
| 413 | + id := uuid.NewString() |
| 414 | + request := capabilityBegin(environment, session, []byte("abc")) |
| 415 | + request.BudgetMS = 10 |
| 416 | + if err := r.Handle(t.Context(), capabilityEnvelope(t, id, request)); err != nil { |
| 417 | + t.Fatal(err) |
| 418 | + } |
| 419 | + capabilitiesReceipt(t, sender, id, "ready") |
| 420 | + time.Sleep(11 * time.Millisecond) |
| 421 | + synctest.Wait() |
| 422 | + capabilitiesReceipt(t, sender, id, "rejected") |
| 423 | + shutdownCapabilitiesRouter(t, r) |
| 424 | + }) |
| 425 | +} |
0 commit comments