Skip to content

fix(accounts): preserve challenge comment updates#60

Merged
tomcasaburi merged 2 commits into
masterfrom
fix/preserve-comment-update-moderation
Jun 4, 2026
Merged

fix(accounts): preserve challenge comment updates#60
tomcasaburi merged 2 commits into
masterfrom
fix/preserve-comment-update-moderation

Conversation

@tomcasaburi

@tomcasaburi tomcasaburi commented Jun 4, 2026

Copy link
Copy Markdown
Member

Summary

  • preserve challengeVerification.commentUpdate fields on published account comments
  • keep pending moderation metadata through the follow-up comment update watcher
  • add a regression test for pending approval reason persistence

Verification

  • yarn prettier
  • yarn build
  • yarn test
  • focused account actions regression test

Note: the repo-wide hooks/stores coverage verifier currently fails on pre-existing coverage gaps unrelated to this change.


Note

Medium Risk
Touches comment publish and persistence with security-sensitive filtering of commentUpdate; scope is limited to accounts publish flow and covered by a new regression test.

Overview
publishComment now merges challengeVerification.commentUpdate onto the in-flight publication and onto the cloned publication used for startUpdatingAccountCommentOnCommentUpdateEvents, so moderation fields (e.g. pendingApproval, reason, cid) reach stored and persisted account comments instead of being dropped after challenge success.

Merging uses a small helper that copies only own keys from commentUpdate and skips __proto__, constructor, and prototype to avoid prototype pollution from hostile update payloads.

A regression test simulates a moderated challenge verification (including polluted enumerable keys) and asserts callbacks, in-memory store, and database all keep the expected metadata without unsafe properties.

Reviewed by Cursor Bugbot for commit d5eea80. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Tests

    • Added a test verifying comment publishing during moderation challenge verification, including propagation and sanitization of comment updates.
  • Bug Fixes

    • Ensure moderation-provided comment updates are applied to both in-memory and persisted comments, and unsafe properties are ignored to prevent prototype pollution.

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: eee8d62d-1b0e-4a25-87e0-1a47ab646fdb

📥 Commits

Reviewing files that changed from the base of the PR and between 3148cd3 and d5eea80.

📒 Files selected for processing (2)
  • src/stores/accounts/accounts-actions.test.ts
  • src/stores/accounts/accounts-actions.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/stores/accounts/accounts-actions.ts
  • src/stores/accounts/accounts-actions.test.ts

📝 Walkthrough

Walkthrough

Adds a sanitizer-aware helper to apply challengeVerification.commentUpdate fields onto publications and wires it into publishComment at two points; includes a test that emits a challengeverification with a commentUpdate payload and asserts the in-memory and persisted comment records reflect the sanitized updates.

Changes

Challenge verification comment updates

Layer / File(s) Summary
Helper function and publishComment integration
src/stores/accounts/accounts-actions.ts
Adds applyChallengeVerificationCommentUpdateToPublication which validates commentUpdate is a non-array object and applies keys to a target publication while skipping unsafe property names. The helper is invoked in publishComment's challengeverification handler before calling onChallengeVerification, and reapplied after reconstructing updatingComment so subsequent listeners observe updated fields.
Test verification of comment update persistence
src/stores/accounts/accounts-actions.test.ts
Adds a Vitest that mocks createComment and simulates a challengeverification event emitting a commentUpdate (including prototype-like properties). The test polls the store and asserts onChallengeVerification is called with the payload, and that both the in-memory account comment and the DB-persisted comment include the expected sanitized fields and do not retain prototype-pollution properties.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A tiny helper hops in line,

it weeds the fields that cross the vine,
comments bloom clean, no proto-spill,
tests nod softly — all is still. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(accounts): preserve challenge comment updates' directly matches the main objective: preserving challengeVerification.commentUpdate fields on published account comments.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/preserve-comment-update-moderation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/stores/accounts/accounts-actions.test.ts (1)

