Files
trade-message-center/apps/server/test/onetalk-read-domain.test.ts
T

825 lines
29 KiB
TypeScript

// 验证 OneTalk 安全读取领域投影
import assert from "node:assert/strict";
import test from "node:test";
import type {
OneTalkMessage,
OneTalkRenderedCardContent,
} from "@trade-message-center/onetalk-contract";
import {
createOneTalkReadService,
decodeOneTalkHistoryReadCursor,
decodeOneTalkListCursor,
encodeOneTalkHistoryReadCursor,
encodeOneTalkListCursor,
normalizeOneTalkReadQuery,
type CenterMessage,
type OneTalkReadConversationQuery,
type OneTalkReadConversationRow,
type OneTalkReadHistoryQuery,
type OneTalkReadListQuery,
type OneTalkReadMessageRow,
type OneTalkReadRepository,
} from "../src/onetalk/index.ts";
import { projectCenterMessage, toOneTalkCenterMessage } from "../src/onetalk/read-projection.ts";
const scope = {
mindUserId: "mind-user-1",
workspaceId: "workspace-1",
channelAccountId: "account-1",
};
const conversation = (
overrides: Partial<OneTalkReadConversationRow> = {},
): OneTalkReadConversationRow => ({
channelAccountId: scope.channelAccountId,
conversationId: "conversation-1",
name: null,
avatarUrl: null,
countryCode: null,
companyName: null,
lastContactTimeMs: null,
messagePreview: null,
buyerTags: null,
buyerFeatures: null,
latestMessageId: null,
latestMessageAtMs: null,
historyComplete: false,
messageCount: 0,
syncPhase: "initial",
syncResult: "incomplete",
...overrides,
});
const message = (overrides: Partial<OneTalkReadMessageRow> = {}): OneTalkReadMessageRow => ({
channelAccountId: scope.channelAccountId,
conversationId: "conversation-1",
messageId: "message-1",
senderId: "sender-1",
direction: "received",
sentAtMs: 1_700_000_000_000,
content: { version: 1, kind: "text", text: "hello" },
participantIds: ["sender-1", "account-1"],
readStatus: 1,
...overrides,
});
const messageFieldsForProjectionTest = () => ({
messageId: "message-1",
conversationId: "conversation-1",
senderId: "sender-1",
participantIds: ["sender-1", "account-1"],
direction: "received" as const,
sentAtMs: 1_700_000_000_000,
readStatus: "read" as const,
});
const createRepositoryHarness = (
options: {
listRows?: () => OneTalkReadConversationRow[];
conversation?: () => OneTalkReadConversationRow | null;
messageRows?: () => OneTalkReadMessageRow[];
} = {},
) => {
const listQueries: OneTalkReadListQuery[] = [];
const conversationQueries: OneTalkReadConversationQuery[] = [];
const historyQueries: OneTalkReadHistoryQuery[] = [];
const repository: OneTalkReadRepository = {
listConversations: async (query) => {
listQueries.push(query);
return options.listRows?.() ?? [];
},
findConversation: async (query) => {
conversationQueries.push(query);
return options.conversation?.() ?? conversation();
},
listMessages: async (query) => {
historyQueries.push(query);
return options.messageRows?.() ?? [];
},
};
return { repository, listQueries, conversationQueries, historyQueries };
};
test("normalizes names as trimmed Unicode-insensitive query values", () => {
assert.equal(normalizeOneTalkReadQuery(" ÅNGSTRÖM "), "ångström");
assert.equal(normalizeOneTalkReadQuery(" "), null);
assert.equal(normalizeOneTalkReadQuery(null), null);
});
test("projects one direct conversation with the exact public field whitelist", async () => {
const harness = createRepositoryHarness({
conversation: () =>
conversation({
name: "Current profile",
avatarUrl: "https://cdn.example.com/avatar.jpg",
buyerTags: ["high-potential"],
buyerFeatures: ["repeat-buyer"],
email: "buyer@example.test",
registrationDate: "2025-03-12",
companyWebsite: "https://example.test/",
countryCode: "US",
companyName: "Current Profile LLC",
lastContactTimeMs: 1_700_000_000_100,
messagePreview: "latest preview",
latestMessageId: "persisted-latest-message",
latestMessageAtMs: 1_700_000_000_100,
historyComplete: true,
messageCount: 3,
syncPhase: "incremental",
syncResult: "succeeded",
}),
});
const service = createOneTalkReadService(harness.repository, {
now: () => new Date("2026-09-03T00:00:00.000Z"),
});
const result = await service.readConversation({ scope, conversationId: "conversation-1" });
assert.equal(result.status, "accepted");
if (result.status !== "accepted") return;
assert.deepEqual(result.conversation, {
conversationId: "conversation-1",
conversationType: "direct",
name: "Current profile",
avatarUrl: "https://cdn.example.com/avatar.jpg",
customerProfile: {
name: "Current profile",
avatarUrl: "https://cdn.example.com/avatar.jpg",
buyerTags: ["high-potential"],
buyerFeatures: ["repeat-buyer"],
email: "buyer@example.test",
registrationDate: "2025-03-12",
companyWebsite: "https://example.test/",
countryCode: "US",
companyName: "Current Profile LLC",
},
lastContactTimeLong: 1_700_000_000_100,
messagePreview: "latest preview",
});
assert.deepEqual(Object.keys(result.conversation).sort(), [
"avatarUrl",
"conversationId",
"conversationType",
"customerProfile",
"lastContactTimeLong",
"messagePreview",
"name",
]);
});
test("uses a canonical list cursor and permits current profile changes on later pages", async () => {
let laterName = "Before update";
const first = conversation({
conversationId: "conversation-a",
name: "First profile",
latestMessageId: "message-a",
latestMessageAtMs: 200,
lastContactTimeMs: 200,
});
const later = () =>
conversation({
conversationId: "conversation-b",
name: laterName,
latestMessageId: "message-b",
latestMessageAtMs: 100,
lastContactTimeMs: 100,
});
const harness = createRepositoryHarness({
listRows: () => (harness.listQueries.at(-1)?.cursor ? [later()] : [first, later()]),
});
const service = createOneTalkReadService(harness.repository, {
now: () => new Date("2026-09-03T00:00:00.000Z"),
});
const firstPage = await service.listConversations({ scope, query: " FIRST ", limit: 1 });
assert.equal(firstPage.status, "accepted");
if (firstPage.status !== "accepted") return;
assert.equal(firstPage.conversations[0]?.name, "First profile");
assert.equal(firstPage.page.hasMore, true);
assert.ok(firstPage.page.nextCursor);
const cursor = decodeOneTalkListCursor(firstPage.page.nextCursor);
assert.deepEqual(cursor, {
channelAccountId: "account-1",
conversationId: "conversation-a",
lastContactTimeMs: 200,
query: "first",
});
laterName = "Current profile";
const secondPage = await service.listConversations({
scope,
query: "first",
cursor: firstPage.page.nextCursor,
limit: 1,
});
assert.equal(secondPage.status, "accepted");
if (secondPage.status !== "accepted") return;
assert.equal(secondPage.conversations[0]?.name, "Current profile");
assert.equal(harness.listQueries[1]?.cursor?.lastContactTimeMs, cursor?.lastContactTimeMs);
const mismatchedQuery = await service.listConversations({
scope,
query: "different",
cursor: firstPage.page.nextCursor,
});
assert.deepEqual(mismatchedQuery, { status: "rejected", reason: "invalid_cursor" });
assert.equal(harness.listQueries.length, 2);
});
test("rejects malformed list cursors and unsupported limits before querying", async () => {
const harness = createRepositoryHarness();
const service = createOneTalkReadService(harness.repository);
const canonical = encodeOneTalkListCursor({
channelAccountId: scope.channelAccountId,
conversationId: "conversation-1",
query: null,
lastContactTimeMs: null,
});
assert.equal(decodeOneTalkListCursor(`${canonical}=`), null);
assert.deepEqual(decodeOneTalkListCursor(canonical), {
channelAccountId: scope.channelAccountId,
conversationId: "conversation-1",
query: null,
lastContactTimeMs: null,
});
assert.deepEqual(await service.listConversations({ scope, cursor: canonical, limit: 0 }), {
status: "rejected",
reason: "invalid_limit",
});
assert.deepEqual(
await service.listConversations({
scope: { ...scope, channelAccountId: "other" },
cursor: canonical,
}),
{ status: "rejected", reason: "invalid_cursor" },
);
assert.equal(harness.listQueries.length, 0);
});
test("round-trips a valid list cursor with a long query", () => {
const cursor = {
channelAccountId: scope.channelAccountId,
conversationId: "conversation-1",
query: "q".repeat(2_000),
lastContactTimeMs: null,
};
const encoded = encodeOneTalkListCursor(cursor);
assert.deepEqual(decodeOneTalkListCursor(encoded), cursor);
});
test("uses one snapshot and half-open window for history page pairs", async () => {
const rows = [
message({ messageId: "message-a", sentAtMs: 10 }),
message({ messageId: "message-b", sentAtMs: 20 }),
];
const harness = createRepositoryHarness({
conversation: () => conversation({ historyComplete: true }),
messageRows: () => rows,
});
const service = createOneTalkReadService(harness.repository, {
now: () => new Date("2026-09-03T00:00:00.000Z"),
});
const firstPage = await service.readHistory({
scope,
conversationId: "conversation-1",
fromSentAtMs: 10,
toSentAtMs: 30,
limit: 1,
});
assert.equal(firstPage.status, "accepted");
if (firstPage.status !== "accepted") return;
assert.deepEqual(
firstPage.messages.map(({ messageId }) => messageId),
["message-a"],
);
assert.equal(firstPage.page.hasMore, true);
assert.ok(firstPage.page.nextCursor);
assert.deepEqual(harness.historyQueries[0]?.window, { fromSentAtMs: 10, toSentAtMs: 30 });
const cursor = decodeOneTalkHistoryReadCursor(firstPage.page.nextCursor);
assert.equal(cursor?.asOfMs, undefined);
const mismatch = await service.readHistory({
scope,
conversationId: "conversation-1",
fromSentAtMs: 10,
toSentAtMs: 31,
cursor: firstPage.page.nextCursor,
});
assert.deepEqual(mismatch, { status: "rejected", reason: "invalid_cursor" });
assert.deepEqual(
await service.readHistory({
scope,
conversationId: "conversation-1",
fromSentAtMs: 30,
toSentAtMs: 30,
}),
{ status: "rejected", reason: "invalid_time_range" },
);
assert.equal(harness.historyQueries.length, 1);
});
test("paginates more than one hundred history messages toward older records", async () => {
const rows = Array.from({ length: 101 }, (_, index) =>
message({
messageId: `message-${101 - index}`,
sentAtMs: 1_000 - index,
}),
);
const harness = createRepositoryHarness({
conversation: () => conversation({ historyComplete: true }),
messageRows: () => {
const query = harness.historyQueries.at(-1);
if (!query?.cursor) return rows;
return rows.filter(
(row) =>
row.sentAtMs < query.cursor!.sentAtMs ||
(row.sentAtMs === query.cursor!.sentAtMs &&
row.messageId < query.cursor!.messageId),
);
},
});
const service = createOneTalkReadService(harness.repository);
const firstPage = await service.readHistory({
scope,
conversationId: "conversation-1",
fromSentAtMs: 0,
toSentAtMs: 2_000,
limit: 100,
purpose: "communication_summary_read",
});
assert.equal(firstPage.status, "accepted");
if (firstPage.status !== "accepted") return;
assert.equal(firstPage.messages.length, 100);
assert.equal(firstPage.messages[0]?.sentAtMs, 901);
assert.equal(firstPage.messages.at(-1)?.sentAtMs, 1_000);
assert.equal(firstPage.page.hasMore, true);
const secondPage = await service.readHistory({
scope,
conversationId: "conversation-1",
fromSentAtMs: 0,
toSentAtMs: 2_000,
limit: 100,
cursor: firstPage.page.nextCursor,
purpose: "communication_summary_read",
});
assert.equal(secondPage.status, "accepted");
if (secondPage.status !== "accepted") return;
assert.deepEqual(
secondPage.messages.map(({ sentAtMs }) => sentAtMs),
[900],
);
assert.deepEqual(secondPage.page, { hasMore: false, nextCursor: null });
});
test("keeps partial ordinary history readable but gates summary reads", async () => {
const harness = createRepositoryHarness({ messageRows: () => [message()] });
const service = createOneTalkReadService(harness.repository);
const ordinary = await service.readHistory({ scope, conversationId: "conversation-1" });
assert.equal(ordinary.status, "accepted");
assert.equal(harness.historyQueries.length, 1);
const summary = await service.readHistory({
scope,
conversationId: "conversation-1",
purpose: "communication_summary_read",
});
assert.deepEqual(summary, { status: "rejected", reason: "history_incomplete" });
assert.equal(harness.historyQueries.length, 1);
});
test("projects normalized text, image, and file content without raw reinterpretation", async () => {
const image = {
version: 1,
kind: "image",
fileId: "image-file-1",
extension: "jpg",
sizeBytes: 42_000,
isOriginal: true,
md5: "a".repeat(32),
previewUrl:
"https://clouddisk.alibaba.com/file/redirectFileUrl.htm?appkey=oneTalk&fileAction=imagePreview&id=image-file-1&scene=oneTalk",
urlScope: "onetalk_session",
} as const;
const file = {
version: 1,
kind: "file",
fileId: "file-1",
parentId: "parent-1",
fileName: "quote.pdf",
extension: "pdf",
sizeBytes: 24_000,
md5: "b".repeat(32),
previewUrl: null,
thumbnailUrl: null,
downloadUrl:
"https://clouddisk.alibaba.com/file/redirectFileUrl.htm?appkey=oneTalk&fileAction=download&id=file-1&parentId=parent-1&scene=oneTalk",
downloadState: "available",
urlScope: "onetalk_session",
} as const;
const harness = createRepositoryHarness({
conversation: () => conversation({ historyComplete: true }),
messageRows: () => [
message({ messageId: "file", sentAtMs: 30, content: file, readStatus: 0 }),
message({ messageId: "image", sentAtMs: 20, content: image }),
message({
messageId: "text",
sentAtMs: 10,
content: { version: 1, kind: "text", text: "hello" },
}),
],
});
const service = createOneTalkReadService(harness.repository);
const result = await service.readHistory({ scope, conversationId: "conversation-1", limit: 3 });
assert.equal(result.status, "accepted");
if (result.status !== "accepted") return;
assert.deepEqual(
result.messages.map(({ content }) => content),
[{ version: 1, kind: "text", text: "hello" }, image, file],
);
for (const projected of result.messages) {
assert.deepEqual(Object.keys(projected).sort(), [
"content",
"conversationId",
"direction",
"messageId",
"participantIds",
"senderId",
"sentAtMs",
]);
assert.equal("contentType" in projected, false);
assert.equal("messageStatus" in projected, false);
assert.equal("unreadCount" in projected, false);
}
});
test("projects business cards from the linked customer profile and leaves missing profiles as markers", async () => {
const businessCard = { version: 1 as const, kind: "business_card" as const };
const profileHarness = createRepositoryHarness({
conversation: () =>
conversation({
name: "Customer One",
avatarUrl: "https://cdn.example.test/customer.jpg",
countryCode: "US",
companyName: "Customer One LLC",
}),
messageRows: () => [message({ content: businessCard })],
});
const profileResult = await createOneTalkReadService(profileHarness.repository).readHistory({
scope,
conversationId: "conversation-1",
});
assert.equal(profileResult.status, "accepted");
if (profileResult.status !== "accepted") return;
assert.deepEqual(profileResult.messages[0]?.content, {
...businessCard,
contactName: "Customer One",
companyName: "Customer One LLC",
countryCode: "US",
avatarUrl: "https://cdn.example.test/customer.jpg",
});
assert.equal(profileHarness.historyQueries.length, 1);
const missingProfileHarness = createRepositoryHarness({
messageRows: () => [message({ content: businessCard })],
});
const missingProfileResult = await createOneTalkReadService(
missingProfileHarness.repository,
).readHistory({ scope, conversationId: "conversation-1" });
assert.equal(missingProfileResult.status, "accepted");
if (missingProfileResult.status !== "accepted") return;
assert.deepEqual(missingProfileResult.messages[0]?.content, businessCard);
assert.deepEqual(businessCard, { version: 1, kind: "business_card" });
});
test("projects one persisted media fact identically for history and message.created", () => {
const fact: OneTalkMessage = {
messageId: "image-1",
conversationId: "conversation-1",
senderId: "sender-1",
participantIds: ["sender-1", "account-1"],
direction: "received",
sentAtMs: 1_700_000_000_000,
readStatus: 1,
messageStatus: 2,
unreadCount: 0,
content: {
version: 1,
kind: "image",
fileId: "image-file-1",
extension: "jpg",
sizeBytes: 42_000,
isOriginal: true,
md5: null,
previewUrl: null,
urlScope: "onetalk_session",
},
};
const historyMessage = projectCenterMessage({
channelAccountId: scope.channelAccountId,
...fact,
});
const liveMessage = toOneTalkCenterMessage(fact);
assert.deepEqual(historyMessage, liveMessage);
assert.equal("contentType" in historyMessage, false);
assert.equal("text" in historyMessage, false);
});
test("projects one persisted product fact identically and rejects raw-query JSONB at the read boundary", () => {
const product = {
version: 1 as const,
kind: "product" as const,
sourceUrl:
"https://chinese.alibaba.com/product-detail/HAGO-Men-s-Breathable-Mid-Rise-1601456609478.html",
productId: "1601456609478",
};
const fact: OneTalkMessage = {
messageId: "product-1",
conversationId: "conversation-1",
senderId: "sender-1",
participantIds: ["sender-1", "account-1"],
direction: "received",
sentAtMs: 1_700_000_000_000,
readStatus: 1,
messageStatus: 2,
unreadCount: 0,
content: product,
};
const historyMessage = projectCenterMessage({
channelAccountId: scope.channelAccountId,
...fact,
});
const liveMessage = toOneTalkCenterMessage(fact);
assert.deepEqual(historyMessage, liveMessage);
assert.deepEqual(historyMessage.content, product);
assert.equal(JSON.stringify(historyMessage).includes("chatToken"), false);
const malformed = {
...fact,
content: { ...product, sourceUrl: `${product.sourceUrl}?chatToken=secret` },
} as unknown as OneTalkMessage;
assert.throws(
() => toOneTalkCenterMessage(malformed),
/Invalid persisted OneTalk message content/,
);
});
test("projects inquiry, product, and order supplements without changing their base kinds", () => {
const product = {
version: 1 as const,
kind: "product" as const,
sourceUrl:
"https://chinese.alibaba.com/product-detail/HAGO-Men-s-Breathable-Mid-Rise-1601456609478.html",
productId: "1601456609478",
};
const cases: Array<{
content: OneTalkMessage["content"];
renderedCardContent: OneTalkRenderedCardContent;
}> = [
{
content: { version: 1 as const, kind: "inquiry" as const },
renderedCardContent: {
version: 1 as const,
kind: "rendered_inquiry" as const,
product: { imageUrl: "https://img.alicdn.com/item.jpg", title: "Widget" },
purchaseQuantity: { value: "100", unit: "pieces" },
requirementText: "Need delivery this week",
inquiryReference: "inquiry-1",
actions: [{ label: "Reply", available: true }],
},
},
{
content: product,
renderedCardContent: {
version: 1 as const,
kind: "rendered_product" as const,
product: {
imageUrl: "https://img.alicdn.com/item.jpg",
title: "Widget",
sourceUrl: product.sourceUrl,
productId: product.productId,
},
priceDisplay: "US $10.00",
minimumOrder: { value: "1", unit: "piece" },
serviceBadges: ["Fast dispatch"],
},
},
{
content: {
version: 1 as const,
kind: "order" as const,
orderId: "order-1",
bizCode: null,
contractId: null,
id: null,
tenant: null,
orderAmount: 10,
orderAmountCurrency: "USD",
paymentAmount: 10,
paymentAmountCurrency: "USD",
statusMessageKey: "paid",
actions: [],
},
renderedCardContent: {
version: 1 as const,
kind: "rendered_order" as const,
title: "Order summary",
products: [{ imageUrl: "https://img.alicdn.com/item.jpg", title: "Widget" }],
productCount: 1,
status: { code: "paid", text: "Paid" },
payment: { totalDisplay: "US $10.00", discountDisplay: null },
delivery: {
shippingAddress: "1 Market Street",
methodLabel: null,
dateLabel: null,
},
action: { label: "View order", status: "available" },
},
},
];
for (const entry of cases) {
const persisted = message({
content: entry.content,
renderedCardContent: entry.renderedCardContent,
});
const history = projectCenterMessage(persisted);
const live = toOneTalkCenterMessage(persisted, entry.renderedCardContent);
assert.deepEqual(history, live);
assert.equal(history.content.kind, entry.content.kind);
assert.equal(history.content.kind.startsWith("rendered_"), false);
}
});
test("attaches a generic DOM card beside an unchanged base message", () => {
const renderedCard = {
version: 1 as const,
status: "Paid",
total: "US $10.00",
image: "https://img.alicdn.com/card.jpg",
};
const result = toOneTalkCenterMessage(message(), renderedCard);
assert.deepEqual(result.content, message().content);
assert.deepEqual(result.renderedCard, renderedCard);
});
test("rejects a rendered-card supplement with a different base kind or product identity", () => {
const product = {
version: 1 as const,
kind: "product" as const,
sourceUrl:
"https://chinese.alibaba.com/product-detail/HAGO-Men-s-Breathable-Mid-Rise-1601456609479.html",
productId: "1601456609479",
};
const renderedProduct = {
version: 1 as const,
kind: "rendered_product" as const,
product: {
imageUrl: "https://img.alicdn.com/item.jpg",
title: "Widget",
sourceUrl:
"https://chinese.alibaba.com/product-detail/HAGO-Men-s-Breathable-Mid-Rise-1601456609478.html",
productId: "1601456609478",
},
priceDisplay: "US $10.00",
minimumOrder: { value: "1", unit: "piece" },
serviceBadges: ["Fast dispatch"],
};
assert.throws(
() => toOneTalkCenterMessage(message({ content: product }), renderedProduct),
/identity does not match/,
);
assert.throws(() => toOneTalkCenterMessage(message(), renderedProduct), /kind does not match/);
});
test("rejects malformed rendered-card JSONB at the read boundary", () => {
assert.throws(
() =>
projectCenterMessage({
...message(),
renderedCardContent: {
version: 1,
kind: "rendered_order",
} as never,
}),
/Invalid persisted OneTalk rendered-card supplement/,
);
});
test("CenterMessage accepts normalized media and structured-card values", () => {
const verifiedImage = {
...messageFieldsForProjectionTest(),
content: {
version: 1,
kind: "image",
fileId: "image-file-1",
extension: "jpg",
sizeBytes: 42_000,
isOriginal: true,
md5: null,
previewUrl: null,
urlScope: "onetalk_session",
},
} satisfies CenterMessage;
const verifiedFile = {
...messageFieldsForProjectionTest(),
content: {
version: 1,
kind: "file",
fileId: "file-1",
parentId: "parent-1",
fileName: "quote.pdf",
extension: "pdf",
sizeBytes: 24_000,
md5: null,
previewUrl: null,
thumbnailUrl: null,
downloadUrl: null,
downloadState: "not_provided",
urlScope: "onetalk_session",
},
} satisfies CenterMessage;
const verifiedOrder = {
...messageFieldsForProjectionTest(),
content: {
version: 1,
kind: "order",
orderId: "order-1",
bizCode: 42,
contractId: "contract-1",
id: "id-1",
tenant: "tenant-1",
orderAmount: 12.5,
orderAmountCurrency: "USD",
paymentAmount: 10,
paymentAmountCurrency: "USD",
statusMessageKey: "order.pending_payment",
actions: [{ name: "pay", messageKey: "order.pay", payStep: "deposit" }],
},
} satisfies CenterMessage;
const verifiedBusinessCard = {
...messageFieldsForProjectionTest(),
content: { version: 1, kind: "business_card" },
} satisfies CenterMessage;
assert.equal(verifiedImage.content.kind, "image");
assert.equal(verifiedFile.content.fileName, "quote.pdf");
assert.equal(verifiedOrder.content.statusMessageKey, "order.pending_payment");
assert.equal(verifiedBusinessCard.content.kind, "business_card");
});
test("rejects persisted image content with retired dimensions at the read boundary", () => {
const legacyImage = {
...messageFieldsForProjectionTest(),
content: {
version: 1,
kind: "image",
fileId: "image-file-1",
extension: "jpg",
sizeBytes: 42_000,
width: 1280,
height: 720,
isOriginal: true,
md5: null,
previewUrl: null,
urlScope: "onetalk_session",
},
} as unknown as OneTalkMessage;
assert.throws(
() => toOneTalkCenterMessage(legacyImage),
/Invalid persisted OneTalk message content/,
);
});
test("keeps history cursor codecs separate from legacy and list cursor shapes", () => {
const historyCursor = encodeOneTalkHistoryReadCursor({
channelAccountId: "account-1",
conversationId: "conversation-1",
fromSentAtMs: null,
toSentAtMs: 20,
asOfMs: 10,
sentAtMs: 11,
messageId: "message-1",
});
assert.equal(decodeOneTalkListCursor(historyCursor), null);
assert.equal(decodeOneTalkHistoryReadCursor("eyJ2IjoxLCJhIjoiYSJ9"), null);
});