Skip to content

Add Xquik social posting service#18

Open
kriptoburak wants to merge 2 commits into
framerslab:masterfrom
kriptoburak:codex/add-xquik-social-posting-service
Open

Add Xquik social posting service#18
kriptoburak wants to merge 2 commits into
framerslab:masterfrom
kriptoburak:codex/add-xquik-social-posting-service

Conversation

@kriptoburak

@kriptoburak kriptoburak commented Jul 2, 2026

Copy link
Copy Markdown

Summary

  • Add an opt-in Xquik social posting service for the existing social-posting module.
  • Map successful create-tweet responses to AgentOS platform results.
  • Map pending confirmation responses to the existing pending platform result state.
  • Export the service and add focused tests with stubbed fetch calls.

Validation

  • pnpm exec vitest run tests/social-posting/XquikSocialPostingService.spec.ts
  • pnpm exec tsc --noEmit
  • pnpm exec prettier --check src/io/channels/social-posting/XquikSocialPostingService.ts src/io/channels/social-posting/index.ts tests/social-posting/XquikSocialPostingService.spec.ts
  • pnpm exec eslint src/io/channels/social-posting/XquikSocialPostingService.ts

Summary by Sourcery

Add a new Xquik-based social posting service and wire it into the existing social-posting module.

New Features:

  • Introduce an XquikSocialPostingService for publishing tweets via the Xquik API, including configurable account, API key, base URL, and platform mapping.
  • Expose XquikSocialPostingService and its related types through the social-posting module index for external use.

Tests:

  • Add unit tests for XquikSocialPostingService covering successful publishes, pending confirmations, and publishing adapted platform content from SocialPost objects.

Summary by CodeRabbit

  • New Features
    • Added Xquik/X posting support to publish posts directly from the app.
    • Automatic platform-aware content selection with support for attachments, media, replies, and optional overrides.
  • Bug Fixes
    • Improved handling of pending write confirmations to report posts as pending instead of failing.
    • Publishing now returns a consistent success result including the post link and published timestamp.
    • Unexpected or malformed responses are rejected with a clear error.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@sourcery-ai

sourcery-ai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Reviewer's Guide

Introduces a new Xquik-based social posting service wired into the existing social-posting module, including request/response mapping to the AgentOS social platform result model and focused tests using stubbed fetch calls.

Sequence diagram for XquikSocialPostingService.publishPost flow

sequenceDiagram
  participant Caller
  participant XquikSocialPostingService
  participant SocialAbstractService
  participant XquikAPI

  Caller->>XquikSocialPostingService: publishPost(post, options)
  XquikSocialPostingService->>XquikSocialPostingService: publish(input, options)
  XquikSocialPostingService->>XquikSocialPostingService: createRequestBody(input)
  XquikSocialPostingService->>SocialAbstractService: fetchJson(url, requestInit, options)
  SocialAbstractService->>XquikAPI: POST /api/v1/x/tweets
  XquikAPI-->>SocialAbstractService: XquikCreateTweetResponse
  SocialAbstractService-->>XquikSocialPostingService: XquikCreateTweetResponse
  alt [response.success]
    XquikSocialPostingService-->>Caller: SocialPostPlatformResult(status=success, postId, url)
  else [pending_confirmation]
    XquikSocialPostingService-->>Caller: SocialPostPlatformResult(status=pending)
  end
Loading

File-Level Changes

Change Details Files
Expose the new Xquik social posting service from the social-posting module entrypoint.
  • Switched barrel exports in the social-posting index file to double-quote style for consistency with the new file.
  • Exported XquikSocialPostingService along with its config and input types from the social-posting index module.
src/io/channels/social-posting/index.ts
Implement XquikSocialPostingService to call the Xquik create-tweet endpoint and map responses into SocialPostPlatformResult.
  • Defined XquikSocialPostingConfig and XquikPublishInput types extending the existing SocialServiceConfig and modeling Xquik tweet creation fields.
  • Added internal XquikCreateTweetBody and response-type interfaces (success vs pending) to strongly type the wire format.
  • Constructed the XquikSocialPostingService class that extends SocialAbstractService, normalizing baseUrl and platform defaults in the constructor.
  • Implemented publish() to POST a JSON body to /api/v1/x/tweets with x-api-key authentication, then map success responses to a successful SocialPostPlatformResult (including URL) and all other typed responses to a pending status.
  • Implemented publishPost() to derive XquikPublishInput from a SocialPost, preferring platform-specific adaptations over base content and forwarding media URLs.
  • Implemented createRequestBody() helper to transform the high-level publish input into the Xquik wire format, including optional fields only when provided.
