mirror of
https://github.com/sinanyuntu/trade-message-center.git
synced 2026-09-17 13:22:11 +08:00
491 lines
18 KiB
TypeScript
491 lines
18 KiB
TypeScript
// 验证 OneTalk 读取 SQL 的快照与 direct 边界
|
|
|
|
import assert from "node:assert/strict";
|
|
import { randomUUID } from "node:crypto";
|
|
import test from "node:test";
|
|
|
|
import postgres from "postgres";
|
|
|
|
import { createDatabase, type DatabaseConnection } from "../src/database/index.ts";
|
|
import { runMigrations } from "../src/database/migrate.ts";
|
|
import { createOneTalkReadRepository, createOneTalkReadService } from "../src/onetalk/index.ts";
|
|
|
|
const databaseUrl = process.env.TEST_DATABASE_URL?.trim();
|
|
const imageDimensionMigrationTimestamp = 1_789_102_776_000;
|
|
|
|
test(
|
|
"reads only direct snapshot facts with real latest-message and profile semantics",
|
|
{
|
|
skip: databaseUrl
|
|
? false
|
|
: "TEST_DATABASE_URL is not set; PostgreSQL read integration test not run",
|
|
timeout: 60_000,
|
|
},
|
|
async () => {
|
|
if (!databaseUrl) return;
|
|
|
|
await runMigrations(databaseUrl);
|
|
const database: DatabaseConnection = createDatabase(databaseUrl);
|
|
const client = postgres(databaseUrl, { max: 1 });
|
|
const channelAccountId = `read-integration-${randomUUID()}`;
|
|
const asOf = new Date("2026-09-03T00:00:00.000Z");
|
|
const afterAsOf = new Date("2026-09-04T00:00:00.000Z");
|
|
const scope = {
|
|
mindUserId: "integration-mind-user",
|
|
workspaceId: "integration-workspace",
|
|
channelAccountId,
|
|
};
|
|
const service = createOneTalkReadService(createOneTalkReadRepository(database.db), {
|
|
now: () => asOf,
|
|
});
|
|
|
|
try {
|
|
await client`
|
|
delete from drizzle.__drizzle_migrations
|
|
where created_at = ${imageDimensionMigrationTimestamp}
|
|
`;
|
|
await client`
|
|
insert into onetalk_conversation (
|
|
channel_account_id,
|
|
conversation_id,
|
|
conversation_kind,
|
|
history_complete,
|
|
first_discovered_at,
|
|
last_observed_at
|
|
) values (
|
|
${channelAccountId},
|
|
'migration-image',
|
|
'direct',
|
|
true,
|
|
${asOf},
|
|
${asOf}
|
|
)
|
|
`;
|
|
const legacyImage = {
|
|
version: 1,
|
|
kind: "image",
|
|
fileId: "migration-image-file",
|
|
extension: "jpg",
|
|
sizeBytes: 42_000,
|
|
width: 1280,
|
|
height: 720,
|
|
isOriginal: true,
|
|
md5: "a".repeat(32),
|
|
previewUrl: null,
|
|
urlScope: "onetalk_session",
|
|
};
|
|
const canonicalImage = {
|
|
version: 1,
|
|
kind: "image",
|
|
fileId: "migration-image-file",
|
|
extension: "jpg",
|
|
sizeBytes: 42_000,
|
|
isOriginal: true,
|
|
md5: "a".repeat(32),
|
|
previewUrl: null,
|
|
urlScope: "onetalk_session",
|
|
};
|
|
const imageWithOnlyWidth = {
|
|
...canonicalImage,
|
|
fileId: "image-with-only-width",
|
|
width: 800,
|
|
};
|
|
const imageWithOnlyHeight = {
|
|
...canonicalImage,
|
|
fileId: "image-with-only-height",
|
|
height: 600,
|
|
};
|
|
const imageWithoutDimensions = {
|
|
...canonicalImage,
|
|
fileId: "image-with-only-width",
|
|
};
|
|
const imageWithoutHeight = {
|
|
...canonicalImage,
|
|
fileId: "image-with-only-height",
|
|
};
|
|
const imageWithPreservedKey = {
|
|
...legacyImage,
|
|
fileId: "image-with-preserved-key",
|
|
retainedMigrationProbe: { source: "pre-v6" },
|
|
};
|
|
const preservedImage = {
|
|
...canonicalImage,
|
|
fileId: "image-with-preserved-key",
|
|
retainedMigrationProbe: { source: "pre-v6" },
|
|
};
|
|
const textSentinel = {
|
|
version: 1,
|
|
kind: "text",
|
|
text: "unmodified text",
|
|
width: 900,
|
|
height: 901,
|
|
};
|
|
for (const [conversationId, messageId, content] of [
|
|
["migration-image", "legacy-image", legacyImage],
|
|
["migration-preservation", "image-only-width", imageWithOnlyWidth],
|
|
["migration-preservation", "image-only-height", imageWithOnlyHeight],
|
|
["migration-preservation", "image-preserved-key", imageWithPreservedKey],
|
|
["migration-preservation", "text-sentinel", textSentinel],
|
|
] as const) {
|
|
await client`
|
|
insert into onetalk_message (
|
|
channel_account_id,
|
|
conversation_id,
|
|
message_id,
|
|
sender_id,
|
|
binding,
|
|
mind_user_id,
|
|
workspace_id,
|
|
device_id,
|
|
direction,
|
|
observation_type,
|
|
sent_at_ms,
|
|
content,
|
|
participant_ids,
|
|
read_status,
|
|
message_status,
|
|
unread_count,
|
|
first_observed_at,
|
|
last_observed_at
|
|
) values (
|
|
${channelAccountId},
|
|
${conversationId},
|
|
${messageId},
|
|
'sender-1',
|
|
'binding-1',
|
|
'mind-user-1',
|
|
'workspace-1',
|
|
'device-1',
|
|
'received',
|
|
'history',
|
|
1,
|
|
${client.json(content)},
|
|
${["sender-1", channelAccountId]},
|
|
1,
|
|
2,
|
|
0,
|
|
${asOf},
|
|
${asOf}
|
|
)
|
|
`;
|
|
}
|
|
await runMigrations(databaseUrl);
|
|
const migratedRows = await client<
|
|
{
|
|
content: Record<string, unknown>;
|
|
message_id: string;
|
|
}[]
|
|
>`
|
|
select message_id, content
|
|
from onetalk_message
|
|
where channel_account_id = ${channelAccountId}
|
|
order by message_id
|
|
`;
|
|
assert.deepEqual(migratedRows, [
|
|
{ message_id: "image-only-height", content: imageWithoutHeight },
|
|
{ message_id: "image-only-width", content: imageWithoutDimensions },
|
|
{ message_id: "image-preserved-key", content: preservedImage },
|
|
{ message_id: "legacy-image", content: canonicalImage },
|
|
{ message_id: "text-sentinel", content: textSentinel },
|
|
]);
|
|
const migratedHistory = await service.readHistory({
|
|
scope,
|
|
conversationId: "migration-image",
|
|
});
|
|
assert.equal(migratedHistory.status, "accepted");
|
|
if (migratedHistory.status !== "accepted") return;
|
|
assert.deepEqual(migratedHistory.messages[0]?.content, canonicalImage);
|
|
await runMigrations(databaseUrl);
|
|
const migrationRecords = await client<{ count: string }[]>`
|
|
select count(*) as count
|
|
from drizzle.__drizzle_migrations
|
|
where created_at = ${imageDimensionMigrationTimestamp}
|
|
`;
|
|
assert.equal(migrationRecords[0]?.count, "1");
|
|
|
|
for (const [conversationId, conversationKind, discoveredAt, lastMessageAtMs] of [
|
|
["direct-a", "direct", asOf, 300],
|
|
["direct-b", "direct", asOf, 250],
|
|
["direct-c", "direct", asOf, 200],
|
|
["group", "group", asOf, 400],
|
|
["unknown", null, asOf, 500],
|
|
["future-direct", "direct", afterAsOf, 600],
|
|
] as const) {
|
|
await client`
|
|
insert into onetalk_conversation (
|
|
channel_account_id,
|
|
conversation_id,
|
|
conversation_kind,
|
|
last_message_at_ms,
|
|
first_discovered_at,
|
|
last_observed_at
|
|
) values (
|
|
${channelAccountId},
|
|
${conversationId},
|
|
${conversationKind},
|
|
${lastMessageAtMs},
|
|
${discoveredAt},
|
|
${discoveredAt}
|
|
)
|
|
`;
|
|
}
|
|
await client`
|
|
insert into onetalk_contact_profile (
|
|
channel_account_id,
|
|
conversation_id,
|
|
ali_id,
|
|
name,
|
|
avatar_url,
|
|
observed_at_ms,
|
|
profile_fingerprint,
|
|
observation_status,
|
|
received_at
|
|
) values (
|
|
${channelAccountId},
|
|
'direct-a',
|
|
'ali-a',
|
|
'Alpha Profile',
|
|
'https://cdn.example.com/alpha.jpg',
|
|
100,
|
|
'profile-a',
|
|
'confirmed',
|
|
${asOf}
|
|
)
|
|
`;
|
|
await client`
|
|
insert into onetalk_contact_profile (
|
|
channel_account_id,
|
|
conversation_id,
|
|
ali_id,
|
|
name,
|
|
observed_at_ms,
|
|
profile_fingerprint,
|
|
observation_status,
|
|
received_at
|
|
) values (
|
|
${channelAccountId},
|
|
'direct-c',
|
|
'ali-c',
|
|
'Later Profile',
|
|
100,
|
|
'profile-c',
|
|
'confirmed',
|
|
${asOf}
|
|
)
|
|
`;
|
|
await client`
|
|
insert into onetalk_contact_profile (
|
|
channel_account_id,
|
|
conversation_id,
|
|
ali_id,
|
|
name,
|
|
observed_at_ms,
|
|
profile_fingerprint,
|
|
observation_status,
|
|
received_at
|
|
) values (
|
|
${channelAccountId},
|
|
'profile-only',
|
|
'ali-only',
|
|
'Profile without conversation',
|
|
100,
|
|
'profile-only',
|
|
'confirmed',
|
|
${asOf}
|
|
)
|
|
`;
|
|
for (const [messageId, sentAtMs, firstObservedAt] of [
|
|
["anchor-only", 100, asOf],
|
|
["latest-a", 200, asOf],
|
|
["latest-z", 200, asOf],
|
|
["after-snapshot", 900, afterAsOf],
|
|
] as const) {
|
|
await client`
|
|
insert into onetalk_message (
|
|
channel_account_id,
|
|
conversation_id,
|
|
message_id,
|
|
sender_id,
|
|
binding,
|
|
mind_user_id,
|
|
workspace_id,
|
|
device_id,
|
|
direction,
|
|
observation_type,
|
|
sent_at_ms,
|
|
content,
|
|
participant_ids,
|
|
read_status,
|
|
message_status,
|
|
unread_count,
|
|
first_observed_at,
|
|
last_observed_at
|
|
) values (
|
|
${channelAccountId},
|
|
'direct-a',
|
|
${messageId},
|
|
'sender-1',
|
|
'binding-1',
|
|
'mind-user-1',
|
|
'workspace-1',
|
|
'device-1',
|
|
'received',
|
|
'history',
|
|
${sentAtMs},
|
|
${client.json({ version: 1, kind: "text", text: "visible text" })},
|
|
${["sender-1", channelAccountId]},
|
|
1,
|
|
2,
|
|
0,
|
|
${firstObservedAt},
|
|
${firstObservedAt}
|
|
)
|
|
`;
|
|
}
|
|
await client`
|
|
update onetalk_conversation
|
|
set message_count = 4,
|
|
latest_message_id = 'anchor-only'
|
|
where channel_account_id = ${channelAccountId}
|
|
and conversation_id = 'direct-a'
|
|
`;
|
|
|
|
const firstPage = await service.listConversations({ scope, limit: 1 });
|
|
assert.equal(firstPage.status, "accepted");
|
|
if (firstPage.status !== "accepted") return;
|
|
assert.deepEqual(
|
|
firstPage.conversations.map(({ conversationId }) => conversationId),
|
|
["direct-a"],
|
|
);
|
|
assert.equal(firstPage.conversations[0]?.name, "Alpha Profile");
|
|
assert.equal(firstPage.page.hasMore, true);
|
|
assert.ok(firstPage.page.nextCursor);
|
|
|
|
await client`
|
|
insert into onetalk_contact_profile (
|
|
channel_account_id,
|
|
conversation_id,
|
|
ali_id,
|
|
name,
|
|
observed_at_ms,
|
|
profile_fingerprint,
|
|
observation_status,
|
|
received_at
|
|
) values (
|
|
${channelAccountId},
|
|
'direct-b',
|
|
'ali-b',
|
|
'Current second profile',
|
|
101,
|
|
'profile-b',
|
|
'confirmed',
|
|
${afterAsOf}
|
|
)
|
|
`;
|
|
const secondPage = await service.listConversations({
|
|
scope,
|
|
cursor: firstPage.page.nextCursor,
|
|
limit: 1,
|
|
});
|
|
assert.equal(secondPage.status, "accepted");
|
|
if (secondPage.status !== "accepted") return;
|
|
assert.deepEqual(
|
|
secondPage.conversations.map(({ conversationId }) => conversationId),
|
|
["direct-b"],
|
|
);
|
|
assert.equal(secondPage.conversations[0]?.name, "Current second profile");
|
|
assert.equal(secondPage.page.hasMore, true);
|
|
assert.ok(secondPage.page.nextCursor);
|
|
|
|
const filtered = await service.listConversations({
|
|
scope,
|
|
query: " alpha ",
|
|
limit: 10,
|
|
});
|
|
assert.equal(filtered.status, "accepted");
|
|
if (filtered.status !== "accepted") return;
|
|
assert.deepEqual(
|
|
filtered.conversations.map(({ conversationId }) => conversationId),
|
|
["direct-a"],
|
|
);
|
|
|
|
const profileFiltered = await service.listConversations({
|
|
scope,
|
|
query: "later",
|
|
limit: 1,
|
|
});
|
|
assert.equal(profileFiltered.status, "accepted");
|
|
if (profileFiltered.status !== "accepted") return;
|
|
assert.deepEqual(
|
|
profileFiltered.conversations.map(({ conversationId }) => conversationId),
|
|
["direct-c"],
|
|
);
|
|
assert.equal(profileFiltered.page.hasMore, false);
|
|
assert.equal(profileFiltered.page.nextCursor, null);
|
|
|
|
const detailWithoutProfile = await service.readConversation({
|
|
scope,
|
|
conversationId: "direct-b",
|
|
});
|
|
assert.equal(detailWithoutProfile.status, "accepted");
|
|
if (detailWithoutProfile.status !== "accepted") return;
|
|
assert.equal(detailWithoutProfile.conversation.avatarUrl, null);
|
|
|
|
const ordinaryHistory = await service.readHistory({
|
|
scope,
|
|
conversationId: "direct-a",
|
|
fromSentAtMs: 100,
|
|
toSentAtMs: 900,
|
|
limit: 2,
|
|
});
|
|
assert.equal(ordinaryHistory.status, "accepted");
|
|
if (ordinaryHistory.status !== "accepted") return;
|
|
assert.deepEqual(
|
|
ordinaryHistory.messages.map(({ messageId }) => messageId),
|
|
["latest-a", "latest-z"],
|
|
);
|
|
assert.equal(ordinaryHistory.page.hasMore, true);
|
|
assert.ok(ordinaryHistory.page.nextCursor);
|
|
const olderHistory = await service.readHistory({
|
|
scope,
|
|
conversationId: "direct-a",
|
|
fromSentAtMs: 100,
|
|
toSentAtMs: 900,
|
|
cursor: ordinaryHistory.page.nextCursor,
|
|
limit: 2,
|
|
});
|
|
assert.equal(olderHistory.status, "accepted");
|
|
if (olderHistory.status !== "accepted") return;
|
|
assert.deepEqual(
|
|
olderHistory.messages.map(({ messageId }) => messageId),
|
|
["anchor-only"],
|
|
);
|
|
assert.equal(olderHistory.page.hasMore, false);
|
|
assert.deepEqual(
|
|
await service.readHistory({
|
|
scope,
|
|
conversationId: "direct-a",
|
|
purpose: "communication_summary_read",
|
|
}),
|
|
{ status: "rejected", reason: "history_incomplete" },
|
|
);
|
|
} finally {
|
|
await runMigrations(databaseUrl);
|
|
await client`
|
|
delete from onetalk_message
|
|
where channel_account_id = ${channelAccountId}
|
|
`;
|
|
await client`
|
|
delete from onetalk_contact_profile
|
|
where channel_account_id = ${channelAccountId}
|
|
`;
|
|
await client`
|
|
delete from onetalk_conversation
|
|
where channel_account_id = ${channelAccountId}
|
|
`;
|
|
await client.end({ timeout: 5 });
|
|
await database.close();
|
|
}
|
|
},
|
|
);
|