# 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`: ```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`: ```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`: ```markdown # 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** ```bash 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): ```bash 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`: ```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: ```bash 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: ```bash 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: ```bash 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** ```bash 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`: ```bash #!/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): ```bash 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: ```bash 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** ```bash 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.