src/io/channels/social-posting/XquikSocialPostingService.ts
Add focused tests covering XquikSocialPostingService behavior with mocked fetch responses.
  • Stubbed global fetch using vitest to simulate successful and pending Xquik tweet creation responses.
  • Verified that publish() hits the correct URL with the expected HTTP method, headers, and JSON body, and that it returns a properly shaped success platform result.
  • Verified that pending_confirmation responses map to a pending platform result and that empty text with media produces the expected request body.
  • Verified that publishPost() uses adapted platform content (platform-specific adaptation overriding twitter fallback and base content) when building the request payload.
tests/social-posting/XquikSocialPostingService.spec.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a new Xquik social posting service, exports its types from the social-posting barrel, and expands tests around request shaping, response handling, and platform-content fallback.

Changes

Xquik Social Posting Service

Layer / File(s) Summary
Service implementation and exports
src/io/channels/social-posting/XquikSocialPostingService.ts, src/io/channels/social-posting/index.ts
Adds the Xquik publishing service, its config/input types, response classification, request-body mapping, and barrel exports for the new service and types.
Behavior coverage
tests/social-posting/XquikSocialPostingService.spec.ts
Adds Vitest coverage for success and pending responses, optional field serialization, invalid-response rejection, and platform-specific content fallback in publishPost.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant XquikSocialPostingService
  participant XquikAPI

  Caller->>XquikSocialPostingService: publishPost(post, options)
  XquikSocialPostingService->>XquikSocialPostingService: createRequestBody(input)
  XquikSocialPostingService->>XquikAPI: POST /api/v1/x/tweets (x-api-key)
  XquikAPI-->>XquikSocialPostingService: tweet response or pending_confirmation
  XquikSocialPostingService-->>Caller: SocialPostPlatformResult (success or pending)
Loading

Related PRs: None identified.

Suggested labels: enhancement, social-posting

Suggested reviewers: None identified.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding the Xquik social posting service.
Description check ✅ Passed The description covers the summary and validation details, though the checklist and related issue section from the template are missing.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add Xquik social posting service with success/pending result mapping

✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Add an opt-in Xquik-backed social posting service for the social-posting module.
• Map Xquik create-tweet responses to platform "success" or "pending" results.
• Export the service and add Vitest coverage with stubbed fetch calls.
Diagram

