perlsky is a Perl 5 implementation of an AT Protocol Personal Data Server.
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

Scaffold standalone smoke suite package

alice 3495ed31 ea8c4f24

+350
+2
README.md
··· 58 58 - DMs are intentionally deferred from the current browser-smoke tranche; see `docs/BROWSER_SMOKE.md` for current scope and rationale. 59 59 - Fresh-account creation is still available through the explicit `bootstrap-*` commands, but it is no longer the normal path for repeated browser smoke runs. 60 60 - Detailed browser-smoke workflow, current interaction coverage, and the env-gated `prove` wrapper live in `docs/BROWSER_SMOKE.md`. 61 + - Extraction work toward a cross-PDS standalone package now starts in `pds-smoke-suite/`, with bring-your-own-account and `perlsky` adapter helpers defining the neutral config boundary. 62 + - For now, `script/perlsky-browser-smoke` remains the active `perlsky` adapter and runtime entrypoint while the generic package boundary stabilizes. 61 63 62 64 Moderation and labels: 63 65
+76
pds-smoke-suite/README.md
··· 1 + # pds-smoke-suite 2 + 3 + This directory is the extraction staging area for a standalone `bsky.app` 4 + compatibility smoke suite that can be used by multiple PDS implementations, not 5 + just `perlsky`. 6 + 7 + ## Current Scope 8 + 9 + The existing browser automation is already strong enough to be useful outside 10 + this repo: 11 + 12 + - reusable-account `bsky.app` smoke flows 13 + - post, image post, like, repost, quote, reply, bookmark, follow 14 + - list lifecycle 15 + - profile edit and avatar upload 16 + - notifications checks 17 + - settings-depth flows 18 + - strict artifacts with screenshots, console output, failed requests, failed 19 + HTTP responses, and recent XRPC traffic 20 + 21 + DMs are intentionally deferred for now. The current suite is focused on stable 22 + social, list, and settings interactions first. 23 + 24 + ## Extraction Shape 25 + 26 + The target standalone project shape is: 27 + 28 + 1. Generic core browser flows and artifact handling 29 + 2. A bring-your-own-accounts mode with minimal configuration 30 + 3. Thin per-PDS adapters for provisioning and implementation-specific defaults 31 + 32 + Right now, the runtime still lives under `tools/browser-automation/`, while this 33 + directory captures the neutral config and adapter surface we want to preserve 34 + during extraction. 35 + 36 + ## Minimal Configuration Goal 37 + 38 + The default experience for other PDS developers should be: 39 + 40 + - provide a `pdsUrl` 41 + - provide one or two existing account credentials 42 + - optionally provide a `targetHandle` 43 + - run the suite against `bsky.app` 44 + 45 + Provisioning is intentionally adapter-specific. That means `perlsky` can keep a 46 + helpful invite/bootstrap path, while other PDSes like `rsky` or `pegasus` can 47 + add their own adapters without changing the core browser flows. 48 + 49 + ## Current Adapter Contract 50 + 51 + The staging helpers in `src/` model two layers: 52 + 53 + - `adapters/bring-your-own.mjs` 54 + For the lowest-friction mode where callers supply existing credentials 55 + - `adapters/perlsky.mjs` 56 + For `perlsky`-specific defaults like cleanup prefixes and adapter tagging 57 + 58 + The current config contract is intentionally small: 59 + 60 + - suite-level settings: 61 + `pdsUrl`, `artifactsDir`, `appUrl`, `publicApiUrl`, `targetHandle`, 62 + `publicCheckTimeoutMs`, `headless`, `strictErrors`, `publicChecks`, 63 + `browserExecutablePath`, `adapter` 64 + - account-level settings: 65 + `handle`, `password`, `birthdate`, `postText`, `mediaPostText`, `quoteText`, 66 + `replyText`, `profileNote`, `cleanupPostPrefixes` 67 + 68 + ## Planned Next Steps 69 + 70 + - move the actual browser runtime from `tools/browser-automation/` into this 71 + package 72 + - add package-owned CLI entrypoints for single-account and dual-account runs 73 + - keep `script/perlsky-browser-smoke` as a thin `perlsky` adapter over the 74 + generic package 75 + - revisit a JS-to-TS migration later, after the standalone package boundary is 76 + stable
+11
pds-smoke-suite/package.json
··· 1 + { 2 + "name": "pds-smoke-suite", 3 + "private": true, 4 + "type": "module", 5 + "exports": { 6 + ".": "./src/index.mjs", 7 + "./config": "./src/config.mjs", 8 + "./adapters/bring-your-own": "./src/adapters/bring-your-own.mjs", 9 + "./adapters/perlsky": "./src/adapters/perlsky.mjs" 10 + } 11 + }
+31
pds-smoke-suite/src/adapters/bring-your-own.mjs
··· 1 + import { 2 + createAccountConfig, 3 + createDualRunConfig, 4 + createSingleRunConfig, 5 + } from '../config.mjs'; 6 + 7 + export const createBringYourOwnAccount = (account = {}) => { 8 + return createAccountConfig(account); 9 + }; 10 + 11 + export const createBringYourOwnSingleConfig = ({ 12 + account, 13 + ...rest 14 + } = {}) => { 15 + return createSingleRunConfig({ 16 + ...rest, 17 + account: createBringYourOwnAccount(account), 18 + }); 19 + }; 20 + 21 + export const createBringYourOwnDualConfig = ({ 22 + primary, 23 + secondary, 24 + ...rest 25 + } = {}) => { 26 + return createDualRunConfig({ 27 + ...rest, 28 + primary: createBringYourOwnAccount(primary), 29 + secondary: createBringYourOwnAccount(secondary), 30 + }); 31 + };
+60
pds-smoke-suite/src/adapters/perlsky.mjs
··· 1 + import { 2 + createAccountConfig, 3 + createDualRunConfig, 4 + createSingleRunConfig, 5 + } from '../config.mjs'; 6 + 7 + export const PERLSKY_PRIMARY_CLEANUP_PREFIXES = Object.freeze([ 8 + 'perlsky browser smoke ', 9 + ]); 10 + 11 + export const PERLSKY_SECONDARY_CLEANUP_PREFIXES = Object.freeze([ 12 + 'perlsky browser secondary ', 13 + ]); 14 + 15 + export const createPerlskyAccountConfig = ({ 16 + role = 'primary', 17 + ...account 18 + } = {}) => { 19 + const cleanupPostPrefixes = role === 'secondary' 20 + ? PERLSKY_SECONDARY_CLEANUP_PREFIXES 21 + : PERLSKY_PRIMARY_CLEANUP_PREFIXES; 22 + 23 + return createAccountConfig({ 24 + cleanupPostPrefixes, 25 + ...account, 26 + }); 27 + }; 28 + 29 + export const createPerlskySingleConfig = ({ 30 + account, 31 + ...rest 32 + } = {}) => { 33 + return createSingleRunConfig({ 34 + ...rest, 35 + adapter: 'perlsky', 36 + account: createPerlskyAccountConfig({ 37 + role: 'primary', 38 + ...account, 39 + }), 40 + }); 41 + }; 42 + 43 + export const createPerlskyDualConfig = ({ 44 + primary, 45 + secondary, 46 + ...rest 47 + } = {}) => { 48 + return createDualRunConfig({ 49 + ...rest, 50 + adapter: 'perlsky', 51 + primary: createPerlskyAccountConfig({ 52 + role: 'primary', 53 + ...primary, 54 + }), 55 + secondary: createPerlskyAccountConfig({ 56 + role: 'secondary', 57 + ...secondary, 58 + }), 59 + }); 60 + };
+167
pds-smoke-suite/src/config.mjs
··· 1 + const DEFAULTS = { 2 + appUrl: 'https://bsky.app', 3 + publicApiUrl: 'https://public.api.bsky.app', 4 + publicCheckTimeoutMs: 180000, 5 + birthdate: '1990-01-01', 6 + headless: true, 7 + strictErrors: false, 8 + publicChecks: true, 9 + }; 10 + 11 + const requireString = (value, label) => { 12 + if (typeof value !== 'string' || value.trim() === '') { 13 + throw new Error(`${label} is required`); 14 + } 15 + return value; 16 + }; 17 + 18 + const optionalString = (value) => { 19 + if (value === undefined || value === null) { 20 + return undefined; 21 + } 22 + if (typeof value !== 'string') { 23 + throw new Error('optional string values must be strings when provided'); 24 + } 25 + const trimmed = value.trim(); 26 + return trimmed === '' ? undefined : trimmed; 27 + }; 28 + 29 + const normalizeCleanupPrefixes = (prefixes) => { 30 + if (prefixes === undefined) { 31 + return []; 32 + } 33 + if (!Array.isArray(prefixes)) { 34 + throw new Error('cleanupPostPrefixes must be an array when provided'); 35 + } 36 + return prefixes 37 + .map((value) => { 38 + if (typeof value !== 'string') { 39 + throw new Error('cleanup post prefixes must be strings'); 40 + } 41 + return value.length ? value : undefined; 42 + }) 43 + .filter(Boolean); 44 + }; 45 + 46 + export const createAccountConfig = ({ 47 + handle, 48 + password, 49 + birthdate = DEFAULTS.birthdate, 50 + postText, 51 + mediaPostText, 52 + quoteText, 53 + replyText, 54 + profileNote, 55 + cleanupPostPrefixes, 56 + ...rest 57 + } = {}) => { 58 + const normalized = { 59 + handle: requireString(handle, 'account.handle'), 60 + password: requireString(password, 'account.password'), 61 + birthdate: optionalString(birthdate) || DEFAULTS.birthdate, 62 + cleanupPostPrefixes: normalizeCleanupPrefixes(cleanupPostPrefixes), 63 + ...rest, 64 + }; 65 + 66 + const post = optionalString(postText); 67 + const mediaPost = optionalString(mediaPostText); 68 + const quote = optionalString(quoteText); 69 + const reply = optionalString(replyText); 70 + const note = optionalString(profileNote); 71 + 72 + if (post) { 73 + normalized.postText = post; 74 + } 75 + if (mediaPost) { 76 + normalized.mediaPostText = mediaPost; 77 + } 78 + if (quote) { 79 + normalized.quoteText = quote; 80 + } 81 + if (reply) { 82 + normalized.replyText = reply; 83 + } 84 + if (note) { 85 + normalized.profileNote = note; 86 + } 87 + 88 + return normalized; 89 + }; 90 + 91 + export const createSuiteConfig = ({ 92 + pdsUrl, 93 + artifactsDir, 94 + appUrl = DEFAULTS.appUrl, 95 + publicApiUrl = DEFAULTS.publicApiUrl, 96 + publicCheckTimeoutMs = DEFAULTS.publicCheckTimeoutMs, 97 + targetHandle, 98 + headless = DEFAULTS.headless, 99 + strictErrors = DEFAULTS.strictErrors, 100 + publicChecks = DEFAULTS.publicChecks, 101 + browserExecutablePath, 102 + adapter, 103 + ...rest 104 + } = {}) => { 105 + const normalized = { 106 + pdsUrl: requireString(pdsUrl, 'pdsUrl'), 107 + artifactsDir: requireString(artifactsDir, 'artifactsDir'), 108 + appUrl: optionalString(appUrl) || DEFAULTS.appUrl, 109 + publicApiUrl: optionalString(publicApiUrl) || DEFAULTS.publicApiUrl, 110 + publicCheckTimeoutMs: Number(publicCheckTimeoutMs || DEFAULTS.publicCheckTimeoutMs), 111 + headless: !!headless, 112 + strictErrors: !!strictErrors, 113 + publicChecks: !!publicChecks, 114 + ...rest, 115 + }; 116 + 117 + const maybeTarget = optionalString(targetHandle); 118 + if (maybeTarget) { 119 + normalized.targetHandle = maybeTarget; 120 + } 121 + 122 + const maybeBrowserExecutablePath = optionalString(browserExecutablePath); 123 + if (maybeBrowserExecutablePath) { 124 + normalized.browserExecutablePath = maybeBrowserExecutablePath; 125 + } 126 + 127 + const maybeAdapter = optionalString(adapter); 128 + if (maybeAdapter) { 129 + normalized.adapter = maybeAdapter; 130 + } 131 + 132 + return normalized; 133 + }; 134 + 135 + export const createSingleRunConfig = ({ 136 + account, 137 + editProfile = false, 138 + ...rest 139 + } = {}) => { 140 + return { 141 + ...createSuiteConfig(rest), 142 + ...createAccountConfig(account), 143 + editProfile: !!editProfile, 144 + }; 145 + }; 146 + 147 + export const createDualRunConfig = ({ 148 + primary, 149 + secondary, 150 + accountSource, 151 + ...rest 152 + } = {}) => { 153 + const normalized = { 154 + ...createSuiteConfig(rest), 155 + primary: createAccountConfig(primary), 156 + secondary: createAccountConfig(secondary), 157 + }; 158 + 159 + const maybeAccountSource = optionalString(accountSource); 160 + if (maybeAccountSource) { 161 + normalized.accountSource = maybeAccountSource; 162 + } 163 + 164 + return normalized; 165 + }; 166 + 167 + export const suiteDefaults = Object.freeze({ ...DEFAULTS });
+3
pds-smoke-suite/src/index.mjs
··· 1 + export * from './config.mjs'; 2 + export * from './adapters/bring-your-own.mjs'; 3 + export * from './adapters/perlsky.mjs';