mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(webchat): archive flag + revoked-visitor table + view fields (epic #355 PR 1)
Schema foundations for the rest of the epic. Three additions: 1. mate_conversation.archived — INT default 0. Lets a visitor "soft-close" a thread: stays on disk (history preserved, addressable, downloadable) but excluded from default /sessions listing. Pinned/archived are orthogonal; archive dominates (archived+pinned still hidden by default). 2. webchat_revoked_visitor — registry table consumed in PR 2 by the WebChatTokenRevocationService. Unique on (channel_id, visitor_id, deleted) so re-revoke is idempotent and deleted=1 un-revokes. Migration written for all three DBs (h2 IF NOT EXISTS, MySQL INFORMATION_SCHEMA guard, KingbaseES native IF NOT EXISTS) following the V147 pattern. 3. WebChatSessionView gains pinned/archived/streamStatus so the visitor- side listing surfaces the same state the admin console sees. Closes the "field exposure" gap from the epic. loadVisitorSessions gains an includeArchived flag (default false) — the default hides archived threads and excludes them from the empty-session quota, since the visitor already declared they're done with them. GET /sessions and GET /sessions/page thread an `includeArchived=true` query param through. Tests (WebChatSchemaFieldsTest, @SpringBootTest + H2 + V148): - revoked-visitor table is queryable - archived column is read/write - view exposes pinned/archived/streamStatus - archived hidden by default, visible with includeArchived=true - archived empty threads don't saturate the 5-thread quota Regression: WebChatCreateSessionTest (9/9), WebChatSessionManagementTest (5/5) — both updated for the new includeArchived param. Part of epic #355.
This commit is contained in:
parent
23c1d49241
commit
332f3339a9
@ -413,7 +413,8 @@ public class WebChatController {
|
||||
public R<List<WebChatSessionView>> listSessions(
|
||||
@RequestHeader("X-MC-Key") String apiKey,
|
||||
@RequestHeader(value = "X-MC-Visitor-Token", required = false) String visitorToken,
|
||||
@RequestParam String visitorId) {
|
||||
@RequestParam String visitorId,
|
||||
@RequestParam(defaultValue = "false") boolean includeArchived) {
|
||||
ChannelEntity channel = resolveChannel(apiKey);
|
||||
if (channel == null) {
|
||||
return R.fail(401, "Invalid API Key");
|
||||
@ -421,7 +422,7 @@ public class WebChatController {
|
||||
if (!verifyVisitorToken(visitorTokenSecret, channel.getId(), visitorId, visitorToken)) {
|
||||
return R.fail(401, "Invalid or missing visitor token");
|
||||
}
|
||||
return R.ok(loadVisitorSessions(apiKey, visitorId));
|
||||
return R.ok(loadVisitorSessions(apiKey, visitorId, includeArchived));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -437,7 +438,8 @@ public class WebChatController {
|
||||
@RequestParam String visitorId,
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "20") int size,
|
||||
@RequestParam(required = false) String keyword) {
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(defaultValue = "false") boolean includeArchived) {
|
||||
ChannelEntity channel = resolveChannel(apiKey);
|
||||
if (channel == null) {
|
||||
return R.fail(401, "Invalid API Key");
|
||||
@ -448,7 +450,7 @@ public class WebChatController {
|
||||
if (page < 1) page = 1;
|
||||
if (size < 1 || size > 200) size = 20;
|
||||
|
||||
List<WebChatSessionView> all = loadVisitorSessions(apiKey, visitorId);
|
||||
List<WebChatSessionView> all = loadVisitorSessions(apiKey, visitorId, includeArchived);
|
||||
if (keyword != null && !keyword.isBlank()) {
|
||||
String kw = keyword.trim().toLowerCase(java.util.Locale.ROOT);
|
||||
all = all.stream()
|
||||
@ -516,6 +518,19 @@ public class WebChatController {
|
||||
* for legacy rows created before that column existed.
|
||||
*/
|
||||
private List<WebChatSessionView> loadVisitorSessions(String apiKey, String visitorId) {
|
||||
return loadVisitorSessions(apiKey, visitorId, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overload that lets the caller opt into archived threads. By default
|
||||
* (used by /sessions listing and the empty-session quota check) archived
|
||||
* rows are filtered out — they still exist on disk and are addressable
|
||||
* by sessionId, but don't pollute the active listing and don't count
|
||||
* against the "≤ 5 empty threads" quota (the visitor already declared
|
||||
* they're done with them).
|
||||
*/
|
||||
private List<WebChatSessionView> loadVisitorSessions(String apiKey, String visitorId,
|
||||
boolean includeArchived) {
|
||||
String base = deriveConversationId(apiKey, visitorId, null);
|
||||
String channelPrefix = "webchat:" + apiKey.substring(0, Math.min(8, apiKey.length())) + ":";
|
||||
String owner = webchatUsername(visitorId);
|
||||
@ -526,9 +541,16 @@ public class WebChatController {
|
||||
return conversationService.listWebchatConversations(owner).stream()
|
||||
.filter(c -> c.getConversationId() != null
|
||||
&& c.getConversationId().startsWith(channelPrefix))
|
||||
.filter(c -> includeArchived
|
||||
|| c.getArchived() == null
|
||||
|| c.getArchived() == 0)
|
||||
.map(c -> {
|
||||
String sid = recoverSessionId(c, base);
|
||||
return new WebChatSessionView(sid, c.getTitle(), c.getLastActiveTime(), c.getMessageCount());
|
||||
return new WebChatSessionView(sid, c.getTitle(), c.getLastActiveTime(),
|
||||
c.getMessageCount(),
|
||||
c.getPinned() != null ? c.getPinned() : 0,
|
||||
c.getArchived() != null ? c.getArchived() : 0,
|
||||
c.getStreamStatus() != null ? c.getStreamStatus() : "idle");
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
@ -1047,6 +1069,12 @@ public class WebChatController {
|
||||
private String title;
|
||||
private LocalDateTime lastActiveTime;
|
||||
private Integer messageCount;
|
||||
/** 1 if the visitor pinned this thread, 0 otherwise. */
|
||||
private Integer pinned;
|
||||
/** 1 if the visitor archived this thread, 0 otherwise. */
|
||||
private Integer archived;
|
||||
/** {@code running} if a stream is in progress on this thread, else {@code idle}. */
|
||||
private String streamStatus;
|
||||
}
|
||||
|
||||
/** Body for {@code POST /sessions} — explicitly create an empty thread. */
|
||||
|
||||
@ -51,6 +51,16 @@ public class ConversationEntity {
|
||||
/** Pin flag: 0 = normal, 1 = pinned to the top of the sidebar list */
|
||||
private Integer pinned;
|
||||
|
||||
/**
|
||||
* Archive flag (webchat): 0 = active (default), 1 = archived.
|
||||
* Archived threads stay in the DB (history preserved, still addressable
|
||||
* by sessionId, downloadable) but are excluded from the default
|
||||
* /sessions listing. A visitor opts into seeing them via
|
||||
* {@code includeArchived=true}. Archive dominates pin — an archived
|
||||
* AND pinned thread is still hidden by default.
|
||||
*/
|
||||
private Integer archived;
|
||||
|
||||
/**
|
||||
* Provider id of the model this conversation is pinned to. NULL means
|
||||
* "inherit" — fall back to the agent's model override, then the global
|
||||
|
||||
@ -66,6 +66,7 @@ public class ConversationVO extends ConversationEntity {
|
||||
vo.setLastActiveTime(entity.getLastActiveTime());
|
||||
vo.setWorkspaceId(entity.getWorkspaceId());
|
||||
vo.setPinned(entity.getPinned() != null ? entity.getPinned() : 0);
|
||||
vo.setArchived(entity.getArchived() != null ? entity.getArchived() : 0);
|
||||
vo.setModelProvider(entity.getModelProvider());
|
||||
vo.setModelName(entity.getModelName());
|
||||
vo.setWebchatSessionId(entity.getWebchatSessionId());
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
-- V148: webchat visitor-session archive flag + visitor-token revocation registry.
|
||||
--
|
||||
-- 1) mate_conversation.archived — INT, 0 (default) = active, 1 = archived.
|
||||
-- Lets a visitor "soft-close" a thread: it stays in the DB (history
|
||||
-- preserved, downloadable, addressable by sessionId) but is excluded
|
||||
-- from the default /sessions listing. Pinned/archived are orthogonal:
|
||||
-- archive dominates (an archived+pinned thread is still hidden by default).
|
||||
--
|
||||
-- 2) webchat_revoked_visitor — registry of visitors whose visitorToken HMAC
|
||||
-- is no longer accepted on management endpoints (list/messages/title/
|
||||
-- delete/stop/upload/regenerate). /stream is intentionally NOT bound by
|
||||
-- this: a revoked visitor can still start a fresh /stream, which mints
|
||||
-- a new token; the revocation applies to the old token presented on
|
||||
-- management endpoints. The (channel_id, visitor_id) pair is unique among
|
||||
-- non-deleted rows so a re-revoke is idempotent; setting deleted=1
|
||||
-- un-revokes.
|
||||
|
||||
ALTER TABLE mate_conversation ADD COLUMN IF NOT EXISTS archived INT NOT NULL DEFAULT 0;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS webchat_revoked_visitor (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
channel_id BIGINT NOT NULL,
|
||||
visitor_id VARCHAR(128) NOT NULL,
|
||||
revoked_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
reason VARCHAR(255),
|
||||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted INT NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_webchat_revoked_visitor
|
||||
ON webchat_revoked_visitor (channel_id, visitor_id, deleted);
|
||||
CREATE INDEX IF NOT EXISTS idx_webchat_revoked_visitor_lookup
|
||||
ON webchat_revoked_visitor (channel_id, visitor_id, deleted);
|
||||
@ -0,0 +1,19 @@
|
||||
-- V148: webchat visitor-session archive flag + visitor-token revocation registry (KingbaseES / PostgreSQL).
|
||||
-- See the H2 copy for full context.
|
||||
|
||||
ALTER TABLE mate_conversation ADD COLUMN IF NOT EXISTS archived INT NOT NULL DEFAULT 0;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS webchat_revoked_visitor (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
channel_id BIGINT NOT NULL,
|
||||
visitor_id VARCHAR(128) NOT NULL,
|
||||
revoked_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
reason VARCHAR(255),
|
||||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted INT NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_webchat_revoked_visitor
|
||||
ON webchat_revoked_visitor (channel_id, visitor_id, deleted);
|
||||
CREATE INDEX IF NOT EXISTS idx_webchat_revoked_visitor_lookup
|
||||
ON webchat_revoked_visitor (channel_id, visitor_id, deleted);
|
||||
@ -0,0 +1,30 @@
|
||||
-- V148: webchat visitor-session archive flag + visitor-token revocation registry (MySQL).
|
||||
-- See the H2 copy for full context.
|
||||
|
||||
-- 1) archived column — MySQL 8.0 has no ADD COLUMN IF NOT EXISTS.
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'mate_conversation'
|
||||
AND COLUMN_NAME = 'archived'
|
||||
);
|
||||
SET @ddl := IF(@col_exists = 0,
|
||||
'ALTER TABLE mate_conversation ADD COLUMN archived INT NOT NULL DEFAULT 0 COMMENT ''webchat: 0 = active, 1 = archived (hidden from default /sessions listing)''',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 2) revoked-visitor registry
|
||||
CREATE TABLE IF NOT EXISTS webchat_revoked_visitor (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
channel_id BIGINT NOT NULL,
|
||||
visitor_id VARCHAR(128) NOT NULL,
|
||||
revoked_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
reason VARCHAR(255),
|
||||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted INT NOT NULL DEFAULT 0,
|
||||
UNIQUE KEY uk_webchat_revoked_visitor (channel_id, visitor_id, deleted),
|
||||
KEY idx_webchat_revoked_visitor_lookup (channel_id, visitor_id, deleted)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@ -195,7 +195,7 @@ class WebChatCreateSessionTest {
|
||||
controller.createSession(API_KEY, req("visitorI", "s-listed", null));
|
||||
|
||||
String token = WebChatController.computeVisitorToken(SECRET, CHANNEL_ID, "visitorI");
|
||||
R<?> r = controller.listSessions(API_KEY, token, "visitorI");
|
||||
R<?> r = controller.listSessions(API_KEY, token, "visitorI", false);
|
||||
assertThat(r.getCode()).isEqualTo(200);
|
||||
assertThat(((java.util.List<WebChatController.WebChatSessionView>) (Object) r.getData()))
|
||||
.extracting(WebChatController.WebChatSessionView::getSessionId)
|
||||
|
||||
@ -0,0 +1,173 @@
|
||||
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.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import vip.mate.MateClawApplication;
|
||||
import vip.mate.channel.webchat.WebChatController.WebChatCreateSessionRequest;
|
||||
import vip.mate.channel.webchat.WebChatController.WebChatSessionView;
|
||||
import vip.mate.common.result.R;
|
||||
import vip.mate.workspace.conversation.ConversationService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* End-to-end verification of the V148 schema additions and the
|
||||
* {@code archived} filtering / view-field exposure that rides on them:
|
||||
* <ul>
|
||||
* <li>{@code mate_conversation.archived} column exists and is read/write.</li>
|
||||
* <li>{@code webchat_revoked_visitor} table exists (full DDL validation
|
||||
* happens implicitly — Flyway would have failed to apply the migration
|
||||
* otherwise; here we only verify the table is queryable).</li>
|
||||
* <li>{@link WebChatSessionView} now carries {@code pinned/archived/
|
||||
* streamStatus}, so the visitor-side listing surfaces the same state
|
||||
* the admin console sees.</li>
|
||||
* <li>{@code loadVisitorSessions} filters out archived threads by default;
|
||||
* {@code includeArchived=true} opts back in.</li>
|
||||
* </ul>
|
||||
*/
|
||||
@SpringBootTest(
|
||||
classes = MateClawApplication.class,
|
||||
webEnvironment = SpringBootTest.WebEnvironment.NONE
|
||||
)
|
||||
@TestPropertySource(properties = {
|
||||
"spring.datasource.url=jdbc:h2:mem:webchat_schema_${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"
|
||||
})
|
||||
class WebChatSchemaFieldsTest {
|
||||
|
||||
private static final String SECRET = "webchat-it-secret-0123456789";
|
||||
private static final String API_KEY = "testkey1abcdefgh";
|
||||
private static final long CHANNEL_ID = 9_147_301L;
|
||||
private static final long AGENT_ID = 9_147_3011L;
|
||||
|
||||
@Autowired private WebChatController controller;
|
||||
@Autowired private ConversationService conversationService;
|
||||
@Autowired private JdbcTemplate jdbc;
|
||||
|
||||
@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-schema-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 + "\"}");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("revoked-visitor table is queryable (DDL applied)")
|
||||
void revokedVisitorTableExists() {
|
||||
// Insert + read back a row to prove the table + columns are live.
|
||||
jdbc.update("INSERT INTO webchat_revoked_visitor (id, channel_id, visitor_id, reason) " +
|
||||
"VALUES (?, ?, ?, ?)", 9991L, CHANNEL_ID, "schema-probe", "test");
|
||||
Integer count = jdbc.queryForObject(
|
||||
"SELECT COUNT(*) FROM webchat_revoked_visitor WHERE channel_id = ? AND visitor_id = ?",
|
||||
Integer.class, CHANNEL_ID, "schema-probe");
|
||||
assertThat(count).isEqualTo(1);
|
||||
jdbc.update("DELETE FROM webchat_revoked_visitor WHERE id = ?", 9991L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("archived column on mate_conversation is read/write")
|
||||
void archivedColumnReadWrite() {
|
||||
controller.createSession(API_KEY, req("visitorArch", "s1"));
|
||||
String cid = WebChatController.deriveConversationId(API_KEY, "visitorArch", "s1");
|
||||
|
||||
Integer before = jdbc.queryForObject(
|
||||
"SELECT archived FROM mate_conversation WHERE conversation_id = ?",
|
||||
Integer.class, cid);
|
||||
assertThat(before).isZero();
|
||||
|
||||
jdbc.update("UPDATE mate_conversation SET archived = 1 WHERE conversation_id = ?", cid);
|
||||
Integer after = jdbc.queryForObject(
|
||||
"SELECT archived FROM mate_conversation WHERE conversation_id = ?",
|
||||
Integer.class, cid);
|
||||
assertThat(after).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("WebChatSessionView exposes pinned/archived/streamStatus")
|
||||
void viewExposesNewFields() {
|
||||
controller.createSession(API_KEY, req("visitorView", "s1"));
|
||||
// Flip pinned via the service (endpoint comes in PR 3) so we can assert the view mirrors it.
|
||||
String cid = WebChatController.deriveConversationId(API_KEY, "visitorView", "s1");
|
||||
conversationService.setPinned(cid, true);
|
||||
|
||||
R<List<WebChatSessionView>> r = controller.listSessions(
|
||||
API_KEY, tokenFor("visitorView"), "visitorView", false);
|
||||
assertThat(r.getCode()).isEqualTo(200);
|
||||
assertThat(r.getData()).hasSize(1);
|
||||
WebChatSessionView view = r.getData().get(0);
|
||||
assertThat(view.getPinned()).isEqualTo(1);
|
||||
assertThat(view.getArchived()).isZero();
|
||||
assertThat(view.getStreamStatus()).isEqualTo("idle");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("archived threads are hidden from /sessions by default")
|
||||
@SuppressWarnings("unchecked")
|
||||
void archivedHiddenByDefault() {
|
||||
controller.createSession(API_KEY, req("visitorHide", "active"));
|
||||
controller.createSession(API_KEY, req("visitorHide", "stale"));
|
||||
|
||||
String staleCid = WebChatController.deriveConversationId(API_KEY, "visitorHide", "stale");
|
||||
jdbc.update("UPDATE mate_conversation SET archived = 1 WHERE conversation_id = ?", staleCid);
|
||||
|
||||
// Default: only "active" is returned.
|
||||
R<?> def = controller.listSessions(API_KEY, tokenFor("visitorHide"), "visitorHide", false);
|
||||
assertThat(((List<WebChatSessionView>) def.getData()))
|
||||
.extracting(WebChatSessionView::getSessionId)
|
||||
.containsExactly("active");
|
||||
|
||||
// includeArchived=true: both.
|
||||
R<?> all = controller.listSessions(API_KEY, tokenFor("visitorHide"), "visitorHide", true);
|
||||
assertThat(((List<WebChatSessionView>) all.getData()))
|
||||
.extracting(WebChatSessionView::getSessionId)
|
||||
.containsExactlyInAnyOrder("active", "stale");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("archived empty threads don't count against the 5-empty-session quota")
|
||||
void archivedExcludedFromQuota() {
|
||||
// Pre-seed 5 archived empty threads + verify a 6th (active) creation still succeeds —
|
||||
// the quota gate filters archived out, so the active count is 0 here.
|
||||
String owner = WebChatController.webchatUsername("visitorQuota");
|
||||
for (int i = 1; i <= 5; i++) {
|
||||
String cid = WebChatController.deriveConversationId(API_KEY, "visitorQuota", "arch" + i);
|
||||
conversationService.getOrCreateWebchatConversation(
|
||||
cid, AGENT_ID, owner, 1L, "arch" + i);
|
||||
jdbc.update("UPDATE mate_conversation SET archived = 1 WHERE conversation_id = ?", cid);
|
||||
}
|
||||
|
||||
R<?> r = controller.createSession(API_KEY, req("visitorQuota", "fresh"));
|
||||
assertThat(r.getCode())
|
||||
.as("archived threads must not saturate the empty-session quota")
|
||||
.isEqualTo(200);
|
||||
}
|
||||
}
|
||||
@ -76,7 +76,7 @@ class WebChatSessionManagementTest {
|
||||
@DisplayName("listSessions includes the hashed long-id thread with its sessionId recovered")
|
||||
@SuppressWarnings("unchecked")
|
||||
void listsHashedThread() {
|
||||
R<List<WebChatSessionView>> r = (R<List<WebChatSessionView>>) (R<?>) controller.listSessions(API_KEY, token, VISITOR);
|
||||
R<List<WebChatSessionView>> r = (R<List<WebChatSessionView>>) (R<?>) controller.listSessions(API_KEY, token, VISITOR, false);
|
||||
assertThat(r.getCode()).isEqualTo(200);
|
||||
List<WebChatSessionView> sessions = r.getData();
|
||||
assertThat(sessions).hasSize(3);
|
||||
@ -88,21 +88,21 @@ class WebChatSessionManagementTest {
|
||||
@DisplayName("bad visitor token is rejected")
|
||||
@SuppressWarnings("unchecked")
|
||||
void rejectsBadToken() {
|
||||
R<List<WebChatSessionView>> r = (R<List<WebChatSessionView>>) (R<?>) controller.listSessions(API_KEY, "bogus", VISITOR);
|
||||
R<List<WebChatSessionView>> r = (R<List<WebChatSessionView>>) (R<?>) controller.listSessions(API_KEY, "bogus", VISITOR, false);
|
||||
assertThat(r.getCode()).isEqualTo(401);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("pageSessions paginates and keyword-searches by title")
|
||||
void pagesAndSearches() {
|
||||
R<Map<String, Object>> page = controller.pageSessions(API_KEY, token, VISITOR, 1, 2, null);
|
||||
R<Map<String, Object>> page = controller.pageSessions(API_KEY, token, VISITOR, 1, 2, null, false);
|
||||
assertThat(page.getCode()).isEqualTo(200);
|
||||
assertThat(page.getData().get("total")).isEqualTo(3L);
|
||||
assertThat((List<?>) page.getData().get("items")).hasSize(2);
|
||||
|
||||
// Rename one thread, then search for it.
|
||||
controller.renameSession(API_KEY, token, VISITOR, "s1", Map.of("title", "QuarterlyReport"));
|
||||
R<Map<String, Object>> hit = controller.pageSessions(API_KEY, token, VISITOR, 1, 20, "quarterly");
|
||||
R<Map<String, Object>> hit = controller.pageSessions(API_KEY, token, VISITOR, 1, 20, "quarterly", false);
|
||||
assertThat((List<?>) hit.getData().get("items")).hasSize(1);
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user