graph TD
  A["SocialPostManager / caller"] --> B["XquikSocialPostingService"] --> C{{"Xquik API"}}
  C --> B --> D["SocialPostPlatformResult"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Preserve writeActionId in pending results
  • ➕ Enables future confirmation polling / reconciliation without additional storage
  • ➕ Improves observability/debuggability for pending confirmations
  • ➖ Requires extending SocialPostPlatformResult (or adding a metadata field) and updating downstream consumers
  • ➖ Slightly increases result-shape complexity across all platforms
2. Model pending confirmation as an explicit status subtype
  • ➕ Makes the "pending_confirmation" state explicit instead of generic "pending"
  • ➕ Allows richer UX/reporting for confirmation-required flows
  • ➖ Introduces a new state to handle across the SocialPostManager and any callers
  • ➖ May be premature if pending confirmations are rare or not acted upon

Recommendation: The current approach is reasonable for an opt-in first integration: treat non-success Xquik create-tweet responses as the existing generic "pending" result to fit the current platform state model. If the product intends to support confirmation follow-ups, consider extending the result shape to retain writeActionId (and optionally a dedicated pending-confirmation subtype) in a follow-up PR.

Files changed (3) +272 / -3

Enhancement (2) +156 / -3
XquikSocialPostingService.tsAdd Xquik service to publish tweets and map API results +147/-0

Add Xquik service to publish tweets and map API results

• Introduces XquikSocialPostingService extending the shared SocialAbstractService. Implements publish() and publishPost() to call Xquik's create-tweet endpoint and map success responses to a normalized success result, while mapping confirmation-required responses to the existing pending state. Adds request-body shaping for optional media, replies, attachments, community, and note-tweet flags.

src/io/channels/social-posting/XquikSocialPostingService.ts

index.tsExport XquikSocialPostingService and related types +9/-3

Export XquikSocialPostingService and related types

• Re-exports the new Xquik service and its config/input types from the social-posting module entrypoint. Also normalizes quote style in existing export statements to match the new additions.

src/io/channels/social-posting/index.ts

Tests (1) +116 / -0
XquikSocialPostingService.spec.tsAdd Vitest coverage for Xquik publish and result mapping +116/-0

Add Vitest coverage for Xquik publish and result mapping

• Adds focused tests that stub global fetch to validate request URL/headers/body, success mapping to a platform result including URL, pending-confirmation mapping to a pending result, and publishPost() selection of adapted platform content.

tests/social-posting/XquikSocialPostingService.spec.ts

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jul 2, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Non-JSON response crashes ✓ Resolved 🐞 Bug ☼ Reliability
Description
XquikSocialPostingService.publish uses the in operator on the value returned from fetchJson; if
the response is non-JSON (or missing an application/json content-type), fetchJson returns a string
and "success" in response throws a TypeError. This causes publish() to fail unexpectedly on
otherwise "ok" HTTP responses with non-JSON bodies.
Code

src/io/channels/social-posting/XquikSocialPostingService.ts[R69-83]

+    const response = await this.fetchJson<XquikCreateTweetResponse>(
+      `${this.baseUrl}/api/v1/x/tweets`,
+      {
+        body: JSON.stringify(this.createRequestBody(input)),
+        headers: {
+          "content-type": "application/json",
+          "x-api-key": this.apiKey,
+        },
+        method: "POST",
+      },
+      options,
+    );
+
+    if ("success" in response && response.success) {
+      return {
Evidence
fetchJson can legally return a string when the upstream response is not JSON, and the new code uses
"success" in response without guarding for non-object values, which will throw at runtime.

src/io/channels/social-posting/SocialAbstractService.ts[64-77]
src/io/channels/social-posting/XquikSocialPostingService.ts[65-90]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`publish()` assumes `fetchJson()` always returns an object and immediately executes `"success" in response`. When `fetchJson()` returns a string (non-JSON / missing JSON content-type), this throws a `TypeError`.
### Issue Context
`SocialAbstractService.fetchJson()` returns `res.text()` (string) when the response content-type is not `application/json`.
### Fix Focus Areas
- src/io/channels/social-posting/XquikSocialPostingService.ts[65-96]
- src/io/channels/social-posting/SocialAbstractService.ts[64-77]
### Suggested fix
- After `fetchJson()`, add a runtime guard:
- If `typeof response !== "object" || response === null`, throw a descriptive error (include the raw response string if available).
- Only use the `in` operator / property access after the guard passes.
- (Optional) Add an `accept: application/json` header to reduce the odds of non-JSON responses.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Unexpected responses marked pending ✓ Resolved 🐞 Bug ≡ Correctness
Description
XquikSocialPostingService.publish returns {status:"pending"} for every response that is not
{success:true}, without validating that it matches the documented pending-confirmation shape. This
can misclassify real API failures (new/unknown JSON error payloads) as pending and prevent errors
from surfacing in SocialPostManager results.
Code

src/io/channels/social-posting/XquikSocialPostingService.ts[R82-95]

+    if ("success" in response && response.success) {
+      return {
+        platform: this.platform,
+        postId: response.tweetId,
+        publishedAt: new Date().toISOString(),
+        status: "success",
+        url: `https://x.com/i/web/status/${response.tweetId}`,
+      };
+    }
+
+    return {
+      platform: this.platform,
+      status: "pending",
+    };
Evidence
The code defines a narrow pending-confirmation response type but does not check for it, and instead
returns pending for any response that isn’t a success object.

src/io/channels/social-posting/XquikSocialPostingService.ts[35-50]
src/io/channels/social-posting/XquikSocialPostingService.ts[82-96]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`publish()` currently treats any non-success JSON response as "pending". This hides unexpected error payloads and can leave the post permanently in an ambiguous pending state.
### Issue Context
The code defines a specific pending-confirmation response shape (`error: "x_write_unconfirmed"`, `status: "pending_confirmation"`), but does not check for it before returning pending.
### Fix Focus Areas
- src/io/channels/social-posting/XquikSocialPostingService.ts[35-50]
- src/io/channels/social-posting/XquikSocialPostingService.ts[65-96]
### Suggested fix
- Replace the current fallback with explicit branching:
1) If `response.success === true`, return success.
2) Else if `response.error === "x_write_unconfirmed" && response.status === "pending_confirmation"`, return pending.
3) Else throw an Error like `Unexpected Xquik response: ${JSON.stringify(response)}` so callers/SocialPostManager capture it as a platform error.
- Consider adding a test case for an unknown JSON payload (e.g. `{ error: "invalid_api_key" }`) verifying it becomes an error (throw) rather than pending.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread src/io/channels/social-posting/XquikSocialPostingService.ts Outdated
Comment thread src/io/channels/social-posting/XquikSocialPostingService.ts Outdated

@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: 1

🧹 Nitpick comments (1)
tests/social-posting/XquikSocialPostingService.spec.ts (1)

44-79: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add coverage for a genuine (non-pending-confirmation) error response.

Current tests only exercise the success and exact pending_confirmation shapes. Since publish() currently maps any other response to "pending" (see comment on XquikSocialPostingService.ts Lines 82-95), a test asserting that a real error response yields status: "error" would have caught that gap.

