Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/stores/accounts/accounts-actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2118,7 +2118,7 @@ describe("accounts-actions", () => {
await new Promise((r) => setTimeout(r, 150));
});

test("publishComment with clients.ipfsGateways triggers non-chainTicker callback", async () => {
test("publishComment with clients.ipfsGateways does not disrupt the pending comment state", async () => {
const account = Object.values(accountsStore.getState().accounts)[0];
const EventEmitter = (await import("events")).default;
const ipfsClient = new EventEmitter();
Expand All @@ -2143,8 +2143,7 @@ describe("accounts-actions", () => {

await new Promise((r) => setTimeout(r, 100));
const comments = accountsStore.getState().accountsComments[account.id] || [];
const commentWithClients = comments.find((c: any) => c.clients?.ipfsGateways);
expect(commentWithClients).toBeDefined();
expect(comments.find((c: any) => c.content === "ipfs")).toBeDefined();
});

test("publishComment publish throws: onError called", async () => {
Expand Down
83 changes: 71 additions & 12 deletions src/stores/accounts/accounts-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
fetchCommentLinkDimensions,
getAccountCommentDepth,
addShortAddressesToAccountComment,
sanitizeAccountCommentForState,
sanitizeStoredAccountComment,
} from "./utils";
import isEqual from "lodash.isequal";
Expand All @@ -58,6 +59,48 @@ type PublishSession = {
const activePublishSessions = new Map<string, PublishSession>();
const abandonedPublishSessionIds = new Set<string>();

const getClientsSnapshotForState = (clients: any): any => {
if (!clients || typeof clients !== "object") {
return undefined;
}
if (typeof clients.on === "function" || "state" in clients) {
return { state: clients.state };
}

const snapshot: Record<string, any> = {};
for (const key in clients) {
const childSnapshot = getClientsSnapshotForState(clients[key]);
if (childSnapshot !== undefined) {
snapshot[key] = childSnapshot;
}
}
return Object.keys(snapshot).length > 0 ? snapshot : undefined;
};

const syncCommentClientsSnapshot = (
publishSessionId: string,
accountId: string,
publication: any,
) => {
const session = getPublishSession(publishSessionId);
if (session?.currentIndex === undefined) {
return;
}

const snapshot = getClientsSnapshotForState(publication?.clients);
accountsStore.setState(({ accountsComments }) =>
maybeUpdateAccountComment(accountsComments, accountId, session.currentIndex, (ac, acc) => {
const updatedAccountComment = { ...acc };
if (snapshot === undefined) {
delete updatedAccountComment.clients;
} else {
updatedAccountComment.clients = snapshot;
}
ac[session.currentIndex] = updatedAccountComment;
}),
);
};
Comment on lines +62 to +102
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify test coverage for the new helper functions by checking if tests exercise the publishComment flow with clients state changes.

echo "=== Checking for tests that exercise publishComment with clients ==="
rg -n -C5 'publishComment|clients|statechange' --type=ts src/stores/accounts/accounts-actions.test.ts | head -100

echo ""
echo "=== Checking for direct tests of getClientsSnapshotForState (if exported elsewhere) ==="
rg -n 'getClientsSnapshotForState' --type=ts

echo ""
echo "=== Checking for syncCommentClientsSnapshot references in tests ==="
rg -n 'syncCommentClientsSnapshot' --type=ts

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 4548


🏁 Script executed:

# Get the publishComment tests and their assertions
sed -n '565,620p' src/stores/accounts/accounts-actions.test.ts

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 2030


🏁 Script executed:

# Also check around line 604 for the other publishComment test
sed -n '590,640p' src/stores/accounts/accounts-actions.test.ts

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 1985


🏁 Script executed:

# Search for any test that specifically mentions or checks 'clients' in the test file
rg -n 'clients' src/stores/accounts/accounts-actions.test.ts -A3 -B3

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 2501


🏁 Script executed:

# Look at the assertions in the clients-related tests
sed -n '2021,2084p' src/stores/accounts/accounts-actions.test.ts | tail -60

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 2512


🏁 Script executed:

# Check the other clients tests assertions
sed -n '2086,2150p' src/stores/accounts/accounts-actions.test.ts

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 2823


🏁 Script executed:

# Check if there are any assertions about clients being present/absent in the stored comments
rg -n 'expect.*clients' src/stores/accounts/accounts-actions.test.ts -B5 -A2

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 60


🏁 Script executed:

# Look at the complete test for clients.ipfsGateways to see all assertions
sed -n '2121,2160p' src/stores/accounts/accounts-actions.test.ts

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 1867


🏁 Script executed:

# Search for any assertion that checks whether clients are present or absent in stored comments
rg -n 'clients' src/stores/accounts/accounts-actions.test.ts | grep -i 'expect\|assert\|undefined\|delete'

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 60


🏁 Script executed:

# Check the full content of the test to see what it actually asserts about clients
sed -n '2121,2155p' src/stores/accounts/accounts-actions.test.ts

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 1748


🏁 Script executed:

# Check if there's any verification of the comment structure stored in accountsComments
rg -n 'accountsComments' src/stores/accounts/accounts-actions.test.ts -A2 -B2 | head -80

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 3603


🏁 Script executed:

# Check what assertions exist in the clients edge case test (after the state clear/emit)
sed -n '2086,2125p' src/stores/accounts/accounts-actions.test.ts

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 1833


🏁 Script executed:

# Also check the chainProviders test for assertions
sed -n '2021,2055p' src/stores/accounts/accounts-actions.test.ts

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 1588


🏁 Script executed:

# Check if there are any tests that specifically verify clients is NOT in the stored comment
rg -n 'NOT.*clients|!.*clients|undefined.*clients|delete.*clients' src/stores/accounts/accounts-actions.test.ts -B3 -A3

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 60


🏁 Script executed:

# Check for any assertions after the setTimeout in the chainProviders test
sed -n '2021,2080p' src/stores/accounts/accounts-actions.test.ts | tail -40

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 1851


🏁 Script executed:

# Search for tests that specifically check what's NOT in the stored comment (clients should be removed or transformed)
rg -n 'toEqual|toStrictEqual' src/stores/accounts/accounts-actions.test.ts | grep -i 'client\|comment' -B2 -A2

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 60


🏁 Script executed:

# Get the full test file size and look for all test names containing "client"
wc -l src/stores/accounts/accounts-actions.test.ts

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 127


🏁 Script executed:

# Find all test names related to clients
rg -n 'test\(' src/stores/accounts/accounts-actions.test.ts | grep -i 'client'

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 508


🏁 Script executed:

# Check what the storedComment structure looks like in tests - does it verify clients?
sed -n '1345,1365p' src/stores/accounts/accounts-actions.test.ts

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 975


🏁 Script executed:

# Look at the line where getClientsSnapshotForState is used at line 1202
sed -n '1195,1210p' src/stores/accounts/accounts-actions.ts

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 1062


🏁 Script executed:

# Check if any test verifies the clients property in stored comments
rg -n '\\.clients' src/stores/accounts/accounts-actions.test.ts -B2 -A2

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 60


🏁 Script executed:

# Check if there are any test assertions that validate the clients snapshot behavior
rg -n 'expect.*storedComment' src/stores/accounts/accounts-actions.test.ts | head -20

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 487


🏁 Script executed:

# Look for tests that specifically verify clients property in stored comment
rg -n 'storedComment.*clients|clients.*stored' src/stores/accounts/accounts-actions.test.ts -B3 -A3

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 60


🏁 Script executed:

# Check the full test around line 2121 to see all its assertions
sed -n '2121,2160p' src/stores/accounts/accounts-actions.test.ts

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 1867


🏁 Script executed:

# Check if there are any assertions inside the client tests that we might have missed
sed -n '2021,2084p' src/stores/accounts/accounts-actions.test.ts | grep -n 'expect\|assert'

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 114


🏁 Script executed:

# Check the full chainProviders test to see if it has any assertions we missed
sed -n '2021,2055p' src/stores/accounts/accounts-actions.test.ts

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 1588


🏁 Script executed:

# Look for any test that validates the snapshot behavior or clients structure
rg -n 'getClientsSnapshot|snapshot' src/stores/accounts/accounts-actions.test.ts -B2 -A2

Repository: bitsocialnet/bitsocial-react-hooks

Length of output: 60


Add assertions to verify getClientsSnapshotForState snapshot behavior in tests.

The three publishComment tests that exercise clients (chainProviders, ipfsGateways, clientsOnStateChange) have no assertions verifying the snapshot transformation. Tests at lines 2021, 2086, and 2121 run publishComment with clients but don't assert on the resulting stored client snapshot. Add assertions to verify that:

  • Clients are properly snapshotted and stored in accountsComments
  • Undefined clients are handled correctly (deleted from stored comment)
  • The snapshot transformation works for nested clients structures

This is required per coding guidelines: mandatory 100% test coverage for stores must include verification of function behavior, not just execution of code paths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stores/accounts/accounts-actions.ts` around lines 62 - 102, Add
assertions in the three publishComment tests that exercise clients to validate
getClientsSnapshotForState behavior: after calling publishComment, read
accountsStore.getState().accountsComments and locate the stored comment for the
publish session index (use getPublishSession(publishSessionId).currentIndex or
the same index used in the test) and assert (1) when publication.clients
contains client objects with state/on, the accountsComments entry for that index
contains a clients snapshot equal to the expected shape produced by
getClientsSnapshotForState; (2) when publication.clients yields no snapshot
(getClientsSnapshotForState returns undefined), the stored comment has no
clients key (clients was deleted by syncCommentClientsSnapshot); and (3) for
nested client structures, assert the nested keys map to snapshots (state
objects) matching the transformation. Reference getClientsSnapshotForState,
syncCommentClientsSnapshot and maybeUpdateAccountComment to locate where to
assert stored results in accountsStore.


const accountOwnsCommunityLocally = (account: Account, communityAddress: string) => {
const localCommunityAddresses = getPlebbitCommunityAddresses(account.plebbit);
if (localCommunityAddresses.includes(communityAddress)) {
Expand Down Expand Up @@ -953,16 +996,19 @@ export const publishComment = async (
const isUpdate = savedOnce;
const session = getPublishSession(publishSessionId);
const currentIndex = session?.currentIndex ?? accountCommentIndex;
const sanitizedAccountComment = addShortAddressesToAccountComment(
const persistedAccountComment = addShortAddressesToAccountComment(
sanitizeStoredAccountComment(accountComment),
) as AccountComment;
const liveAccountComment = addShortAddressesToAccountComment(
sanitizeAccountCommentForState(accountComment),
) as AccountComment;
const liveAccountComments = accountsStore.getState().accountsComments[account.id] || [];
if (isUpdate && !liveAccountComments[currentIndex]) {
return;
}
await accountsDatabase.addAccountComment(
account.id,
sanitizedAccountComment,
persistedAccountComment,
isUpdate ? currentIndex : undefined,
);
savedOnce = true;
Expand All @@ -972,7 +1018,7 @@ export const publishComment = async (
return {};
}
accountComments[currentIndex] = {
...sanitizedAccountComment,
...liveAccountComment,
index: currentIndex,
accountId: account.id,
};
Expand All @@ -992,7 +1038,7 @@ export const publishComment = async (
accountId: account.id,
};
createdAccountComment = addShortAddressesToAccountComment(
sanitizeStoredAccountComment(createdAccountComment),
sanitizeAccountCommentForState(createdAccountComment),
);
await saveCreatedAccountComment(createdAccountComment);
publishCommentOptions._onPendingCommentIndex?.(accountCommentIndex, createdAccountComment);
Expand All @@ -1014,6 +1060,7 @@ export const publishComment = async (
await account.plebbit.createComment(createCommentOptions),
createCommentOptions,
);
syncCommentClientsSnapshot(publishSessionId, account.id, comment);
publishAndRetryFailedChallengeVerification();
log("accountsActions.publishComment", { createCommentOptions });
})();
Expand Down Expand Up @@ -1043,6 +1090,7 @@ export const publishComment = async (
await account.plebbit.createComment(createCommentOptions),
createCommentOptions,
);
syncCommentClientsSnapshot(publishSessionId, account.id, comment);
lastChallenge = undefined;
publishAndRetryFailedChallengeVerification();
} else {
Expand All @@ -1053,19 +1101,30 @@ export const publishComment = async (
if (!session || isPublishSessionAbandoned(publishSessionId)) return;
queueMicrotask(() => cleanupPublishSessionOnTerminal(publishSessionId));
if (challengeVerification?.commentUpdate?.cid) {
const commentWithCid = addShortAddressesToAccountComment(
const persistedCommentWithCid = addShortAddressesToAccountComment(
sanitizeStoredAccountComment(normalizePublicationOptionsForStore(comment as any)),
);
delete (commentWithCid as any).clients;
delete (commentWithCid as any).publishingState;
delete (commentWithCid as any).error;
delete (commentWithCid as any).errors;
await accountsDatabase.addAccountComment(account.id, commentWithCid, currentIndex);
const liveCommentWithCid = addShortAddressesToAccountComment(
sanitizeAccountCommentForState(normalizePublicationOptionsForStore(comment as any)),
);
delete (persistedCommentWithCid as any).clients;
delete (persistedCommentWithCid as any).publishingState;
delete (persistedCommentWithCid as any).error;
delete (persistedCommentWithCid as any).errors;
delete (liveCommentWithCid as any).clients;
delete (liveCommentWithCid as any).publishingState;
delete (liveCommentWithCid as any).error;
delete (liveCommentWithCid as any).errors;
await accountsDatabase.addAccountComment(
account.id,
persistedCommentWithCid,
currentIndex,
);
accountsStore.setState(
({ accountsComments, accountsCommentsIndexes, commentCidsToAccountsComments }) => {
const updatedAccountComments = [...accountsComments[account.id]];
const updatedAccountComment = {
...commentWithCid,
...liveCommentWithCid,
index: currentIndex,
accountId: account.id,
};
Expand Down Expand Up @@ -1140,7 +1199,7 @@ export const publishComment = async (
const currentIndex = session.currentIndex;
accountsStore.setState(({ accountsComments }) =>
maybeUpdateAccountComment(accountsComments, account.id, currentIndex, (ac, acc) => {
const clients = { ...comment.clients };
const clients = getClientsSnapshotForState(comment.clients) || {};
const client = { state: clientState };
if (chainTicker) {
const chainProviders = { ...clients[clientType][chainTicker], [clientUrl]: client };
Expand Down
124 changes: 120 additions & 4 deletions src/stores/accounts/accounts-database.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -769,7 +769,30 @@ describe("accounts-database", () => {
communityAddress: "sub",
timestamp: 1,
author: { address: "addr" },
clients: {
ipfsGateways: {
best: {
state: "stopped",
},
},
},
raw: {
comment: {
content: "legacy",
},
},
original: {
content: "legacy",
signature: { signature: "sig" },
},
replies: {
clients: {
ipfsGateways: {
best: {
state: "stopped",
},
},
},
pages: {
best: {
comments: [{ cid: "reply-1", content: "reply" }],
Expand All @@ -785,9 +808,17 @@ describe("accounts-database", () => {

expect(exported.accountComments[0].replies?.pages).toBeUndefined();
expect(exported.accountComments[0].replies?.pageCids).toEqual({ best: "page-1" });
expect(exported.accountComments[0].replies?.clients).toBeUndefined();
expect(exported.accountComments[0].clients).toBeUndefined();
expect(exported.accountComments[0].raw).toBeUndefined();
expect(exported.accountComments[0].original).toBeUndefined();
expect(storedComment.replies?.pages).toBeUndefined();
expect(storedComment.replies?.pageCids).toEqual({ best: "page-1" });
expect(await commentsDb.getItem("__storageVersion")).toBe(1);
expect(storedComment.replies?.clients).toBeUndefined();
expect(storedComment.clients).toBeUndefined();
expect(storedComment.raw).toBeUndefined();
expect(storedComment.original).toBeUndefined();
expect(await commentsDb.getItem("__storageVersion")).toBe(2);
});
});

Expand Down Expand Up @@ -815,7 +846,7 @@ describe("accounts-database", () => {
expect(comments[1].cid).toBe("cid2");
});

test("addAccountComment strips nested replies.pages payloads but keeps core comment fields", async () => {
test("addAccountComment strips nested runtime payloads but keeps renderable comment fields", async () => {
const acc = makeAccount({ id: "comment-slim", name: "CommentSlim" });
await accountsDatabase.addAccount(acc);
await accountsDatabase.addAccountComment(acc.id, {
Expand All @@ -824,20 +855,52 @@ describe("accounts-database", () => {
communityAddress: "sub",
timestamp: 1,
author: { address: "addr" },
clients: { ipfsGateways: { best: { state: "stopped" } } },
raw: { comment: { content: "hello" } },
replies: {
clients: { ipfsGateways: { best: { state: "stopped" } } },
pages: {
best: {
comments: [{ cid: "reply-1", content: "reply" }],
},
},
pageCids: { best: "page-1" },
},
edit: true,
original: {
content: "before edit",
title: "original title",
author: {
address: "addr",
wallets: {
eth: {
address: "0xabc",
},
},
},
signature: {
signature: "sig",
},
},
} as any);
const comments = await accountsDatabase.getAccountComments(acc.id);
const exported = JSON.parse(await accountsDatabase.getExportedAccountJson(acc.id));
expect(comments[0].replies?.pages).toBeUndefined();
expect(comments[0].replies?.clients).toBeUndefined();
expect(comments[0].replies?.pageCids).toEqual({ best: "page-1" });
expect(comments[0].clients).toBeUndefined();
expect(comments[0].raw).toBeUndefined();
expect(comments[0].original).toEqual({
content: "before edit",
title: "original title",
author: {
address: "addr",
},
});
expect(exported.accountComments[0].replies?.pages).toBeUndefined();
expect(exported.accountComments[0].replies?.clients).toBeUndefined();
expect(exported.accountComments[0].clients).toBeUndefined();
expect(exported.accountComments[0].raw).toBeUndefined();
});

test("getAccountComments compacts legacy stored comments on read", async () => {
Expand All @@ -850,7 +913,10 @@ describe("accounts-database", () => {
communityAddress: "sub",
timestamp: 1,
author: { address: "addr" },
clients: { ipfsGateways: { best: { state: "stopped" } } },
raw: { comment: { content: "legacy" } },
replies: {
clients: { ipfsGateways: { best: { state: "stopped" } } },
pages: {
best: {
comments: [{ cid: "reply-1", content: "reply" }],
Expand All @@ -866,9 +932,51 @@ describe("accounts-database", () => {

expect(comments[0].replies?.pages).toBeUndefined();
expect(comments[0].replies?.pageCids).toEqual({ best: "page-1" });
expect(comments[0].replies?.clients).toBeUndefined();
expect(comments[0].clients).toBeUndefined();
expect(comments[0].raw).toBeUndefined();
expect(storedComment.replies?.pages).toBeUndefined();
expect(storedComment.replies?.pageCids).toEqual({ best: "page-1" });
expect(await commentsDb.getItem("__storageVersion")).toBe(1);
expect(storedComment.replies?.clients).toBeUndefined();
expect(storedComment.clients).toBeUndefined();
expect(storedComment.raw).toBeUndefined();
expect(await commentsDb.getItem("__storageVersion")).toBe(2);
});

test("getAccountComments densifies sparse legacy comment slots during compaction", async () => {
const acc = makeAccount({ id: "legacy-sparse-comments", name: "LegacySparseComments" });
await accountsDatabase.addAccount(acc);
const commentsDb = createPerAccountDatabase("accountComments", acc.id);
await commentsDb.setItem("0", {
cid: "legacy-sparse-comment-1",
content: "legacy-1",
communityAddress: "sub",
timestamp: 1,
author: { address: "addr" },
});
await commentsDb.setItem("2", {
cid: "legacy-sparse-comment-2",
content: "legacy-2",
communityAddress: "sub",
timestamp: 2,
author: { address: "addr" },
raw: { comment: { content: "legacy-2" } },
});
await commentsDb.setItem("length", 3);

const comments = await accountsDatabase.getAccountComments(acc.id);

expect(comments).toHaveLength(2);
expect(comments[0].cid).toBe("legacy-sparse-comment-1");
expect(comments[1].cid).toBe("legacy-sparse-comment-2");
expect(await commentsDb.getItem("1")).toEqual(
expect.objectContaining({
cid: "legacy-sparse-comment-2",
}),
);
expect(await commentsDb.getItem("2")).toBeNull();
expect(await commentsDb.getItem("length")).toBe(2);
expect(await commentsDb.getItem("__storageVersion")).toBe(2);
});

test("deleteAccountComment compacts legacy stored comments before mutating", async () => {
Expand All @@ -881,7 +989,9 @@ describe("accounts-database", () => {
communityAddress: "sub",
timestamp: 1,
author: { address: "addr" },
clients: { ipfsGateways: { best: { state: "stopped" } } },
replies: {
clients: { ipfsGateways: { best: { state: "stopped" } } },
pages: {
best: {
comments: [{ cid: "reply-1", content: "reply" }],
Expand All @@ -896,7 +1006,9 @@ describe("accounts-database", () => {
communityAddress: "sub",
timestamp: 2,
author: { address: "addr" },
raw: { comment: { content: "legacy-2" } },
replies: {
clients: { ipfsGateways: { best: { state: "stopped" } } },
pages: {
best: {
comments: [{ cid: "reply-2", content: "reply" }],
Expand All @@ -914,10 +1026,14 @@ describe("accounts-database", () => {
expect(storedComment.cid).toBe("legacy-delete-comment-2");
expect(storedComment.replies?.pages).toBeUndefined();
expect(storedComment.replies?.pageCids).toEqual({ best: "page-2" });
expect(storedComment.replies?.clients).toBeUndefined();
expect(storedComment.raw).toBeUndefined();
expect(exported.accountComments).toHaveLength(1);
expect(exported.accountComments[0].replies?.pages).toBeUndefined();
expect(exported.accountComments[0].replies?.pageCids).toEqual({ best: "page-2" });
expect(await commentsDb.getItem("__storageVersion")).toBe(1);
expect(exported.accountComments[0].replies?.clients).toBeUndefined();
expect(exported.accountComments[0].raw).toBeUndefined();
expect(await commentsDb.getItem("__storageVersion")).toBe(2);
});

test("addAccountComment asserts accountCommentIndex < length", async () => {
Expand Down
Loading
Loading