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'sapp/,routes/,database/,resources/,composer.json, or rootpackage.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--headedflag) 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 code0or1respectively, 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_modulesat repo root).
File Structure
sidecars/reddit-post/package.json— new (declares@playwright/testdependency)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 viaafterEach)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/testavailable vianpx playwright test; a config that readsHEADLESS(defaulttrue) fromprocess.env, sets a single Chromium project, disables retries, and enablesscreenshot: '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)'sHEADLESSenv var behavior. -
Produces: a Playwright test named
"logs into Reddit and creates a post"that readsREDDIT_USERNAME,REDDIT_PASSWORD,SUBREDDIT,POST_TITLE,POST_BODYfromprocess.env; writes its outcome tosidecars/reddit-post/test-results/result.jsonas{"success": true, "url": string}or{"success": false, "error": string}via anafterEachhook 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 itstest-results/result.jsonoutput file. -
Produces: an executable
run.shaccepting--username=,--password=,--subreddit=,--title=,--body=, and optional--headed; on invocation it runs the Playwright test and, regardless of pass/fail, prints the contents oftest-results/result.jsonas the last line of stdout and exits with the same codenpx playwright testproduced. -
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
--headedoverride (Tasks 1 & 3), standalone project structure isolated from the Laravel app (Task 1), gitignore fornode_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 acrossplaywright.config.js(Task 1),reddit-post.spec.js(Task 2), andrun.sh(Task 3). The result file path (test-results/result.json, relative tosidecars/reddit-post/) is consistent between the test'swriteResult()and the wrapper'sRESULT_FILEvariable.