package vip.mate.channel.webchat;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
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.web.servlet.mvc.method.annotation.SseEmitter;
import reactor.core.publisher.Flux;
import vip.mate.MateClawApplication;
import vip.mate.agent.AgentService;
import vip.mate.agent.model.AgentEntity;
import vip.mate.approval.ApprovalWorkflowService;
import vip.mate.approval.PendingApproval;
import vip.mate.channel.web.ChatStreamTracker;
import vip.mate.channel.webchat.WebChatController.WebChatCreateSessionRequest;
import vip.mate.common.result.R;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
/**
* Verifies ISSUE #413 P1: the WebChat (API-Key) channel can now resolve tool
* approvals. Before the fix a ToolGuard-protected tool would park the turn in
* a pending approval the visitor could never clear — it hung for 30 min until
* the GC timeout and the turn was wasted.
*
*
Covers the synchronous paths (deny + stop-sweep). The approve path drives
* a live agent replay stream and is exercised separately; the auth + ownership
* guards it shares with deny are validated here.
*/
@SpringBootTest(
classes = MateClawApplication.class,
webEnvironment = SpringBootTest.WebEnvironment.NONE
)
@TestPropertySource(properties = {
"spring.datasource.url=jdbc:h2:mem:webchat_approve_${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.jwt.secret=webchat-it-secret-0123456789",
"mateclaw.webchat.orphan-grace-sec=-1"
})
class WebChatApprovalInteractionTest {
private static final String SECRET = "webchat-it-secret-0123456789";
private static final String API_KEY = "testkey1abcdefgh"; // key8 = "testkey1"
private static final long CHANNEL_ID = 9_147_310L;
private static final long AGENT_ID = 9_147_3101L;
@Autowired private WebChatController controller;
@Autowired private ApprovalWorkflowService approvalService;
@Autowired private ChatStreamTracker streamTracker;
@Autowired private JdbcTemplate jdbc;
@MockBean private AgentService agentService;
@BeforeEach
void setUp() {
jdbc.update("DELETE FROM mate_channel WHERE id = ?", CHANNEL_ID);
jdbc.update("DELETE FROM mate_agent WHERE id = ?", AGENT_ID);
jdbc.update(
"MERGE INTO mate_agent (id, name, agent_type, system_prompt, max_iterations, enabled, " +
"workspace_id, create_time, update_time, deleted) " +
"KEY(id) VALUES (?, 'wc-approve-agent', 'react', '', 10, TRUE, 1, " +
"CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0)",
AGENT_ID);
jdbc.update("INSERT INTO mate_channel (id, name, channel_type, agent_id, config_json, enabled, " +
"workspace_id, create_time, update_time, deleted) " +
"VALUES (?, 'wc', 'webchat', ?, ?, TRUE, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0)",
CHANNEL_ID, AGENT_ID, "{\"api_key\":\"" + API_KEY + "\"}");
AgentEntity agent = new AgentEntity();
agent.setId(AGENT_ID);
agent.setWorkspaceId(1L);
Mockito.when(agentService.getAgent(AGENT_ID)).thenReturn(agent);
}
private WebChatCreateSessionRequest req(String visitorId, String sessionId) {
WebChatCreateSessionRequest r = new WebChatCreateSessionRequest();
r.setVisitorId(visitorId);
r.setSessionId(sessionId);
return r;
}
private String tokenFor(String visitorId) {
return WebChatController.computeVisitorToken(SECRET, CHANNEL_ID, visitorId);
}
private String seedPending(String visitorId, String sessionId) {
controller.createSession(API_KEY, req(visitorId, sessionId));
String cid = WebChatController.deriveConversationId(API_KEY, visitorId, sessionId);
// The actor stored on the approval is the webchat username, mirroring
// how chatStream sets it via webchatUsername(visitorId).
String actor = "webchat:" + API_KEY.substring(0, 8) + ":" + visitorId;
return approvalService.createPending(
cid, actor, "write_file", "{}", "high-severity edit",
"{}", "[]", String.valueOf(AGENT_ID));
}
// ---------------- deny ----------------
@Test
@DisplayName("deny resolves a pending approval and broadcasts tool_approval_resolved")
void denyResolvesPending() {
String pendingId = seedPending("visitorA", "s1");
String cid = WebChatController.deriveConversationId(API_KEY, "visitorA", "s1");
// Register the stream so the broadcast has a live subscriber state.
streamTracker.register(cid);
R