@test-lab-ai/cli 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -120,8 +120,13 @@ each, defaulting to Claude Code if none are found.
120
120
 
121
121
  Add `--global` for Claude Code or Codex (installs under your home directory);
122
122
  Cursor user rules are set in Cursor's Settings. Restart your agent to load it.
123
- It installs from the public [`Test-Lab-ai/skills`](https://github.com/Test-Lab-ai/skills)
124
- mirror, so you always get the latest version.
123
+ The skill is **bundled with the CLI** and version-locked to it (offline, reproducible
124
+ installs); the public [`Test-Lab-ai/skills`](https://github.com/Test-Lab-ai/skills)
125
+ repo is the browsable source.
126
+
127
+ A skill change ships as a new CLI version, so `npm i -g @test-lab-ai/cli@latest`
128
+ updates it: the CLI auto-refreshes installed copies on first run after an upgrade,
129
+ and `testlab skills update` re-installs the bundled version on demand.
125
130
 
126
131
  ## For AI agents
127
132
 
package/bin/testlab.mjs CHANGED
@@ -23,8 +23,8 @@ import { apiFetch } from "../lib/api.mjs"
23
23
  import { loadImportFile, runImport } from "../lib/import.mjs"
24
24
  import { browserLogin } from "../lib/login.mjs"
25
25
  import { EXAMPLES_TEXT } from "../lib/examples.mjs"
26
- import { TESTLAB_SKILLS, AGENTS, detectAgents, installSkill } from "../lib/skills.mjs"
27
- import { checkForUpdate, currentVersion } from "../lib/update-check.mjs"
26
+ import { TESTLAB_SKILLS, AGENTS, detectAgents, installSkill, installedSkillLocations } from "../lib/skills.mjs"
27
+ import { checkForUpdate, currentVersion, previousRunVersion } from "../lib/update-check.mjs"
28
28
 
29
29
  const log = (...a) => console.log(...a)
30
30
  function errExit(msg) {
@@ -51,6 +51,7 @@ Usage:
51
51
  resource (designed for AI agents)
52
52
  testlab skills install [--agent ...] Install the test-lab-plan skill (auto-detects
53
53
  Claude/Codex/Cursor; --agent claude|codex|cursor|all)
54
+ testlab skills update Refresh installed skills (also auto-runs after a CLI upgrade)
54
55
 
55
56
  Options:
56
57
  --key <tl_…> API key (else $TESTLAB_API_KEY or ~/.test-lab/config.json)
@@ -392,6 +393,46 @@ function cmdSkillsList() {
392
393
  log(`\nInstall with: testlab skills install [name] [--agent ${AGENTS.join("|")}|all]`)
393
394
  }
394
395
 
396
+ async function cmdSkillsUpdate() {
397
+ let refreshed = 0
398
+ for (const name of TESTLAB_SKILLS) {
399
+ for (const loc of installedSkillLocations(name)) {
400
+ try {
401
+ const res = await installSkill(name, loc.agent, { global: loc.global })
402
+ refreshed++
403
+ log(`✓ ${name} → ${loc.agent}${loc.global ? " (global)" : ""}: ${res.dest}`)
404
+ } catch (e) {
405
+ log(` ✗ ${name} (${loc.agent}): ${e.message}`)
406
+ }
407
+ }
408
+ }
409
+ if (refreshed === 0) log("No installed skills found here. Run `testlab skills install` first.")
410
+ else log(`\nRefreshed ${refreshed} location(s). Restart your agent to load the changes.`)
411
+ }
412
+
413
+ // After a CLI upgrade, refresh any already-installed skills in place — runs once
414
+ // per version bump (best effort). Skipped in CI / when NO_UPDATE_NOTIFIER is set.
415
+ async function maybeRefreshSkillsOnUpgrade() {
416
+ if (process.env.CI || process.env.NO_UPDATE_NOTIFIER) return
417
+ const ver = currentVersion()
418
+ const prev = previousRunVersion(ver)
419
+ if (!prev || prev === ver) return
420
+ let refreshed = 0
421
+ for (const name of TESTLAB_SKILLS) {
422
+ for (const loc of installedSkillLocations(name)) {
423
+ try {
424
+ await installSkill(name, loc.agent, { global: loc.global })
425
+ refreshed++
426
+ } catch {
427
+ /* best effort — never break the actual command */
428
+ }
429
+ }
430
+ }
431
+ if (refreshed > 0) {
432
+ process.stderr.write(`↻ CLI upgraded ${prev} → ${ver}; refreshed ${refreshed} installed skill location(s).\n`)
433
+ }
434
+ }
435
+
395
436
  async function main() {
396
437
  let parsed
397
438
  try {
@@ -414,6 +455,8 @@ async function main() {
414
455
  return
415
456
  }
416
457
 
458
+ await maybeRefreshSkillsOnUpgrade()
459
+
417
460
  switch (args[0]) {
418
461
  case "login":
419
462
  return cmdLogin(flags)
@@ -441,8 +484,9 @@ async function main() {
441
484
  return cmdExamples()
442
485
  case "skills":
443
486
  if (args[1] === "install") return cmdSkillsInstall(flags, args)
487
+ if (args[1] === "update") return cmdSkillsUpdate()
444
488
  if (args[1] === "list") return cmdSkillsList()
445
- return errExit("usage: testlab skills <install|list> [name] [--global]")
489
+ return errExit("usage: testlab skills <install|update|list> [name] [--global]")
446
490
  case "import":
447
491
  return cmdImport(flags, args)
448
492
  default:
package/lib/skills.mjs CHANGED
@@ -1,7 +1,13 @@
1
1
  /**
2
- * Install test-lab skills (e.g. test-lab-plan) into a local AI coding agent by
3
- * fetching them from the public Test-Lab-ai/skills mirror. The mirror is the
4
- * single source of truth, so this always installs the latest published skill.
2
+ * Install test-lab skills (e.g. test-lab-plan) into a local AI coding agent from
3
+ * the copy BUNDLED with this CLI. The bundle lives at packages/cli/skills/ and is
4
+ * populated at publish time from the monorepo's skills/ (see scripts/bundle-skill.mjs);
5
+ * when running from source the install falls back to that monorepo copy.
6
+ *
7
+ * The skill is therefore version-locked to the CLI: a skill change ships as a new
8
+ * CLI version, so upgrading the CLI is what updates the skill (and the on-upgrade
9
+ * auto-refresh keeps installed copies current). The public Test-Lab-ai/skills repo
10
+ * stays as the browsable, open-source view; it is no longer fetched at runtime.
5
11
  *
6
12
  * Each agent has its own convention (verified against current docs):
7
13
  * claude → .claude/skills/<name>/SKILL.md (project) | ~/.claude/skills (--global)
@@ -9,19 +15,15 @@
9
15
  * cursor → .cursor/rules/<name>.mdc (project only — Cursor has no
10
16
  * global *file*; user rules live in Settings)
11
17
  *
12
- * claude + codex use the identical SKILL.md folder format, so they share the
13
- * write path (only the base dir differs). cursor uses a single .mdc rule file
14
- * with its own frontmatter, so we convert SKILL.md on the way out.
18
+ * claude + codex share the SKILL.md folder format (only the base dir differs);
19
+ * cursor gets a single .mdc rule with its own frontmatter, converted on the way out.
15
20
  *
16
- * Zero-dep: global fetch + node:fs.
21
+ * Zero-dep, no network.
17
22
  */
18
23
  import fs from "node:fs"
19
24
  import os from "node:os"
20
25
  import path from "node:path"
21
-
22
- const MIRROR = "Test-Lab-ai/skills"
23
- const MIRROR_BRANCH = "main"
24
- const UA = { "User-Agent": "test-lab-cli" }
26
+ import { fileURLToPath } from "node:url"
25
27
 
26
28
  // Skills published by test-lab. Add new skill directory names here.
27
29
  export const TESTLAB_SKILLS = ["test-lab-plan"]
@@ -51,6 +53,23 @@ export function detectAgents() {
51
53
  return [...found]
52
54
  }
53
55
 
56
+ /**
57
+ * Where skill `name` is already installed — project (cwd) and global locations
58
+ * across all agents — filtered to the paths that actually exist. Used to
59
+ * refresh installed skills (on `skills update` and after a CLI upgrade).
60
+ */
61
+ export function installedSkillLocations(name) {
62
+ const home = os.homedir()
63
+ const cwd = process.cwd()
64
+ return [
65
+ { agent: "claude", global: false, path: path.join(cwd, ".claude", "skills", name) },
66
+ { agent: "codex", global: false, path: path.join(cwd, ".agents", "skills", name) },
67
+ { agent: "cursor", global: false, path: path.join(cwd, ".cursor", "rules", `${name}.mdc`) },
68
+ { agent: "claude", global: true, path: path.join(home, ".claude", "skills", name) },
69
+ { agent: "codex", global: true, path: path.join(home, ".agents", "skills", name) },
70
+ ].filter((c) => fs.existsSync(c.path))
71
+ }
72
+
54
73
  /** Resolve the install target (base dir + format) for one agent. */
55
74
  export function agentTarget(agent, { global } = {}) {
56
75
  const home = os.homedir()
@@ -70,23 +89,24 @@ export function agentTarget(agent, { global } = {}) {
70
89
  }
71
90
  }
72
91
 
73
- async function listSkillFiles(name) {
74
- const r = await fetch(
75
- `https://api.github.com/repos/${MIRROR}/git/trees/${MIRROR_BRANCH}?recursive=1`,
76
- { headers: { ...UA, Accept: "application/vnd.github+json" } }
77
- )
78
- if (!r.ok) throw new Error(`could not list ${MIRROR} (${r.status})`)
79
- const tree = await r.json()
80
- const prefix = `skills/${name}/`
81
- return (tree.tree || [])
82
- .filter((e) => e.type === "blob" && typeof e.path === "string" && e.path.startsWith(prefix))
83
- .map((e) => e.path)
92
+ /** Locate the skill's source dir: the bundled copy, or the monorepo when run from source. */
93
+ function skillSourceDir(name) {
94
+ const bundled = fileURLToPath(new URL(`../skills/${name}`, import.meta.url)) // packages/cli/skills/<name>
95
+ if (fs.existsSync(bundled)) return bundled
96
+ const monorepo = fileURLToPath(new URL(`../../../skills/${name}`, import.meta.url)) // repo-root skills/<name>
97
+ if (fs.existsSync(monorepo)) return monorepo
98
+ throw new Error(`skill "${name}" is not bundled with this CLI`)
84
99
  }
85
100
 
86
- async function fetchText(p) {
87
- const r = await fetch(`https://raw.githubusercontent.com/${MIRROR}/${MIRROR_BRANCH}/${p}`, { headers: UA })
88
- if (!r.ok) throw new Error(`could not fetch ${p} (${r.status})`)
89
- return r.text()
101
+ /** Relative paths of every file under dir (recursive). */
102
+ function walkFiles(dir, base = dir) {
103
+ const out = []
104
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
105
+ const full = path.join(dir, entry.name)
106
+ if (entry.isDirectory()) out.push(...walkFiles(full, base))
107
+ else out.push(path.relative(base, full))
108
+ }
109
+ return out
90
110
  }
91
111
 
92
112
  /** Convert a Claude SKILL.md into a Cursor .mdc rule (Agent Requested mode). */
@@ -100,30 +120,26 @@ function toCursorRule(skillMd) {
100
120
  return `---\ndescription: ${description}\nalwaysApply: false\n---\n\n${body}\n`
101
121
  }
102
122
 
103
- /** Install one skill for one agent. Returns { name, agent, count, dest }. */
123
+ /** Install one skill (from the bundled copy) for one agent. Returns { name, agent, count, dest }. */
104
124
  export async function installSkill(name, agent, opts = {}) {
105
125
  const { kind, base } = agentTarget(agent, opts)
106
- const files = await listSkillFiles(name)
107
- if (files.length === 0) throw new Error(`skill "${name}" not found in ${MIRROR}`)
108
- const prefix = `skills/${name}/`
126
+ const srcDir = skillSourceDir(name)
109
127
 
110
128
  if (kind === "cursor-rule") {
111
- const skillPath = files.find((p) => p.endsWith("/SKILL.md"))
112
- if (!skillPath) throw new Error(`skill "${name}" has no SKILL.md`)
129
+ const skillMd = path.join(srcDir, "SKILL.md")
130
+ if (!fs.existsSync(skillMd)) throw new Error(`skill "${name}" has no SKILL.md`)
113
131
  const dest = path.join(base, `${name}.mdc`)
114
132
  fs.mkdirSync(base, { recursive: true })
115
- fs.writeFileSync(dest, toCursorRule(await fetchText(skillPath)))
133
+ fs.writeFileSync(dest, toCursorRule(fs.readFileSync(skillMd, "utf8")))
116
134
  return { name, agent, count: 1, dest }
117
135
  }
118
136
 
119
- // skill-dir: claude + codex (identical SKILL.md folder layout).
120
- let count = 0
121
- for (const p of files) {
122
- const rel = p.slice(prefix.length)
137
+ // skill-dir: claude + codex (full SKILL.md folder).
138
+ const files = walkFiles(srcDir)
139
+ for (const rel of files) {
123
140
  const dest = path.join(base, name, rel)
124
141
  fs.mkdirSync(path.dirname(dest), { recursive: true })
125
- fs.writeFileSync(dest, await fetchText(p))
126
- count++
142
+ fs.copyFileSync(path.join(srcDir, rel), dest)
127
143
  }
128
- return { name, agent, count, dest: path.join(base, name) }
144
+ return { name, agent, count: files.length, dest: path.join(base, name) }
129
145
  }
@@ -2,7 +2,7 @@
2
2
  * Best-effort "update available" notice.
3
3
  *
4
4
  * Design constraints: never block, slow, or break a command. It reads a cached
5
- * latest-version (checked against the npm registry at most once/day), prints a
5
+ * latest-version (checked against the npm registry at most every few hours), prints a
6
6
  * one-line notice to STDERR when the running version is behind, and kicks off a
7
7
  * background refresh for next time. Any failure (offline, slow/forbidden
8
8
  * registry) is swallowed. Suppressed when stderr isn't a TTY (pipes / CI /
@@ -15,7 +15,7 @@ import path from "node:path"
15
15
  const PKG = "@test-lab-ai/cli"
16
16
  const REGISTRY = "https://registry.npmjs.org/@test-lab-ai%2Fcli"
17
17
  const CACHE = path.join(os.homedir(), ".test-lab", "update-check.json")
18
- const DAY = 24 * 60 * 60 * 1000
18
+ const CHECK_TTL = 3 * 60 * 60 * 1000 // re-check npm at most every 3h
19
19
 
20
20
  export function currentVersion() {
21
21
  try {
@@ -70,16 +70,29 @@ export function checkForUpdate() {
70
70
  const notice = updateNotice(current, cache.latest)
71
71
  if (notice) process.stderr.write(notice + "\n")
72
72
 
73
- if (!cache.lastCheck || Date.now() - cache.lastCheck > DAY) {
73
+ if (!cache.lastCheck || Date.now() - cache.lastCheck > CHECK_TTL) {
74
74
  const ctrl = new AbortController()
75
75
  const timer = setTimeout(() => ctrl.abort(), 2000)
76
76
  fetch(REGISTRY, { signal: ctrl.signal, headers: { Accept: "application/vnd.npm.install-v1+json" } })
77
77
  .then((r) => (r.ok ? r.json() : null))
78
78
  .then((j) => {
79
79
  const latest = j && j["dist-tags"] && j["dist-tags"].latest
80
- if (latest) writeCache({ lastCheck: Date.now(), latest })
80
+ if (latest) writeCache({ ...cache, lastCheck: Date.now(), latest })
81
81
  })
82
82
  .catch(() => {})
83
83
  .finally(() => clearTimeout(timer))
84
84
  }
85
85
  }
86
+
87
+ /**
88
+ * Record the running CLI version and return the version seen on the PREVIOUS
89
+ * run (null on first run). Lets the CLI notice "I was just upgraded" so it can
90
+ * refresh installed skills. Merges into the shared cache so it doesn't clobber
91
+ * the update-check state.
92
+ */
93
+ export function previousRunVersion(current) {
94
+ const cache = readCache()
95
+ const prev = cache.cliVersion || null
96
+ if (prev !== current) writeCache({ ...cache, cliVersion: current })
97
+ return prev
98
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@test-lab-ai/cli",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Import existing test plans into test-lab.ai from the command line (or an AI agent).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,12 +13,13 @@
13
13
  "bin",
14
14
  "lib",
15
15
  "examples",
16
+ "skills",
16
17
  "README.md",
17
18
  "AGENTS.md"
18
19
  ],
19
20
  "scripts": {
20
21
  "test": "node test/toposort.test.mjs",
21
- "prepublishOnly": "npm test"
22
+ "prepublishOnly": "node scripts/bundle-skill.mjs && npm test"
22
23
  },
23
24
  "keywords": [
24
25
  "test-lab",
package/skills/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 test-lab.ai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,66 @@
1
+ # test-lab.ai skills
2
+
3
+ Claude Code (and Cursor / Codex / OpenCode / Cline / Warp / 50+ other agents) skills for [test-lab.ai](https://test-lab.ai), the AI QA platform that runs natural-language tests against websites.
4
+
5
+ This directory is the canonical source. The contents are mirrored to [github.com/Test-Lab-ai/skills](https://github.com/Test-Lab-ai/skills) on every push to `main`, and that mirror is what the install commands below point at.
6
+
7
+ ## Available skills
8
+
9
+ - **[`test-lab-plan`](./test-lab-plan/SKILL.md)** – Convert a flow description into a paste-ready test-lab.ai test plan with explicit URLs, numbered acceptance criteria, mode and agent type, and proper credential syntax. Slash command: `/test-lab-plan`.
10
+
11
+ More skills planned: result analysis, CI integration, pipeline design.
12
+
13
+ ## Install
14
+
15
+ ### Via the [`skills`](https://www.npmjs.com/package/skills) CLI (recommended)
16
+
17
+ Works for Claude Code, Cursor, Codex, OpenCode, Cline, Warp, and 50+ other agents:
18
+
19
+ ```sh
20
+ # Install all test-lab skills to a single agent (Claude Code shown)
21
+ npx skills add Test-Lab-ai/skills -a claude-code
22
+
23
+ # Install a specific skill
24
+ npx skills add Test-Lab-ai/skills --skill test-lab-plan -a claude-code
25
+
26
+ # Install globally (available across all projects)
27
+ npx skills add Test-Lab-ai/skills --skill test-lab-plan -g -a claude-code
28
+
29
+ # Update later
30
+ npx skills update test-lab-plan
31
+ ```
32
+
33
+ For Claude Code specifically, this drops the skill into `~/.claude/skills/test-lab-plan/` (with `-g`) or `<project>/.claude/skills/test-lab-plan/` (without `-g`).
34
+
35
+ ### Manually (Claude Code)
36
+
37
+ Clone or copy the skill directory into your Claude Code skills folder:
38
+
39
+ ```sh
40
+ # Global
41
+ git clone https://github.com/Test-Lab-ai/skills.git /tmp/test-lab-skills
42
+ cp -r /tmp/test-lab-skills/skills/test-lab-plan ~/.claude/skills/
43
+
44
+ # Or project-local
45
+ cp -r /tmp/test-lab-skills/skills/test-lab-plan <your-project>/.claude/skills/
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ After install, invoke via the slash command:
51
+
52
+ ```
53
+ /test-lab-plan
54
+ ```
55
+
56
+ Or just describe what you want to test ("write a test for our checkout flow at /cart") and the skill triggers on its own.
57
+
58
+ The skill outputs a copy-pasteable plan ready for the test-lab.ai dashboard. It does **not** submit plans on your behalf — see [`test-lab-plan/references/run-via-api.md`](./test-lab-plan/references/run-via-api.md) for the API contract if you want to trigger runs from CI.
59
+
60
+ ## Contributing
61
+
62
+ The canonical source lives in this directory of the [test-lab](https://github.com/AdrianNeatu/test-lab) monorepo. Edits here are auto-synced to the public mirror. PRs against the mirror should be opened against the monorepo instead.
63
+
64
+ ## License
65
+
66
+ MIT. See [LICENSE](./LICENSE).
@@ -0,0 +1,254 @@
1
+ ---
2
+ name: test-lab-plan
3
+ description: Write production-ready test plans for test-lab.ai, the AI QA platform that runs natural-language tests against websites. Use this skill whenever the user wants to write a test for test-lab.ai, draft a "natural language test" / "english test" / "AI test" / "test plan", set up acceptance criteria for a user flow, describe a journey to test, or generate prompts for the test-lab.ai dashboard. Trigger on mentions of test-lab, test-lab.ai, or testlab, and on requests like "test my login", "write a QA test for [page]", "smoke test for [flow]", or any browser-test description that does not reference Playwright/Cypress/Jest by name. Outputs a copy-pasteable test plan with explicit URLs, numbered acceptance criteria, mode + agent type recommendation, and {{credentials.<key>}} syntax for sensitive values.
4
+ allowed-tools:
5
+ - Read
6
+ - Glob
7
+ - Grep
8
+ - WebFetch
9
+ - Bash
10
+ - Write
11
+ ---
12
+
13
+ # test-lab.ai test plans
14
+
15
+ [test-lab.ai](https://test-lab.ai) is an AI QA platform. A test plan is a plain-English prompt that describes a single user flow; an AI agent reads the prompt, drives a real browser, and reports pass/fail against the acceptance criteria you wrote. Your job in this skill is to turn whatever the user describes into a paste-ready plan that follows the conventions below.
16
+
17
+ You are **writing the prompt** (you don't run the test). There are two ways your output gets used — offer the second whenever it's available:
18
+
19
+ 1. **Copy-paste** into the test-lab.ai dashboard (Test Plans → New) — the default.
20
+ 2. **Create it directly** with the `@test-lab-ai/cli` (`testlab`), which creates the credentials, labels, test data (data fixtures), AND the plan(s) in the user's account in one step. See **"Creating it with the CLI"** below.
21
+
22
+ Either way the design rules are identical (explicit URLs, declarative criteria, credentials syntax) — the CLI just uploads what you'd otherwise hand back to paste.
23
+
24
+ ## Workflow
25
+
26
+ Follow these steps in order. Each step has a reason — when you're tempted to skip one, re-read the reason.
27
+
28
+ ### 1. Gather scope
29
+
30
+ Ask (or infer from context) four things, then confirm before drafting:
31
+
32
+ - **The flow**: one sentence — "user signs up", "shopper checks out with a saved card", "admin disables a schedule"
33
+ - **The starting URL or path**: `/login`, `https://example.com/cart`, etc. The agent has to navigate somewhere to begin.
34
+ - **What success looks like**: the visible signal that the flow worked (redirect, message, element appearing). The agent needs verifiable acceptance criteria, not "verify it works."
35
+ - **Who performs it**: anonymous visitor, logged-in user, admin. If the flow needs a logged-in user or other setup state, configure that as a pre-step on the plan in the dashboard — don't add it to the prompt body (see references/syntax.md).
36
+
37
+ Why first: every later step depends on this. Drafting before scope is set produces plans that need rewriting.
38
+
39
+ #### Read the source if it's in the repo
40
+
41
+ If you're operating inside a repo that contains the target site's code, **read the relevant components and routes before drafting**. This is the difference between guessing and knowing. Use Glob / Grep / Read to find:
42
+
43
+ - The page or form component (often in `app/`, `components/`, `src/components/`, or similar)
44
+ - The API route handler the form posts to
45
+ - Shared widgets the form composes (captcha, modal, error renderer)
46
+
47
+ Anchor every acceptance criterion to real DOM text or real response shape. If the success state is "the form is replaced by a card with the heading 'Message Sent!'", say that exactly — not "a confirmation message appears."
48
+
49
+ If the source is **not** available (you're not in the repo, or it's a third-party site), pull a snapshot via WebFetch or ask the user for screenshots of the key states. Do not invent placeholder text — vague criteria like "a success banner appears" make the AI agent flag false negatives whenever copy changes.
50
+
51
+ ### 2. Pick the test mode
52
+
53
+ Two modes exist:
54
+
55
+ - **Quick** — single agent, ~10 steps max, ~2-5 min. Use for smoke tests, frequent CI runs, the happy path.
56
+ - **Deep** — multiple agents, 20-40+ steps, ~5-10 min. Use for critical pre-release flows, edge cases, sad paths, exploratory branches.
57
+
58
+ Default to **Quick** unless the user asked for thorough coverage or the flow has obvious sad paths the user wants exercised. A Quick plan with 30 acceptance criteria is wrong; rewrite it as Deep or split it.
59
+
60
+ Note the vocabulary mismatch: the dashboard says "Quick / Deep"; the API and DB use `quickTest` / `deepTest`. Teach the user both — they'll paste into different surfaces.
61
+
62
+ ### 3. Pick the agent type
63
+
64
+ Two agent types are user-pickable in the dashboard today:
65
+
66
+ - **Functional** (default) — workflows, forms, navigation, CRUD, happy and sad paths. Use this unless the user is explicitly asking about accessibility.
67
+ - **Accessibility** — WCAG behavior, keyboard navigation, focus management, screen-reader-relevant patterns. Use only when the test is about a11y; do not silently pick it for general tests.
68
+
69
+ Other agent types (UI/UX, exploratory, performance, security) exist in the platform but are not yet exposed in the dashboard; do not recommend them.
70
+
71
+ **Labels:** assign exactly **one** label by default: the single most relevant tag, such as the feature area (`onboarding`, `auth`, `checkout`) or `smoke` for a basic health check. Add a second label only if the user explicitly asks for more. In the dashboard you set this on the plan; in a CLI bundle it is the plan's `labels` array, which should normally have a single entry.
72
+
73
+ ### 4. Draft the plan from the template
74
+
75
+ Use the Template section below. Fill in the steps in the order a user would do them. Keep prose natural — write like you're briefing a human tester, not coding a DSL. Numbered steps are fine but not required for the action sequence; what *must* be numbered is the acceptance criteria block.
76
+
77
+ ### 5. Write declarative, verifiable acceptance criteria
78
+
79
+ After the action sequence, add a numbered `Verify that:` block. Each item describes a state the agent can observe, not an action to take.
80
+
81
+ - Good: `2. The user menu in the header shows the logged-in user's email.`
82
+ - Bad: `2. Click the user menu and check that login worked.`
83
+
84
+ The agent will execute each item independently as a check. Vague items like "verify it works" produce noisy reports.
85
+
86
+ ### 6. Plug in credentials and dynamic data
87
+
88
+ If the flow uses sensitive values (passwords, API keys, real emails), reference them through credentials instead of inlining:
89
+
90
+ - Use `{{credentials.<name>}}` — **no spaces** inside the braces. The dashboard validates this and rejects spaced variants.
91
+ - Never write a literal password into a plan you hand back. If the user gives you one, replace it with `{{credentials.<name>}}` and list the credential name in an "Assumes credentials:" footer so the user knows what to set up in Settings → Credentials.
92
+
93
+ If the flow needs to **input generated or unique data** — a fresh email per run, a random name, a unique order ref — define a **data fixture** and reference it as `{{data.<fixtureKey>.<fieldKey>}}` (no spaces). A fixture field is either *static* (a literal value) or *dynamic* (a generator like `internet.email`, `person.firstName`, or `string.uuid` that rolls a fresh value every run). Prefer this over a brittle hardcoded value or asking the user to pre-make one. You create fixtures with the CLI (see "Creating it with the CLI"); run `testlab examples` for the field shape and the full generator list.
94
+
95
+ For pipeline inputs (only in pre-steps), the syntax is `{{ input.<name> }}` **with spaces** — and the fallback form `{{ input.<name> | credentials.<fallback> }}`. The two syntaxes are intentionally different; do not mix them. Full detail in `references/syntax.md`.
96
+
97
+ For dynamic values in **acceptance criteria** (data you *check*, not data you *enter*), write the criterion as a pattern: "verify *a* product appears" rather than "verify 'Blue Widget' appears." Fixtures are for input; patterns are for assertions.
98
+
99
+ ### 7. Self-check, then hand back
100
+
101
+ Before outputting the plan, run it through the Self-check section below. Fix anything the checklist catches. Then output exactly:
102
+
103
+ 1. A single 3-backtick fenced code block containing the plan body (so the user can copy it cleanly).
104
+ 2. Immediately after, a one-line summary in plain prose with mode + agent type + assumed credentials.
105
+
106
+ Do NOT wrap the output in another fence (e.g. 4-backticks around the whole thing). The user wants the code block to be copy-pasteable as-is — nesting fences leaks stray ``` lines into the visible output.
107
+
108
+ If the user's request is ambiguous about scope (step 1), ask before drafting — do not guess and produce a wrong-shaped plan.
109
+
110
+ ## Template
111
+
112
+ ```
113
+ Go to <URL or path>.
114
+
115
+ <Action 1 — one or two sentences in natural prose.>
116
+
117
+ <Action 2 — including any data the user provides.>
118
+
119
+ <… more actions, in the order a real user would perform them.>
120
+
121
+ Verify that:
122
+ 1. <Observable state 1.>
123
+ 2. <Observable state 2.>
124
+ 3. <Observable state 3.>
125
+ ```
126
+
127
+ Setup state (logged-in user, fixture data, etc.) is configured as a pre-step on the plan in the dashboard — never as a `Pre-condition:` line in the prompt body. Don't include such a line.
128
+
129
+ For sensitive values, references go inline:
130
+ ```
131
+ Enter email {{credentials.testEmail}} and password {{credentials.testPassword}}.
132
+ ```
133
+
134
+ End with an "Assumes credentials:" footer when applicable:
135
+ ```
136
+ Assumes credentials: testEmail, testPassword (configure in Settings → Credentials before running).
137
+ ```
138
+
139
+ ## Inline example
140
+
141
+ User says: "Write a test for our login. Lands on /login, real email and password from credentials, expects to land on /dashboard."
142
+
143
+ You produce (one 3-backtick fenced code block, then the summary line as plain prose — no outer wrapper):
144
+
145
+ ```
146
+ Go to /login.
147
+
148
+ Enter the email {{credentials.testEmail}} and the password {{credentials.testPassword}}.
149
+
150
+ Click the "Sign in" button.
151
+
152
+ Verify that:
153
+ 1. The browser navigates to /dashboard.
154
+ 2. A user menu or avatar is visible in the page header.
155
+ 3. The header shows text matching the logged-in user's email or display name.
156
+ 4. No error banner appears at the top of the page.
157
+ ```
158
+
159
+ **Mode:** Quick · **Agent:** Functional · **Assumes credentials:** `testEmail`, `testPassword` (set in Settings → Credentials).
160
+
161
+ ## Self-check (apply before handing back)
162
+
163
+ Walk this list before output. Each item failed = fix the plan, don't ship it.
164
+
165
+ 1. **Start URL is explicit.** "Go to <something>" appears in the first action sentence.
166
+ 2. **Acceptance criteria are numbered and declarative.** Each `Verify that:` item describes an observable state, not an action. No "verify it works."
167
+ 3. **No inline secrets.** No literal passwords, API keys, or tokens in the prose. Sensitive values use `{{credentials.<name>}}` with no spaces.
168
+ 4. **One flow only.** The plan covers a single user journey. "Test login and then test signup" → split into two plans.
169
+ 5. **Mode + agent type declared** in the summary line beneath the fenced block.
170
+ 6. **No brittle fixtures.** Where data is dynamic ("a product", "a recent order"), the criterion uses a pattern not a literal value. Where the user explicitly named a value, it stays.
171
+ 7. **Variable spacing is correct.** `{{credentials.x}}` has no spaces; `{{ input.x }}` has spaces. Re-scan if the plan uses either.
172
+ 8. **Credentials footer present** if any `{{credentials.x}}` appears.
173
+ 9. **No `Pre-condition:` line in the prompt body.** Setup state (logged-in user, fixture data) belongs in the plan's pre-step config in the dashboard, not in the prose. If a draft has a `Pre-condition:` line, drop it.
174
+ 10. **Acceptance criteria match real source where source was readable.** If you read the form/route code in step 1, every assertable text or shape comes from there, not from a guess. Quoting the wrong success-state copy is the most common drift cause.
175
+ 11. **Output is a single 3-backtick fenced code block + a Mode/Agent/Credentials prose line, no nested fences.** Wrapping the output in a 4-backtick (or any outer) fence leaks stray ``` lines into the rendered output. One fence around the plan, then prose.
176
+
177
+ ## Anti-patterns (refuse or fix)
178
+
179
+ These are the most common ways a draft goes wrong. Name the failure to the user when you correct it — they learn from watching you self-correct.
180
+
181
+ | Anti-pattern | Why it's wrong | Fix |
182
+ |---|---|---|
183
+ | `Test the login` | The agent has no flow to follow and no criteria to check. Reports come back vague. | Expand into actions + a numbered `Verify that:` block. |
184
+ | `Email: alice@example.com / Password: hunter2` inlined | Secrets in prose leak into reports and version control. | Replace with `{{credentials.x}}` and add an "Assumes credentials" footer. |
185
+ | `Test login, then create a project, then invite a user` | Multiple flows in one plan blur pass/fail; one failed step poisons the rest. | Split into separate plans. If they share state, chain them as a Pipeline (see `examples/pipelines.md`). |
186
+ | `Verify the product 'Blue Widget' shows up` | The agent now requires that exact product to exist; the test breaks the day it sells out. | `Verify that a product card with name, price, and image is shown.` |
187
+ | `{{ credentials.x }}` (with spaces) | Dashboard validation rejects this; the test will fail to parse. | `{{credentials.x}}` — no spaces inside the braces. |
188
+ | `Click the button and check it works` | The agent doesn't know what "works" means. | Split into the click action and a separate `Verify that:` item describing the resulting visible state. |
189
+ | Plan starts with re-doing the login the pre-step already did | The pre-step already authenticated the browser; re-logging in is wasted steps and may break shared state. | If a pre-step exists, the main plan starts after it. See `examples/pipelines.md`. |
190
+
191
+ ## Vocabulary
192
+
193
+ | UI label | API / DB string |
194
+ |---|---|
195
+ | Quick mode | `quickTest` |
196
+ | Deep mode | `deepTest` |
197
+ | Functional | `functional` |
198
+ | Accessibility | `accessibility` |
199
+
200
+ When the user is pasting into the dashboard, use UI labels in your summary. When the user is calling the API or writing a CI script, use the string form. If you don't know which surface, default to UI labels and note the API equivalent in parentheses.
201
+
202
+ ## Creating it with the CLI
203
+
204
+ The `@test-lab-ai/cli` (command `testlab`) creates everything you've designed — credentials, labels, data fixtures, and the plan(s) — directly in the user's test-lab account, so they don't copy-paste. Offer this whenever it's set up.
205
+
206
+ **1. Check it's available and authenticated.** Run `testlab whoami` (or, with no install, `npx @test-lab-ai/cli whoami`). If it says "not authenticated," the user runs `testlab login` once or sets `TESTLAB_API_KEY` — do NOT create anything until auth works. If the `testlab` command isn't found, fall back to `npx @test-lab-ai/cli …`.
207
+
208
+ **2. Survey what already exists, and reuse it.** Before creating anything, inventory the account so you don't duplicate resources or ask for things that already exist:
209
+ - `testlab projects list` (projects)
210
+ - `testlab credentials list` (credential keys; values are never shown)
211
+ - `testlab labels list` (labels)
212
+ - `testlab data list` (data fixtures)
213
+ - `testlab plans list` (existing plans)
214
+
215
+ Then design the plan to REUSE what fits:
216
+ - reference an existing credential key (e.g. `{{credentials.testPassword}}`) instead of asking for a secret that already exists;
217
+ - reuse an existing label and an existing data fixture rather than making near-duplicates;
218
+ - if the flow needs setup state (a login, a seeded record), wire an EXISTING plan as a pre-step by name (e.g. a "Login" plan for an auth-gated page) instead of writing a new one;
219
+ - choose the project: no projects means account-level; exactly one is used automatically; if there are several, **show the user the list and ask** (never silently fall back to `--project none`), and **propose a name-matching project** when one fits (e.g. "TestLab Admin" for an admin-dashboard test). An agent can't answer the CLI's interactive prompt, so resolve this now.
220
+
221
+ Create only the resources that are missing.
222
+
223
+ **3. Ask first.** `testlab import` writes to the user's account. Confirm they want you to create the resources (vs. just receiving the plan to paste).
224
+
225
+ **4. Build one import bundle** with only the NEW resources the plan needs (plus references to the existing ones found in step 2), created in order (credentials → labels → fixtures → plans). Run `testlab examples` for the exact shape of every resource. For example, write `bundle.json`:
226
+
227
+ ```json
228
+ {
229
+ "credentials": [ { "key": "password", "value": "<the user gives you this — never invent one>" } ],
230
+ "labels": ["smoke"],
231
+ "fixtures": [
232
+ { "key": "newUser", "fields": [
233
+ { "key": "email", "mode": "dynamic", "generator": "internet.email" }
234
+ ] }
235
+ ],
236
+ "plans": [
237
+ { "name": "Sign up", "prompt": "Go to https://app.example.com/signup and register with {{data.newUser.email}} / {{credentials.password}}. Confirm the welcome screen.", "labels": ["smoke"] }
238
+ ]
239
+ }
240
+ ```
241
+
242
+ **5. Preview, then create:** `testlab import bundle.json --dry-run`, then `testlab import bundle.json --project <id|name>` (use the project resolved in step 2; omit `--project` only when the account has zero or one projects).
243
+
244
+ Rules: get secret VALUES from the user (the CLI stores them encrypted, never echoed). Reference fixtures as `{{data.<fixture>.<field>}}` and credentials as `{{credentials.<key>}}` in the prompt. Wire plans together with pre-steps via a `ref` handle. The CLI ships a deep agent guide as `AGENTS.md`; `testlab examples` is the canonical, always-current reference.
245
+
246
+ ## Going further
247
+
248
+ - **Create plans (and their credentials, labels, and data) directly** instead of pasting — the `@test-lab-ai/cli`. See "Creating it with the CLI" above.
249
+ - **Two ways to drive this skill** (author a test while you build a feature, or import tests you already have) — see `examples/workflows.md`.
250
+ - **Variable syntax in depth** (pre-steps, pipeline inputs, devices) — see `references/syntax.md`.
251
+ - **Triggering plans from CI** (only when the user has an API key + an existing `testPlanId`) — see `references/run-via-api.md`.
252
+ - **Auth flow templates** to adapt — `examples/auth.md`.
253
+ - **Pipeline patterns** for multi-step flows that share browser state — `examples/pipelines.md`.
254
+ - **The product's own example library** with cookbook plans for ecommerce, SaaS, social, booking, content, and general web — [test-lab.ai/docs/examples](https://test-lab.ai/docs/examples).
@@ -0,0 +1,145 @@
1
+ # Auth flow templates
2
+
3
+ Adapt these for any auth surface. Each block is paste-ready into the test-lab.ai dashboard. Replace bracketed placeholders, keep `{{credentials.x}}` references intact.
4
+
5
+ These templates favor patterns over fixtures (e.g., "a user menu is visible" rather than "the text 'alice' is shown"). When the user's app has a fixed text element to assert on, swap the pattern for the literal.
6
+
7
+ ---
8
+
9
+ ## Login — happy path
10
+
11
+ **Mode:** Quick · **Agent:** Functional · **Assumes credentials:** `loginEmail`, `loginPassword`
12
+
13
+ ```
14
+ Go to /login.
15
+
16
+ Enter the email {{credentials.loginEmail}} and the password {{credentials.loginPassword}}.
17
+
18
+ Click the "Sign in" button.
19
+
20
+ Verify that:
21
+ 1. The browser navigates away from /login (typically to /dashboard, /home, or /).
22
+ 2. A user menu, avatar, or "Sign out" control is visible in the page header.
23
+ 3. The header shows text matching the logged-in user's email or display name.
24
+ 4. No error banner is shown at the top of the page.
25
+ ```
26
+
27
+ ---
28
+
29
+ ## Login — wrong password (sad path)
30
+
31
+ **Mode:** Quick · **Agent:** Functional · **Assumes credentials:** `loginEmail`
32
+
33
+ ```
34
+ Go to /login.
35
+
36
+ Enter the email {{credentials.loginEmail}} and the password "deliberately-wrong-password-1234!".
37
+
38
+ Click the "Sign in" button.
39
+
40
+ Verify that:
41
+ 1. The browser stays on /login (no redirect to a logged-in surface).
42
+ 2. An error message about invalid credentials is visible near the form.
43
+ 3. The error does not reveal whether the email exists in the system (no "user not found" or "email not registered" wording).
44
+ 4. The password field is empty or the form is in an "error" state ready for retry.
45
+ ```
46
+
47
+ ---
48
+
49
+ ## Signup — happy path
50
+
51
+ **Mode:** Quick · **Agent:** Functional
52
+
53
+ ```
54
+ Go to /signup.
55
+
56
+ Fill the form with:
57
+ - Email: a unique address using the pattern test-{timestamp}@example.com
58
+ - Password: TestPassword123!
59
+ - Confirm password: TestPassword123! (only if the form has this field)
60
+
61
+ Submit the form.
62
+
63
+ Verify that one of the following happens:
64
+ 1. The page shows a "check your email to verify" message, OR
65
+ 2. The browser redirects to a logged-in surface (typically /dashboard, /onboarding, or /welcome), OR
66
+ 3. A success banner indicates the account was created.
67
+
68
+ Verify that:
69
+ 4. No error message is shown at any point during submission.
70
+ 5. The form does not stay in an unsubmitted state (no spinner stuck indefinitely).
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Forgot password
76
+
77
+ **Mode:** Quick · **Agent:** Functional · **Assumes credentials:** `loginEmail`
78
+
79
+ ```
80
+ Go to /forgot-password (or click "Forgot password?" from /login).
81
+
82
+ Enter the email {{credentials.loginEmail}}.
83
+
84
+ Submit the form.
85
+
86
+ Verify that:
87
+ 1. A confirmation message appears indicating an email has been sent (e.g., "Check your inbox").
88
+ 2. The message does not reveal whether the email is registered (it should look the same for both registered and unregistered emails — this is a security property).
89
+ 3. The browser stays on the forgot-password surface or moves to a confirmation surface; it does not redirect to /login or /dashboard.
90
+ 4. No error banner is shown.
91
+ ```
92
+
93
+ ---
94
+
95
+ ## Change password (authenticated)
96
+
97
+ **Mode:** Quick · **Agent:** Functional · **Assumes credentials:** `loginPassword` · **Requires:** logged-in pre-step
98
+
99
+ ```
100
+ Pre-condition: user is logged in (configure a login pre-step on this plan; see examples/pipelines.md).
101
+
102
+ Go to /settings/password (or /account/security).
103
+
104
+ Fill the password change form with:
105
+ - Current password: {{credentials.loginPassword}}
106
+ - New password: NewTestPassword456!
107
+ - Confirm new password: NewTestPassword456!
108
+
109
+ Submit the form.
110
+
111
+ Verify that:
112
+ 1. A success message appears confirming the password was changed.
113
+ 2. The form clears or moves to a confirmation state.
114
+ 3. No error banner is shown.
115
+ ```
116
+
117
+ (If you adapt this to actually verify the new password works, run a separate logout-then-login plan after this one — don't bundle the verification into this plan.)
118
+
119
+ ---
120
+
121
+ ## Logout
122
+
123
+ **Mode:** Quick · **Agent:** Functional · **Requires:** logged-in pre-step
124
+
125
+ ```
126
+ Pre-condition: user is logged in (configure a login pre-step on this plan).
127
+
128
+ Go to any authenticated surface (e.g., /dashboard).
129
+
130
+ Open the user menu (typically an avatar or initials in the header) and click "Sign out" or "Log out".
131
+
132
+ Verify that:
133
+ 1. The browser redirects to /login, /, or a public landing surface.
134
+ 2. The user menu / avatar is no longer visible in the header.
135
+ 3. Visiting /dashboard now redirects to /login (the session is fully cleared).
136
+ ```
137
+
138
+ ---
139
+
140
+ ## Notes for adapting these
141
+
142
+ - **Path conventions vary.** Swap `/login`, `/signup`, `/forgot-password`, `/dashboard` for whatever the target site uses. If the user's site uses `/sign-in` or `/account/login`, keep the prose natural and use their convention.
143
+ - **Field labels vary.** "Email" might be "Username" or "Work email"; "Sign in" might be "Continue" or "Log in". Use the wording from the actual page when known; otherwise the natural-language descriptions above ("the email field", "the sign in button") are flexible enough for the agent to map.
144
+ - **MFA / 2FA** is not covered here. If the flow requires a code, the test will fail at that step unless the credential is a TOTP-derived value the credential store can produce — outside the scope of this template set.
145
+ - **CAPTCHAs (reCAPTCHA, Cloudflare Turnstile)** can break automated runs. If the target page has one, mention it to the user — they may need to allowlist test-lab.ai's runner IPs or move the protection to a non-test environment.
@@ -0,0 +1,163 @@
1
+ # Pipeline patterns
2
+
3
+ Pipelines chain test plans on the same browser instance. Use them when a flow requires being already-logged-in, or when a multi-step user journey is more debuggable as separate plans than one giant one.
4
+
5
+ A pipeline is **two or more plans** in the dashboard:
6
+ - One or more **pre-steps** (regular plans with the "Use as a pre-step" checkbox enabled)
7
+ - One **main plan** that the user attaches the pre-step(s) to
8
+
9
+ The agent in each step is fresh (no memory of previous steps), but the **browser state carries forward** — cookies, localStorage, the current URL. That's how "log in once, test ten things" works.
10
+
11
+ When you produce a pipeline, output **both plans** and label which is the pre-step and which is the main plan. The user has to create them as separate entries in the dashboard.
12
+
13
+ ---
14
+
15
+ ## Pattern 1 — Reusable login pre-step
16
+
17
+ The most common pipeline. Build this once; attach it to every authed test.
18
+
19
+ ### Pre-step: "Login (reusable)"
20
+
21
+ **Mode:** Quick · **Agent:** Functional · **Use as a pre-step:** ✅
22
+
23
+ ```
24
+ Go to /login.
25
+
26
+ Enter the email {{ input.email | credentials.loginEmail }} in the email field.
27
+
28
+ Enter the password {{ input.password | credentials.loginPassword }} in the password field.
29
+
30
+ Click the "Sign in" button.
31
+
32
+ Verify that:
33
+ 1. The browser navigates away from /login.
34
+ 2. A user menu, avatar, or "Sign out" control is visible in the page header.
35
+ ```
36
+
37
+ The `{{ input.x | credentials.y }}` form lets the same pre-step serve any caller: most tests will leave the inputs empty and fall back to the default credential, but a specific test can override with different inputs (e.g., admin vs. regular user).
38
+
39
+ ### Main plan: "Dashboard loads correctly"
40
+
41
+ **Mode:** Quick · **Agent:** Functional · **Pre-step:** Login (reusable), with **Fail entire test if a pre-step fails** ✅
42
+
43
+ ```
44
+ Go to /dashboard.
45
+
46
+ Verify that:
47
+ 1. The dashboard page loads without an error banner.
48
+ 2. The user's name or email is visible in the header.
49
+ 3. The primary navigation (sidebar or top nav) is visible with the expected sections.
50
+ 4. No loading spinner remains on the page after 5 seconds.
51
+ ```
52
+
53
+ Note: the main plan starts with `Go to /dashboard`, **not** with a re-login. The pre-step has already authenticated the browser. Re-doing login in the main plan wastes steps and may break shared state.
54
+
55
+ ---
56
+
57
+ ## Pattern 2 — Multi-role testing
58
+
59
+ Test admin and regular user perspectives in sequence. Two pre-steps, each with a different credential.
60
+
61
+ ### Pre-step A: "Login as admin"
62
+
63
+ **Use as a pre-step:** ✅ · **Assumes credentials:** `adminEmail`, `adminPassword`
64
+
65
+ ```
66
+ Go to /login.
67
+ Enter {{ input.email | credentials.adminEmail }} and {{ input.password | credentials.adminPassword }}.
68
+ Click "Sign in" and verify the dashboard loads.
69
+ ```
70
+
71
+ ### Pre-step B: "Login as regular user"
72
+
73
+ Same prompt, but defaults to `userEmail` / `userPassword` instead.
74
+
75
+ ### Main plan: "Permission boundaries visible"
76
+
77
+ Attach **only** the admin or only the user pre-step (depending on which role you're testing), then:
78
+
79
+ ```
80
+ Go to /settings.
81
+
82
+ Verify that:
83
+ 1. The "Team Management" section is visible (admin) OR not visible (user).
84
+ 2. The "Billing" tab is clickable (admin) OR shows a "contact your admin" message (user).
85
+ 3. The "Audit Log" link is present (admin) OR absent (user).
86
+ ```
87
+
88
+ Two main plans — one per role — give you clean pass/fail per role. Don't try to put both roles in one plan.
89
+
90
+ ---
91
+
92
+ ## Pattern 3 — Multi-step CRUD with shared state
93
+
94
+ Each step is independently debuggable. If "delete" breaks, you re-run only that step.
95
+
96
+ ### Pre-step: "Login" (the reusable one from Pattern 1)
97
+
98
+ ### Step 1: "Create a project"
99
+
100
+ **Use as a pre-step:** ✅ (so step 2 can attach this as its pre-step)
101
+
102
+ ```
103
+ Go to /projects/new.
104
+
105
+ Fill the form with:
106
+ - Name: "Pipeline Test Project {{ input.suffix | 'default' }}"
107
+ - URL: https://example.com
108
+
109
+ Click "Create".
110
+
111
+ Verify that:
112
+ 1. The browser redirects to /projects/<id> (the new project's surface).
113
+ 2. A success message confirms creation.
114
+ 3. The project name appears in the page header.
115
+ ```
116
+
117
+ ### Step 2 (main plan): "List shows the new project, can be deleted"
118
+
119
+ **Pre-steps in order:** Login → Create a project (with `suffix: "step2"`)
120
+
121
+ ```
122
+ Go to /projects.
123
+
124
+ Verify that:
125
+ 1. A project card with name containing "Pipeline Test Project step2" is visible.
126
+ 2. The card has visible "Edit" and "Delete" controls.
127
+
128
+ Click the "Delete" control on that project card.
129
+
130
+ Confirm the deletion in the dialog that appears.
131
+
132
+ Verify that:
133
+ 3. The card disappears from the list.
134
+ 4. A "Project deleted" confirmation appears (toast or banner).
135
+ 5. Reloading /projects does not bring the project back.
136
+ ```
137
+
138
+ The `suffix` input on step 1 lets you reference the exact name in step 2's verification, since the same data created in step 1 is what step 2 expects to see.
139
+
140
+ ---
141
+
142
+ ## Anti-patterns
143
+
144
+ - **Pre-step that does too much.** A single pre-step that logs in *and* seeds data *and* navigates to a section gives you one big black box if something fails. Split: one step per setup concern.
145
+ - **Main plan re-runs setup.** If the pre-step logs in, the main plan starts logged-in. Don't add `Go to /login` at the top of the main plan.
146
+ - **Hardcoded credentials in pre-step prose.** Same rule as solo plans: use `{{ input.x | credentials.y }}` so callers can override and so values aren't leaked.
147
+ - **Forgetting fail-fast.** A pre-step failure usually means later steps will fail in unhelpful ways (missing auth, missing data). Default to **Fail entire test if a pre-step fails ✅** unless the steps are genuinely independent.
148
+ - **Pre-step verifies too aggressively.** A login pre-step's verifications should confirm "I'm logged in" — not "the dashboard renders perfectly." Heavy verification belongs in the main plan, not the setup.
149
+
150
+ ---
151
+
152
+ ## When to recommend a pipeline
153
+
154
+ Recommend splitting into a pipeline when:
155
+ - The flow requires being logged in (separate login pre-step).
156
+ - The user describes setup that could be reused across many tests (login, seed-a-project, pick-a-tenant).
157
+ - The user describes a sequence where each step is independently meaningful (CRUD, multi-role).
158
+
159
+ Don't split into a pipeline when:
160
+ - The whole flow is a single user journey ("user lands on /pricing → clicks Buy → fills card → sees confirmation"). One plan.
161
+ - The user just wants a smoke test. One plan.
162
+
163
+ Pipelines have overhead (more dashboard config, more reports to scan). Default to a single plan; introduce pipelines when the structure earns it.
@@ -0,0 +1,85 @@
1
+ # Two ways to drive the test-lab-plan skill
2
+
3
+ The skill turns a described flow into a test-lab plan. There are two common
4
+ workflows. In both, the design rules are identical (explicit URL, declarative
5
+ `Verify that:` criteria, `{{credentials.<key>}}` for secrets, `{{data.<fixture>.<field>}}` for
6
+ generated data, and **one label** by default). They differ in where the input
7
+ comes from and whether you create one plan or many.
8
+
9
+ ## A. Author a test while building or changing a feature
10
+
11
+ You are in the repo adding or changing behavior and want a test-lab test that
12
+ covers it.
13
+
14
+ 1. Trigger the skill ("write a test-lab test for the new password-reset page").
15
+ 2. The skill **reads the real source** (the new or changed component and route),
16
+ so the criteria quote actual on-screen text and response shapes, not guesses.
17
+ 3. It drafts one plan, with a single label (the feature area).
18
+ 4. If the `testlab` CLI is set up, it **offers to create the plan directly** in
19
+ your account (see SKILL.md, "Creating it with the CLI"). Otherwise it hands
20
+ back a paste-ready plan for the dashboard.
21
+
22
+ Example. You just added `/account/reset-password`. After reading the form
23
+ component, the skill produces:
24
+
25
+ ```
26
+ Go to https://app.example.com/account/reset-password.
27
+
28
+ Request a reset for {{data.user.email}}, then open the reset link and set a new password that meets the strength rules shown on the form.
29
+
30
+ Verify that:
31
+ 1. A confirmation reading "Check your email" appears after requesting the reset.
32
+ 2. After the new password is set, the page redirects to /login.
33
+ 3. A success banner with the text "Password updated" is shown.
34
+ ```
35
+ **Mode:** Quick · **Agent:** Functional · **Label:** `auth` · **Fixture:** `user.email`
36
+
37
+ With the CLI set up, the skill writes a bundle and runs `testlab import bundle.json`:
38
+
39
+ ```json
40
+ {
41
+ "fixtures": [
42
+ { "key": "user", "fields": [ { "key": "email", "mode": "dynamic", "generator": "internet.email" } ] }
43
+ ],
44
+ "plans": [
45
+ { "name": "Reset password", "prompt": "Go to https://app.example.com/account/reset-password. …", "labels": ["auth"] }
46
+ ]
47
+ }
48
+ ```
49
+
50
+ ## B. Import test plans you already have
51
+
52
+ You have tests elsewhere (Playwright/Cypress specs, Cucumber `.feature` files, a
53
+ TestRail/Zephyr export, or a prose doc) and want them in test-lab.
54
+
55
+ 1. Point the skill at them ("convert these Playwright specs into test-lab plans").
56
+ 2. For each test, it produces a plan: explicit URL in the prompt, secrets as
57
+ `{{credentials.<key>}}`, generated data as `{{data.<fixture>.<field>}}`, and one label.
58
+ 3. It assembles a single **import bundle** (credentials + fixtures + plans) and
59
+ runs `testlab import ./plans` (with `--dry-run` first). See the CLI's
60
+ `AGENTS.md` and `testlab examples` for the exact shapes.
61
+
62
+ Example bundle from one converted login spec:
63
+
64
+ ```json
65
+ {
66
+ "credentials": [ { "key": "password", "value": "<the user provides this>" } ],
67
+ "fixtures": [
68
+ { "key": "user", "fields": [ { "key": "email", "mode": "dynamic", "generator": "internet.email" } ] }
69
+ ],
70
+ "plans": [
71
+ { "name": "Login", "prompt": "Go to https://app.example.com/login and sign in with {{data.user.email}} / {{credentials.password}}. Confirm the dashboard loads.", "labels": ["smoke"] }
72
+ ]
73
+ }
74
+ ```
75
+
76
+ The difference from A: importing is usually **batch** (many tests at once, often
77
+ a directory of `*.json`), while authoring-while-building is usually **one** plan
78
+ grounded in the code you just wrote.
79
+
80
+ ## The CLI and the skill, in one line
81
+
82
+ The **skill writes** plans (this skill); the **CLI imports** them
83
+ (`@test-lab-ai/cli`). Install the CLI with `npm i -g @test-lab-ai/cli`, and the
84
+ skill itself with `testlab skills install`. The canonical, always-current
85
+ reference for every resource shape is `testlab examples`.
@@ -0,0 +1,97 @@
1
+ # Triggering plans from CI / via API
2
+
3
+ Read this file **only** when the user explicitly asks how to run a plan from CI, programmatically, or via API. The skill's primary job is writing plans; running them is a separate concern that requires an API key and an existing `testPlanId` the user already created in the dashboard.
4
+
5
+ If the user has not yet saved their plan in the dashboard, they cannot run it via API yet — the API takes IDs, not prose. Steer them through paste → save → grab ID → then return to this guide.
6
+
7
+ ## Endpoint
8
+
9
+ ```
10
+ POST https://test-lab.ai/api/v1/run
11
+ Authorization: Bearer tl_xxxxx
12
+ Content-Type: application/json
13
+ ```
14
+
15
+ API keys come from **Settings → API Keys** in the dashboard. Treat them like passwords; do not commit them. In CI, supply via secret env (e.g., `TESTLAB_API_KEY`).
16
+
17
+ ## Request body
18
+
19
+ Exactly **one** of `testPlanIds`, `projectId`, or `label` is required. Every selector is scoped to the API key's account — the API never resolves plans, projects, or labels owned by other accounts.
20
+
21
+ | Field | Type | Description |
22
+ |---|---|---|
23
+ | `testPlanIds` | number[] or comma-string | Run one or more plans (e.g., `[1,2,3]` or `"1,2,3"`) |
24
+ | `projectId` | number | Run every plan in the project |
25
+ | `label` | string | Run every plan tagged with this label name (matched by name within the account) |
26
+ | `testType` | `"quickTest"` or `"deepTest"` | Optional - overrides the plan's saved default |
27
+ | `buildId` | string (≤100 chars) | Optional - your CI commit SHA / build number for traceability |
28
+ | `cookies` | array of `{name, value, domain}` | Optional - runtime cookies; override stored ones |
29
+ | `preferScript` | boolean | Optional - when true, each plan runs as its saved Playwright script if one exists (deterministic, no LLM cost). Falls back to AI when no script is on file. |
30
+ | `triggerPipelinePreSteps` | boolean | Optional, default `false`. Plans configured as a pipeline pre-step (referenced by another plan as a pre-step) are silently excluded from batch runs by default — they expect input parameters or a specific browser state and produce false-failures when run solo. Set `true` to include them (e.g. you want to smoke-test the login pre-step on its own with default credentials). |
31
+
32
+ ## Response
33
+
34
+ Always the same array shape regardless of selector:
35
+
36
+ ```json
37
+ {
38
+ "jobs": [{ "jobId": "uuid", "testPlanId": 123, "testPlanName": "...", "testType": "quickTest", "status": "running" }, ...],
39
+ "triggered": 3,
40
+ "failed": 0,
41
+ "skipped": 0,
42
+ "buildId": "abc123"
43
+ }
44
+ ```
45
+
46
+ `status` per job is one of: `running`, `queued`, `pending`, `error`, `skipped`. A `skipped` entry carries an `error` message explaining why (e.g. "Plan is configured as a pipeline pre-step. Pass triggerPipelinePreSteps: true to include pre-step plans in batch runs."). `triggered` excludes both `error` and `skipped`.
47
+
48
+ The endpoint returns immediately after queueing — it does not wait for tests to finish. Poll the job, set up a webhook, or use the `buildId` to look up status from a CI status check later.
49
+
50
+ ## CI example (GitHub Actions)
51
+
52
+ ```yaml
53
+ - name: Trigger test-lab.ai smoke
54
+ env:
55
+ TESTLAB_API_KEY: ${{ secrets.TESTLAB_API_KEY }}
56
+ run: |
57
+ curl -fsSL -X POST https://test-lab.ai/api/v1/run \
58
+ -H "Authorization: Bearer $TESTLAB_API_KEY" \
59
+ -H "Content-Type: application/json" \
60
+ -d "{\"projectId\": ${{ vars.TESTLAB_PROJECT_ID }}, \"testType\": \"quickTest\", \"buildId\": \"$GITHUB_SHA\"}"
61
+ ```
62
+
63
+ For per-PR runs, `projectId` runs the whole project's plans; `testPlanIds` lets you pick a subset.
64
+
65
+ ## Webhooks
66
+
67
+ Configure webhooks at **Settings → Webhooks** to get notified when a job completes (instead of polling). The webhook payload includes `jobId`, `status`, `testPlanId`, `buildId`, and a link to the report. See [test-lab.ai/docs/webhooks](https://test-lab.ai/docs/webhooks) for the full event schema.
68
+
69
+ ## Common errors
70
+
71
+ | Status | Body | What to check |
72
+ |---|---|---|
73
+ | 401 | `Invalid API key` | Token revoked, typo in `Authorization` header, missing `Bearer ` prefix |
74
+ | 402 | `Insufficient credits...` | Top up the org's credit balance |
75
+ | 404 | `No test plans found` | Wrong `testPlanId` / `projectId`, or the key belongs to a different org |
76
+ | 400 | `One of testPlanIds, projectId, or label is required` | Body missing the selector |
77
+
78
+ ## When pipelines / pre-steps are involved
79
+
80
+ A plan with pre-steps configured in the dashboard runs as a pipeline automatically — pre-steps execute first, then the main test, all sharing browser state. No special API parameter needed; just include the master plan in `testPlanIds` (or its `projectId` / `label`).
81
+
82
+ With `preferScript: true`: if **every step** (every pre-step + the main) has a saved script for the chosen device, the whole pipeline runs as a script pipeline (state chains via Playwright `storageState`, no LLM cost). If **any step is missing a script**, the entire pipeline falls back to AI mode — mixing script + AI mid-pipeline can't share state cleanly. All-or-nothing.
83
+
84
+ The `jobs[]` entry returned carries the **main plan's** job ID. Pre-step jobs share the same `pipeline_id` + `run_group_id` and can be looked up by querying jobs with that group ID.
85
+
86
+ Don't confuse `triggerPipelinePreSteps` (above) with this: that flag controls whether plans that ARE pre-steps (used by others) get triggered when listed in a batch — independent from the auto-pipeline-execution behavior here, which fires for plans that HAVE pre-steps.
87
+
88
+ ## Skill behavior
89
+
90
+ When you cite this file to the user, output:
91
+ 1. The minimal `curl` for their case (testPlanIds / projectId / label)
92
+ 2. A note about which env var to set the API key in
93
+ 3. A reminder that the API takes IDs (or label names) and only resolves them on the API key's account; the plan / project / label must already exist there
94
+ 4. If they want script-mode runs (cheaper, no LLM cost), include `"preferScript": true` in the body and explain it falls back to AI per-plan when no script is on file
95
+ 5. A pointer to webhooks if they ask "how do I know when it's done"
96
+
97
+ Do **not** generate API keys, do **not** infer `testPlanIds` / `projectId` / `label` values, and do **not** offer to actually call the API. The skill's role ends at "here is the curl you would run."
@@ -0,0 +1,94 @@
1
+ # Variable, pre-step, and device syntax
2
+
3
+ Read this file when a test plan needs credentials, pipeline inputs, multi-step shared state, or a non-default device. Skip it for plain plans that just describe a flow.
4
+
5
+ ## Credentials
6
+
7
+ Store credentials in the dashboard at **Settings → Credentials** (key/value pairs, organization-scoped). Reference them in plans:
8
+
9
+ ```
10
+ Enter the email {{credentials.loginEmail}} and the password {{credentials.loginPassword}}.
11
+ ```
12
+
13
+ The AI agent never sees the actual values — they're injected directly into form fields at runtime.
14
+
15
+ ### Syntax rules (validated by the dashboard)
16
+
17
+ | Pattern | Valid | Notes |
18
+ |---|---|---|
19
+ | `{{credentials.loginEmail}}` | yes | Canonical form |
20
+ | `{{credentials.user_password}}` | yes | Underscores allowed |
21
+ | `{{credentials.api2Key}}` | yes | Numbers allowed (not at start) |
22
+ | `{{credentials.2faCode}}` | no | Name cannot start with a number |
23
+ | `{{ credentials.email }}` | no | No spaces inside braces |
24
+ | `{credentials.email}` | no | Must use double braces |
25
+
26
+ Names are case-sensitive. The dashboard rejects plans that reference a credential that doesn't exist, so **always include an "Assumes credentials:" footer** in your output listing what the user needs to set up.
27
+
28
+ ### Where credentials work
29
+
30
+ - **Test prompts** (the plan body) – primary use case.
31
+ - **Project/plan cookie values**: `Value: {{credentials.sessionToken}}`
32
+ - **Custom HTTP headers**: `Value: Bearer {{credentials.apiKey}}`
33
+
34
+ ## Pipelines and pre-steps
35
+
36
+ A **pipeline** chains test plans on the same browser instance. Cookies, localStorage, and DOM state persist across steps. The most common pattern is a login pre-step + a feature test that runs already authenticated.
37
+
38
+ ### Pre-steps
39
+
40
+ A pre-step is just a regular test plan with the **"Use as a pre-step for other test plans"** checkbox enabled. It accepts inputs declared with `{{ input.<name> }}` syntax:
41
+
42
+ ```
43
+ Go to https://myapp.com/login.
44
+ Enter {{ input.email | credentials.loginEmail }} in the email field.
45
+ Enter {{ input.password | credentials.loginPassword }} in the password field.
46
+ Click Sign In and verify the dashboard loads.
47
+ ```
48
+
49
+ ### Input syntax (pre-steps only — **with spaces**)
50
+
51
+ | Form | Meaning |
52
+ |---|---|
53
+ | `{{ input.email }}` | Required parameter, no default |
54
+ | `{{ input.email \| 'fallback@test.com' }}` | Parameter with a literal default |
55
+ | `{{ input.email \| credentials.loginEmail }}` | Parameter defaulting to a stored credential |
56
+
57
+ ### Why two syntaxes
58
+
59
+ `{{credentials.x}}` and `{{ input.x }}` are intentionally distinguishable. Credentials are static lookups (no spaces, terse). Inputs are template expressions that may include filters (spaces, more like Liquid). Don't normalize the spacing — the dashboard validators rely on the difference.
60
+
61
+ ### Attaching pre-steps
62
+
63
+ In the dashboard:
64
+ 1. Open the main plan, click **Add pre-step**
65
+ 2. Pick the pre-step from the dropdown
66
+ 3. Fill the input values (use the key icon to select credentials)
67
+ 4. Toggle **"Fail entire test if a pre-step fails"** when later steps depend on earlier ones (almost always for login pre-steps)
68
+
69
+ When the user describes a flow that requires being logged in, the right-shaped output is **two plans**: a login pre-step + the main plan that starts after login. Don't put the login at the top of the main plan.
70
+
71
+ ### When to use pipelines vs cookie injection
72
+
73
+ - **Cookie injection** (configured at the project or plan level) – fastest; use when you can extract session cookies from your app and don't need to test the login UI itself.
74
+ - **Pipelines** – when you're testing the login flow, when cookies are HTTP-only and hard to extract, or when setup needs multiple browser steps. Recommend pipelines if the user wants reusable, shareable building blocks.
75
+
76
+ ## Devices
77
+
78
+ Plans run on Playwright device descriptors. The dashboard's default is `Desktop Chrome`. Common values:
79
+
80
+ - `Desktop Chrome`, `Desktop Firefox`, `Desktop Safari`, `Desktop Edge`
81
+ - `iPhone 15 Pro`, `iPhone SE`
82
+ - `Pixel 8`, `Pixel 5`
83
+ - `iPad Pro 11`
84
+
85
+ A single plan can be configured to run on multiple devices — the same plan executes once per device, producing one report each. Use this when a flow has known mobile/desktop differences (responsive nav, mobile-only menus, touch interactions).
86
+
87
+ ## Test type strings
88
+
89
+ When pasting into the dashboard, the user picks **Quick mode** or **Deep mode** from a dropdown. When the same plan is triggered via the API or stored in the DB, the strings are:
90
+
91
+ - `quickTest` – Quick mode
92
+ - `deepTest` – Deep mode
93
+
94
+ These are the only two valid values for `testType` in the `/api/v1/run` payload and for the `default_test_type` column. There is no third option.