🤖 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 `@tests/social-posting/XquikSocialPostingService.spec.ts` around lines 44 - 79,
Add a test in XquikSocialPostingService.spec.ts that covers a
non-pending-confirmation error response from XquikSocialPostingService.publish,
using the existing fetchMock/service setup. Verify that when the API returns a
real error shape (not the pending_confirmation case), publish() maps it to a
platform result with status "error" instead of "pending".
🤖 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/io/channels/social-posting/XquikSocialPostingService.ts`:
- Around line 82-95: The fallback in XquikSocialPostingService should not treat
every non-success response as pending, because that hides real failures and
drops pending metadata. Update the response handling in the posting method to
explicitly discriminate between the documented success and pending_confirmation
shapes, return pending only for a genuine pending_confirmation response while
preserving its writeActionId, and map any other unexpected or failed payload to
SocialPostPlatformResult.status = "error" with the available error details so
SocialPostManager can surface it correctly.

---

Nitpick comments:
In `@tests/social-posting/XquikSocialPostingService.spec.ts`:
- Around line 44-79: Add a test in XquikSocialPostingService.spec.ts that covers
a non-pending-confirmation error response from
XquikSocialPostingService.publish, using the existing fetchMock/service setup.
Verify that when the API returns a real error shape (not the
pending_confirmation case), publish() maps it to a platform result with status
"error" instead of "pending".
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 03be8d6c-e142-4240-a829-7ff04f3a8781

📥 Commits

Reviewing files that changed from the base of the PR and between fcb09d3 and 86303a3.

📒 Files selected for processing (3)
  • src/io/channels/social-posting/XquikSocialPostingService.ts
  • src/io/channels/social-posting/index.ts
  • tests/social-posting/XquikSocialPostingService.spec.ts

Comment on lines +82 to +95
if ("success" in response && response.success) {
return {
platform: this.platform,
postId: response.tweetId,
publishedAt: new Date().toISOString(),
status: "success",
url: `https://x.com/i/web/status/${response.tweetId}`,
};
}

