fix(chat): preserve locked message content on unlock

This commit is contained in:
2026-07-01 10:47:55 +08:00
parent 9c45c7eb69
commit 617f1d4777
8 changed files with 130 additions and 29 deletions
@@ -480,7 +480,7 @@ describe("chatMachine transitions", () => {
historyMessages: [
{
id: "msg-private-locked",
content: "",
content: "Original private message content.",
isFromAI: true,
date: "2026-06-29",
locked: true,
@@ -492,7 +492,7 @@ describe("chatMachine transitions", () => {
unlockMessageOutput: {
messageId: "msg-private-locked",
response: makeUnlockPrivateResponse({
content: "This is the unlocked private message.",
content: "This response content must be ignored.",
}),
},
}),
@@ -518,7 +518,7 @@ describe("chatMachine transitions", () => {
expect(actor.getSnapshot().context.messages).toMatchObject([
{
id: "msg-private-locked",
content: "This is the unlocked private message.",
content: "Original private message content.",
locked: false,
lockReason: null,
lockedPrivate: false,
@@ -541,7 +541,7 @@ describe("chatMachine transitions", () => {
},
{
id: "msg-shared-id",
content: "",
content: "Original AI private message",
isFromAI: true,
date: "2026-06-29",
locked: true,
@@ -553,7 +553,7 @@ describe("chatMachine transitions", () => {
unlockMessageOutput: {
messageId: "msg-shared-id",
response: makeUnlockPrivateResponse({
content: "This is the unlocked private message.",
content: "This response content must be ignored.",
}),
},
}),
@@ -582,7 +582,7 @@ describe("chatMachine transitions", () => {
},
{
id: "msg-shared-id",
content: "This is the unlocked private message.",
content: "Original AI private message",
isFromAI: true,
locked: false,
lockReason: null,
@@ -594,6 +594,112 @@ describe("chatMachine transitions", () => {
actor.stop();
});
it("keeps image message text empty after a single image unlock succeeds", async () => {
const actor = createActor(
createTestChatMachine({
historyMessages: [
{
id: "msg-image-locked",
content: "",
isFromAI: true,
date: "2026-06-29",
imageUrl: "https://example.com/locked.jpg",
imagePaywalled: true,
locked: true,
lockReason: "image",
},
],
unlockMessageOutput: {
messageId: "msg-image-locked",
response: makeUnlockPrivateResponse({
content: "This text should not render below the image.",
}),
},
}),
).start();
actor.send({ type: "ChatUserLogin", token: "token" });
await waitFor(actor, (snapshot) =>
snapshot.matches({ userSession: "ready" }),
);
actor.send({
type: "ChatUnlockMessageRequested",
messageId: "msg-image-locked",
kind: "image",
});
await waitFor(actor, (snapshot) =>
snapshot.matches({ userSession: "ready" }),
);
expect(actor.getSnapshot().context.messages).toMatchObject([
{
id: "msg-image-locked",
content: "",
imageUrl: "https://example.com/locked.jpg",
imagePaywalled: false,
locked: false,
lockReason: null,
},
]);
actor.stop();
});
it("keeps voice message content unchanged after unlock succeeds", async () => {
const actor = createActor(
createTestChatMachine({
historyMessages: [
{
id: "msg-voice-locked",
content: "Original voice transcript.",
isFromAI: true,
date: "2026-06-29",
audioUrl: "https://example.com/voice.mp3",
locked: true,
lockReason: "voice_message",
privateMessageHint: "A voice message is waiting.",
},
],
unlockMessageOutput: {
messageId: "msg-voice-locked",
response: makeUnlockPrivateResponse({
content: "This response content must be ignored.",
}),
},
}),
).start();
actor.send({ type: "ChatUserLogin", token: "token" });
await waitFor(actor, (snapshot) =>
snapshot.matches({ userSession: "ready" }),
);
actor.send({
type: "ChatUnlockMessageRequested",
messageId: "msg-voice-locked",
kind: "voice",
});
await waitFor(actor, (snapshot) =>
snapshot.matches({ userSession: "ready" }),
);
expect(actor.getSnapshot().context.messages).toMatchObject([
{
id: "msg-voice-locked",
content: "Original voice transcript.",
audioUrl: "https://example.com/voice.mp3",
locked: false,
lockReason: null,
privateMessageHint: null,
},
]);
actor.stop();
});
it("requests payment when single message unlock lacks credits", async () => {
const actor = createActor(
createTestChatMachine({