mirror of
https://github.com/sinanyuntu/trade-message-center.git
synced 2026-09-17 13:22:11 +08:00
447 lines
16 KiB
JavaScript
447 lines
16 KiB
JavaScript
// 验证 MAIN 图片下载、上传回调隔离、生命周期围栏与 45 秒终态。
|
|
|
|
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { sendOneTalkImage } from "../src/onetalk/main-page/image-send.ts";
|
|
import { createSendObservationCorrelator } from "../src/onetalk/main-page/message-observer/send-observation.ts";
|
|
|
|
const createClock = () => {
|
|
let now = 0;
|
|
let sequence = 0;
|
|
const timers = new Map();
|
|
const schedule = (callback, delay) => {
|
|
const id = ++sequence;
|
|
timers.set(id, { callback, due: now + delay });
|
|
return id;
|
|
};
|
|
const cancel = (id) => timers.delete(id);
|
|
const advance = (duration) => {
|
|
now += duration;
|
|
for (const [id, timer] of [...timers]) {
|
|
if (timer.due > now) continue;
|
|
timers.delete(id);
|
|
timer.callback();
|
|
}
|
|
};
|
|
return { now: () => now, schedule, cancel, advance };
|
|
};
|
|
|
|
const createPageWindow = () => ({
|
|
location: { href: "https://onetalk.alibaba.com/message/weblitePWA.htm" },
|
|
addEventListener: () => {},
|
|
currentUserAccountId: "seller-account",
|
|
__conversationListFullData__: [
|
|
{
|
|
cid: "conversation-1",
|
|
accountId: "buyer-account-1",
|
|
aliId: "buyer-ali-1",
|
|
contact: { aliId: "buyer-ali-1" },
|
|
owner: { accountId: "seller-account", aliId: "seller-ali" },
|
|
},
|
|
{
|
|
cid: "conversation-2",
|
|
accountId: "buyer-account-2",
|
|
aliId: "buyer-ali-2",
|
|
contact: { aliId: "buyer-ali-2" },
|
|
owner: { accountId: "seller-account", aliId: "seller-ali" },
|
|
},
|
|
],
|
|
});
|
|
|
|
const imageSource = (name = "image.jpg") => ({
|
|
kind: "image",
|
|
source: {
|
|
downloadUrl: "https://mind.example.test/bridge/image?temporary=secret",
|
|
fileName: name,
|
|
mimeType: "image/jpeg",
|
|
},
|
|
});
|
|
|
|
const observedImage = (conversationId, messageId, fileId) => ({
|
|
messageType: "new",
|
|
upstreamType: 1,
|
|
messageId,
|
|
conversationId,
|
|
senderId: "seller@icbu",
|
|
direction: "sent",
|
|
sentAtMs: 1,
|
|
content: {
|
|
version: 1,
|
|
kind: "image",
|
|
fileId,
|
|
extension: "jpg",
|
|
sizeBytes: 123,
|
|
isOriginal: true,
|
|
md5: `hash-${fileId}`,
|
|
previewUrl: null,
|
|
urlScope: "onetalk_session",
|
|
},
|
|
participantIds: ["buyer@icbu", "seller@icbu"],
|
|
readStatus: 0,
|
|
messageStatus: 1,
|
|
unreadCount: 0,
|
|
});
|
|
|
|
// Raw-only relation callback fixture: dimensions are hostile input and must not enter expected metadata.
|
|
const metadata = (fileId) => ({
|
|
mediaInfo: {
|
|
nodeSize: 123,
|
|
width: "ignored-raw-value",
|
|
height: -1,
|
|
md5: `hash-${fileId}`,
|
|
fileId,
|
|
},
|
|
});
|
|
|
|
const relationMetadata = (fileId) => ({
|
|
nodeSize: 123,
|
|
md5: `hash-${fileId}`,
|
|
id: 4_567_890,
|
|
materialType: "image",
|
|
});
|
|
|
|
const createUploader = () => {
|
|
const uploads = [];
|
|
const owner = {
|
|
sendFile: () => undefined,
|
|
sendFileToOss: (input) => {
|
|
uploads.push(input);
|
|
return undefined;
|
|
},
|
|
};
|
|
return {
|
|
uploads,
|
|
owner,
|
|
uploader: { owner, sendFile: owner.sendFile, sendFileToOss: owner.sendFileToOss },
|
|
};
|
|
};
|
|
|
|
test("confirms an actual pending image send from a matching no-dimension live observation", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const pageWindow = createPageWindow();
|
|
const clock = createClock();
|
|
const { uploader, owner, uploads } = createUploader();
|
|
const correlator = createSendObservationCorrelator({ ...clock, textTimeoutMs: 10 });
|
|
globalThis.fetch = async () => new Response(new Uint8Array([1, 2, 3]), { status: 200 });
|
|
try {
|
|
const result = sendOneTalkImage({
|
|
pageWindow,
|
|
conversationId: "conversation-2",
|
|
content: imageSource("final-name.jpg"),
|
|
sendObservation: correlator,
|
|
findUploader: () => uploader,
|
|
runtime: clock,
|
|
});
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.equal(uploads.length, 1);
|
|
assert.equal(uploads[0].file.name, "final-name.jpg");
|
|
assert.equal(uploads[0].file.type, "image/jpeg");
|
|
assert.equal(uploads[0].contact.cid, "conversation-2");
|
|
assert.deepEqual(uploads[0].fromTo, {
|
|
from: "seller-account",
|
|
to: "buyer-account-2",
|
|
fromAliId: "seller-ali",
|
|
toAliId: "buyer-ali-2",
|
|
});
|
|
// Production OneTalk calls sendFile(relationInfo, tmpKey, conversation, callback).
|
|
// relationInfo has nodeSize + md5 but no image dimensions.
|
|
void owner.sendFile(
|
|
relationMetadata("file-2"),
|
|
uploads[0].tmpKey,
|
|
{ cid: "conversation-2" },
|
|
() => undefined,
|
|
);
|
|
correlator.observe([observedImage("conversation-2", "message-2", "file-2")]);
|
|
const value = await result;
|
|
assert.equal(value.status, "confirmed_sent");
|
|
assert.equal(JSON.stringify(value).includes("mind.example.test"), false);
|
|
assert.equal(JSON.stringify(value).includes("temporary=secret"), false);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("binds default deadline timers to the page global instead of the runtime record", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const originalSetTimeout = globalThis.setTimeout;
|
|
const originalClearTimeout = globalThis.clearTimeout;
|
|
const pageWindow = createPageWindow();
|
|
const clock = createClock();
|
|
const { uploader, owner, uploads } = createUploader();
|
|
const receivers = [];
|
|
globalThis.fetch = async () => new Response(new Uint8Array([1]), { status: 200 });
|
|
globalThis.setTimeout = function (...args) {
|
|
receivers.push(this);
|
|
return originalSetTimeout.apply(globalThis, args);
|
|
};
|
|
globalThis.clearTimeout = function (...args) {
|
|
return originalClearTimeout.apply(globalThis, args);
|
|
};
|
|
try {
|
|
const correlator = createSendObservationCorrelator({ ...clock, textTimeoutMs: 10 });
|
|
const result = sendOneTalkImage({
|
|
pageWindow,
|
|
conversationId: "conversation-1",
|
|
content: imageSource(),
|
|
sendObservation: correlator,
|
|
findUploader: () => uploader,
|
|
});
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
void owner.sendFile(uploads[0].tmpKey, metadata("file-1"));
|
|
correlator.observe([observedImage("conversation-1", "message-1", "file-1")]);
|
|
assert.equal((await result).status, "confirmed_sent");
|
|
assert.equal(
|
|
receivers.every((receiver) => receiver === globalThis),
|
|
true,
|
|
);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
globalThis.setTimeout = originalSetTimeout;
|
|
globalThis.clearTimeout = originalClearTimeout;
|
|
}
|
|
});
|
|
|
|
test("keeps concurrent different-conversation uploads isolated by tmpKey", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const pageWindow = createPageWindow();
|
|
const clock = createClock();
|
|
const { uploader, owner, uploads } = createUploader();
|
|
const correlator = createSendObservationCorrelator({ ...clock, textTimeoutMs: 10 });
|
|
globalThis.fetch = async () => new Response(new Uint8Array([1]), { status: 200 });
|
|
try {
|
|
const first = sendOneTalkImage({
|
|
pageWindow,
|
|
conversationId: "conversation-1",
|
|
content: imageSource(),
|
|
sendObservation: correlator,
|
|
findUploader: () => uploader,
|
|
runtime: clock,
|
|
});
|
|
const second = sendOneTalkImage({
|
|
pageWindow,
|
|
conversationId: "conversation-2",
|
|
content: imageSource(),
|
|
sendObservation: correlator,
|
|
findUploader: () => uploader,
|
|
runtime: clock,
|
|
});
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.equal(uploads.length, 2);
|
|
void owner.sendFile(uploads[1].tmpKey, metadata("file-2"));
|
|
correlator.observe([observedImage("conversation-2", "message-2", "file-2")]);
|
|
void owner.sendFile(uploads[0].tmpKey, metadata("file-1"));
|
|
correlator.observe([observedImage("conversation-1", "message-1", "file-1")]);
|
|
assert.equal((await first).status, "confirmed_sent");
|
|
assert.equal((await second).status, "confirmed_sent");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("uses a deterministic 45-second image budget rather than the text observer timeout", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const pageWindow = createPageWindow();
|
|
const clock = createClock();
|
|
const { uploader } = createUploader();
|
|
const correlator = createSendObservationCorrelator({ ...clock, textTimeoutMs: 1 });
|
|
globalThis.fetch = async () => new Response(new Uint8Array([1]), { status: 200 });
|
|
try {
|
|
let settled = false;
|
|
const result = sendOneTalkImage({
|
|
pageWindow,
|
|
conversationId: "conversation-1",
|
|
content: imageSource(),
|
|
sendObservation: correlator,
|
|
findUploader: () => uploader,
|
|
runtime: clock,
|
|
}).then((value) => {
|
|
settled = true;
|
|
return value;
|
|
});
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
clock.advance(10_000);
|
|
await Promise.resolve();
|
|
assert.equal(settled, false);
|
|
clock.advance(35_000);
|
|
assert.deepEqual(await result, { status: "delivery_unknown", reason: "send_timeout" });
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("stops a source fetch held beyond the MAIN defensive command deadline before upload", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const pageWindow = createPageWindow();
|
|
const clock = createClock();
|
|
const { uploader, uploads } = createUploader();
|
|
globalThis.fetch = () => new Promise(() => {});
|
|
try {
|
|
const result = sendOneTalkImage({
|
|
pageWindow,
|
|
conversationId: "conversation-1",
|
|
content: imageSource(),
|
|
sendObservation: createSendObservationCorrelator({ ...clock, textTimeoutMs: 1 }),
|
|
findUploader: () => uploader,
|
|
runtime: clock,
|
|
});
|
|
clock.advance(45_000);
|
|
assert.deepEqual(await result, { status: "delivery_unknown", reason: "send_timeout" });
|
|
assert.deepEqual(uploads, []);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("invokes fetch for rejection and non-2xx, then fails closed before native upload", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const pageWindow = createPageWindow();
|
|
const { uploader, uploads } = createUploader();
|
|
let calls = 0;
|
|
try {
|
|
globalThis.fetch = async () => {
|
|
calls += 1;
|
|
throw new Error("network");
|
|
};
|
|
assert.deepEqual(
|
|
await sendOneTalkImage({
|
|
pageWindow,
|
|
conversationId: "conversation-1",
|
|
content: imageSource(),
|
|
sendObservation: createSendObservationCorrelator(),
|
|
findUploader: () => uploader,
|
|
}),
|
|
{ status: "delivery_unknown", reason: "send_connection_lost" },
|
|
);
|
|
globalThis.fetch = async () => {
|
|
calls += 1;
|
|
return new Response(null, { status: 403 });
|
|
};
|
|
assert.deepEqual(
|
|
await sendOneTalkImage({
|
|
pageWindow,
|
|
conversationId: "conversation-1",
|
|
content: imageSource(),
|
|
sendObservation: createSendObservationCorrelator(),
|
|
findUploader: () => uploader,
|
|
}),
|
|
{ status: "delivery_unknown", reason: "send_connection_lost" },
|
|
);
|
|
assert.equal(calls, 2);
|
|
assert.deepEqual(uploads, []);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("normalizes a synchronous native uploader throw and cleans up its pending command", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const pageWindow = createPageWindow();
|
|
const { owner, uploader } = createUploader();
|
|
owner.sendFileToOss = () => {
|
|
throw new Error("native-uploader-throw");
|
|
};
|
|
uploader.sendFileToOss = owner.sendFileToOss;
|
|
globalThis.fetch = async () => new Response(new Uint8Array([1]), { status: 200 });
|
|
try {
|
|
assert.deepEqual(
|
|
await sendOneTalkImage({
|
|
pageWindow,
|
|
conversationId: "conversation-1",
|
|
content: imageSource(),
|
|
sendObservation: createSendObservationCorrelator(),
|
|
findUploader: () => uploader,
|
|
}),
|
|
{ status: "delivery_unknown", reason: "send_connection_lost" },
|
|
);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("fails closed when the logged-in account changes while the source fetch is pending", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const pageWindow = createPageWindow();
|
|
const { uploader, uploads } = createUploader();
|
|
let release;
|
|
globalThis.fetch = () =>
|
|
new Promise((resolve) => {
|
|
release = () => resolve(new Response(new Uint8Array([1]), { status: 200 }));
|
|
});
|
|
try {
|
|
const result = sendOneTalkImage({
|
|
pageWindow,
|
|
conversationId: "conversation-1",
|
|
content: imageSource(),
|
|
sendObservation: createSendObservationCorrelator(),
|
|
findUploader: () => uploader,
|
|
});
|
|
pageWindow.currentUserAccountId = "other-seller";
|
|
release();
|
|
assert.deepEqual(await result, {
|
|
status: "delivery_unknown",
|
|
reason: "send_connection_lost",
|
|
});
|
|
assert.deepEqual(uploads, []);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("fences target identity after Blob read and before the irreversible native send", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const pageWindow = createPageWindow();
|
|
const { uploader, owner, uploads } = createUploader();
|
|
let releaseBlob;
|
|
let originalSendCalls = 0;
|
|
owner.sendFile = () => {
|
|
originalSendCalls += 1;
|
|
return undefined;
|
|
};
|
|
uploader.sendFile = owner.sendFile;
|
|
globalThis.fetch = async () => ({
|
|
ok: true,
|
|
blob: () =>
|
|
new Promise((resolve) => {
|
|
releaseBlob = () => resolve(new Blob([new Uint8Array([1])]));
|
|
}),
|
|
});
|
|
try {
|
|
const blobBoundary = sendOneTalkImage({
|
|
pageWindow,
|
|
conversationId: "conversation-1",
|
|
content: imageSource(),
|
|
sendObservation: createSendObservationCorrelator(),
|
|
findUploader: () => uploader,
|
|
});
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
pageWindow.currentUserAccountId = "other-seller";
|
|
releaseBlob();
|
|
assert.deepEqual(await blobBoundary, {
|
|
status: "delivery_unknown",
|
|
reason: "send_connection_lost",
|
|
});
|
|
assert.deepEqual(uploads, []);
|
|
|
|
pageWindow.currentUserAccountId = "seller-account";
|
|
globalThis.fetch = async () => new Response(new Uint8Array([1]), { status: 200 });
|
|
const nativeBoundary = sendOneTalkImage({
|
|
pageWindow,
|
|
conversationId: "conversation-1",
|
|
content: imageSource(),
|
|
sendObservation: createSendObservationCorrelator(),
|
|
findUploader: () => uploader,
|
|
});
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
pageWindow.currentUserAccountId = "other-seller";
|
|
void owner.sendFile(uploads[0].tmpKey, metadata("file-1"));
|
|
assert.deepEqual(await nativeBoundary, {
|
|
status: "delivery_unknown",
|
|
reason: "send_connection_lost",
|
|
});
|
|
assert.equal(originalSendCalls, 0);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|