mateclaw/mateclaw-server/src/test/java/vip/mate/agent/AgentServiceTurnAdmissionTest.java
taobig 2fa2e60170 feat(goal): persist continuous execution across bounded turns
- Add a database-backed supervisor with durable scheduling, fenced leases,
  cooldowns, bounded worker concurrency and expired-lease restart recovery.
- Default new goals to persistent execution with zero meaning unlimited
  cumulative budget; preserve legacy goals and explicit positive limits.
- Yield bounded graph segments to the supervisor instead of ending unfinished
  goals at graph-local continuation limits. Require persisted checklist
  evidence before accepting completion, including concurrent criterion edits.
- Share conversation admission across interactive and background execution;
  preserve partial replies, usage and queued user input during interruption.
- Persist Stop and missing-input pauses, respect approval boundaries, and
  commit resume and approval-denial transitions with correct transactions.
- Retry identifiable transient failures with backoff; retain visible pauses
  for budget limits and errors that require review instead of replaying tools.
- Expose owner-authorized execution status and reconnectable scheduling events;
  add H2, MySQL and Kingbase migrations, API types and bilingual documentation.

Validation: 298 focused backend tests passed, including persistence, restart
scheduling, approval races, cancellation, admission and existing runtime tests.
Frontend type checking, bundled-doc parity and ID precision checks passed.
V188 is registered and all three dialects have unique migration versions;
the migration-map audit still reports 95 pre-existing missing registrations.

Scope: single-backend native runtime. Recovery checks existing state before
repeating effects; this does not promise exactly-once external tool execution.
2026-08-26 05:46:15 -04:00

52 lines
2.9 KiB
Java

package vip.mate.agent;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import reactor.core.publisher.Flux;
import vip.mate.memory.MemoryProperties;
import java.util.function.BiFunction;
import java.util.function.Function;
import static org.junit.jupiter.api.Assertions.*;
class AgentServiceTurnAdmissionTest {
@Test void onlyInteractiveCompletionWakesApprovalWaitEvenAfterContextHasExited() {
MemoryProperties properties = new MemoryProperties();
properties.setLifecycleMediatorEnabled(false);
AgentService service = new AgentService(null,null,null,null,properties,null,null);
var events = org.mockito.Mockito.mock(org.springframework.context.ApplicationEventPublisher.class);
ReflectionTestUtils.setField(service,"events",events);
var source = reactor.core.publisher.Sinks.<String>empty();
BiFunction<String,String,Flux<String>> invoke = (message,conversation) -> source.asMono().flux();
Function<String,String> content = Function.identity();
Flux<String> automatic = ReflectionTestUtils.invokeMethod(service,"withLifecycleFlux",1L,"a","conv",invoke,content);
vip.mate.agent.context.GoalContinuationContext.call(automatic::subscribe);
source.tryEmitEmpty();
org.mockito.Mockito.verifyNoInteractions(events);
BiFunction<String,String,Flux<String>> interactiveInvoke = (message,conversation) -> Flux.empty();
Flux<String> interactive = ReflectionTestUtils.invokeMethod(service,"withLifecycleFlux",1L,"b","conv",interactiveInvoke,content);
interactive.blockLast();
org.mockito.Mockito.verify(events).publishEvent(new vip.mate.goal.service.GoalExecutionSignal.TurnFinished("conv"));
}
@Test void admissionIsLazySharedAndReleasedOnCancellation() {
MemoryProperties properties = new MemoryProperties();
properties.setLifecycleMediatorEnabled(false);
AgentService service = new AgentService(null,null,null,null,properties,null,null);
BiFunction<String,String,Flux<String>> invoke = (message,conversation) -> Flux.never();
Function<String,String> content = Function.identity();
Flux<String> first = ReflectionTestUtils.invokeMethod(service,"withLifecycleFlux",1L,"a","conv",invoke,content);
Flux<String> second = ReflectionTestUtils.invokeMethod(service,"withLifecycleFlux",1L,"b","conv",invoke,content);
assertNotNull(first); assertNotNull(second);
var subscription = first.subscribe();
var error = new java.util.concurrent.atomic.AtomicReference<Throwable>();
second.subscribe(value -> {},error::set);
assertNotNull(error.get(),"a second turn must not execute alongside the first");
subscription.dispose();
error.set(null);
var third = second.subscribe(value -> {},error::set);
assertNull(error.get());
third.dispose();
}
}