fix(goal): keep managed acceptance reads fresh in agent graphs

This commit is contained in:
mateaix 2026-09-14 23:09:54 +08:00
parent 23f0ca6f54
commit 36a269d3dd
2 changed files with 164 additions and 1 deletions

View File

@ -82,7 +82,7 @@ public class ActionNode implements NodeAction {
"extract_document_text", "extract_pdf_text", "extract_docx_text",
"detect_file_type",
"getCurrentDateTime", "getCurrentDate", "getCurrentTime",
"listSubagents"
"listSubagents", "getManagedGoalJsonSlots"
);
private final ToolExecutionExecutor executor;

View File

@ -0,0 +1,163 @@
package vip.mate.goal;
import com.alibaba.cloud.ai.graph.CompiledGraph;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.ToolResponseMessage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.util.ReflectionTestUtils;
import reactor.core.publisher.Flux;
import vip.mate.MateClawApplication;
import vip.mate.agent.AgentGraphBuilder;
import vip.mate.agent.context.ChatOrigin;
import vip.mate.agent.context.ChatOriginHolder;
import vip.mate.agent.context.ConversationWindowManager;
import vip.mate.agent.graph.StateGraphReActAgent;
import vip.mate.goal.model.*;
import vip.mate.goal.service.*;
import vip.mate.memory.spi.MemoryManager;
import vip.mate.tool.ToolRegistry;
import vip.mate.workspace.conversation.ConversationService;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/** Real compiled graph and callback executor; model choices are a deterministic offline fixture. */
@SpringBootTest(classes = MateClawApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
@TestPropertySource(properties = {
"spring.datasource.url=jdbc:h2:mem:json_graph_${random.uuid};MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;DB_CLOSE_DELAY=-1",
"spring.ai.dashscope.api-key=test-key", "spring.main.web-application-type=none",
"mateclaw.goal.enabled=true", "mateclaw.plugin.enabled=false", "mateclaw.skill.workspace.auto-init=false",
"mateclaw.skill.workspace.root=${java.io.tmpdir}/mateclaw-json-graph-skills-${random.uuid}"
})
class GoalJsonGraphIntegrationTest {
@MockBean private MemoryManager memory;
@MockBean private GoalEvaluationService evaluator;
@MockBean private GoalContinuationSupervisor supervisor;
@Autowired private GoalService goals;
@Autowired private GoalJsonAcceptanceService requirements;
@Autowired private GoalJsonBindingService bindings;
@Autowired private GoalContinuationStore continuations;
@Autowired private GoalRunCoordinator coordinator;
@Autowired private JdbcTemplate jdbc;
@Autowired private ObjectMapper json;
@Autowired private AgentGraphBuilder builder;
@Autowired private ToolRegistry tools;
@Autowired private ConversationService conversations;
@Autowired private ConversationWindowManager window;
@Autowired private vip.mate.planning.service.PlanningService planning;
@Autowired private vip.mate.agent.progress.ProgressLedgerService progress;
@org.junit.jupiter.params.ParameterizedTest
@org.junit.jupiter.params.provider.CsvSource({"false,false", "false,true", "true,false", "true,true"})
void realGraphsPreserveAuthenticatedOriginThroughReadPublishCheckAndComplete(boolean plan, boolean scheduled) throws Exception {
String username = "graph-" + UUID.randomUUID();
long userId = IdWorker.getId();
String conversation = UUID.randomUUID().toString();
jdbc.update("INSERT INTO mate_user(id,username,password,enabled,role,create_time,update_time,deleted) VALUES (?,?,?,TRUE,'user',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,0)", userId, username, "unused");
jdbc.update("INSERT INTO mate_conversation(id,conversation_id,username,workspace_id,agent_id,create_time,update_time,deleted) VALUES (?,?,?,1,1,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,0)", IdWorker.getId(), conversation, username);
var request = new GoalCreateRequest(); request.setConversationId(conversation); request.setAgentId(1L); request.setWorkspaceId(1L);
request.setTitle("Managed graph fixture"); request.setDescription("Produce JSON"); request.setPersistentExecution(scheduled); request.setAutoFollowupEnabled(false);
GoalEntity goal = goals.create(request, username);
goals.appendCriterion(goal.getId(), "Produce the report", username);
goals.recordEvaluation(goal.getId(), new GoalEvaluationResult(1, "offline semantic fixture", "completed", true,
"fixture", 1, 0, List.of(new GoalChecklistVerdict.CriterionVerdict("C1", true, "fixture only")), null), 1, 1);
requirements.configure(goal.getId(), "r", new GoalJsonAcceptanceService.ConfigureRequest(0L, "report", List.of("summary")), username);
GoalRunCoordinator.ClaimedRun run = null;
ChatOrigin origin = ChatOrigin.web(conversation, username, 1L, null, null, userId).withAgent(1L);
if (scheduled) {
jdbc.update("UPDATE mate_agent_goal SET auto_followup_enabled=TRUE WHERE id=?", goal.getId());
var now = java.time.LocalDateTime.now(); continuations.discover(now);
run = coordinator.claim(continuations.get(goal.getId()), goals.getById(goal.getId()), now);
assertNotNull(run); assertTrue(coordinator.markRunning(run, now));
origin = ChatOrigin.web(conversation, username, 1L, null).withAgent(1L)
.withExecutionAttribution(new vip.mate.agent.context.ExecutionAttribution(goal.getId(), run.attempt().id(), null, null, run.attempt().leaseToken()));
}
when(evaluator.evaluate(any(), anyList(), anyString())).thenReturn(GoalEvaluationResult.fallback("offline_graph_fixture"));
var toolSet = tools.getEnabledToolSet().withAllowedToolsOnly(Set.of("getManagedGoalJsonSlots", "publishManagedGoalJson", "checkManagedGoalJson", "completeGoal"));
assertEquals(4, toolSet.callbacks().size());
ChatModel model = mock(ChatModel.class);
AtomicInteger calls = new AtomicInteger();
java.util.concurrent.atomic.AtomicReference<String> revision = new java.util.concurrent.atomic.AtomicReference<>();
org.mockito.stubbing.Answer<ChatResponse> script = invocation -> {
Prompt prompt = invocation.getArgument(0);
int step = calls.getAndIncrement();
if (plan && step == 0) {
return new ChatResponse(List.of(new Generation(new AssistantMessage(
"{\"needs_planning\":true,\"steps\":[\"Produce, publish, check and complete the managed JSON report\"]}"))));
}
if (plan) step--;
List<ToolResponseMessage.ToolResponse> responses = prompt.getInstructions().stream()
.filter(ToolResponseMessage.class::isInstance).map(ToolResponseMessage.class::cast)
.flatMap(m -> m.getResponses().stream()).toList();
JsonNode last = responses.isEmpty() ? null : json.readTree(responses.getLast().responseData());
String name; String arguments = "{}";
switch (step) {
case 0 -> name = "completeGoal";
case 1 -> { assertTrue(last.path("error").asBoolean(), String.valueOf(last)); name = "getManagedGoalJsonSlots"; }
case 2 -> {
assertTrue(last.path("required").asBoolean(), String.valueOf(last));
revision.set(last.path("requirements").get(0).path("revision").asText());
name = "publishManagedGoalJson";
arguments = json.writeValueAsString(Map.of("artifactSlot", "report", "expectedGeneration", "0", "jsonContent", "{\"summary\":false}"));
}
case 3 -> {
assertEquals(scheduled ? "goal-attempt" : "account-runtime", last.path("producerKind").asText(), String.valueOf(last));
name = "checkManagedGoalJson";
arguments = json.writeValueAsString(Map.of("criterionKey", "r", "expectedRequirementRevision", revision.get(),
"artifactId", last.path("artifactId").asText(), "expectedGeneration", last.path("generation").asText()));
}
case 4 -> { assertTrue(last.path("acceptanceEligible").asBoolean(), String.valueOf(last)); name = "completeGoal"; }
default -> {
if (last != null) assertEquals("completed", last.path("status").asText(), String.valueOf(last));
assertEquals(GoalStatus.COMPLETED, goals.getById(goal.getId()).getStatus());
return new ChatResponse(List.of(new Generation(new AssistantMessage("Managed JSON fixture completed."))));
}
}
return new ChatResponse(List.of(new Generation(AssistantMessage.builder().content("")
.toolCalls(List.of(new AssistantMessage.ToolCall("json-" + step, "function", name, arguments))).build())));
};
when(model.call(any(Prompt.class))).thenAnswer(script);
when(model.stream(any(Prompt.class))).thenAnswer(invocation -> Flux.just(script.answer(invocation)));
CompiledGraph graph = ReflectionTestUtils.invokeMethod(builder, plan ? "buildPlanExecuteGraph" : "buildReActGraph", toolSet, model, 12, null);
assertNotNull(graph);
vip.mate.agent.BaseAgent agent = plan
? new vip.mate.agent.graph.plan.StateGraphPlanExecuteAgent(mock(ChatClient.class), conversations, graph, planning, model, window, toolSet)
: new StateGraphReActAgent(mock(ChatClient.class), conversations, graph, model, window, toolSet);
ReflectionTestUtils.setField(agent, "agentId", "1");
ReflectionTestUtils.setField(agent, "agentName", "JSON graph fixture");
ReflectionTestUtils.setField(agent, "systemPrompt", "Follow the user's managed JSON requirements.");
ReflectionTestUtils.setField(agent, "goalService", goals);
ChatOriginHolder.set(origin);
try { assertNotNull(agent.chat("Produce and check the managed JSON report.", conversation)); }
finally { ChatOriginHolder.clear(); }
assertEquals(GoalStatus.COMPLETED, goals.getById(goal.getId()).getStatus());
assertTrue(bindings.state(goal.getId(), username).getFirst().acceptanceEligible());
if (scheduled) {
assertTrue(coordinator.settle(run, new SegmentOutcome.Complete("graph fixture"), java.time.LocalDateTime.now()));
assertEquals("completed", continuations.get(goal.getId()).state());
}
assertFalse(progress.load(conversation).asMap().containsKey("auto_getManagedGoalJsonSlots"),
"Current acceptance reads must not become a permanent done step that discourages reloading");
assertTrue(calls.get() >= 6 && calls.get() <= 10, "Bounded scripted model calls: " + calls.get());
}
}