return {
platform: this.platform,
status: "pending",
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Non-success responses are unconditionally treated as "pending", masking real errors.

The only two documented response shapes are success and pending_confirmation (Line 41-45), but the runtime response isn't validated against that discriminant before falling through. Any other error response (auth failure, rate limiting, validation error, unexpected payload) will also hit this branch and be reported as status: "pending" instead of status: "error", silently hiding failures from the rest of the posting pipeline (SocialPostPlatformResult.status per SocialPostManager.ts:58-71 supports an 'error' state specifically for this).

Additionally, the writeActionId from a genuine pending response is discarded — without it, there's no reference the caller can use later to reconcile/poll the tweet's final status.

🐛 Proposed fix to validate the discriminant and preserve error info
     if ("success" in response && response.success) {
       return {
         platform: this.platform,
         postId: response.tweetId,
         publishedAt: new Date().toISOString(),
         status: "success",
         url: `https://x.com/i/web/status/${response.tweetId}`,
       };
     }
 
-    return {
-      platform: this.platform,
-      status: "pending",
-    };
+    if (
+      "status" in response &&
+      response.status === "pending_confirmation"
+    ) {
+      return {
+        platform: this.platform,
+        status: "pending",
+      };
+    }
+
+    return {
+      error: "Unexpected response from Xquik create-tweet endpoint",
+      platform: this.platform,
+      status: "error",
+    };
🤖 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/io/channels/social-posting/XquikSocialPostingService.ts` around lines 82
- 95, The fallback in XquikSocialPostingService should not treat every
non-success response as pending, because that hides real failures and drops
pending metadata. Update the response handling in the posting method to
explicitly discriminate between the documented success and pending_confirmation
shapes, return pending only for a genuine pending_confirmation response while
preserving its writeActionId, and map any other unexpected or failed payload to
SocialPostPlatformResult.status = "error" with the available error details so
SocialPostManager can surface it correctly.

@sourcery-ai sourcery-ai 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.

Hey - I've found 3 issues, and left some high level feedback:

  • In publish, any non-success response is treated as pending; consider explicitly checking for the x_write_unconfirmed/pending_confirmation shape and surfacing unexpected response formats as errors to avoid silently misclassifying failures.
  • The returned url is always https://x.com/... even when platform and/or baseUrl are overridden; if these configurations are meant to support alternative environments or platforms, consider deriving the URL from config to keep the result consistent with the target platform.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `publish`, any non-`success` response is treated as `pending`; consider explicitly checking for the `x_write_unconfirmed`/`pending_confirmation` shape and surfacing unexpected response formats as errors to avoid silently misclassifying failures.
- The returned `url` is always `https://x.com/...` even when `platform` and/or `baseUrl` are overridden; if these configurations are meant to support alternative environments or platforms, consider deriving the URL from config to keep the result consistent with the target platform.

## Individual Comments

### Comment 1
<location path="tests/social-posting/XquikSocialPostingService.spec.ts" line_range="6" />
<code_context>
+import { XquikSocialPostingService } from "../../src/io/channels/social-posting/XquikSocialPostingService.js";
+import type { SocialPost } from "../../src/io/channels/social-posting/SocialPostManager.js";
+
+describe("XquikSocialPostingService", () => {
+  afterEach(() => {
+    vi.restoreAllMocks();
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for all optional Xquik request fields (replyToTweetId, attachmentUrl, communityId, isNoteTweet, mediaUrls, account override) to lock in the camel→snake mapping behavior.

Right now the tests only cover `text` and the `mediaUrls`-when-empty behavior. Please add one or two focused tests that build an `XquikPublishInput` with these optional properties set and assert that the resulting `XquikCreateTweetBody` matches the expected shape. That will lock in the mapping behavior and guard against regressions if the request-building logic changes.

Suggested implementation:

```typescript
describe("XquikSocialPostingService", () => {
  afterEach(() => {
    vi.restoreAllMocks();
  });

  it("maps optional Xquik publish input fields to snake_case request body", async () => {
    const fetchMock = vi.fn().mockResolvedValue(
      new Response(JSON.stringify({ success: true, tweetId: "12345" }), {
        headers: { "content-type": "application/json" },
        status: 200,
      }),
    );
    vi.stubGlobal("fetch", fetchMock);

    const service = new XquikSocialPostingService({
      apiBaseUrl: "https://xquik.example.com",
      apiKey: "test-api-key",
    });

    const post: SocialPost = {
      platform: "x",
      text: "Optional fields test",
      mediaUrls: ["https://example.com/image.png"],
      // Optional Xquik-specific fields we want to lock in
      replyToTweetId: "9876543210",
      attachmentUrl: "https://example.com/attachment.pdf",
      communityId: "community-123",
      isNoteTweet: true,
      accountOverride: "override-account-id",
    } as SocialPost;

    await service.publish(post);

    expect(fetchMock).toHaveBeenCalledTimes(1);
    const [, requestInit] = fetchMock.mock.calls[0];

    expect(requestInit).toBeDefined();
    const body = JSON.parse((requestInit as RequestInit).body as string);

    expect(body).toEqual({
      text: post.text,
      media_urls: post.mediaUrls,
      reply_to_tweet_id: post.replyToTweetId,
      attachment_url: post.attachmentUrl,
      community_id: post.communityId,
      is_note_tweet: post.isNoteTweet,
      account_override: post.accountOverride,
    });
  });

  it("publishes a tweet through the Xquik create tweet endpoint", async () => {

```

You may need to adjust:
1. The construction of `XquikSocialPostingService` (its constructor options) to match the actual implementation.
2. The `SocialPost` shape and property names (`replyToTweetId`, `attachmentUrl`, `communityId`, `isNoteTweet`, `accountOverride`) if your `SocialPost`/`XquikPublishInput` type uses different names.
3. The expected snake_case keys (`reply_to_tweet_id`, `attachment_url`, `community_id`, `is_note_tweet`, `account_override`) if the actual `XquikCreateTweetBody` uses slightly different field names.
4. The method name `publish` if the service exposes a different method for posting (e.g. `publishToX`, `publishTweet`, etc.).
</issue_to_address>

### Comment 2
<location path="tests/social-posting/XquikSocialPostingService.spec.ts" line_range="81" />
<code_context>
+    );
+  });
+
+  it("publishes the adapted platform content from a social post", async () => {
+    const fetchMock = vi.fn().mockResolvedValue(
+      new Response(JSON.stringify({ success: true, tweetId: "67890" }), {
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for the fallback order of `publishPost` content (platform adaptation → twitter adaptation → baseContent).

This only covers the happy path where `adaptations.x` exists. To validate the documented fallback logic, please add tests for: (1) `adaptations.x` missing but `adaptations.twitter` present, and (2) both missing so `baseContent` is used. Each should assert the exact body passed to `fetch`, as in the current test.

Suggested implementation:

```typescript
  it("publishes the adapted platform content from a social post", async () => {
    const fetchMock = vi.fn().mockResolvedValue(
      new Response(JSON.stringify({ success: true, tweetId: "67890" }), {
        headers: { "content-type": "application/json" },
        status: 200,
      }),
    );
    vi.stubGlobal("fetch", fetchMock);

    const service = new XquikSocialPostingService({
      account: "@agent",
      apiKey: "test-key",
      platform: "x",
    });

    const socialPost = {
      baseContent: "Base content",
      adaptations: {
        x: "X adaptation",
        twitter: "Twitter adaptation",
      },
      media: ["https://example.com/image.png"],
    };

    await service.publishPost(socialPost);

    const init = fetchMock.mock.calls[0]?.[1] as RequestInit;
    expect(init.body).toBe(
      JSON.stringify({
        account: "@agent",
        media: ["https://example.com/image.png"],
        content: "X adaptation",
      }),
    );
  });

  it("falls back to twitter adaptation when platform-specific adaptation is missing", async () => {
    const fetchMock = vi.fn().mockResolvedValue(
      new Response(JSON.stringify({ success: true, tweetId: "67890" }), {
        headers: { "content-type": "application/json" },
        status: 200,
      }),
    );
    vi.stubGlobal("fetch", fetchMock);

    const service = new XquikSocialPostingService({
      account: "@agent",
      apiKey: "test-key",
      platform: "x",
    });

    const socialPost = {
      baseContent: "Base content",
      adaptations: {
        twitter: "Twitter adaptation",
      },
      media: ["https://example.com/image.png"],
    };

    await service.publishPost(socialPost);

    const init = fetchMock.mock.calls[0]?.[1] as RequestInit;
    expect(init.body).toBe(
      JSON.stringify({
        account: "@agent",
        media: ["https://example.com/image.png"],
        content: "Twitter adaptation",
      }),
    );
  });

  it("falls back to baseContent when no adaptations are present", async () => {
    const fetchMock = vi.fn().mockResolvedValue(
      new Response(JSON.stringify({ success: true, tweetId: "67890" }), {
        headers: { "content-type": "application/json" },
        status: 200,
      }),
    );
    vi.stubGlobal("fetch", fetchMock);

    const service = new XquikSocialPostingService({
      account: "@agent",
      apiKey: "test-key",
      platform: "x",
    });

    const socialPost = {
      baseContent: "Base content",
      media: ["https://example.com/image.png"],
    };

    await service.publishPost(socialPost);

    const init = fetchMock.mock.calls[0]?.[1] as RequestInit;
    expect(init.body).toBe(
      JSON.stringify({
        account: "@agent",
        media: ["https://example.com/image.png"],
        content: "Base content",
      }),
    );
  });

```

- Align the `socialPost` shape with the actual type used in this file (e.g., if there is a `SocialPost` type or helper factory, use it instead of a raw object).
- If the existing happy-path test already creates a `socialPost` fixture differently (e.g., via a factory or with additional fields), reuse that pattern to avoid duplication and keep the tests consistent.
- If `publishPost` requires additional options/parameters in this file, ensure those are passed in these new tests in the same way as in the existing happy-path test.
</issue_to_address>

### Comment 3
<location path="tests/social-posting/XquikSocialPostingService.spec.ts" line_range="24-33" />
<code_context>
+      account: "@agent",
+      apiKey: "test-key",
+    });
+    const result = await service.publish({ text: "hello" });
+
+    expect(result).toMatchObject({
</code_context>
<issue_to_address>
**suggestion (testing):** Assert that `publishedAt` is set on successful platform results to fully validate the mapping contract.

Since this test is validating the Xquik → AgentOS mapping, it should also cover the `publishedAt` field that the implementation sets. Please add an assertion that `result.publishedAt` is defined (and ideally a valid ISO timestamp) so this part of the mapping can’t be accidentally removed or changed without test failures.

```suggestion
    const result = await service.publish({ text: "hello" });

    expect(result).toMatchObject({
      platform: "twitter",
      postId: "12345",
      status: "success",
      url: "https://x.com/i/web/status/12345",
    });

    expect(result.publishedAt).toBeDefined();
    expect(typeof result.publishedAt).toBe("string");
    expect(Number.isNaN(Date.parse(result.publishedAt as string))).toBe(false);

    const call = fetchMock.mock.calls[0];
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

import { XquikSocialPostingService } from "../../src/io/channels/social-posting/XquikSocialPostingService.js";
import type { SocialPost } from "../../src/io/channels/social-posting/SocialPostManager.js";

describe("XquikSocialPostingService", () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Add tests for all optional Xquik request fields (replyToTweetId, attachmentUrl, communityId, isNoteTweet, mediaUrls, account override) to lock in the camel→snake mapping behavior.

Right now the tests only cover text and the mediaUrls-when-empty behavior. Please add one or two focused tests that build an XquikPublishInput with these optional properties set and assert that the resulting XquikCreateTweetBody matches the expected shape. That will lock in the mapping behavior and guard against regressions if the request-building logic changes.

Suggested implementation:

describe("XquikSocialPostingService", () => {
  afterEach(() => {
    vi.restoreAllMocks();
  });

  it("maps optional Xquik publish input fields to snake_case request body", async () => {
    const fetchMock = vi.fn().mockResolvedValue(
      new Response(JSON.stringify({ success: true, tweetId: "12345" }), {
        headers: { "content-type": "application/json" },
        status: 200,
      }),
    );
    vi.stubGlobal("fetch", fetchMock);

    const service = new XquikSocialPostingService({
      apiBaseUrl: "https://xquik.example.com",
      apiKey: "test-api-key",
    });

    const post: SocialPost = {
      platform: "x",
      text: "Optional fields test",
      mediaUrls: ["https://example.com/image.png"],
      // Optional Xquik-specific fields we want to lock in
      replyToTweetId: "9876543210",
      attachmentUrl: "https://example.com/attachment.pdf",
      communityId: "community-123",
      isNoteTweet: true,
      accountOverride: "override-account-id",
    } as SocialPost;

    await service.publish(post);

    expect(fetchMock).toHaveBeenCalledTimes(1);
    const [, requestInit] = fetchMock.mock.calls[0];

    expect(requestInit).toBeDefined();
    const body = JSON.parse((requestInit as RequestInit).body as string);

    expect(body).toEqual({
      text: post.text,
      media_urls: post.mediaUrls,
      reply_to_tweet_id: post.replyToTweetId,
      attachment_url: post.attachmentUrl,
      community_id: post.communityId,
      is_note_tweet: post.isNoteTweet,
      account_override: post.accountOverride,
    });
  });

  it("publishes a tweet through the Xquik create tweet endpoint", async () => {

You may need to adjust:

  1. The construction of XquikSocialPostingService (its constructor options) to match the actual implementation.
  2. The SocialPost shape and property names (replyToTweetId, attachmentUrl, communityId, isNoteTweet, accountOverride) if your SocialPost/XquikPublishInput type uses different names.
  3. The expected snake_case keys (reply_to_tweet_id, attachment_url, community_id, is_note_tweet, account_override) if the actual XquikCreateTweetBody uses slightly different field names.
  4. The method name publish if the service exposes a different method for posting (e.g. publishToX, publishTweet, etc.).

);
});

it("publishes the adapted platform content from a social post", async () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Add tests for the fallback order of publishPost content (platform adaptation → twitter adaptation → baseContent).

This only covers the happy path where adaptations.x exists. To validate the documented fallback logic, please add tests for: (1) adaptations.x missing but adaptations.twitter present, and (2) both missing so baseContent is used. Each should assert the exact body passed to fetch, as in the current test.

Suggested implementation:

  it("publishes the adapted platform content from a social post", async () => {
    const fetchMock = vi.fn().mockResolvedValue(
      new Response(JSON.stringify({ success: true, tweetId: "67890" }), {
        headers: { "content-type": "application/json" },
        status: 200,
      }),
    );
    vi.stubGlobal("fetch", fetchMock);

    const service = new XquikSocialPostingService({
      account: "@agent",
      apiKey: "test-key",
      platform: "x",
    });

    const socialPost = {
      baseContent: "Base content",
      adaptations: {
        x: "X adaptation",
        twitter: "Twitter adaptation",
      },
      media: ["https://example.com/image.png"],
    };

    await service.publishPost(socialPost);

    const init = fetchMock.mock.calls[0]?.[1] as RequestInit;
    expect(init.body).toBe(
      JSON.stringify({
        account: "@agent",
        media: ["https://example.com/image.png"],
        content: "X adaptation",
      }),
    );
  });

  it("falls back to twitter adaptation when platform-specific adaptation is missing", async () => {
    const fetchMock = vi.fn().mockResolvedValue(
      new Response(JSON.stringify({ success: true, tweetId: "67890" }), {
        headers: { "content-type": "application/json" },
        status: 200,
      }),
    );
    vi.stubGlobal("fetch", fetchMock);

    const service = new XquikSocialPostingService({
      account: "@agent",
      apiKey: "test-key",
      platform: "x",
    });

    const socialPost = {
      baseContent: "Base content",
      adaptations: {
        twitter: "Twitter adaptation",
      },
      media: ["https://example.com/image.png"],
    };

    await service.publishPost(socialPost);

    const init = fetchMock.mock.calls[0]?.[1] as RequestInit;
    expect(init.body).toBe(
      JSON.stringify({
        account: "@agent",
        media: ["https://example.com/image.png"],
        content: "Twitter adaptation",
      }),
    );
  });

  it("falls back to baseContent when no adaptations are present", async () => {
    const fetchMock = vi.fn().mockResolvedValue(
      new Response(JSON.stringify({ success: true, tweetId: "67890" }), {
        headers: { "content-type": "application/json" },
        status: 200,
      }),
    );
    vi.stubGlobal("fetch", fetchMock);

    const service = new XquikSocialPostingService({
      account: "@agent",
      apiKey: "test-key",
      platform: "x",
    });

    const socialPost = {
      baseContent: "Base content",
      media: ["https://example.com/image.png"],
    };

    await service.publishPost(socialPost);

    const init = fetchMock.mock.calls[0]?.[1] as RequestInit;
    expect(init.body).toBe(
      JSON.stringify({
        account: "@agent",
        media: ["https://example.com/image.png"],
        content: "Base content",
      }),
    );
  });
  • Align the socialPost shape with the actual type used in this file (e.g., if there is a SocialPost type or helper factory, use it instead of a raw object).
  • If the existing happy-path test already creates a socialPost fixture differently (e.g., via a factory or with additional fields), reuse that pattern to avoid duplication and keep the tests consistent.
  • If publishPost requires additional options/parameters in this file, ensure those are passed in these new tests in the same way as in the existing happy-path test.

Comment on lines +24 to +33
const result = await service.publish({ text: "hello" });

expect(result).toMatchObject({
platform: "twitter",
postId: "12345",
status: "success",
url: "https://x.com/i/web/status/12345",
});

const call = fetchMock.mock.calls[0];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Assert that publishedAt is set on successful platform results to fully validate the mapping contract.

Since this test is validating the Xquik → AgentOS mapping, it should also cover the publishedAt field that the implementation sets. Please add an assertion that result.publishedAt is defined (and ideally a valid ISO timestamp) so this part of the mapping can’t be accidentally removed or changed without test failures.

Suggested change
const result = await service.publish({ text: "hello" });
expect(result).toMatchObject({
platform: "twitter",
postId: "12345",
status: "success",
url: "https://x.com/i/web/status/12345",
});
const call = fetchMock.mock.calls[0];
const result = await service.publish({ text: "hello" });
expect(result).toMatchObject({
platform: "twitter",
postId: "12345",
status: "success",
url: "https://x.com/i/web/status/12345",
});
expect(result.publishedAt).toBeDefined();
expect(typeof result.publishedAt).toBe("string");
expect(Number.isNaN(Date.parse(result.publishedAt as string))).toBe(false);
const call = fetchMock.mock.calls[0];

@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.

🧹 Nitpick comments (2)
tests/social-posting/XquikSocialPostingService.spec.ts (2)

41-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Body assertions are coupled to exact key insertion order.

Comparing init.body against JSON.stringify({...}) string literals passes only because the literal's key order in each test happens to match createRequestBody's field-insertion order. Any harmless reordering of fields in createRequestBody (e.g. moving text after media) would break these tests despite producing an equivalent JSON object.

♻️ Suggested pattern: parse then compare structurally
-    expect(init.body).toBe(
-      JSON.stringify({ account: "`@agent`", text: "hello" }),
-    );
+    expect(JSON.parse(init.body as string)).toEqual({
+      account: "`@agent`",
+      text: "hello",
+    });

Also applies to: 75-80, 108-118, 171-173, 216-221

🤖 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 `@tests/social-posting/XquikSocialPostingService.spec.ts` around lines 41 - 43,
The body checks in XquikSocialPostingService.spec.ts are relying on exact JSON
string order instead of the actual payload shape. Update the assertions around
createRequestBody/init.body to parse the body and compare the resulting object
structurally, so the tests stay stable if field insertion order changes. Apply
the same approach in all affected XquikSocialPostingService.spec.ts cases using
init.body and JSON.stringify literals.

154-166: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated SocialPost fixture construction across tests.

The post literals in these two tests differ by only a few fields (adaptations, baseContent, id, seedId). Consider extracting a small factory (e.g. buildTestPost(overrides)) to reduce repetition.

Also applies to: 190-209

🤖 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 `@tests/social-posting/XquikSocialPostingService.spec.ts` around lines 154 -
166, The SocialPost fixture is duplicated across multiple tests with only small
field differences, so extract a shared factory in
XquikSocialPostingService.spec.ts such as buildTestPost(overrides) and use it in
the affected test cases. Keep the default SocialPost shape in one place and pass
per-test overrides for fields like adaptations, baseContent, id, and seedId to
reduce repetition and make future changes easier.
🤖 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.

Nitpick comments:
In `@tests/social-posting/XquikSocialPostingService.spec.ts`:
- Around line 41-43: The body checks in XquikSocialPostingService.spec.ts are
relying on exact JSON string order instead of the actual payload shape. Update
the assertions around createRequestBody/init.body to parse the body and compare
the resulting object structurally, so the tests stay stable if field insertion
order changes. Apply the same approach in all affected
XquikSocialPostingService.spec.ts cases using init.body and JSON.stringify
literals.
- Around line 154-166: The SocialPost fixture is duplicated across multiple
tests with only small field differences, so extract a shared factory in
XquikSocialPostingService.spec.ts such as buildTestPost(overrides) and use it in
the affected test cases. Keep the default SocialPost shape in one place and pass
per-test overrides for fields like adaptations, baseContent, id, and seedId to
reduce repetition and make future changes easier.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 525b7592-8644-4b1a-b60d-845353115d96

📥 Commits

Reviewing files that changed from the base of the PR and between 86303a3 and 172bb06.

📒 Files selected for processing (2)
  • src/io/channels/social-posting/XquikSocialPostingService.ts
  • tests/social-posting/XquikSocialPostingService.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/io/channels/social-posting/XquikSocialPostingService.ts

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