520-528: 💤 Low value

Consider using a more robust wait utility instead of manual polling.

The manual polling loop works but could be replaced with a test utility like waitFor from @testing-library/react or the existing testUtils.createWaitFor. This would make the test more maintainable and reduce timeout/flakiness risks.

♻️ Example using existing test utility
-      const startedAt = Date.now();
-      while (Date.now() - startedAt < 2000) {
-        await act(async () => {});
-        const comment = accountsStore.getState().accountsComments[account.id]?.[0];
-        if (comment?.reason === "AI moderation reason") {
-          break;
-        }
-        await new Promise((resolve) => setTimeout(resolve, 25));
-      }
+      const rendered = renderHook(() => accountsStore.getState().accountsComments[account.id]?.[0]);
+      const waitFor = testUtils.createWaitFor(rendered);
+      await waitFor(() => rendered.result.current?.reason === "AI moderation reason");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stores/accounts/accounts-actions.test.ts` around lines 520 - 528, Replace
the manual polling loop that checks
accountsStore.getState().accountsComments[account.id]?.[0] with a robust waiting
utility (e.g., testing-library's waitFor or the repository's
testUtils.createWaitFor). Specifically, remove the while loop and use waitFor to
repeatedly assert that
accountsStore.getState().accountsComments[account.id]?.[0]?.reason === "AI
moderation reason" (or use createWaitFor to await that condition), ensuring you
still wrap any state-updating calls in act if needed and set an appropriate
timeout to avoid flakiness.
src/stores/accounts/accounts-actions.ts (1)

80-96: ⚡ Quick win

Consider stronger typing for the publication parameter.

The publication: any parameter reduces type safety. Consider using a more specific type or a generic constraint to ensure compile-time validation.

♻️ Suggested type improvement
-const applyChallengeVerificationCommentUpdateToPublication = (
-  challengeVerification: ChallengeVerification | undefined,
-  publication: any,
-) => {
+const applyChallengeVerificationCommentUpdateToPublication = <T extends Record<string, unknown>>(
+  challengeVerification: ChallengeVerification | undefined,
+  publication: T,
+): void => {

Or define an explicit publication interface if the shape is known.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stores/accounts/accounts-actions.ts` around lines 80 - 96, The function
applyChallengeVerificationCommentUpdateToPublication currently accepts
publication: any which loses type safety; change its signature to use a specific
Publication type or a generic like <T extends Record<string, unknown>> (or
Publication | null) so the compiler enforces the expected shape, update the
parameter in applyChallengeVerificationCommentUpdateToPublication to use that
type/generic, and adjust any callers to pass the correctly typed object; ensure
commentUpdate is typed (e.g., Partial<Publication>) so
Object.assign(publication, commentUpdate) is type-safe.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/stores/accounts/accounts-actions.test.ts`:
- Around line 481-556: Add direct unit tests that exercise
applyChallengeVerificationCommentUpdateToPublication's validation branches:
create tests calling applyChallengeVerificationCommentUpdateToPublication with
commentUpdate values null, undefined, [], non-object types (string/number), and
with publication values null, undefined, or non-object to assert no throw and no
unintended mutation; also add a test that passes a valid nested commentUpdate to
document the shallow-merge behavior. Export or import
applyChallengeVerificationCommentUpdateToPublication from accounts-actions.ts in
the test file and add assertions that the publication object remains unchanged
for invalid inputs and is shallow-merged for valid inputs to satisfy 100%
store/hooks coverage.

In `@src/stores/accounts/accounts-actions.ts`:
- Around line 80-96: The function
applyChallengeVerificationCommentUpdateToPublication uses
Object.assign(publication, commentUpdate) which can cause prototype pollution if
commentUpdate is untrusted; instead, validate and copy only allowed own
properties from commentUpdate into publication (e.g., iterate
Object.keys(commentUpdate) and only set keys that are in an explicit allow-list
or that are not dangerous like "__proto__", "constructor", "prototype"), or
create a new object with a null prototype and shallow-merge only sanitized
fields; update applyChallengeVerificationCommentUpdateToPublication to perform
that safe copy and document the trust assumption if you rely on prior
sanitization.

---

Nitpick comments:
In `@src/stores/accounts/accounts-actions.test.ts`:
- Around line 520-528: Replace the manual polling loop that checks
accountsStore.getState().accountsComments[account.id]?.[0] with a robust waiting
utility (e.g., testing-library's waitFor or the repository's
testUtils.createWaitFor). Specifically, remove the while loop and use waitFor to
repeatedly assert that
accountsStore.getState().accountsComments[account.id]?.[0]?.reason === "AI
moderation reason" (or use createWaitFor to await that condition), ensuring you
still wrap any state-updating calls in act if needed and set an appropriate
timeout to avoid flakiness.

In `@src/stores/accounts/accounts-actions.ts`:
- Around line 80-96: The function
applyChallengeVerificationCommentUpdateToPublication currently accepts
publication: any which loses type safety; change its signature to use a specific
Publication type or a generic like <T extends Record<string, unknown>> (or
Publication | null) so the compiler enforces the expected shape, update the
parameter in applyChallengeVerificationCommentUpdateToPublication to use that
type/generic, and adjust any callers to pass the correctly typed object; ensure
commentUpdate is typed (e.g., Partial<Publication>) so
Object.assign(publication, commentUpdate) is type-safe.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9c311f87-421b-4cdf-8bb6-780cea83fc09

📥 Commits

Reviewing files that changed from the base of the PR and between a8e54df and 3148cd3.

📒 Files selected for processing (2)
  • src/stores/accounts/accounts-actions.test.ts
  • src/stores/accounts/accounts-actions.ts

Comment on lines +481 to +556
test("publishComment preserves challenge commentUpdate fields on the account comment", async () => {
await act(async () => {
await accountsActions.createAccount();
});
const account = Object.values(accountsStore.getState().accounts)[0];
const createComment = account.pkc.createComment.bind(account.pkc);
vi.spyOn(account.pkc, "createComment").mockImplementation(async (opts: any) => {
const publication: any = await createComment(opts);
vi.spyOn(publication, "simulateChallengeVerificationEvent").mockImplementation(() => {
const cid = "moderated comment cid";
publication.cid = cid;
publication.emit("challengeverification", {
type: "CHALLENGEVERIFICATION",
challengeRequestId: publication.challengeRequestId,
challengeAnswerId: publication.challengeAnswerId,
challengeSuccess: true,
commentUpdate: {
cid,
pendingApproval: true,
reason: "AI moderation reason",
removed: false,
},
});
publication.publishingState = "succeeded";
publication.emit("publishingstatechange", "succeeded");
});
return publication;
});
const onChallengeVerification = vi.fn();

await act(async () => {
await accountsActions.publishComment({
communityAddress: "sub.eth",
content: "moderated comment",
onChallenge: (challenge: any, comment: any) => comment.publishChallengeAnswers(["4"]),
onChallengeVerification,
});
});

const startedAt = Date.now();
while (Date.now() - startedAt < 2000) {
await act(async () => {});
const comment = accountsStore.getState().accountsComments[account.id]?.[0];
if (comment?.reason === "AI moderation reason") {
break;
}
await new Promise((resolve) => setTimeout(resolve, 25));
}

const storedComment = accountsStore.getState().accountsComments[account.id]?.[0];
expect(onChallengeVerification).toHaveBeenCalledWith(
expect.objectContaining({
commentUpdate: expect.objectContaining({ reason: "AI moderation reason" }),
}),
expect.objectContaining({
cid: "moderated comment cid",
pendingApproval: true,
reason: "AI moderation reason",
removed: false,
}),
);
expect(storedComment).toMatchObject({
cid: "moderated comment cid",
pendingApproval: true,
reason: "AI moderation reason",
removed: false,
});

const persistedComments = await accountsDatabase.getAccountComments(account.id);
expect(persistedComments[0]).toMatchObject({
cid: "moderated comment cid",
pendingApproval: true,
reason: "AI moderation reason",
removed: false,
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add direct unit tests for the helper function's edge cases.

The test validates the integration path but doesn't directly test applyChallengeVerificationCommentUpdateToPublication's validation logic (lines 85-92 in accounts-actions.ts). Per coding guidelines, stores require mandatory 100% test coverage.

Add test cases covering:

  • commentUpdate is null, undefined, or an array
  • commentUpdate is not an object (string, number, etc.)
  • publication is null, undefined, or not an object
  • Valid commentUpdate with nested objects (to document shallow-merge behavior)

Based on learnings: "Maintain mandatory 100% test coverage for hooks and stores (src/hooks/, src/stores/); every feature or bug fix in these areas must include/adjust tests to keep coverage at 100%, verified via coverage run + node scripts/verify-hooks-stores-coverage.mjs"

📋 Example edge-case tests
test("applyChallengeVerificationCommentUpdateToPublication handles invalid commentUpdate", () => {
  const publication = { content: "test" };
  
  // Should not throw or mutate when commentUpdate is invalid
  applyChallengeVerificationCommentUpdateToPublication(undefined, publication);
  applyChallengeVerificationCommentUpdateToPublication({ commentUpdate: null } as any, publication);
  applyChallengeVerificationCommentUpdateToPublication({ commentUpdate: [] } as any, publication);
  applyChallengeVerificationCommentUpdateToPublication({ commentUpdate: "invalid" } as any, publication);
  
  expect(publication).toEqual({ content: "test" });
});

test("applyChallengeVerificationCommentUpdateToPublication handles invalid publication", () => {
  const commentUpdate = { cid: "test", reason: "test" };
  
  // Should not throw when publication is invalid
  applyChallengeVerificationCommentUpdateToPublication({ commentUpdate } as any, null);
  applyChallengeVerificationCommentUpdateToPublication({ commentUpdate } as any, undefined);
  applyChallengeVerificationCommentUpdateToPublication({ commentUpdate } as any, "invalid" as any);
});

Note: You may need to export the helper function for testing, or add an integration test that exercises these branches via publishComment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stores/accounts/accounts-actions.test.ts` around lines 481 - 556, Add
direct unit tests that exercise
applyChallengeVerificationCommentUpdateToPublication's validation branches:
create tests calling applyChallengeVerificationCommentUpdateToPublication with
commentUpdate values null, undefined, [], non-object types (string/number), and
with publication values null, undefined, or non-object to assert no throw and no
unintended mutation; also add a test that passes a valid nested commentUpdate to
document the shallow-merge behavior. Export or import
applyChallengeVerificationCommentUpdateToPublication from accounts-actions.ts in
the test file and add assertions that the publication object remains unchanged
for invalid inputs and is shallow-merged for valid inputs to satisfy 100%
store/hooks coverage.

Comment thread src/stores/accounts/accounts-actions.ts
@tomcasaburi

Copy link
Copy Markdown
Member Author

Addressed the valid security review finding in d5eea80: commentUpdate fields are now copied key-by-key while skipping __proto__, constructor, and prototype, with the integration regression covering both live and persisted account comments.

Triage notes: I did not export the private helper just to unit-test invalid helper inputs; the integration path is the behavior this package exposes, and the repo-wide hooks/stores coverage verifier is currently failing on pre-existing coverage gaps unrelated to this PR. The polling/type-safety comments were treated as non-blocking nits; the helper signature is now narrower than any for the publication target.

@tomcasaburi tomcasaburi merged commit 73022ef into master Jun 4, 2026
8 checks passed
@tomcasaburi tomcasaburi deleted the fix/preserve-comment-update-moderation branch June 4, 2026 10:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant