Files
muninn/docs/superpowers/plans/2026-07-18-reddit-post-sidecar.md
T

18 KiB

Reddit Post Sidecar Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build a standalone Playwright acceptance test under sidecars/reddit-post/ that logs into Reddit and creates a text post in a given subreddit, invocable via a CLI wrapper script that accepts --username/--password/--subreddit/--title/--body/--headed flags and always emits a single JSON result line to stdout with a matching exit code.

Architecture: A self-contained Node project (own package.json, independent of the Laravel app's PHP/npm toolchain) using @playwright/test. One test file drives the browser: login → navigate to submit page → create text post → verify. The test writes its result (success+url, or failure+error) to a fixed JSON file inside its own working directory on every run, success or failure, via a test.afterEach hook — this guarantees the file always exists regardless of which assertion failed partway through. A bash wrapper script parses friendly CLI flags, exports them as the env vars the Playwright config/test read, shells out to npx playwright test, then reads the result file and echoes it as the final stdout line, exiting with Playwright's own exit code.

Tech Stack: Node.js (already installed, v22), @playwright/test (installed fresh in this sidecar's own node_modules), Chromium (installed via npx playwright install chromium), bash for the wrapper script.

Global Constraints

  • This sidecar lives entirely under sidecars/reddit-post/ — no changes to the Laravel app's app/, routes/, database/, resources/, composer.json, or root package.json.
  • Text posts only — no link/image/poll post support.
  • No interactive CAPTCHA/2FA handling — if login doesn't reach a logged-in state within a bounded timeout, the test fails with a descriptive error. No human-in-the-loop fallback.
  • Headless by default; HEADLESS=false (settable via the wrapper's --headed flag) switches to a visible browser.
  • Output contract, regardless of success or failure: the wrapper's last stdout line is exactly one JSON object — {"success": true, "url": "..."} on success, {"success": false, "error": "..."} on failure — with exit code 0 or 1 respectively, matching Playwright's own exit code.
  • No automated retries in the Playwright config — a retry would mask flakiness in the login/post flow rather than surface it as a real failure.
  • sidecars/reddit-post/node_modules/ and any Playwright output directories (test-results/, playwright-report/) must be gitignored; they are not currently covered by the repo's root .gitignore (which only ignores the anchored /node_modules at repo root).

File Structure

  • sidecars/reddit-post/package.json — new (declares @playwright/test dependency)
  • sidecars/reddit-post/playwright.config.js — new (headless-by-default config, no retries, screenshot-on-failure)
  • sidecars/reddit-post/tests/reddit-post.spec.js — new (the actual login+post test, writes result JSON via afterEach)
  • sidecars/reddit-post/run.sh — new (CLI wrapper: flags → env vars → npx playwright test → echo result JSON → matching exit code)
  • sidecars/reddit-post/README.md — new (one-time setup note: npm install && npx playwright install chromium; usage example)
  • sidecars/reddit-post/.gitignore — new (node_modules/, test-results/, playwright-report/)

Task 1: Sidecar project scaffold and Playwright config

Files:

  • Create: sidecars/reddit-post/package.json
  • Create: sidecars/reddit-post/playwright.config.js
  • Create: sidecars/reddit-post/.gitignore
  • Create: sidecars/reddit-post/README.md

Interfaces:

  • Produces: an installable Node project at sidecars/reddit-post/ with @playwright/test available via npx playwright test; a config that reads HEADLESS (default true) from process.env, sets a single Chromium project, disables retries, and enables screenshot: 'only-on-failure'.

  • Step 1: Create the package.json

Create sidecars/reddit-post/package.json:

{
  "name": "reddit-post-sidecar",
  "private": true,
  "version": "1.0.0",
  "description": "Playwright acceptance test that logs into Reddit and creates a text post, invoked via run.sh",
  "scripts": {
    "test": "playwright test"
  },
  "devDependencies": {
    "@playwright/test": "^1.61.1"
  }
}
  • Step 2: Install dependencies and the Chromium browser

Run: cd sidecars/reddit-post && npm install Expected: node_modules/@playwright/test installed, package-lock.json created.

Run: cd sidecars/reddit-post && npx playwright install chromium Expected: Chromium browser binary downloaded successfully (this can take a minute; look for "Chromium ... downloaded to ..." in the output).

  • Step 3: Create the Playwright config

Create sidecars/reddit-post/playwright.config.js:

// @ts-check
const { defineConfig, devices } = require('@playwright/test');

module.exports = defineConfig({
  testDir: './tests',
  timeout: 60_000,
  expect: { timeout: 15_000 },
  retries: 0,
  reporter: [['list']],
  use: {
    headless: process.env.HEADLESS !== 'false',
    screenshot: 'only-on-failure',
    trace: 'retain-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
});
  • Step 4: Create the sidecar's own .gitignore

Create sidecars/reddit-post/.gitignore:

node_modules/
test-results/
playwright-report/
  • Step 5: Create a short README

Create sidecars/reddit-post/README.md:

# Reddit Post Sidecar

A Playwright acceptance test that logs into Reddit with supplied credentials
and creates a text post in a given subreddit.

## One-time setup

    cd sidecars/reddit-post
    npm install
    npx playwright install chromium

## Usage

    ./run.sh --username=YOUR_USERNAME --password=YOUR_PASSWORD \
      --subreddit=test --title="Hello" --body="Post body text" [--headed]

Prints a single JSON line to stdout and exits 0 on success, exits 1 on
failure:

    {"success": true, "url": "https://www.reddit.com/r/test/comments/.../"}
    {"success": false, "error": "Login did not complete — possible CAPTCHA or 2FA prompt"}
  • Step 6: Verify the scaffold is installable and runnable (no test file yet, so expect "no tests found")

Run: cd sidecars/reddit-post && npx playwright test Expected: Playwright runs and reports "No tests found" (or similar) — this confirms the config loads without errors. This is NOT a failure at this step; there is no test file yet (Task 2 adds it).

  • Step 7: Commit
git add sidecars/reddit-post/package.json sidecars/reddit-post/playwright.config.js sidecars/reddit-post/.gitignore sidecars/reddit-post/README.md
git commit -m "chore: scaffold reddit-post Playwright sidecar project"

Note: package-lock.json and node_modules/ are created by Step 2 but node_modules/ is gitignored (Step 4) — only commit package-lock.json alongside the files above if it was generated (it should be):

git add sidecars/reddit-post/package-lock.json
git commit --amend --no-edit

Task 2: Login and post-creation Playwright test

Files:

  • Create: sidecars/reddit-post/tests/reddit-post.spec.js

Interfaces:

  • Consumes: playwright.config.js (Task 1)'s HEADLESS env var behavior.

  • Produces: a Playwright test named "logs into Reddit and creates a post" that reads REDDIT_USERNAME, REDDIT_PASSWORD, SUBREDDIT, POST_TITLE, POST_BODY from process.env; writes its outcome to sidecars/reddit-post/test-results/result.json as {"success": true, "url": string} or {"success": false, "error": string} via an afterEach hook that runs regardless of pass/fail.

  • Step 1: Write the test file

Create sidecars/reddit-post/tests/reddit-post.spec.js:

// @ts-check
const { test, expect } = require('@playwright/test');
const fs = require('fs');
const path = require('path');

const RESULT_PATH = path.join(__dirname, '..', 'test-results', 'result.json');

function writeResult(result) {
  fs.mkdirSync(path.dirname(RESULT_PATH), { recursive: true });
  fs.writeFileSync(RESULT_PATH, JSON.stringify(result));
}

test.describe('reddit post', () => {
  let outcome = { success: false, error: 'Test did not complete' };

  test.afterEach(async () => {
    writeResult(outcome);
  });

  test('logs into Reddit and creates a post', async ({ page }) => {
    const username = process.env.REDDIT_USERNAME;
    const password = process.env.REDDIT_PASSWORD;
    const subreddit = process.env.SUBREDDIT;
    const title = process.env.POST_TITLE;
    const body = process.env.POST_BODY;

    for (const [name, value] of Object.entries({
      REDDIT_USERNAME: username,
      REDDIT_PASSWORD: password,
      SUBREDDIT: subreddit,
      POST_TITLE: title,
      POST_BODY: body,
    })) {
      if (!value) {
        outcome = { success: false, error: `Missing required environment variable: ${name}` };
        throw new Error(outcome.error);
      }
    }

    try {
      await page.goto('https://www.reddit.com/login/');

      await page.fill('input[name="username"]', /** @type {string} */ (username));
      await page.fill('input[name="password"]', /** @type {string} */ (password));
      await page.click('button[type="submit"]');

      try {
        await expect(page.locator('span[name="avatar"], #USER_DROPDOWN_ID, [icon-name="account-circle-outline"]').first()).toBeVisible({ timeout: 20_000 });
      } catch {
        outcome = {
          success: false,
          error: 'Login did not complete — possible CAPTCHA, 2FA prompt, or invalid credentials',
        };
        throw new Error(outcome.error);
      }

      await page.goto(`https://www.reddit.com/r/${subreddit}/submit`);

      const textTab = page.getByRole('button', { name: /^Text$/i }).first();
      if (await textTab.isVisible({ timeout: 10_000 }).catch(() => false)) {
        await textTab.click();
      }

      await page.getByPlaceholder(/title/i).first().fill(/** @type {string} */ (title));

      const bodyField = page.locator('[data-testid="post-composer-rte"], div[role="textbox"]').first();
      await bodyField.click();
      await bodyField.fill(/** @type {string} */ (body));

      const submitButton = page.getByRole('button', { name: /^Post$/i }).first();
      await expect(submitButton).toBeEnabled({ timeout: 10_000 });
      await submitButton.click();

      try {
        await page.waitForURL(/\/comments\//, { timeout: 20_000 });
      } catch {
        outcome = { success: false, error: 'Post submission did not complete — no redirect to the new post' };
        throw new Error(outcome.error);
      }

      outcome = { success: true, url: page.url() };
      expect(outcome.success).toBe(true);
    } catch (err) {
      if (outcome.success !== false || !outcome.error) {
        outcome = { success: false, error: err instanceof Error ? err.message : String(err) };
      }
      throw err;
    }
  });
});
  • Step 2: Manually verify against a real (test) Reddit account

This step requires real Reddit credentials for a test account you control — do not use production credentials during development.

Run:

cd sidecars/reddit-post
REDDIT_USERNAME=your_test_username \
REDDIT_PASSWORD=your_test_password \
SUBREDDIT=test \
POST_TITLE="Sidecar test post" \
POST_BODY="Verifying the reddit-post sidecar works end to end." \
HEADLESS=false \
npx playwright test

Expected: browser opens (since HEADLESS=false), logs in, navigates to r/test/submit, fills and submits a text post, test passes. Confirm sidecars/reddit-post/test-results/result.json contains {"success": true, "url": "https://www.reddit.com/r/test/comments/..."}, and that the URL is a real, visitable post.

If Reddit's actual DOM selectors don't match (Reddit's UI changes frequently and the selectors above are best-effort), inspect the live page with Playwright's codegen tool to find correct selectors:

npx playwright codegen https://www.reddit.com/login/

Update the selectors in reddit-post.spec.js to match what you observe, then re-run until the flow passes end-to-end.

  • Step 3: Verify the failure path with wrong credentials

Run:

cd sidecars/reddit-post
REDDIT_USERNAME=your_test_username \
REDDIT_PASSWORD=definitely_wrong_password \
SUBREDDIT=test \
POST_TITLE="Should not post" \
POST_BODY="This should fail at login" \
HEADLESS=false \
npx playwright test

Expected: test fails (non-zero exit), test-results/result.json contains {"success": false, "error": "Login did not complete — possible CAPTCHA, 2FA prompt, or invalid credentials"} (or a similar login-failure message — exact wording depends on what Reddit shows for bad credentials, adjust the spec's login-detection logic in Step 1 if needed so this failure is distinguishable).

  • Step 4: Commit
git add sidecars/reddit-post/tests/reddit-post.spec.js
git commit -m "feat: add Reddit login and post-creation Playwright test"

Task 3: CLI wrapper script

Files:

  • Create: sidecars/reddit-post/run.sh

Interfaces:

  • Consumes: sidecars/reddit-post/tests/reddit-post.spec.js (Task 2)'s env var contract and its test-results/result.json output file.

  • Produces: an executable run.sh accepting --username=, --password=, --subreddit=, --title=, --body=, and optional --headed; on invocation it runs the Playwright test and, regardless of pass/fail, prints the contents of test-results/result.json as the last line of stdout and exits with the same code npx playwright test produced.

  • Step 1: Write the wrapper script

Create sidecars/reddit-post/run.sh:

#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"

USERNAME=""
PASSWORD=""
SUBREDDIT=""
TITLE=""
BODY=""
HEADLESS_FLAG="true"

for arg in "$@"; do
  case "$arg" in
    --username=*) USERNAME="${arg#*=}" ;;
    --password=*) PASSWORD="${arg#*=}" ;;
    --subreddit=*) SUBREDDIT="${arg#*=}" ;;
    --title=*) TITLE="${arg#*=}" ;;
    --body=*) BODY="${arg#*=}" ;;
    --headed) HEADLESS_FLAG="false" ;;
    *)
      echo "{\"success\": false, \"error\": \"Unknown argument: $arg\"}"
      exit 1
      ;;
  esac
done

MISSING=""
[ -z "$USERNAME" ] && MISSING="$MISSING --username"
[ -z "$PASSWORD" ] && MISSING="$MISSING --password"
[ -z "$SUBREDDIT" ] && MISSING="$MISSING --subreddit"
[ -z "$TITLE" ] && MISSING="$MISSING --title"
[ -z "$BODY" ] && MISSING="$MISSING --body"

if [ -n "$MISSING" ]; then
  echo "{\"success\": false, \"error\": \"Missing required argument(s):$MISSING\"}"
  exit 1
fi

export REDDIT_USERNAME="$USERNAME"
export REDDIT_PASSWORD="$PASSWORD"
export SUBREDDIT="$SUBREDDIT"
export POST_TITLE="$TITLE"
export POST_BODY="$BODY"
export HEADLESS="$HEADLESS_FLAG"

set +e
npx playwright test tests/reddit-post.spec.js
PLAYWRIGHT_EXIT_CODE=$?
set -e

RESULT_FILE="test-results/result.json"

if [ -f "$RESULT_FILE" ]; then
  cat "$RESULT_FILE"
  echo
else
  echo "{\"success\": false, \"error\": \"Test did not produce a result file\"}"
  exit 1
fi

exit "$PLAYWRIGHT_EXIT_CODE"
  • Step 2: Make it executable

Run: chmod +x sidecars/reddit-post/run.sh

  • Step 3: Verify the wrapper's argument parsing and missing-argument path (no real Reddit call needed for this check)

Run: cd sidecars/reddit-post && ./run.sh --username=x Expected: prints {"success": false, "error": "Missing required argument(s): --password --subreddit --title --body"} and exits 1. This confirms argument parsing and the missing-argument short-circuit work before ever invoking Playwright.

  • Step 4: Verify the full wrapper path against a real (test) Reddit account

Run (using real test-account credentials, same caution as Task 2 Step 2):

cd sidecars/reddit-post
./run.sh --username=your_test_username --password=your_test_password \
  --subreddit=test --title="Wrapper test post" \
  --body="Verifying run.sh end to end." --headed

Expected: last line of stdout is {"success": true, "url": "https://www.reddit.com/r/test/comments/..."}, exit code 0 (check with echo $? immediately after). Confirm the post is real and visible at that URL.

  • Step 5: Verify the wrapper's failure exit path

Run with intentionally wrong credentials:

cd sidecars/reddit-post
./run.sh --username=your_test_username --password=wrong \
  --subreddit=test --title="Should fail" --body="Should fail" --headed
echo "Exit code: $?"

Expected: last line of stdout is a {"success": false, "error": "..."} JSON object, exit code is 1.

  • Step 6: Commit
git add sidecars/reddit-post/run.sh
git commit -m "feat: add CLI wrapper script for the reddit-post sidecar"

Self-Review Notes

  • Spec coverage: Login with descriptive failure on non-completion (Task 2), text-only post creation with title/body (Task 2), subreddit argument (Tasks 2 & 3), JSON success/failure output contract with matching exit codes (Tasks 2 & 3), headless-by-default with --headed override (Tasks 1 & 3), standalone project structure isolated from the Laravel app (Task 1), gitignore for node_modules/Playwright output (Task 1) — all spec requirements are covered.
  • Placeholder scan: No TBD/TODO markers. Task 2's Reddit DOM selectors are explicitly flagged as best-effort with a concrete fallback procedure (Playwright codegen) rather than left as a vague "figure out selectors" placeholder — this is a real, actionable step, not an unfinished spec.
  • Type consistency: Env var names (REDDIT_USERNAME, REDDIT_PASSWORD, SUBREDDIT, POST_TITLE, POST_BODY, HEADLESS) are used identically across playwright.config.js (Task 1), reddit-post.spec.js (Task 2), and run.sh (Task 3). The result file path (test-results/result.json, relative to sidecars/reddit-post/) is consistent between the test's writeResult() and the wrapper's RESULT_FILE variable.